【问题标题】:How to assign a property value of an IQueryable<T>?如何分配 IQueryable<T> 的属性值?
【发布时间】:2011-08-04 03:22:38
【问题描述】:

我使用的是 Entity Framework 4.1 Code First。在我的实体中,我有三个日期/时间属性:

public class MyEntity
{
    [Key]
    public Id { get; set; }

    public DateTime FromDate { get; set; }

    public DateTime ToDate { get; set; }

    [NotMapped]
    public DateTime? QueryDate { get; set; }

    // and some other fields, of course
}

在数据库中,我总是填写从/到日期。我使用一个简单的 where 子句查询它们。但在结果集中,我想包含我查询的日期。我需要坚持这一点,以使其他一些业务逻辑正常工作。

我正在研究一种扩展方法来做到这一点,但我遇到了问题:

public static IQueryable<T> WhereDateInRange<T>(this IQueryable<T> queryable, DateTime queryDate) where T : MyEntity
{
    // this part works fine
    var newQueryable = queryable.Where(e => e.FromDate <= queryDate &&
                                            e.ToDate >= queryDate);

    // in theory, this is what I want to do
    newQueryable = newQueryable.Select(e =>
                                           {
                                               e.QueryDate = queryDate;
                                               return e;
                                           });
    return newQueryable;
}

这不起作用。如果我使用 IEnumerable,它可以工作,但我想将其保留为 IQueryable,以便一切都在数据库端运行,并且这种扩展方法仍然可以用于另一个查询的任何部分。当它是 IQueryable 时,我收到以下编译错误:

带有语句体的 lambda 表达式不能转换为表达式树

如果这是 SQL,我会这样做:

SELECT *, @QueryDate as QueryDate
FROM MyEntities
WHERE @QueryDate BETWEEN FromDate AND ToDate

所以问题是,我怎样才能转换我已经必须包含这个额外属性分配的表达式树?我研究了 IQueryable.Expression 和 IQueryable.Provider.CreateQuery - 那里有一个解决方案。也许可以将赋值表达式附加到现有的表达式树?我对表达式树方法不够熟悉,无法弄清楚这一点。有什么想法吗?

示例用法

澄清一下,目标是能够执行如下操作:

var entity = dataContext.Set<MyEntity>()
                        .WhereDateInRange(DateTime.Now)
                        .FirstOrDefault();

并且将 DateTime.Now 保留到结果行的 QueryDate 中,而不会从数据库查询返回多于一行。 (使用 IEnumerable 解决方案,在 FirstOrDefault 选择我们想要的行之前返回多行。)

另一个想法

我可以继续将 QueryDate 映射为真实字段,并将其 DatabaseGeneratedOption 设置为 Computed。但是我需要一些方法将“@QueryDate as QueryDate”注入到由 EF 的 select 语句创建的 SQL 中。由于它是计算出来的,EF 不会在更新或插入期间尝试提供值。那么如何才能将自定义 SQL 注入到 select 语句中呢?

