Utilisation de collections nestedes d’expressions lambda pour créer un graphe d’object

Je suis intéressé à utiliser des expressions lambda pour créer un arbre de sélecteurs de propriétés.

Le scénario d’utilisation est que nous avons du code qui fait une reflection récursive sur un graphe d’object, et pour limiter l’étendue de la récursion, nous utilisons actuellement des atsortingbuts pour marquer les propriétés à traverser. Obtenir toutes les propriétés d’object décorées, si cette propriété est un type de référence avec des propriétés décorées, répétez l’opération pour chacune d’elles également.

L’utilisation des atsortingbuts est limitée par le fait que vous ne pouvez les placer que sur les types pour lesquels vous contrôlez la source. Une arborescence d’expressions lambda permet de définir la scope sur les membres publics de tout type arbitraire.

Il serait pratique d’avoir un raccourci pour définir ces expressions, qui reflète la structure du graphe d’objects.

En fin de compte, j’aimerais avoir quelque chose comme ça:

Selector selector = new [] { (t => Property1), (t => Property2) { p => NestedProperty1, p => NestedProperty2 } }; 

Pour le moment, le mieux que je puisse faire déclare explicitement une instance pour chaque noeud:

 var selector = new Selector() { new SelectorNode(t => Property1), new SelectorNode(t => Property2) { new SelectorNode(p => NestedProperty1), new SelectorNode(p => NestedProperty2) }, }; 

Il n’y a rien de mal avec ce code, mais vous devez écrire explicitement les arguments de type pour chaque noeud, car le compilateur ne peut pas déduire les arguments de type. C’est pénible. Et laid. J’ai vu des sucres syntaxiques incroyables, et je suis sûr qu’il doit y avoir un meilleur moyen.

En raison de mon manque de compréhension des concepts C # ‘plus élevés’ tels que la dynamic, les génériques co / contravariants et les arbres d’expression, j’ai pensé poser la question et voir si des gourous savent comment y parvenir (ou quelque chose comme: il?)


Pour référence, ce sont les déclarations pour les classes Selector et SelectorNode qui réalisent la structure que j’ai décrite dans mon post:

 public interface ISelectorNode {} public class Selector: List<ISelectorNode>{} public class SelectorNode: List<ISelectorNode>, ISelectorNode { public SelectorNode(Expression<Func> select) {} } //Examples of Usage below public class Dummy { public ChildDummy Child { get; set; } } public class ChildDummy { public ssortingng FakeProperty { get; set; } } public class Usage { public Usage() { var selector = new Selector { new SelectorNode(m => m.Child) { new SelectorNode(m => m.FakeProperty) } }; } } 

Édité dans le but d’élargir la réponse de nawal:

En utilisant la syntaxe d’initialisation de la collection C #, nous pouvons obtenir du code ressemblant à ceci:

 var selector = new Selector { (m => m.Child), {dummy => dummy.Child, c => c.FakeProperty, c => c.FakeProperty } }; 

Ceci est le cas si notre méthode d’ajout de classe SelectorNode ‘ressemble à ceci:

 public class Selector : List<ISelectorNode> { public SelectorNode Add(Expression<Func> selector, params Expression<Func>[] children) { return SelectorNode.Add(this, this, selector); } } 

Il doit y avoir un moyen de tirer parti de cette syntaxe!

Je dois admettre à ce stade que je suis trop engourdi en pensant à trop d’options, en espérant que ce sera ma dernière .. 🙂

Enfin, celui que vous avez mentionné dans votre question – Expression> route. Je ne sais pas comment améliorer cette situation sans perdre un peu de sécurité lors de la compilation. Très similaire à ma première réponse:

 public class Selector : List> { public static SelectorNode Get(Expression> selector) { return new SelectorNode(selector); } public void Add(Expression> selector) { var node = new SelectorNode(selector); Add(node); } } public class SelectorNode : List>, ISelectorNode { public SelectorNode(Expression> selector) { } public ISelectorNode Add(params Expression>[] selectors) { foreach (var selector in selectors) base.Add(new SelectorNode(selector)); return this; } public ISelectorNode Add(params ISelectorNode[] nodes) { AddRange(nodes); return this; } } 

Et vous appelez:

 var selector = new Selector { Selector.Get(m => m.Address).Add ( Selector
.Get(x => x.Place), Selector
.Get(x => x.ParentName).Add ( x => x.Id, x => x.FirstName, x => x.Surname ) ), Selector.Get(m => m.Name).Add ( x => x.Id, x => x.FirstName, x => x.Surname ), m => m.Age };

