2013-07-01 12 views
5

Sto cercando un modo per chiamare un metodo generico con un'espressione lambda che chiama Contains in una matrice di elementi.Reflection per chiamare il metodo generico con parametro espressione lambda

In questo caso sto utilizzando il metodo Entity Framework Where, ma lo scenario potrebbe essere applicato in altri oggetti IEnumer.

Devo chiamare l'ultima riga del codice sopra tramite Reflection, così posso usare qualsiasi tipo e qualsiasi proprietà per passare al metodo 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?** 

Nella ricerca, ho notato che dovrei usare GetMethod, MakeGenericType ed Expression per conseguire tale, ma non riuscivo a capire come farlo. Sarebbe molto utile avere questo esempio così posso capire come funziona Reflection con i concetti Lambda e Generico.

In sostanza l'obiettivo è quello di scrivere una versione corretta di una funzione come questa:

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

Non sono sicuro se le altre domande affrontare questo scenario, perché non credo che avrei potuto chiaramente capire come Le espressioni funzionano.

Aggiornamento:

non posso usare Generics perché ho solo il tipo e la proprietà da testare (in Contiene) in fase di esecuzione. Nel primo esempio di codice, supponiamo che "MyType" non sia noto al momento della compilazione. Nel secondo esempio di codice, il tipo potrebbe essere passato come parametro alla funzione GetFilteredList o potrebbe essere ottenuto tramite Reflection (GetGenericArguments).

Grazie,

risposta

9

Dopo un'ampia ricerca e un sacco di studio delle espressioni, potrei scrivere una soluzione da solo. Certamente può essere migliorato, ma si adatta perfettamente alle mie esigenze. Spero che possa aiutare qualcun altro.

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

Solo per la cronaca, un miglioramento potrebbe essere un modo per chiamare Invoke anziché DynamicInvoke nell'istruzione return. – natenho

+0

sei un eroe! :) – AmmarCSE

+0

2 anni dopo, è ancora il miglior esempio di creazione di espressioni dinamiche che ho trovato. Aggiungerò che è molto più veloce se si cambia il tipo in 'IQueryable' piuttosto che in' IEnumerable', poiché la query non è forzata da eseguire immediatamente sul lato client e viene invece trasferito all'origine dati (ad esempio eseguito dal server SQL quando si utilizza Linq-to-SQL) –

0

è possibile risolvere il problema utilizzando il seguente insieme di classi.

Per prima cosa, dobbiamo creare una classe Contains che decida quali elementi verranno scelti dall'array sorgente.

Quindi è necessario creare una classe Where che verrà utilizzata per formare un predicato in base a quali elementi verranno selezionati. Dovrebbe essere chiaro che nel nostro caso, useremo la classe Contains per il nostro metodo di predicato.

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

L'ultimo passo è semplicemente quello di richiamare il risultato che abbiamo ottenuto dal dove la classe, che in realtà è di tipo Enumerable.WhereArrayIterator e non di tipo List, dal momento che il risultato del metodo di estensione Dove è un prodotto di differita esecuzione.

Pertanto, è necessario creare un oggetto non differito, chiamando il suo metodo di estensione dell'elenco indirizzi e ottenere i nostri risultati.

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

Alla fine, è possibile semplicemente testare l'intero processo utilizzando la seguente classe.

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, grazie per il vostro tempo, ma la soluzione utilizzando Generics non si adatta la mia richiesta, perché ho solo il tipo e la proprietà di essere testati in fase di esecuzione. Aggiornerò la domanda e cercherò di spiegare meglio – natenho

+0

Ho aggiornato la risposta in modo da non dover utilizzare tipi generici. Invece, userete matrici di tipo oggetto per memorizzare i vostri oggetti. –

+0

funziona solo per tipi di base come int. Il mio caso utilizza i tipi di riferimento, che richiedono una proprietà da valutare con contiene. Per esempio, avrei bisogno di testare un elenco di "Person" contro un elenco di Id int e restituire tutti "Persona" che ha uno di questi Id: 'var arrayOfIds = new [] {1, 2, 3, 4 }; // Questa riga non funziona perché la persona non è nota in fase di compilazione, quindi non posso accedere alla proprietà "Id" elenco restituitoOfObjects.Where (p => arrayOfIds.Contains (p.Id)); ' – natenho

4

Al fine di evitare l'uso di farmaci generici (dal momento che i tipi non sono noti in fase di progettazione) si potrebbe usare un po 'di riflessione e costruire l'espressione "a mano"

si avrebbe bisogno di fare questo definendo un "Contiene" espressione all'interno di una clausola 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); 
    } 

In questo caso "Codigo" è hard-coded, ma potrebbe essere un parametro di ottenere qualsiasi proprietà del tipo definito.

Si potrebbe prove con il programma:

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

Ho utilizzato il codice (per Queryable) per applicare la soluzione per la query DbSet di Entity Framework, cioè utilizzare l'espressione originale DbSet come dbset.AsQueryable(). Espressione nella clausola Where, utilizzata dal metodo CreateQuery di DbSet QueryableProvider. In questo modo, EF genererà internamente una clausola SQL IN perfetta. BTW, alcune parti del codice (ad esempio la linea MakeGenericMethod) potrebbero essere convertite in pura espressione anziché nell'approccio di Reflection. Grazie! – natenho

Problemi correlati