【问题标题】:Apply a dynamically created lambda to an object instance将动态创建的 lambda 应用到对象实例
【发布时间】:2015-03-14 00:26:47
【问题描述】:

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

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

假设有一个类

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

我想:

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

这可能吗?

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

  • 验证输入是否来自一周中的特定日期
  • 验证输入是否在特定时间范围内
  • 验证输入的字段“X”是否小于/等于/大于某个值
  • 验证输入的“Y”字段是否包含值
  • 验证作为集合的输入字段“Z”的计数是否小于/等于/大于某个值

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

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

【问题讨论】:

  • @ShaunLuttin:很抱歉,我认为建议的链接可以解决我的问题
  • 可能是的。不平凡。
  • "我可以从这样的 Criteria 实例开始创建一个像 x => x.Name == "Foo" 这样的 lambda..." 你创建的 lambda 只是一个字符串吗?
  • @ShaunLuttin:不是。是Expression&lt;Func&lt;T, bool&gt;&gt;的实例
  • 这可以用动态 linq weblogs.asp.net/scottgu/… 完成吗?

标签: c# lambda expression expression-trees


【解决方案1】:

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

用法

    [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。您可以在Pragmatic Programmer“实现一种迷你语言”中找到更多详细信息。

【讨论】:

  • 非常感谢您的回答。我已经编辑了我的问题,以便您更好地了解问题。我不会创建一个语法,我完全同意你的看法,需要更好的方法(比如你提供的 lex/yacc 参考)。
  • 还有一件事。我正在尝试调用方法Count() 而不仅仅是属性Count
  • @Lorenzo 这就是 Brian 在说它不能很好地扩展时所指的内容。 “方法”.Count() 比乍一看要复杂得多。你的意思是List.Count() 还是Enumerable.Count(this IEnumerable)?您的工厂代码如何知道在System.Linq.Enumerable 中查找?在这一点上,我们正在迅速进入完整的 .net 语言规范领域。既然如此,为什么不直接使用已经存在的众多 .net 语言中的一种呢?
  • @Aron:不幸的是,起初我并没有完全理解 Brian 回答所指的隐藏警告。我没有意识到实际上有不止一种 Count() 方法。我想使用Enumerable.Count( this IEnumerable ),但实际上并不像我想象的那么容易。我想我会按照布赖恩的建议切换到该物业。非常感谢!
【解决方案2】:

您的问题很有趣,我想了解这些要求。我创建了一个演示,我想知道,该演示与您尝试完成的内容有何不同? 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;
}

参考文献

【讨论】:

  • 首先感谢您的回答。实际上它与我的实际实现并没有太大区别,除了事实上,关于问题的前两点,我试图使用表达式来“验证”一个与使用表达式有点不同的 Receipt 对象“选择”收据类型的对象。但是,我已经能够通过使用像expression.Compile()(receipt) 这样的表达式来解决这个问题,它返回一个布尔值,表示receipt 实例是否满足表达式。然而,第三个问题有点复杂,Brian 明白了
【解决方案3】:

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

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

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

【讨论】:

  • 感谢您的回答,但这并不是我想要的。我已经能够将构造的 lambda 传递给 Where 方法并取回结果。我想做的是针对单个 Receipt 实例执行 lambda 并取回真/假结果。
  • 你能举个例子,如果编译器允许,代码会是什么样子?
  • 对不起,我不确定我是否理解了你的问题。不过,我已经为这个问题添加了一些细节。
猜你喜欢
  • 1970-01-01
  • 2013-10-20
  • 1970-01-01
  • 2016-01-29
  • 2017-09-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多