我正在使用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语句?
答案 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解决方案,但它确实通过从该世界中删除它来实现相同的效果。