2013-07-01 17 views
5

Ich suche nach einer Möglichkeit, eine generische Methode mit einem Lambda-Ausdruck aufzurufen, der Contains in einem Array von Elementen aufruft.Reflection zum Aufrufen einer generischen Methode mit Lambda-Ausdrucksparameter

In diesem Fall verwende ich Entity Framework Where-Methode, aber das Szenario könnte in anderen IEnumerables angewendet werden.

Ich muss die letzte Zeile des obigen Codes durch Reflection aufrufen, damit ich jeden Typ und jede Eigenschaft verwenden kann, um die Contains-Methode zu übergeben.

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 der Forschung habe ich bemerkt, dass ich GetMethod, Makegenerictype und Expression verwenden sollte, um das zu erreichen, aber ich kann nicht herausfinden, wie es zu tun. Es wäre sehr hilfreich, dieses Beispiel zu haben, damit ich nachvollziehen kann, wie Reflection mit Lambda und generischen Konzepten arbeitet.

Grundsätzlich ist das Ziel, eine korrekte Version einer Funktion wie folgt zu schreiben:

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

Ich bin nicht sicher, ob die anderen Fragen, die dieses Szenario befassen, weil ich mich eindeutig nicht denken konnte, zu verstehen, wie Ausdrücke funktionieren.

Update:

Ich kann nicht Generics benutzen, weil ich nur die Art und die Eigenschaft haben, getestet werden (in Enthält) zur Laufzeit. Nehmen Sie im ersten Codebeispiel an, dass "MyType" zur Kompilierungszeit nicht bekannt ist. Im zweiten Codebeispiel könnte der Typ als Parameter an die GetFilteredList-Funktion übergeben werden oder über Reflection (GetGenericArguments) abgerufen werden.

Danke,

Antwort

9

Nach einer breiten Forschung und viel Studium der Ausdrücke konnte ich selbst eine Lösung schreiben. Es kann sicherlich verbessert werden, passt aber genau zu meinen Anforderungen. Hoffentlich kann es jemand anderem helfen.

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

Nur zur Erinnerung, eine Verbesserung könnte eine Möglichkeit sein, Invoke anstelle von DynamicInvoke in der return-Anweisung aufzurufen. – natenho

+0

Sie sind ein Held! :) – AmmarCSE

+0

2 Jahre später, immer noch das beste Beispiel für die Erstellung dynamischer Ausdrücke Ich habe hinzugefügt, dass es viel schneller ist, wenn Sie den Typ zu "IQueryable" statt "IEnumerable" ändern, da die Abfrage nicht erzwungen wird sofort auf der Client-Seite auszuführen und stattdessen an die Datenquelle weitergegeben wird (z. B. vom SQL-Server ausgeführt, wenn Sie Linq-to-SQL verwenden) –

0

Sie Ihr Problem mit dem folgenden Satz von Klassen lösen.

Zunächst müssen wir eine Contains-Klasse erstellen, die entscheidet, welche Elemente aus dem Quell-Array ausgewählt werden.

Dann müssen wir eine Where-Klasse erstellen, die zur Bildung eines Prädikats verwendet wird, basierend darauf, welche Elemente ausgewählt werden. Es sollte klar sein, dass wir in unserem Fall die Contains-Klasse für unsere Prädikatmethode verwenden werden.

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

Der letzte Schritt ist einfach das Ergebnis, das wir von der Wo-Klasse bekam aufrufen, die eigentlich vom Typ Enumerable.WhereArrayIterator ist und nicht vom Typ Liste, da das Ergebnis der Where-Extension-Methode ein Produkt von latenten ist Ausführung.

Daher müssen wir ein nicht zurückgestelltes Objekt erstellen, indem wir die ToList-Erweiterungsmethode aufrufen und unser Ergebnis erhalten.

Am Ende können Sie einfach den gesamten Prozess testen, indem Sie die folgende Klasse verwenden.

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, vielen Dank für Ihre Zeit, aber Ihre Lösung unter Verwendung von Generics nicht meine Anforderung passt, weil ich die Art und die Eigenschaft nur müssen im laufenden Betrieb getestet werden. Ich werde die Frage aktualisieren und versuchen, es besser zu erklären. – natenho

+0

Ich habe die Antwort aktualisiert, so dass Sie keine generischen Typen verwenden müssen. Stattdessen verwenden Sie Arrays vom Typ Objekt, um Ihre Objekte zu speichern. –

+0

es funktioniert nur für Basistypen wie Int. Mein Fall verwendet Referenztypen, die eine Eigenschaft benötigen, die mit contains ausgewertet werden soll. Zum Beispiel müsste ich eine Liste von "Person" gegen eine Liste von int Ids testen und alle "Person" zurückgeben, die eine dieser IDs hat: 'var arrayOfIds = new [] {1, 2, 3, 4 }; // Diese Zeile funktioniert nicht, da Person in der Kompilierzeit nicht bekannt ist, so kann ich nicht auf die Eigenschaft "Id" zugreifen return listOfObjects.Where (p => arrayOfIds.Contains (p.Id)); ' – natenho

4

Um Verwendung von Generika zu vermeiden (da die Typen sind nicht zur Entwurfszeit bekannt) Sie einige Überlegungen verwenden könnte und bauen den Ausdruck „von Hand“

Sie müssten dies durch eine definierende „Enthält“ Ausdruck in einer Where-Klausel:

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 diesem Fall „Codigo“ ist hartcodiert, aber es könnte ein Parameter sein, jede Eigenschaft des Typs erhalten Sie definieren.

Man könnte es testen, indem Sie mit:

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

Ich verwendete Ihren Code (für Queryable), um die Lösung für die Entity Framework DbSet-Abfrage anzuwenden. das heißt, den DbSet-Originalausdruck wie dbset.AsQueryable() zu verwenden. Ausdruck in der Where-Klausel, der von der CreateQuery-Methode des DbSet QueryableProvider verwendet wird. Auf diese Weise generiert EF intern eine perfekte SQL IN-Klausel. BTW, einige Teile Ihres Codes (z. B. MakeGenericMethod-Zeile) könnten anstelle des Reflection-Ansatzes in reinen Ausdruck konvertiert werden. Vielen Dank! – natenho

Verwandte Themen