2013-05-29 5 views
7

Ich versuche, die folgende LINQ-Abfrage zu generieren:LINQ Expression Baum Any() innerhalb Wo()

//Query the database for all AdAccountAlerts that haven't had notifications sent out 
//Then get the entity (AdAccount) the alert pertains to, and find all accounts that 
//are subscribing to alerts on that entity. 
var x = dataContext.Alerts.Where(a => a.NotificationsSent == null) 
    .OfType<AdAccountAlert>() 
    .ToList() 
    .GroupJoin(dataContext.AlertSubscriptions, 
    a => new Tuple<int, string>(a.AdAccountId, typeof(AdAccount).Name), 
    s => new Tuple<int, string>(s.EntityId, s.EntityType), 
    (Alert, Subscribers) => new Tuple<AdAccountAlert, IEnumerable<AlertSubscription>> (Alert, Subscribers)) 
    .Where(s => s.Item2.Any()) 
    .ToDictionary(kvp => (Alert)kvp.Item1, kvp => kvp.Item2.Select(s => s.Username)); 

Mit Expression Trees (das ist der einzige Weg zu sein scheint, dass ich das tun kann, wenn es nötig ist Verwenden Sie Reflektions- und Laufzeittypen. Beachten Sie, dass das AdAccountAlert im reellen Code (siehe unten) tatsächlich durch Reflexion und eine For-Schleife dynamisch ist.

Mein Problem: Ich kann alles bis zu der .Where() -Klausel generieren. Der Aufruf der whereExpression-Methode wird wegen inkompatibler Typen in die Luft gesprengt. Normalerweise weiß ich, was ich dort setzen soll, aber der Methodenaufruf Any() hat mich verwirrt. Ich habe jeden möglichen Typ ausprobiert und kein Glück. Jede Hilfe mit den. Where() und. ToDictionary() würde geschätzt werden.

Hier ist, was ich bisher:

var alertTypes = AppDomain.CurrentDomain.GetAssemblies() 
    .Single(a => a.FullName.StartsWith("Alerts.Entities")) 
    .GetTypes() 
    .Where(t => typeof(Alert).IsAssignableFrom(t) && !t.IsAbstract && !t.IsInterface); 

var alertSubscribers = new Dictionary<Alert, IEnumerable<string>>(); 

//Using tuples for joins to keep everything strongly-typed 
var subscribableType = typeof(Tuple<int, string>); 
var doubleTuple = Type.GetType("System.Tuple`2, mscorlib", true); 

foreach (var alertType in alertTypes) 
{ 
    Type foreignKeyType = GetForeignKeyType(alertType); 
    if (foreignKeyType == null) 
    continue; 

    IQueryable<Alert> unnotifiedAlerts = dataContext.Alerts.Where(a => a.NotificationsSent == null); 

    //Generates: .OfType<alertType>() 
    MethodCallExpression alertsOfType = Expression.Call(typeof(Enumerable).GetMethod("OfType").MakeGenericMethod(alertType), unnotifiedAlerts.Expression); 

    //Generates: .ToList(), which is required for joins on Tuples 
    MethodCallExpression unnotifiedAlertsList = Expression.Call(typeof(Enumerable).GetMethod("ToList").MakeGenericMethod(alertType), alertsOfType); 

    //Generates: a => new { a.{EntityId}, EntityType = typeof(AdAccount).Name } 
    ParameterExpression alertParameter = Expression.Parameter(alertType, "a"); 
    MemberExpression adAccountId = Expression.Property(alertParameter, alertType.GetProperty(alertType.GetForeignKeyId())); 
    NewExpression outerJoinObject = Expression.New(subscribableType.GetConstructor(new Type[] { typeof(int), typeof(string)}), adAccountId, Expression.Constant(foreignKeyType.Name)); 
    LambdaExpression outerSelector = Expression.Lambda(outerJoinObject, alertParameter); 

    //Generates: s => new { s.EntityId, s.EntityType } 
    Type alertSubscriptionType = typeof(AlertSubscription); 
    ParameterExpression subscriptionParameter = Expression.Parameter(alertSubscriptionType, "s"); 
    MemberExpression entityId = Expression.Property(subscriptionParameter, alertSubscriptionType.GetProperty("EntityId")); 
    MemberExpression entityType = Expression.Property(subscriptionParameter, alertSubscriptionType.GetProperty("EntityType")); 
    NewExpression innerJoinObject = Expression.New(subscribableType.GetConstructor(new Type[] { typeof(int), typeof(string) }), entityId, entityType); 
    LambdaExpression innerSelector = Expression.Lambda(innerJoinObject, subscriptionParameter); 

    //Generates: (Alert, Subscribers) => new Tuple<Alert, IEnumerable<AlertSubscription>>(Alert, Subscribers) 
    var joinResultType = doubleTuple.MakeGenericType(new Type[] { alertType, typeof(IEnumerable<AlertSubscription>) }); 
    ParameterExpression alertTupleParameter = Expression.Parameter(alertType, "Alert"); 
    ParameterExpression subscribersTupleParameter = Expression.Parameter(typeof(IEnumerable<AlertSubscription>), "Subscribers"); 
    NewExpression joinResultObject = Expression.New(
    joinResultType.GetConstructor(new Type[] { alertType, typeof(IEnumerable<AlertSubscription>) }), 
    alertTupleParameter, 
    subscribersTupleParameter); 

    LambdaExpression resultsSelector = Expression.Lambda(joinResultObject, alertTupleParameter, subscribersTupleParameter); 

    //Generates: 
    // .GroupJoin(dataContext.AlertSubscriptions, 
    // a => new { a.AdAccountId, typeof(AdAccount).Name }, 
    // s => new { s.EntityId, s.EntityType }, 
    // (Alert, Subscribers) => new Tuple<Alert, IEnumerable<AlertSubscription>>(Alert, Subscribers)) 
    IQueryable<AlertSubscription> alertSubscriptions = dataContext.AlertSubscriptions.AsQueryable(); 
    MethodCallExpression joinExpression = Expression.Call(typeof(Enumerable), 
    "GroupJoin", 
    new Type[] 
    { 
     alertType, 
     alertSubscriptions.ElementType, 
     outerSelector.Body.Type, 
     resultsSelector.ReturnType 
    }, 
    unnotifiedAlertsList, 
    alertSubscriptions.Expression, 
    outerSelector, 
    innerSelector, 
    resultsSelector); 

    //Generates: .Where(s => s.Item2.Any()) 
    ParameterExpression subscribersParameter = Expression.Parameter(resultsSelector.ReturnType, "s"); 
    MemberExpression tupleSubscribers = Expression.Property(subscribersParameter, resultsSelector.ReturnType.GetProperty("Item2")); 
    MethodCallExpression hasSubscribers = Expression.Call(typeof(Enumerable), 
    "Any", 
    new Type[] { alertSubscriptions.ElementType }, 
    tupleSubscribers); 
    LambdaExpression whereLambda = Expression.Lambda(hasSubscribers, subscriptionParameter); 
    MethodCallExpression whereExpression = Expression.Call(typeof(Enumerable), 
    "Where", 
    new Type[] { joinResultType }, 
    joinExpression, 
    whereLambda); 
+19

Nur eine Frage: Denken Sie, dass der Code, den Sie schreiben, leicht lesbar und wartbar ist? – Marco

+3

Der echte Code ist in einzelne Funktionen aufgeteilt, die das Lesen etwas erleichtern. Ich stelle hier alles für den Kontext zusammen. Wenn Sie sich über meine Verwendung dynamisch gebildeter Ausdrucksbäume erkundigen, wie ich in dem Beitrag gesagt habe, war es die beste Option, die ich bisher gefunden habe. PredicateBuilder erledigt diese Aufgabe nicht, ebenso wenig wie die DynamicLinq-Bibliothek. – user1924929

+0

Es ist alles in Ordnung, ich habe mich nur gefragt, weil Sie alles in den Kontext stellen; Ich verstehe was du meinst. – Marco

Antwort

3

Bitte beachten Sie: Alles nach und einschließlich ToList() nicht auf IQueryable<T> arbeiten, aber auf IEnumerable<T>. Aus diesem Grund müssen keine Ausdrucksbäume erstellt werden. Es ist sicherlich nichts, was von EF oder ähnlich interpretiert wird.

Wenn Sie sich den Code ansehen, der vom Compiler für Ihre ursprüngliche Abfrage generiert wird, würden Sie sehen, dass er nur bis kurz vor dem ersten Aufruf von ToList Ausdrucksbäume generiert.

Beispiel:

Der folgende Code:

var query = new List<int>().AsQueryable(); 
query.Where(x => x > 0).ToList().FirstOrDefault(x => x > 10); 

wird vom Compiler dazu übersetzt:

IQueryable<int> query = new List<int>().AsQueryable<int>(); 
IQueryable<int> arg_4D_0 = query; 
ParameterExpression parameterExpression = Expression.Parameter(typeof(int), "x"); 
arg_4D_0.Where(Expression.Lambda<Func<int, bool>>(Expression.GreaterThan(parameterExpression, Expression.Constant(0, typeof(int))), new ParameterExpression[] 
{ 
    parameterExpression 
})).ToList<int>().FirstOrDefault((int x) => x > 10); 

Bitte beachten Sie, wie es Ausdrücke für alles bis zu ToList erzeugt. Alles danach und darunter sind einfach normale Aufrufe von Erweiterungsmethoden.

Wenn Sie dies nicht in Ihrem Code nachahmen, senden Sie tatsächlich einen Anruf an Enumerable.ToList an den LINQ-Provider - die es dann versucht, in SQL zu konvertieren und fehlschlagen.

+0

_ "Aus diesem Grund ist es nicht notwendig, Ausdrucksbäume zu erstellen." _ => Außer, dass die Abfrage, die er schreiben will, abhängt Typen, die zur Kompilierzeit nicht bekannt sind, also ja, er muss sie dynamisch aufbauen. –

+0

Warum dann die ToList()? – Stu

+1

@MikeStrobel Wenn Sie meinen, dass das OP nach der 'ToList' dynamisch Dinge konstruieren muss, dann ist das sicher, aber das muss nicht sein und sollte auch nicht mit Ausdrucksbäumen gemacht werden. Und das Auslassen von Ausdrucksbäumen, wo sie nicht benötigt werden, vereinfacht die Frage erheblich. – hvd

0

Es sieht so aus, als ob Ihr zweiter Parameter bei der Erstellung von whereLambdasubscribersParameter und nicht subscriptionParameter sein sollte. Zumindest wäre das der Grund für Ihre Ausnahme.