【问题讨论】:

    标签: linq entity-framework entity expression-trees iqueryable


    【解决方案1】:

    拉迪斯拉夫是绝对正确的。但是由于您显然希望回答问题的第二部分,因此您可以使用 Assign。不过,这不适用于 EF。

    using System;
    using System.Linq;
    using System.Linq.Expressions;
    
    namespace SO5639951
    {
        static class Program
        {
            static void Main()
            {
                AdventureWorks2008Entities c = new AdventureWorks2008Entities();
                var data = c.Addresses.Select(p => p);
    
                ParameterExpression value = Expression.Parameter(typeof(Address), "value");
                ParameterExpression result = Expression.Parameter(typeof(Address), "result");
                BlockExpression block = Expression.Block(
                    new[] { result },
                    Expression.Assign(Expression.Property(value, "AddressLine1"), Expression.Constant("X")),
                    Expression.Assign(result, value)
                    );
    
                LambdaExpression lambdaExpression = Expression.Lambda<Func<Address, Address>>(block, value);
    
                MethodCallExpression methodCallExpression = 
                    Expression.Call(
                        typeof(Queryable), 
                        "Select", 
                        new[]{ typeof(Address),typeof(Address) } , 
                        new[] { data.Expression, Expression.Quote(lambdaExpression) });
                
                var data2 = data.Provider.CreateQuery<Address>(methodCallExpression);
    
                string result1 = data.ToList()[0].AddressLine1;
                string result2 = data2.ToList()[0].AddressLine1;
            }
        }
    }
    

    更新 1

    这是经过一些调整后的相同代码。我摆脱了 EF 在上面的代码中阻塞的“Block”表达式,以绝对清楚地证明它是 EF 不支持的“Assign”表达式。请注意,Assign 原则上适用于通用表达式树,它是不支持 Assign 的 EF 提供程序。

    using System;
    using System.Linq;
    using System.Linq.Expressions;
    
    namespace SO5639951
    {
        static class Program
        {
            static void Main()
            {
                AdventureWorks2008Entities c = new AdventureWorks2008Entities();
                
                IQueryable<Address> originalData = c.Addresses.AsQueryable();
    
                Type anonType = new { a = new Address(), b = "" }.GetType();
                          
                ParameterExpression assignParameter = Expression.Parameter(typeof(Address), "value");
                var assignExpression = Expression.New(
                    anonType.GetConstructor(new[] { typeof(Address), typeof(string) }),
                    assignParameter,
                    Expression.Assign(Expression.Property(assignParameter, "AddressLine1"), Expression.Constant("X")));
                LambdaExpression lambdaAssignExpression = Expression.Lambda(assignExpression, assignParameter);
    
                var assignData = originalData.Provider.CreateQuery(CreateSelectMethodCall(originalData, lambdaAssignExpression));
                ParameterExpression selectParameter = Expression.Parameter(anonType, "value");
                var selectExpression = Expression.Property(selectParameter, "a");
                LambdaExpression lambdaSelectExpression = Expression.Lambda(selectExpression, selectParameter);
    
                IQueryable<Address> finalData = assignData.Provider.CreateQuery<Address>(CreateSelectMethodCall(assignData, lambdaSelectExpression));
                
                string result = finalData.ToList()[0].AddressLine1;
            }        
        
            static MethodCallExpression CreateSelectMethodCall(IQueryable query, LambdaExpression expression)
            {
                Type[] typeArgs = new[] { query.ElementType, expression.Body.Type };
                return Expression.Call(
                    typeof(Queryable),
                    "Select",
                    typeArgs,
                    new[] { query.Expression, Expression.Quote(expression) });
                
            }
        }
    }
    

    【讨论】:

      【解决方案2】:

      Automapper 有 Queryable Extensions,我认为它可以解决您的需求。 您可以使用 ProjectTo 在运行时计算属性。

      Ef Core 2 set value to ignored property on runtime

      http://docs.automapper.org/en/stable/Queryable-Extensions.html

      示例配置:

       configuration.CreateMap(typeof(MyEntity), typeof(MyEntity))
          .ForMember(nameof(Entity.QueryDate), opt.MapFrom(src => DateTime.Now));
      

      用法:

      queryable.ProjectTo<MyEntity>();
      

      【讨论】:

      • 这个问题是多年前提出的。我会审查您的解决方案,但总的来说我不再这样做了。还是谢谢。
      【解决方案3】:

      感谢您提供的所有宝贵反馈。听起来答案是“不 - 你不能那样做”。

      所以 - 我想出了一个解决方法。这对我的实现来说是非常具体的,但它确实有用。

      public class MyEntity
      {       
          private DateTime? _queryDate;
      
          [ThreadStatic]
          internal static DateTime TempQueryDate;
      
          [NotMapped]
          public DateTime? QueryDate
          {
              get
              {
                  if (_queryDate == null)
                      _queryDate = TempQueryDate;
                  return _queryDate;
              }
          }
      
          ...       
      }
      
      public static IQueryable<T> WhereDateInRange<T>(this IQueryable<T> queryable, DateTime queryDate) where T : MyEntity
      {
          MyEntity.TempQueryDate = queryDate;
      
          return queryable.Where(e => e.FromDate <= queryDate && e.ToDate >= queryDate);
      }
      

      神奇的是我使用线程静态字段来缓存查询日期,以便稍后在同一个线程中使用。我在 QueryDate 的 getter 中取回它的事实是特定于我的需求。

      显然,这不是原始问题的 EF 或 LINQ 解决方案,但通过将其从该世界中移除,它确实实现了相同的效果。

      【讨论】:

      • 如果您的应用程序是 asp.net,我当然不建议您这样做,而是使用 Ladislav 的解决方案。事实上,即使它不是 asp.net,我也会选择 Ladislav 的解决方案,因为您要为线程的潜在问题设置自己,这是最难调试的问题。如果其他人必须在您之后支持您的项目,则尤其如此。另见hanselman.com/blog/…piers7.blogspot.com/2005/11/…
      • 是的,我看到了这些帖子,以及关于线程本地存储和线程静态字段的所有其他警告。但是,在我的情况下,我只需要在第一次设置和下次使用它之间的值是正确的,而这只是在同一个线程中是必需的。所以这样做应该是安全的。这就是为什么我将它复制到实体本身的常规字段的原因。
      • 另外,我不能使用 Ladislav 提出的包装解决方案,因为我需要继续使用我开始使用的实体的 IQueryable。如果我将结果投影到包装器中,那么我现在正在使用包装器的 IEnumerable。这使我无法在数据库中进行额外的过滤/排序。
      • >>“如果我将结果投影到包装器中,那么我现在正在使用包装器的 IEnumerable。”这是一个相当奇怪的说法。为什么要这么说?
      • >>“而且只有在同一个线程中才有必要”。在不了解您的线程假设的情况下,有人可以以您现在无法想象的方式更改您的代码,然后花一周时间试图理解为什么代码有时不起作用。前任。在 ASP.NET MVC 中有异步控制器。稍后将您的东西转换为该控制器的一部分执行,然后您就遇到了麻烦。至少在预期的时间里,这种事情会来咬你。而且,这是您在您支持的其他 ppl 代码中看到的,您希望这些代码不存在。这不是安全的编程。
      【解决方案4】:

      不,我认为没有解决方案。的确,您可以修改表达式树,但您将得到与使用 linq 查询完全相同的异常,因为该查询实际上是您将在表达式树中构建的内容。问题不在于表达式树,而在于映射。 EF 无法将 QueryData 映射到结果。此外,您正在尝试进行投影。无法对映射实体进行投影,无法从方法返回匿名类型。

      您当然可以进行您提到的选择,但您无法将其映射到您的实体。您必须为此创建一个新类型:

      var query = from x in context.MyData
                  where x.FromDate <= queryDate && x.ToDate >= queryDate
                  select new MyDateWrapper
                      {
                         MyData = x,
                         QueryDate = queryDate
                      };
      

      【讨论】:

      • 我认为,问题不在于映射。您完全可以在没有 EF 的情况下重现错误。像 Expression&lt;Func&lt;Person, Person&gt;&gt; e = p =&gt; { p.Name = "X"; return p; }; 这样的东西由于 lambda 内部的语句部分而无法编译。 (他实际上并没有计划,他试图让一个函数返回一个完整的实体,但是一个具有内部“副作用”的函数(查询日期分配)。)我认为这是纯粹的表达式树问题,而不是 EF。 IEnumerable 工作的普通 lambda 表达式:Func&lt;Person, Person&gt; f = p =&gt; { p.Name = "X"; return p; }; 编译。
      • @Slauma:谢谢,我没有这样想。在这种情况下,这是另一个(也是第一个)问题,因为我提到的问题只是队列中的其他问题。
      • @Slauma:没错——我使用的是 EF 还是其他东西都没有关系。我似乎无法将赋值表达式放入现有的表达式树中。有 Expression.Assign() - 但我找不到如何使用它的清晰示例。
      • 即使你找到了使用Assign的方法,它仍然不起作用,因为Ef将无法实现结果。
      猜你喜欢
      • 1970-01-01
      • 2020-12-22
      • 2018-12-26
      • 1970-01-01
      • 1970-01-01
      • 2018-05-15
      • 2021-05-31
      • 2022-01-21
      • 1970-01-01
      相关资源
      最近更新 更多