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

时间:2011-04-12 18:34:16

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

我正在使用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
}

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

另一个想法

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

4 个答案:

答案 0 :(得分:3)

拉迪斯拉夫是绝对正确的。但是,既然您显然希望回答问题的第二部分,那么您可以使用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不支持的“赋值”表达式。请注意,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) });

        }
    }
}

答案 1 :(得分:2)

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

你可以选择你提到的选择,但只是你不能将它映射到你的实体。您必须为此创建一个新类型:

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

答案 2 :(得分:1)

Automapper有可查询扩展,我认为它可以解决您的需求。 您可以使用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 :(得分:0)

感谢您提供所有有价值的反馈。这听起来像答案是“不 - 你不能这样做”。

所以 - 我想出了一个解决方法。这对我的实现非常具体,但它可以解决问题。

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