De tout cela, c’est mon préféré jusqu’à maintenant (si ça sert) ..

Edit: La réponse ci-dessous de moi impardonnablement ne répond pas à la question. Je l’ai mal interprété. Je vais fournir une autre réponse qui pourrait réellement faire le travail. Garder cette réponse ouverte, car cela pourrait aider quelqu’un à l’avenir sur un sujet lié.


C’est quelque chose que vous pouvez gérer avec une interface fluide, mais qui ne vous convient pas.

Avez-vous des classes de sélecteur comme ceci:

 public class Selector : List> { public SelectorNode Add(Expression> selector) { return SelectorNode.Add(this, selector); } } public class SelectorNode : List>, ISelectorNode { //move this common functionality to a third static class if it warrants. internal static SelectorNode Add(List> list, Expression> selector) { var node = new SelectorNode(selector); list.Add(node); return node; } SelectorNode(Expression> selector) //unhide if you want it. { } public SelectorNode Add(Expression> selector) { return SelectorNode.Add(this, selector); } } 

Maintenant, vous pouvez appeler:

 var selector = new Selector(); selector.Add(m => m.Child).Add(m => m.FakeProperty); //just chain the rest.. 

Personnellement, je trouve cela plus lisible que votre approche dans la question, mais ce n’est pas aussi intuitif ni génial 🙂 Je ne pense pas que vous puissiez l’avoir sur une seule ligne (malheureusement :(), mais il pourrait y avoir un moyen difficile.

Mettre à jour:

Un one-liner:

 public class Selector : List> { public SelectorNode Add(Expression> selector) { return SelectorNode.Add(this, this, selector); } } public class SelectorNode : List>, ISelectorNode { //move this common functionality to a third static class if it warrants. internal static SelectorNode Add(Selector parent, List> list, Expression> selector) { var node = new SelectorNode(parent, selector); list.Add(node); return node; } Selector parent; SelectorNode(Selector parent, Expression> selector) //unhide if you want it. { this.parent = parent; } public SelectorNode Add(Expression> selector) { return SelectorNode.Add(parent, this, selector); } public Selector Finish() { return parent; } } 

Usage:

 var selector = new Selector().Add(m => m.Child).Add(m => m.FakeProperty).Finish(); //or the earlier var selector = new Selector(); selector.Add(m => m.Child).Add(m => m.FakeProperty); //just chain the rest, no need of Finish 

Avantage de la première approche:

  1. Plus simple

  2. Ne modifie pas la définition existante (de SelectorNode )

Avantage de seconde:

  1. Offre un appel plus propre.

Un petit inconvénient de ces deux approches pourrait être qu’il existe maintenant une méthode statique interne, Add utilisée pour partager des fonctionnalités communes qui n’a aucune signification en dehors de ces deux classes de sélecteur, mais que je suppose, c’est vivable. Vous pouvez supprimer la méthode et le code en double (ou, à la dure, imbriquer SelectorNode dans Selector et masquer l’application au monde extérieur si SelectorNode n’a aucune signification en dehors de la classe Selector . Ou pire encore, protégez-la et héritez d’une classe de l’autre).

Une suggestion: vous voudrez probablement utiliser la méthode de la composition plutôt que celle de l’ inheritance avec vos List . Les noms de classe (sélecteurs) ne donnent pas une idée d’une collection située en dessous. Bonne question d’ailleurs!

Votre implémentation réelle est très propre et lisible, peut être un peu verbeuse à votre goût – le problème provient du fait que le sucre initialiseur de collection ne fonctionne que lors de l’instanciation de l’instance de la collection (avec le new mot-clé sur le constructeur bien sûr) et malheureusement, C # ne fonctionne pas. déduire le type du constructeur . Maintenant, cela exclut ce que vous essayez de faire, du moins dans une certaine mesure.

Et des syntaxes comme celle-ci

 (m => m.Child) .SomeAddMethod(c => c.FakeProperty) 

ne travaillez pas sans indiquer explicitement ce que le lambda représente réellement même si vous avez la méthode d’extension SomeAddMethod sur Expression> . Je dois dire que ce sont parfois des pita.

Ce qu’il est possible de faire, c’est de minimiser la spécification de type. L’approche la plus courante consiste à créer une classe statique pour laquelle vous ne devez fournir que le type de paramètre formel (dans votre cas T ) et une fois que le type de paramètre formel est connu, le type de retour ( TOut ) est déduit de l’argument Expression> .

Permet de le faire étape par étape. Considérons une hiérarchie de classe un peu plus compliquée:

 public class Person { public Address Address { get; set; } public Name Name { get; set; } public int Age { get; set; } } public class Address { public ssortingng Place { get; set; } public Name ParentName { get; set; } } public class Name { public int Id { get; set; } public ssortingng FirstName { get; set; } public ssortingng Surname { get; set; } } 

Supposons que vous ayez ceci (le plus simple):

 public class Selector : List> { public static SelectorNode Get(Expression> selector) { return new SelectorNode(selector); } } public class SelectorNode : List>, ISelectorNode { internal SelectorNode(Expression> selector) { } } 

Vous pouvez maintenant append tout cela manuellement, mais avec beaucoup moins de saisie de parameters. Quelque chose comme ça:

 var selector = new Selector(); var pA = Selector.Get(m => m.Address); var aS = Selector
.Get(m => m.Place); var aN = Selector
.Get(m => m.ParentName); var nI1 = Selector.Get(m => m.Id); var nS11 = Selector.Get(m => m.FirstName); var nS12 = Selector.Get(m => m.Surname); var pN = Selector.Get(m => m.Name); var nI2 = Selector.Get(m => m.Id); var nS21 = Selector.Get(m => m.FirstName); var nS22 = Selector.Get(m => m.Surname); var pI = Selector.Get(m => m.Age); selector.Add(pA); pA.Add(aS); pA.Add(aN); aN.Add(nI1); aN.Add(nS11); aN.Add(nS12); selector.Add(pN); pN.Add(nI2); pN.Add(nS21); pN.Add(nS22); selector.Add(pI);

Très simple, mais n’est pas aussi intuitif (je préférerais un jour votre syntaxe originale à celle-ci). Peut-être pourrions-nous raccourcir cette période:

 public class Selector : List> { public static SelectorNode Get(Expression> selector) { return new SelectorNode(selector); } public Selector Add(params ISelectorNode[] nodes) { AddRange(nodes); return this; } } public class SelectorNode : List>, ISelectorNode { internal SelectorNode(Expression> selector) { } public ISelectorNode Add(params ISelectorNode[] nodes) { AddRange(nodes); return this; } } 

Maintenant, vous pouvez appeler:

 var selector = new Selector().Add ( Selector.Get(m => m.Address).Add ( Selector
.Get(x => x.Place), Selector
.Get(x => x.ParentName).Add ( Selector.Get(x => x.Id), Selector.Get(x => x.FirstName), Selector.Get(x => x.Surname) ) ), Selector.Get(m => m.Name).Add ( Selector.Get(x => x.Id), Selector.Get(x => x.FirstName), Selector.Get(x => x.Surname) ), Selector.Get(m => m.Age) );

Beaucoup plus propre, mais nous pourrions utiliser la syntaxe d’initialisation de collection pour la rendre légèrement meilleure. Pas besoin de méthode Add(params) dans Selector et vous obtenez:

 public class Selector : List> { public static SelectorNode Get(Expression> selector) { return new SelectorNode(selector); } } var selector = new Selector { Selector.Get(m => m.Address).Add ( Selector
.Get(x => x.Place), Selector
.Get(x => x.ParentName).Add ( Selector.Get(x => x.Id), Selector.Get(x => x.FirstName), Selector.Get(x => x.Surname) ) ), Selector.Get(m => m.Name).Add ( Selector.Get(x => x.Id), Selector.Get(x => x.FirstName), Selector.Get(x => x.Surname) ), Selector.Get(m => m.Age) };

En ayant une autre surcharge Add dans le Selector comme ci-dessous, vous pouvez minimiser la saisie, mais c’est fou:

 public class Selector : List> { public static SelectorNode Get(Expression> selector) { return new SelectorNode(selector); } public void Add(Expression> selector) { var node = new SelectorNode(selector); Add(node); } } var selector = new Selector { Selector.Get(m => m.Address).Add ( Selector
.Get(x => x.Place), Selector
.Get(x => x.ParentName).Add ( Selector.Get(x => x.Id), Selector.Get(x => x.FirstName), Selector.Get(x => x.Surname) ) ), Selector.Get(m => m.Name).Add ( Selector.Get(x => x.Id), Selector.Get(x => x.FirstName), Selector.Get(x => x.Surname) ), m => m.Age // <- the change here };

Cela fonctionne car l'initialiseur de collection peut appeler différentes surcharges Add . Mais je préfère personnellement le style cohérent de l'appel précédent.

Encore plus de gâchis au sucre (initialiseur de collection):

 public class Selector : List> { public void Add(params Selector[] selectors) { Add(this, selectors); } static void Add(List> nodes, Selector[] selectors) { foreach (var selector in selectors) nodes.AddRange(selector); //or just, Array.ForEach(selectors, nodes.AddRange); } public void Add(Expression> selector) { var node = new SelectorNode(selector); Add(node); } //better to have a different name than 'Add' in cases of T == TOut collision - when classes //have properties of its own type, eg Type.BaseType public Selector InnerAdd(params Selector[] selectors) { foreach (SelectorNode node in this) Add(node, selectors); //or just, ForEach(node => Add((SelectorNode)node, selectors)); return this; } } public class SelectorNode : List>, ISelectorNode { internal SelectorNode(Expression> selector) { } } 

Maintenant, appelez comme ça:

 var selector = new Selector { new Selector { m => m.Address }.InnerAdd ( new Selector
{ n => n.Place }, new Selector
{ n => n.ParentName }.InnerAdd ( new Selector { o => o.Id, o => o.FirstName, o => o.Surname } ) ), new Selector { m => m.Name }.InnerAdd ( new Selector { n => n.Id, n => n.FirstName, n => n.Surname } ), m => m.Age };

Est ce que ça aide? Je ne pense pas. Beaucoup geek, mais peu intuitif. Pire encore, pas de sécurité de type inhérente (tout dépend du type que vous fournissez pour l’initialiseur de collection Selector ).

Encore un autre – pas de spécification de type du tout, mais simplement moche 🙂

 static class Selector { //just a mechanism to share code. inline yourself if this is too much abstraction internal static S Add(R list, Expression> selector, Func, S> returner) where R : List> { var node = new SelectorNode(selector); list.Add(node); return returner(node); } } public class Selector : List> { public Selector AddToConcatRest(Expression> selector) { return Selector.Add(this, selector, node => this); } public SelectorNode AddToAddToItsInner(Expression> selector) { return Selector.Add(this, selector, node => node); } } public class SelectorNode : List>, ISelectorNode { internal SelectorNode(Expression> selector) { } public SelectorNode InnerAddToConcatRest(Expression> selector) { return AddToConcatRest(selector); } public SelectorNode InnerAddToAddToItsInnerAgain(Expression> selector) { return AddToAddToItsInner(selector); } //or just 'Concat' ? public SelectorNode AddToConcatRest(Expression> selector) { return Selector.Add(this, selector, node => this); } public SelectorNode AddToAddToItsInner(Expression> selector) { return Selector.Add(this, selector, node => node); } } 

J’ai donné des noms descriptifs à des fonctions pour clarifier l’intention. Je ne vais pas expliquer en détail ce que cela fait individuellement, je suppose que les noms des fonctions seraient suffisants. Aller par l’exemple précédent:

 var selector = new Selector(); var pA = selector.AddToAddToItsInner(m => m.Address); var aN = pA.InnerAddToConcatRest(m => m.Place); var aS = aN.AddToAddToItsInner(m => m.ParentName); var nI1 = aS.InnerAddToConcatRest(m => m.Id); var nS11 = nI1.AddToConcatRest(m => m.FirstName); var nS12 = nS11.AddToConcatRest(m => m.Surname); var pN = selector.AddToAddToItsInner(m => m.Name); var nI2 = pN.InnerAddToConcatRest(m => m.Id); var nS21 = nI2.AddToConcatRest(m => m.FirstName); var nS22 = nS21.AddToConcatRest(m => m.Surname); var pI = selector.AddToConcatRest(m => m.Age); 

ou pour donner une alternative (pour ramener l’idée à la maison):

 var selector = new Selector(); var pA = selector.AddToAddToItsInner(m => m.Address); var aS = pA.InnerAddToConcatRest(m => m.Place); var aN = pA.InnerAddToAddToItsInnerAgain(m => m.ParentName); var nI1 = aN.InnerAddToConcatRest(m => m.Id); var nS11 = nI1.AddToConcatRest(m => m.FirstName); var nS12 = nS11.AddToConcatRest(m => m.Surname); var pN = selector.AddToAddToItsInner(m => m.Name); var nI2 = pN.InnerAddToConcatRest(m => m.Id); var nS21 = nI2.AddToConcatRest(m => m.FirstName); var nS22 = nS21.AddToConcatRest(m => m.Surname); var pI = selector.AddToConcatRest(m => m.Age); 

Maintenant, nous pouvons combiner pour rendre le tout concis et omettre les variables redondantes:

 var selector = new Selector(); selector.AddToConcatRest(m => m.Age).AddToAddToItsInner(m => m.Address) .InnerAddToConcatRest(m => m.Place).AddToAddToItsInner(m => m.ParentName) .InnerAddToConcatRest(m => m.Id).AddToConcatRest(m => m.FirstName).AddToConcatRest(m => m.Surname); selector.AddToAddToItsInner(m => m.Name) .InnerAddToConcatRest(m => m.Id).AddToConcatRest(m => m.FirstName).AddToConcatRest(m => m.Surname); 

Maintenant, vous avez peut-être remarqué que beaucoup de ces fonctions Add effectuent le même travail en interne. J’ai séparé ces méthodes car, du côté de l’appelant, elles ont une sémantique différente à exécuter. Si vous pouvez savoir ce que cela fait / signifie alors le code peut être encore plus court. Remplacez SelectorNode<,> par:

 public class SelectorNode : List>, ISelectorNode { internal SelectorNode(Expression> selector) { } public SelectorNode Add(Expression> selector) { return Selector.Add(this, selector, node => this); } public SelectorNode AddToAddToItsInner(Expression> selector) { return Selector.Add(this, selector, node => node); } } 

Maintenant utilisation:

 var selector = new Selector(); selector.AddToConcatRest(m => m.Age).AddToAddToItsInner(m => m.Address) .Add(m => m.Place).AddToAddToItsInner(m => m.ParentName) .Add(m => m.Id).Add(m => m.FirstName).Add(m => m.Surname); selector.AddToAddToItsInner(m => m.Name) .Add(m => m.Id).Add(m => m.FirstName).Add(m => m.Surname); 

Il y a probablement beaucoup d’autres alternatives possibles, surtout lorsque vous optez pour une combinaison de différentes approches. Dans ce cas particulier de chaînage de méthode, si cela confond l’appelant, une autre possibilité consiste à append à l’aveuglette du côté de l’appelant et à supprimer en interne les doublons. Quelque chose comme ça:

 var selector = new Selector(); selector.Add(m => m.Address).Add(m => m.Place); selector.Add(m => m.Address).Add(m => m.ParentName).Add(m => m.Id); //at this stage discard duplicates selector.Add(m => m.Address).Add(m => m.ParentName).Add(m => m.FirstName); //and so on selector.Add(m => m.Name)... etc selector.Add(m => m.Age); 

Pour cela, vous devrez introduire votre propre comparateur d’égalité pour la classe de nœuds, ce qui le rend extrêmement fragile.

Une autre approche intuitive consiste à enchaîner les propriétés directement dans l’expression. Comme:

 selector.Add(m => m.Address.Place); selector.Add(m => m.Address.ParentName.Id); selector.Add(m => m.Address.ParentName.FirstName); // and so on. 

En interne, vous devrez décomposer l’expression en morceaux et créer votre propre expression à partir de ces informations. Si j’ai le temps, j’y répondrai plus tard.

Une chose que je dois vous demander est pourquoi ne pas utiliser la reflection et vous épargner le souci de fournir des parameters? Vous pouvez utiliser la récursivité pour parcourir des nœuds (propriétés) et créer manuellement des arbres à partir de celui-ci (voir les discussions ci – dessous ). Cela ne vous donnera peut-être pas le genre de flexibilité que vous voudriez peut-être, cependant.

L’expression n’est pas mon fort, alors prenez ceci comme pseudo-code. Vous aurez sûrement plus de travail à faire ici.

 public class Selector : List> { public Selector() { Add(typeof(T), this); } void Add(Type type, List> nodes) { foreach (var property in type.GetProperties()) //with whatever flags { //the second argument is a cool param name I have given, discard-able var paramExpr = Expression.Parameter(type, type.Name[0].ToSsortingng().ToLower()); var propExpr = Expression.Property(paramExpr, property); var innerNode = new SelectorNode(Expression.Lambda(propExpr, paramExpr)); nodes.Add(innerNode); Add(property.PropertyType, innerNode); } } } public class SelectorNode : List>, ISelectorNode { internal SelectorNode(LambdaExpression selector) { } } 

Et utilisation:

 var selector = new Selector(); 

C’est juste ça. Cela produirait les propriétés que vous ne souhaitez peut-être pas, comme les propriétés de types intégrés comme DateTime , ssortingng etc. Ou mieux, vous pouvez créer vos propres règles et les transmettre pour déterminer comment la traversée doit se dérouler.