2013-05-29 17 views
7

Sto cercando di generare la seguente query LINQ:LINQ Expression Albero Qualsiasi() all'interno dove()

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

Usando Expression Trees (che sembra essere l'unico modo per fare questo quando ho bisogno di utilizzare i tipi di riflessione e runtime). Nota che nel codice reale (vedi sotto) l'AdAccountAlert è in realtà dinamico attraverso la riflessione e un ciclo for.

Il mio problema: Posso generare tutto fino alla clausola .Where(). La chiamata al metodo whereExpression esplode a causa di tipi incompatibili. Normalmente so cosa mettere lì, ma la chiamata al metodo Any() mi ha confuso. Ho provato ogni tipo a cui riesco a pensare e senza fortuna. Sarebbe gradito qualsiasi aiuto sia con .Where() che con .ToDictionary().

Ecco quello che ho finora:

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

Solo una domanda: pensi che il codice che stai scrivendo sia facilmente leggibile e mantenibile? – Marco

+3

Il codice reale è suddiviso in funzioni separate che ne semplificano la lettura. Metto tutto insieme qui per il contesto. Se mi stai informando sul mio uso di alberi di espressione dinamicamente costruttivi, come ho affermato nel post, è stata la migliore opzione che ho trovato finora. PredicateBuilder non esegue il lavoro, né la libreria DynamicLinq. – user1924929

+0

Va tutto bene, mi stavo chiedendo perché hai messo tutto nel contesto; Capisco cosa intendi. – Marco

risposta

3

Si prega di notare: Tutto dopo compreso ToList() non funziona su IQueryable<T> ma su IEnumerable<T>. Per questo motivo, non è necessario creare alberi di espressione. Sicuramente non è interpretato da EF o simili.

Se si guarda il codice generato dal compilatore per la query originale, si vedrà che genera alberi di espressioni solo fino alla prima chiamata al numero ToList.

Esempio:

Il seguente codice:

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

è tradotto dal compilatore per questo:

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

Si prega di notare come esso genera espressioni per tutto fino ToList. Tutto dopo e incluso sono semplicemente chiamate normali ai metodi di estensione.

Se non si imita questo codice nel proprio codice, si invierà effettivamente una chiamata a Enumerable.ToList al provider LINQ, che tenterà quindi di convertire in SQL e non riuscire.

+0

_ "Per questo motivo non è necessario creare alberi di espressioni." _ => Tranne che la query che desidera scrivere dipende da tipi che non sono noti al momento della compilazione, quindi sì, ha bisogno di costruirlo in modo dinamico. –

+0

Allora perché la lista()? – Stu

+1

@MikeStrobel Se intendi che l'OP ha bisogno di costruire dinamicamente le cose dopo la 'ToList', allora sicuramente, ma non è necessario che sia e probabilmente non dovrebbe essere fatto usando gli alberi di espressione. E omettere gli alberi di espressione dove non sono necessari semplifica enormemente la domanda. – hvd

0

Sembra che, durante la costruzione di whereLambda, il secondo parametro sia subscribersParameter e non subscriptionParameter. Almeno, questa sarebbe la causa della tua eccezione.