Appliquer un lambda créé dynamiquement à une instance d'objet

c# expression expression-trees lambda

Question

J'ai un code qui crée dynamiquement un lambda à partir de chaînes. Par exemple, j'ai une classe de filtre comme celle-ci:

public class Criteria {
    public string Property { get; set; }
    public string Operator { get; set; }
    public string Value { get; set; }
}

Et je peux créer un lambda comme x => x.Name == "Foo" partir d'une instance de critère comme celle-ci.

Criteria c = new Criteria() {
    Property = "Name",
    Operator = "equal",
    Value = "Foo"
}

Supposer avoir une classe comme

public class Receipt {
    public string Name { get; set; }
    public int Amount { get; set; }
    [other props omitted]
    public ICollection<ReceiptDetail> Details { get; set; }
}

J'aimerais:

  1. Appliquez le lambda à n'importe quel objet (je sais que le lambda doit être créé avec un ParameterExpression de la classe Receipt)
  2. Récupérer le résultat booléen du lambda (par exemple, le nom est-il égal à Foo?)
  3. Appliquez la même logique à la méthode collections Count () (par exemple, Création d’un lambda qui vérifie la réception.

Est-ce possible?

EDIT : Selon les commentaires, je développe un peu plus mes besoins. Ce code me donnera l'occasion de répondre à une exigence que j'ai et qui dit: Si une règle est spécifiée pour mon objet, l'application devrait se comporter un peu différemment. Bien qu’il s’agisse d’une exigence courante, j’aimerais créer un code qui me permettra de l’étendre à mesure que de nouvelles règles seront ajoutées. En fait, je n'ai que 5 types de règles:

  • Vérifier si l'entrée intervient un jour spécifique de la semaine
  • Vérifier si l'entrée est dans une plage de temps spécifique
  • Vérifier si le champ "X" de l'entrée est inférieur / égal / supérieur à une valeur
  • Vérifier si le champ "Y" de l'entrée contient une valeur
  • Vérifier si le champ "Z" de l'entrée, qui est une collection, a un compte inférieur / égal / supérieur à une valeur

Pour les 4 premiers points, j'ai pu créer de manière dynamique une expression lambda, avec un code similaire à celui de la réponse P.Brian.Mackey , que je pourrais appliquer, à l'aide du modèle Specification, à l'objet lui-même.

Le dernier point doit être implémenté presque de la même manière mais la seule différence est que la partie gauche de l'expression était un appel de méthode et non une propriété (en particulier la méthode ICollection<T>.Count() )

Réponse acceptée

Voici quelque chose pour vous aider à démarrer. Il y a beaucoup de place à l'amélioration. Surtout la laide usine. Cette démonstration a pour but de montrer comment utiliser Expressions pour résoudre les problèmes, et non comme une démonstration des meilleures pratiques ou des modèles d’usine. Si quelque chose n'est pas clair, n'hésitez pas à demander des éclaircissements.

Usage

    [Test]
    public void ApplySameLogicToCollectionsCount()
    {
        var receipt = new Receipt();
        var details = new ReceiptDetail();
        var details2 = new ReceiptDetail();
        receipt.Details.Add(details);
        receipt.Details.Add(details2);
        var result = LambdaGeneratorFactory<ICollection<ReceiptDetail>>.Run(detailsCount);
        Assert.IsTrue(result(receipt.Details));
    }

Usine

 public static class LambdaGeneratorFactory<T>
    {
        //This is an ugly implementation of a Factory pattern.
        //You should improve this possibly with interfaces, maybe abstract factory.  I'd start with an ICriteria.
        public static Predicate<T> Run(Criteria criteria)
        {
            if (typeof(T) == typeof (Receipt))
            {
                return CreateLambda(criteria);
            }
            else if (typeof (T) == typeof (ICollection<ReceiptDetail>))
            {
                return CreateLambdaWithCount(criteria);
            }

            return null;
        }
        private static Predicate<T> CreateLambda(Criteria criteria)
        {
            ParameterExpression pe = Expression.Parameter(typeof(T), "i");

            Expression left = Expression.Property(pe, typeof(T).GetProperty(criteria.Property));
            Expression right = Expression.Constant(criteria.Value);

            Expression predicateBody = Expression.Equal(left, right);

            var predicate = Expression.Lambda<Predicate<T>>(predicateBody, new ParameterExpression[] { pe }).Compile();

            return predicate;
        }

        private static Predicate<T> CreateLambdaWithCount(Criteria criteria)
        {
            ParameterExpression pe = Expression.Parameter(typeof(T), "i");

            Expression count = Expression.Property(pe, typeof(T).GetProperty("Count"));
            Expression left = Expression.Call(count, typeof(Object).GetMethod("ToString"));
            Expression right = Expression.Constant(criteria.Value);

            Expression predicateBody = Expression.Equal(left, right);

            var predicate = Expression.Lambda<Predicate<T>>(predicateBody, new ParameterExpression[] { pe }).Compile();

            return predicate;
        }
    }

Critères

    private Criteria detailsCount = new Criteria()
    {
        Property = "Details",
        Operator = "equal",
        Value = "2"
    };

Basculez vers les ICriteria et les choses seront plus propres. Une meilleure usine et pas besoin de ToString . Programmez une interface.

Cela étant dit, ce code est un peu funky. Quel est l'intérêt de générer des fonctions à partir de chaînes? J'ai l'impression que cela va générer du C # à partir d'une grammaire. Je ne suis pas convaincu que cela évoluera bien. Pour les implémentations non triviales, considérez d'abord Lex / Yacc . Vous trouverez plus de détails à ce sujet dans le Programmeur pragmatique "Implémentation d'un mini langage".


Réponse populaire

Votre question est fascinante et j'aimerais comprendre les exigences. J'ai créé une démo et je me demande en quoi cette démo diffère de ce que vous essayez d'accomplir. Il existe une version de travail ici https://dotnetfiddle.net/AEBZ1w aussi.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

public class Program
{
    public static void Main()
    {
        Criteria c = new Criteria() { 
            Property = "Name", 
            Operator = "==", 
            Value = "Foo" };

        var queryable = (new List<Receipt>() { 
            new Receipt { Name = "Foo", Amount = 1 },
            new Receipt { Name = "Foo", Amount = 2 }, 
            new Receipt { Name = "Bar" }  
        }).AsQueryable();

        var parameter = Expression.Parameter(typeof(Receipt), "x");
        var property = Expression.Property(parameter, typeof(Receipt).GetProperty(c.Property));
        var constant = Expression.Constant(c.Value);
        var operation = Expression.Equal(property, constant);
        var expression = Expression.Call(
            typeof(Queryable),
            "Where",
            new Type[] { queryable.ElementType },
            queryable.Expression, 
            Expression.Lambda<Func<Receipt, bool>>(operation, new ParameterExpression[] { parameter })
        );

        Console.WriteLine("Linq Expression: {0} \n", expression.ToString());
        Console.WriteLine("Results: \n");

        var results = queryable.Provider.CreateQuery<Receipt>(expression);
        foreach(var r in results)
        {
            Console.WriteLine("{0}:{1}", r.Name, r.Amount);
        }
    }
}

public class Criteria
{
    public string Property, Operator, Value;
}

public class ReceiptDetail
{
    public string ItemName;
}

public class Receipt
{
    public string Name { get; set; }
    public int Amount;
    public ICollection<ReceiptDetail> Details;
}

Les références



Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow