Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Reflection to call generic method with lambda expression parameter

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

In this case I'm using Entity Framework Where method, but the scenario could be applied in 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 to 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 research, I've noticed that I should use GetMethod, MakeGenericType and Expression to achieve that, but I couldn't figure out how to do it. It would be very helpful to have this sample so I can understand how Reflection works with Lambda and Generic concepts.

Basically the objective is to write a correct version of a function like this:

//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 if the other questions address this scenario, because I don't think I could clearly understand how Expressions work.

Update:

I can't use Generics because I only have the type and the property to be tested (in Contains) at run-time. In the first code sample, suppose "MyType" is not known at compile time. In the second code sample, the type could be passed as a parameter to the GetFilteredList function or could be get via Reflection (GetGenericArguments).

Thanks,

like image 344
natenho Avatar asked Jul 01 '13 21:07

natenho


People also ask

How do I use reflection to call a generic method?

The first step to dynamically invoking a generic method with reflection is to use reflection to get access to the MethodInfo of the generic method. To do that simply do this: var methodInfo = typeof(ClassWithGenericMethod). GetMethod("MethodName");

How do you call a method of a generic type?

You need to use reflection to get the method to start with, then "construct" it by supplying type arguments with MakeGenericMethod: MethodInfo method = typeof(Sample). GetMethod(nameof(Sample. GenericMethod)); MethodInfo generic = method.


2 Answers

After a wide research and a lot of study of Expressions I could write a solution myself. It certainly can be improved, but exactly fits my requirements. Hopefully it 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'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();
}
like image 144
natenho Avatar answered Oct 02 '22 16:10

natenho


In order to avoid using generics (since the types are not known at design time) you could use some reflection and build the expression "by hand"

You would need to do this by defining a "Contains" expression 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 hard-coded, but it could be a parameter to get any property of the type you define.

You could test it by 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);
    }
like image 26
Gabriel Vonlanten C. Lopes Avatar answered Oct 02 '22 17:10

Gabriel Vonlanten C. Lopes