【问题标题】:Reflection to call generic method with lambda expression parameter使用 lambda 表达式参数调用泛型方法的反射
【发布时间】:2013-07-01 21:53:56
【问题描述】:

我正在寻找一种使用 lambda 表达式调用泛型方法的方法,该 lambda 表达式在项目数组中调用 Contains。

在这种情况下,我使用的是实体框架 Where 方法,但该场景可以应用于其他 IEnumerables。

我需要通过Reflection调用上面代码的最后一行,所以我可以使用任何类型和任何属性来传递给Contains方法。

var context = new TestEntities();

var items = new[] {100, 200, 400, 777}; //IN list (will be tested through Contains)
var type = typeof(MyType); 

context.Set(type).Where(e => items.Contains(e.Id)); //**What is equivalent to this line using Reflection?**

在研究中,我注意到我应该使用 GetMethod、MakeGenericType 和 Expression 来实现这一点,但我不知道该怎么做。拥有此示例将非常有帮助,因此我可以了解反射如何与 Lambda 和通用概念一起使用。

基本上,目标是编写一个正确版本的函数,如下所示:

//Return all items from a IEnumerable(target) that has at least one matching Property(propertyName) 
//with its value contained in a IEnumerable(possibleValues)
static IEnumerable GetFilteredList(IEnumerable target, string propertyName, IEnumerable searchValues)
{
    return target.Where(t => searchValues.Contains(t.propertyName));
    //Known the following:
    //1) This function intentionally can't be compiled
    //2) Where function can't be called directly from an untyped IEnumerable
    //3) t is not actually recognized as a Type, so I can't access its property 
    //4) The property "propertyName" in t should be accessed via Linq.Expressions or Reflection
    //5) Contains function can't be called directly from an untyped IEnumerable
}

//Testing environment
static void Main()
{
    var listOfPerson = new List<Person> { new Person {Id = 3}, new Person {Id = 1}, new Person {Id = 5} };
    var searchIds = new int[] { 1, 2, 3, 4 };

    //Requirement: The function must not be generic like GetFilteredList<Person> or have the target parameter IEnumerable<Person>
    //because the I need to pass different IEnumerable types, not known in compile-time
    var searchResult = GetFilteredList(listOfPerson, "Id", searchIds);

    foreach (var person in searchResult)
        Console.Write(" Found {0}", ((Person) person).Id);

    //Should output Found 3 Found 1
}

我不确定其他问题是否解决了这种情况,因为我认为我无法清楚地理解表达式的工作原理。

更新:

我不能使用泛型,因为我只有在运行时要测试的类型和属性(在包含中)。在第一个代码示例中,假设“MyType”在编译时未知。在第二个代码示例中,类型可以作为参数传递给 GetFilteredList 函数,也可以通过反射 (GetGenericArguments) 获取。

谢谢,

