【问题标题】:LINQ Expression Tree Any() inside Where()Where() 内的 LINQ 表达式树 Any()
【发布时间】:2013-05-29 19:26:56
【问题描述】:

我正在尝试生成以下 LINQ 查询:

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

使用表达式树(当我需要使用反射和运行时类型时,这似乎是我能做到这一点的唯一方法)。请注意,在实际代码中(见下文),AdAccountAlert 实际上是通过反射和 for 循环动态实现的。

我的问题:我可以生成直到 .Where() 子句的所有内容。 whereExpression 方法调用由于类型不兼容而崩溃。通常我知道该放什么,但是 Any() 方法调用让我感到困惑。我已经尝试了我能想到的所有类型,但没有运气。 .Where() 和 .ToDictionary() 的任何帮助将不胜感激。

这是我目前所拥有的:

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

【问题讨论】:

  • 只有一个问题:您认为您编写的代码易于阅读和维护吗?
  • 真正的代码被分解成单独的函数,使其更易于阅读。我把所有的东西放在一起作为上下文。如果您正在询问我对动态构建表达式树的使用,正如我在帖子中所述,这是迄今为止我发现的最佳选择。 PredicateBuilder 没有完成这项工作,DynamicLinq 库也没有。
  • 一切都很好,我只是想知道,因为您将所有内容都放在了上下文中;我明白你的意思。
  • 我无法理解最初的 LINQ 试图做什么,更不用说动态生成的 LINQ。
  • 您当前的代码到底有什么问题?它是如何失败的?您是否尝试过比较您生成的表达式树和 C# 从 lambda 生成的表达式树?

标签: c# linq lambda expression-trees


【解决方案1】:

请注意:ToList() 之后的所有内容(包括ToList())都不适用于IQueryable&lt;T&gt;,但适用于IEnumerable&lt;T&gt;。因此,无需创建表达式树。这肯定不是 EF 或类似的解释。

如果您查看编译器为您的原始查询生成的代码,您会发现它仅在第一次调用 ToList 之前生成表达式树。

例子:

以下代码:

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

被编译器翻译成这样:

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

请注意它如何为直到ToList 的所有内容生成表达式。包括它之后的所有内容都是对扩展方法的正常调用。

如果您不在代码中模仿这一点,您实际上会向 LINQ 提供程序发送对 Enumerable.ToList 的调用 - 然后它会尝试将其转换为 SQL 并失败。

【讨论】:

  • “因此,不需要创建表达式树。” => 除了他要写的查询依赖于编译时不知道的类型,所以是的,他需要动态构造它。
  • @MikeStrobel 如果您的意思是 OP 需要在 ToList 之后动态构建事物,那么当然可以,但这不需要也可能不应该使用表达式树来完成。在不需要的地方省略表达式树可以大大简化问题。
  • 如果编译时不知道类型和属性,他应该怎么做呢?
【解决方案2】:

看起来,在构造whereLambda 时,您的第二个参数应该是subscribersParameter 而不是subscriptionParameter。至少,这将是您异常的原因。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-08-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-09-29
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多