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