【问题讨论】:

    标签: c# generics reflection lambda where


    【解决方案1】:

    经过广泛的研究和对表达式的大量研究,我可以自己编写一个解决方案。它当然可以改进,但完全符合我的要求。希望它可以帮助其他人。

    //Return all items from a IEnumerable(target) that has at least one matching Property(propertyName) 
    //with its value contained in a IEnumerable(possibleValues)
    static IEnumerable GetFilteredList(IEnumerable target, string propertyName, IEnumerable searchValues)
    {
        //Get target's T 
        var targetType = target.GetType().GetGenericArguments().FirstOrDefault();
        if (targetType == null)
            throw new ArgumentException("Should be IEnumerable<T>", "target");
    
        //Get searchValues's T
        var searchValuesType = searchValues.GetType().GetGenericArguments().FirstOrDefault();
        if (searchValuesType == null)
            throw new ArgumentException("Should be IEnumerable<T>", "searchValues");
    
        //Create a p parameter with the type T of the items in the -> target IEnumerable<T>
        var containsLambdaParameter = Expression.Parameter(targetType, "p");
    
        //Create a property accessor using the property name -> p.#propertyName#
        var property = Expression.Property(containsLambdaParameter, targetType, propertyName);
    
        //Create a constant with the -> IEnumerable<T> searchValues
        var searchValuesAsConstant = Expression.Constant(searchValues, searchValues.GetType());
    
        //Create a method call -> searchValues.Contains(p.Id)
        var containsBody = Expression.Call(typeof(Enumerable), "Contains", new[] { searchValuesType }, searchValuesAsConstant, property);
    
        //Create a lambda expression with the parameter p -> p => searchValues.Contains(p.Id)
        var containsLambda = Expression.Lambda(containsBody, containsLambdaParameter);
    
        //Create a constant with the -> IEnumerable<T> target
        var targetAsConstant = Expression.Constant(target, target.GetType());
    
        //Where(p => searchValues.Contains(p.Id))
        var whereBody = Expression.Call(typeof(Enumerable), "Where", new[] { targetType }, targetAsConstant, containsLambda);
    
        //target.Where(p => searchValues.Contains(p.Id))
        var whereLambda = Expression.Lambda<Func<IEnumerable>>(whereBody).Compile();
    
        return whereLambda.Invoke();
    }
    

    【讨论】:

    • 仅作记录,改进可能是在返回语句中调用 Invoke 而不是 DynamicInvoke。
    • 2 年后,仍然是我发现的动态表达式创建的最佳示例。我要补充一点,如果将类型更改为 IQueryable 而不是 IEnumerable,它会快得多,因为查询不会强制在客户端立即执行,而是传递到数据源(例如,使用 Linq-to-SQL 时由 SQL 服务器执行)
    【解决方案2】:

    为了避免使用泛型(因为在设计时类型未知),您可以使用一些反射并“手动”构建表达式

    您需要通过在一个 Where 子句中定义“包含”表达式来做到这一点:

    public IQueryable GetItemsFromContainsClause(Type type, IEnumerable<string> items)
        {
            IUnitOfWork session = new SandstoneDbContext();
            var method = this.GetType().GetMethod("ContainsExpression");
            method = method.MakeGenericMethod(new[] { type });
    
            var lambda = method.Invoke(null, new object[] { "Codigo", items });
            var dbset = (session as DbContext).Set(type);
            var originalExpression = dbset.AsQueryable().Expression;
    
            var parameter = Expression.Parameter(type, "");
            var callWhere = Expression.Call(typeof(Queryable), "Where", new[] { type }, originalExpression, (Expression)lambda);
            return dbset.AsQueryable().Provider.CreateQuery(callWhere);
    
        }
    
        public static Expression<Func<T, bool>> ContainsExpression<T>(string propertyName, IEnumerable<string> values)
        {
            var parameterExp = Expression.Parameter(typeof(T), "");
            var propertyExp = Expression.Property(parameterExp, propertyName);
            var someValue = Expression.Constant(values, typeof(IEnumerable<string>));
            var containsMethodExp = Expression.Call(typeof(Enumerable), "Contains", new[] { typeof(string) }, someValue, propertyExp);
            return Expression.Lambda<Func<T, bool>>(containsMethodExp, parameterExp);
        }
    

    在这种情况下,“Codigo”是硬编码的,但它可以是获取您定义类型的任何属性的参数。

    您可以使用以下方法对其进行测试:

    public void LambdaConversionBasicWithEmissor()
        {
            var cust= new Customer();
            var items = new List<string>() { "PETR", "VALE" };
            var type = cust.GetType();
            // Here you have your results from the database
            var result = GetItemsFromContainsClause(type, items);
        }
    

    【讨论】:

    • 我使用您的代码(用于 Queryable)来应用实体框架 DbSet 查询的解决方案,即在 Where 子句中使用 DbSet 原始表达式,如 dbset.AsQueryable().Expression,由DbSet QueryableProvider 的 CreateQuery 方法。这样,EF 将在内部生成一个完美的 SQL IN 子句。顺便说一句,您的代码的某些部分(例如 MakeGenericMethod 行)可以转换为纯表达式而不是反射方法。谢谢!
    【解决方案3】:

    您可以通过使用以下一组类来解决您的问题。

    首先,我们需要创建一个 Contains 类,该类将决定从源数组中选择哪些项目。

    class Contains
    {
        public bool Value { get; set; }
    
        public Contains(object[] items, object item)
        {
            Value = (bool)(typeof(Enumerable).GetMethods()
                                             .Where(x => x.Name.Contains("Contains"))
                                             .First()
                                             .MakeGenericMethod(typeof(object))
                                             .Invoke(items, new object[] { items, item }));
        }
    }
    

    然后我们需要创建一个 Where 类,该类将用于根据将选择的项目来形成谓词。应该清楚的是,在我们的例子中,我们将使用 Contains 类作为我们的谓词方法。

    class Where
    {
        public object Value { get; set; }
    
        public Where(object[] items, object[] items2)
        {
            Value = typeof(Enumerable).GetMethods()
                                      .Where(x => x.Name.Contains("Where"))
                                      .First()
                                      .MakeGenericMethod(typeof(object))
                                      .Invoke(items2, new object[] { items2, new Func<object, bool>(i => new Contains(items, i).Value) });
        }
    }
    

    最后一步只是调用我们从 Where 类中得到的结果,它实际上是 Enumerable.WhereArrayIterator 类型而不是 List 类型,因为 Where Extension 方法的结果是延迟执行的产物。

    因此我们需要创建一个非延迟对象,通过调用它的 ToList 扩展方法,并得到我们的结果。

    class ToList
    {
        public List<object> Value { get; set; }
    
        public ToList(object[] items, object[] items2)
        {
            var where = new Where(items, items2).Value;
    
            Value = (typeof(Enumerable).GetMethods()
                                      .Where(x => x.Name.Contains("ToList"))
                                      .First()
                                      .MakeGenericMethod(typeof(object))
                                      .Invoke(where, new object[] { where })) as List<object>;
        }
    }
    

    最后,您可以使用以下类简单地测试整个过程。

    class Program
    {
        static void Main()
        {
            var items = new object[] { 1, 2, 3, 4 };
            var items2 = new object[] { 2, 3, 4, 5 };
    
            new ToList(items, items2).Value.ForEach(x => Console.WriteLine(x));
    
            Console.Read();
        }
    }
    

    【讨论】:

    • Mario,感谢您抽出宝贵时间,但您使用泛型的解决方案不符合我的要求,因为我只有要在运行时测试的类型和属性。我会更新问题并尝试更好地解释。
    • 我已经更新了答案,这样您就不必使用泛型类型了。相反,您将使用对象类型的数组来存储您的项目。
    • 它仅适用于 int 等基本类型。我的案例使用引用类型,需要使用包含评估的属性。例如,我需要针对 int Id 列表测试“Person”列表并返回具有以下任何 Id 的所有“Person”:var arrayOfIds = new[] { 1, 2, 3, 4 }; //This line doesn't work because Person is not known in compile time, so I can't access the "Id" Property return listOfObjects.Where(p =&gt; arrayOfIds.Contains(p.Id));
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-04-05
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多