如何分配 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<Func<Person, Person>> e = p => p.Name = "X"; return p; ;
这样的东西由于 lambda 内部的语句部分而无法编译。 (他实际上并没有计划,他试图让一个函数返回一个完整的实体,但是一个具有内部“副作用”的函数(查询日期分配)。)我认为这是纯粹的表达式树问题,而不是 EF。 IEnumerable 工作的普通 lambda 表达式:Func<Person, Person> f = p => p.Name = "X"; return p; ;
编译。
@Slauma:谢谢,我没有这样想。在这种情况下,这是另一个(也是第一个)问题,因为我提到的问题只是队列中的其他问题。
@Slauma:没错——我使用的是 EF 还是其他东西都没有关系。我似乎无法将赋值表达式放入现有的表达式树中。有 Expression.Assign() - 但我找不到如何使用它的清晰示例。
即使你找到了使用Assign的方法,它仍然不起作用,因为Ef将无法实现结果。以上是关于如何分配 IQueryable<T> 的属性值?的主要内容,如果未能解决你的问题,请参考以下文章
如何将 IEnumerable<t> 或 IQueryable<t> 转换为 EntitySet<t>?
如何将 ODataQueryOptions<T> 应用于 IQueryable<X>
如何将 Kendo UI Grid 与 ToDataSourceResult()、IQueryable<T>、ViewModel 和 AutoMapper 一起使用?
在 WebAPI 中使用 OData 时如何将 SelectExpandQueryOption 转换为 IQueryable<T>?