Arbre d'expression LINQ N'importe quel () à l'intérieur Où ()

c# expression-trees lambda linq

Question

J'essaie de générer la requête LINQ suivante:

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

Utilisation des arbres d'expression (ce qui semble être le seul moyen de le faire lorsque j'ai besoin d'utiliser des types de réflexion et d'exécution). Notez que dans le code réel (voir ci-dessous), AdAccountAlert est en réalité dynamique par réflexion et par une boucle for.

Mon problème : je peux tout générer jusqu'à la clause .Where (). L'appel à la méthode whereExpression explose en raison de types incompatibles. Normalement, je sais quoi mettre là, mais l'appel de la méthode Any () m'a confondu. J'ai essayé tous les types auxquels je peux penser et pas de chance. Toute aide à la fois avec .Where () et .ToDictionary () serait appréciée.

Voici ce que j'ai jusqu'à présent:

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

Réponse populaire

Remarque: tout ce qui suit et incluant ToList() ne fonctionnera pas sur IQueryable<T> mais sur IEnumerable<T> . Pour cette raison, il n'est pas nécessaire de créer des arbres d'expression. Ce n'est certainement pas ce qui est interprété par EF ou similaire.

Si vous examinez le code généré par le compilateur pour votre requête d'origine, vous constaterez qu'il génère des arbres d'expression uniquement avant le premier appel à ToList .

Exemple:

Le code suivant:

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

Est traduit par le compilateur à ceci:

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

Notez s'il vous plaît comment il génère des expressions pour tout jusqu'à ToList . Tout ce qui suit et tout ce qui précède sont simplement des appels normaux à des méthodes d’extension.

Si vous n'imitez pas cela dans votre code, vous enverrez en fait un appel à Enumerable.ToList au fournisseur LINQ - qu'il tente ensuite de convertir en SQL et échoue.




Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Est-ce KB légal? Oui, apprenez pourquoi
Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Est-ce KB légal? Oui, apprenez pourquoi