Reflection for calling a generic method with lambda expression parameter

I am looking for a way to call a generic method with a lambda expression that calls Contains in an array of elements.

In this case, I use the Entity Framework Where method, but the script can be applied to other IEnumerables.

I need to call the last line of the above code through Reflection, so I can use any type and any property to pass the Contains method.

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?** 

In the study, I noticed that for this I should use GetMethod, MakeGenericType and Expression, but I could not figure out how to do this. It would be very helpful to have this sample so that I can understand how Reflection works with the concepts of Lambda and Generic.

Basically, the goal is to write the correct version of such a function:

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

I’m not sure that other questions relate to this scenario, because I don’t think I can clearly understand how expressions work.

Update:

I cannot use Generics because I only have the type and property to test (in Contains) at runtime. In the first code example, suppose that "MyType" is not known at compile time. In the second code example, the type can be passed as a parameter to the GetFilteredList function or can be obtained through Reflection (GetGenericArguments).

Thanks,

+5
source share
3 answers

After extensive research and extensive study of the expressions, I could write the solution myself. This can certainly be improved, but it fits my requirements exactly. Hope this can help someone else.

 //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 T var targetType = target.GetType().GetGenericArguments().FirstOrDefault(); if (targetType == null) throw new ArgumentException("Should be IEnumerable<T>", "target"); //Get searchValues T var searchValuesType = searchValues.GetType().GetGenericArguments().FirstOrDefault(); if (searchValuesType == null) throw new ArgumentException("Should be IEnumerable<T>", "searchValues"); //Create ap 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(); } 
+9
source

To avoid using generics (since the types are not known at design time), you can use some reflection and construct the expression “manually”

You will need to do this by specifying the expression “Contains” inside one Where clause:

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

In this case, "Codigo" is hardcoded, but it can be a parameter to get any property of the type that you define.

You can check it using:

 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); } 
+4
source

You can solve your problem using the following set of classes.

First, we need to create the Contains class, which will determine which elements will be selected from the original array.

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

Then we need to create the Where class, which will be used to generate the predicate based on which elements will be selected. It should be clear that in our case we will use the Contains class for our predicate method.

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

The last step is to simply call the result obtained from the Where class, which is actually of type Enumerable.WhereArrayIterator, and not of type List, because the result of the Where Extension method is the result of deferred execution.

Thus, we need to create an object not deferred by calling its ToList extension method and get our result.

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

In the end, you can simply test the whole process using the following class.

 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(); } } 
0
source

Source: https://habr.com/ru/post/975157/


All Articles