将动态创建的 lambda 应用于对象实例

Apply a dynamically created lambda to an object instance

我有一些代码可以从字符串开始动态创建一个 lambda。例如,我有一个过滤器 class,如下所示:

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

而且我能够从像这样的 Criteria 实例开始创建像 x => x.Name == "Foo" 这样的 lambda

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

假设有一个class喜欢

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

我愿意:

  1. 将 lambda 应用于任何对象(我知道应该使用 Receipt 的 ParameterExpression 创建 lambda class)
  2. 取回 lambda 的布尔结果(例如名称等于 Foo 吗?)
  3. 将相同的逻辑应用于集合 Count() 方法(例如构建一个 lambda 来检查 receipt.Details.Count()

这可能吗?

编辑:根据评论,我正在详细说明我的需求。这段代码将让我有机会回答我的要求:如果为我的对象指定了规则,那么应用程序的行为应该有所不同。虽然这是一个常见的要求,但我想创建一个代码,允许我在添加更多规则时扩展它。其实我只有5种规则类型:

对于前 4 点,我已经能够使用 中的代码动态创建 lambda 表达式,我可以使用规范模式将其应用于对象本身。

最后一点需要以几乎相同的方式实现,但唯一的区别是表达式的左侧部分是方法调用而不是 属性(特别是 ICollection<T>.Count() 方法)

您可以对泛型使用反射。解决问题的一种方法可能是扩展

public static class EnumerableExtensions
{
    public static IEnumerable<T> Where<T>(this IEnumerable<T> source, Criteria c)
    {
        var sourceType = typeof(T);
        var propertyMember = sourceType.GetProperty(c.Property);
        Func<string, bool> predicate = null;
        switch (c.Operator)
        {
            case "equal":
                predicate = (v) => v == c.Value;
                break;
            // other operators
            default:
                throw new ArgumentException("Unsupported operator.");
        }
        return source.Where(v => predicate((string)propertyMember.GetMethod.Invoke(v, null)));
    }
}

您可以在代码中使用:

    void FooBar()
    {
        Criteria c = new Criteria()
        {
            Property = "Name",
            Operator = "equal",
            Value = "foo"
        };

        var source = new Receipt[2];
        source[0] = new Receipt { Name = "foo", Amount = 1 };
        source[1] = new Receipt { Name = "bar", Amount = 2 };

        var result = source.Where(c);
    }

这只是给你一个想法。改进将是错误处理(属性 未找到、无效转换、空值等)、重构以启用单元测试(例如注入 select "strategy")和性能(例如构建、编译和缓存表达式树而不是反射)。这应该给你足够的关键字来了解。希望这有帮助。

这里有一些可以帮助您入门的东西。有很大的改进空间。尤其是丑陋的工厂。该演示旨在展示如何使用表达式来解决问题,而不是作为最佳实践或工厂模式演示。如果有任何不清楚的地方,请随时要求澄清。

用法

    [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));
    }

工厂

 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;
        }
    }

条件

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

切换到 ICriteria,事情会变得更干净。一个更好的工厂,不需要 ToString。编程到接口。

综上所述,这段代码感觉有点古怪。从字符串生成函数有什么意义?我感觉这正朝着从语法生成 C# 的方向发展。我不相信这会很好地扩展。对于非平凡的实现,请考虑 lex/yacc first. You can find more details for doing this in the Pragmatic Programmer "Implementing a mini language".

你的问题很有趣,我想了解一下要求。我创建了一个演示,我想知道该演示与您想要完成的有何不同?这里也有一个工作版本https://dotnetfiddle.net/AEBZ1w

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;
}

参考资料