如何分配 IQueryable<T> 的属性值?

Posted

技术标签:

【中文标题】如何分配 IQueryable<T> 的属性值?【英文标题】:How to assign a property value of an 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 语句中呢?

【问题讨论】:

【参考方案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将无法实现结果。

以上是关于如何分配 IQueryable<T> 的属性值?的主要内容,如果未能解决你的问题,请参考以下文章

如何连接 IQueryable<T> 和 List<T>

如何将 IEnumerable<t> 或 IQueryable<t> 转换为 EntitySet<t>?

如何将 ODataQueryOptions<T> 应用于 IQueryable<X>

如何将 Kendo UI Grid 与 ToDataSourceResult()、IQueryable<T>、ViewModel 和 AutoMapper 一起使用?

在 WebAPI 中使用 OData 时如何将 SelectExpandQueryOption 转换为 IQueryable<T>?

IQueryable 和 IQueryable<T> 是异步的吗?