简体   繁体   中英

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,

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

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

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

First, we need to create a Contains class which will decide which items will be chosen from the source 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 a Where class which will be used to form a predicate based on which items will be selected. It should be clear that in our case, we are going to 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 simply to invoke the result we got from the Where class, which is actually of type Enumerable.WhereArrayIterator and not of type List, since the result of the Where Extension method is a product of deferred execution.

Thus we need to create a non deferred object, 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 out by 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();
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM