2013-05-29 15 views
7

Próbuję wygenerować LINQ następujące zapytanie:LINQ Expression Drzewo Obojętnie() wewnątrz Gdzie()

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

Korzystanie Expression Trees (co wydaje się być jedynym sposobem mogę to zrobić, gdy muszę używaj typów refleksji i run-time). Zauważ, że w prawdziwym kodzie (zobacz poniżej) AdAccountAlert jest dynamicznie przez odbicie i pętlę for.

Mój problem: Mogę wygenerować wszystko aż do klauzuli .Where(). Wywołanie metody whereExpression wysuwa się z powodu niezgodnych typów. Zwykle wiem, co tam umieścić, ale wywołanie metody Any() mnie zdezorientowało. Próbowałem każdego rodzaju, jaki potrafię wymyślić i bez powodzenia. Pomocna będzie zarówno metoda .Where(), jak i .ToDictionary().

Oto co mam do tej pory:

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

Tylko jedno pytanie: czy uważasz, że kod, który piszesz, jest łatwy do odczytania i konserwacji? – Marco

+3

Rzeczywisty kod jest podzielony na osobne funkcje, dzięki czemu czytanie jest nieco łatwiejsze. Połączyłem wszystko tutaj dla kontekstu. Jeśli pytasz o moje wykorzystanie dynamicznego budowania drzewek ekspresji, jak napisałem w poście, to była to najlepsza opcja, jaką do tej pory znalazłem. PredicateBuilder nie spełnia swojej roli, podobnie jak biblioteka DynamicLinq. – user1924929

+0

Wszystko jest w porządku, po prostu się zastanawiałem, ponieważ wszystko włożyłeś w kontekst; Rozumiem co masz na myśli. – Marco

Odpowiedz

3

Uwaga: Wszystko po tym ToList() i nie będzie działać na IQueryable<T> ale na IEnumerable<T>. Z tego powodu nie ma potrzeby tworzenia drzewek wyrażeń. To z pewnością nic nie jest interpretowane przez EF lub podobne.

Jeśli przyjrzeć się kodowi wygenerowanemu przez kompilator dla pierwotnego zapytania, można zauważyć, że generuje on drzewa wyrażeń tylko do momentu tuż przed pierwszym wywołaniem ToList.

Przykład:

Poniższy kod:

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

jest tłumaczony przez kompilator do tego:

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

Proszę zauważyć, jak to generuje wyrażenia dla wszystkiego do ToList. Wszystko po i włączając to są zwykłe połączenia z metodami rozszerzenia.

Jeśli nie naśladujesz tego w swoim kodzie, faktycznie wyślesz połączenie do Enumerable.ToList do dostawcy LINQ - który następnie próbuje przekonwertować do SQL i nie powiedzie się.

+0

_ "Z tego powodu nie ma potrzeby tworzenia drzewek wyrażeń." _ => Poza tym, że zapytanie, które chce napisać, zależy od typy, które nie są znane w czasie kompilacji, więc tak, musi on konstruować go dynamicznie. –

+0

Więc dlaczego ToList()? – Stu

+1

@MikeStrobel Jeśli masz na myśli, że OP musi konstruować rzeczy dynamicznie po 'ToList', to pewnie, ale to nie musi być i prawdopodobnie nie powinno być robione przy użyciu drzewek wyrażeń. Pomijanie drzewek wyrażeń, które nie są potrzebne, znacznie upraszcza pytanie. – hvd

0

Wygląda na to, że podczas konstruowania whereLambda drugi parametr powinien mieć wartość subscribersParameter, a nie subscriptionParameter. Przynajmniej to byłby powód twojego wyjątku.

Powiązane problemy