2013-07-01 12 views
5

Szukam sposobu wywoływania ogólnej metody z wyrażeniem lambda, które wywołuje Contains w tablicy elementów.Odbicie, aby wywołać metodę ogólną z parametrem wyrażenia lambda

W tym przypadku używam metody Entity Framework Where, ale scenariusz można zastosować w innych IEnumerables.

Muszę zadzwonić do ostatniej linii powyższego kodu przez Reflection, więc mogę użyć dowolnego typu i dowolnej właściwości, aby przejść do metody 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?** 

W badaniach, zauważyłem, że powinienem użyć getMethod, MakeGenericType i ekspresja, aby osiągnąć to, ale nie mogę dowiedzieć się, jak to zrobić. Byłoby bardzo pomocne mieć ten przykład, aby zrozumieć, jak Reflection działa z koncepcjami Lambda i Generic.

Zasadniczo celem jest napisać poprawną wersję funkcji takiego:

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

Nie jestem pewien, czy inne kwestie rozwiązać ten scenariusz, ponieważ nie sądzę, mogę jasno zrozumieć, w jaki sposób Wyrażenia działają.

Aktualizacja:

Nie mogę korzystać Generics bo tylko rodzaj i właściwości mają zostać zbadane (w Zawiera) w czasie wykonywania. W pierwszym przykładzie kodu, załóżmy, że "MyType" nie jest znany podczas kompilacji. W drugim przykładzie kodu typ mógł zostać przekazany jako parametr do funkcji GetFilteredList lub mógł zostać uzyskany za pomocą Reflection (GetGenericArguments).

Dzięki,

Odpowiedz

9

Po wielu badań i wielu badań ekspresji mógłbym napisać rozwiązanie siebie. Z pewnością można go poprawić, ale dokładnie odpowiada moim wymaganiom. Mam nadzieję, że pomoże to komuś innemu.

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

Dla pewności, poprawa może być sposobem wywołania Invoke zamiast DynamicInvoke w instrukcji return. – natenho

+0

jesteś bohaterem! :) – AmmarCSE

+0

Dwa lata później, wciąż jest to najlepszy przykład dynamicznego tworzenia ekspresji, jaki znalazłem. Dodam, że jest o wiele szybszy, jeśli zmienisz typ na 'IQueryable' zamiast' IEnumerable', ponieważ zapytanie nie jest wymuszone wykonać natychmiast po stronie klienta i zamiast tego jest przekazywane do źródła danych (np. wykonywane przez serwer SQL przy użyciu Linq-SQL) –

0

Możesz rozwiązać swój problem, korzystając z poniższego zestawu klas.

Najpierw musimy utworzyć klasę Contains, która zdecyduje, które elementy zostaną wybrane z tablicy źródłowej.

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

Następnie musimy utworzyć klasę Where, która posłuży do utworzenia predykatu na podstawie wybranych elementów. Powinno być jasne, że w naszym przypadku użyjemy klasy Contains dla naszej metody predykatów.

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

Ostatnim krokiem jest po prostu powołać wynik dostaliśmy od Where klasy, która jest faktycznie typu Enumerable.WhereArrayIterator a nie od typu listy, ponieważ wskutek Gdzie metoda rozszerzenie jest produktem odroczony wykonanie.

W ten sposób musimy stworzyć obiekt nieodroczony, wywołując jego metodę rozszerzenia ToList i uzyskać nasz wynik.

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

Na koniec można po prostu przetestować cały proces, korzystając z poniższej klasy.

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

Mario, dzięki za poświęcony czas, ale rozwiązanie z użyciem rodzajowych nie pasuje do mojej wymagania, bo tylko rodzaj i właściwość do badania w czasie wykonywania. Zaktualizuję pytanie i spróbuję wyjaśnić lepiej. – natenho

+0

Zaktualizowałem odpowiedź, aby nie trzeba było używać typów ogólnych. Zamiast tego użyjesz tablic typu obiekt do przechowywania twoich przedmiotów. –

+0

działa tylko dla typów bazowych, takich jak int. Mój przypadek używa typów referencji, które wymagają oceny właściwości za pomocą zawiera. Na przykład, musiałbym przetestować listę "Person" na liście identyfikatorów int i zwrócić całą "Person", która ma którykolwiek z tych ID: 'var arrayOfIds = new [] {1, 2, 3, 4 }; // Ta linia nie działa, ponieważ osoba nie jest znana w czasie kompilacji, więc nie mogę uzyskać dostępu do właściwości "Id" return listOfObjects.Where (p => arrayOfIds.Contains (p.Id)); ' – natenho

4

W celu uniknięcia przy użyciu rodzajowych (od typy nie są znane w czasie projektowania) można korzystać z niektórych refleksji i zbudować wyrażenie „ręcznie”

Trzeba by zrobić to od zdefiniowania „Zawiera” wyrażenie wewnątrz jednego 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); 
    } 

W tym przypadku „Codigo” jest zakodowane, ale może to być dowolny parametr, aby uzyskać właściwość typu zdefiniowanego.

Można go przetestować za pomocą:

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

Użyłem twojego kodu (dla Queryable), aby zastosować rozwiązanie dla zapytania Entity Framework DbSet, to jest użycie oryginalnego wyrażenia DbSet, takiego jak dbset.AsQueryable(). Wyrażenie w klauzuli Where, używane przez metodę CreateQuery w DbSet QueryableProvider. W ten sposób EF wewnętrznie wygeneruje idealną klauzulę SQL IN. BTW, niektóre części kodu (np. Linia MakeGenericMethod) mogą zostać przekonwertowane do czystego wyrażenia zamiast podejścia Odbicie. Dziękujemy! – natenho

Powiązane problemy