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



我使用实体框架4.1代码优先。在我的实体中,我有三个日期/时间属性:

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
}

在数据库中,我总是填充From/To日期。我使用一个简单的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。现在持久化到结果行的QueryDate中,没有从数据库查询返回多行。(使用IEnumerable解决方案,在FirstOrDefault选择我们想要的行之前返回多行。)

另一个想法

我可以像映射一个真正的字段一样映射QueryDate,并将其DatabaseGeneratedOption设置为Computed。但是,我需要一些方法将"@QueryDate as QueryDate"注入到EF的select语句创建的SQL中。因为它是计算的,EF不会尝试在更新或插入期间提供值。那么,如何将自定义SQL注入到选择语句中呢?

Ladislav是绝对正确的。但由于你显然希望问题的第二部分得到回答,下面是如何使用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

下面是经过一些调整后的相同代码。我去掉了"block"表达式,EF在上面的代码中卡住了,以绝对清晰地演示它是"赋值";EF不支持的表达式。请注意,Assign原则上与泛型表达式树一起工作,是EF提供程序不支持Assign。

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

不,我认为没有解决办法。你确实可以修改表达式树,但是你会得到和linq查询完全一样的异常,因为那个查询实际上就是你要在表达式树中构建的。问题不在于表达式树,而在于映射。EF不能将QueryData映射到结果。而且你还在做投影。不能对映射实体进行投影,也不能从该方法返回匿名类型。

你当然可以做你提到的选择,但你不能把它映射到你的实体。你必须为它创建一个新类型:

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

Automapper有可查询的扩展,我认为它可以解决您的需求。你可以使用ProjectTo在运行时计算属性。

Ef Core 2设置值为运行时忽略属性

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

感谢您的宝贵反馈。听起来答案是"不——你不能那样做"。

所以-我想出了一个解决办法。这对我的实现来说是非常特殊的,但它确实奏效了。

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解决方案,但它确实通过从那个世界中删除它来实现相同的效果。

相关内容

  • 没有找到相关文章

最新更新