使用 Entity Framework Core 进行查询时,我使用表达式转换为 DTO 对象,这对于对象和任何子集合都很有效。
一个简化的例子:
型号:
public class Model
{
public int ModelId { get; set; }
public string ModelName { get; set; }
public virtual ICollection<ChildModel> ChildModels { get; set; }
// Other properties, collections, etc.
public static Expression<Func<Model, ModelDto>> AsDto =>
model => new ModelDto
{
ModelId = model.ModelId,
ModelName = model.ModelName,
ChildModels = model.ChildModels.AsQueryable().Select(ChildModel.AsDto).ToList()
};
}
查询:
dbContext.Models.Where(m => SomeCriteria).Select(Model.AsDto).ToList();
我的问题是试图找到一种方法为非集合的孩子做类似的事情。如果我已添加到我的模型中:
public AnotherChildModel AnotherChildModel { get; set; }
我可以在表达式中添加转换:
public static Expression<Func<Model, ModelDto>> AsDto =>
model => new ModelDto
{
ModelId = model.ModelId,
ModelName = model.ModelName,
ChildModels = model.ChildModels.AsQueryable().Select(ChildModel.AsDto).ToList(),
AnotherChildModel = new AnotherChildModelDto
{
AnotherChildModelId = model.AnotherChildModelId
}
};
但是,我还没有找到一种好方法来避免每次需要将第二个子模型转换为 DTO 对象时重复此代码。这些表达式适用于主对象和任何子集合,但不适用于单个实体。有没有办法为单个实体添加 .Select() 的等效项?
有几个库可以以直观的方式做到这一点:
[Expandable(nameof(AsDtoImpl))]
public static ModelDto AsDto(Model model)
{
_asDtoImpl ??= AsDtoImpl() .Compile();
return _asDtoImpl(model);
}
private static Func<Model, ModelDto> _asDtoImpl;
private static Expression<Func<Model, ModelDto>> AsDtoImpl =>
model => new ModelDto
{
ModelId = model.ModelId,
ModelName = model.ModelName,
ChildModels = model.ChildModels.AsQueryable().Select(ChildModel.AsDto).ToList(),
AnotherChildModel = new AnotherChildModelDto
{
AnotherChildModelId = model.AnotherChildModelId
}
};
dbContext.Models
.Where(m => SomeCriteria).Select(m => Model.AsDto(m))
.AsExpandable()
.ToList();
更新:对于EF Core,LINQKit可以全局配置,
AsExpanding()
可以省略。
builder
.UseSqlServer(connectionString)
.WithExpressionExpanding(); // enabling LINQKit extension
NeinLinq - 几乎与 LINQKit 中的相同
[InjectLambda]
public static ModelDto AsDto(Model model)
{
_asDto ??= AsDto() .Compile();
return _asDto(model);
}
private static Func<Model, ModelDto> _asDto;
private static Expression<Func<Model, ModelDto>> AsDto =>
model => new ModelDto
{
ModelId = model.ModelId,
ModelName = model.ModelName,
ChildModels = model.ChildModels.AsQueryable().Select(ChildModel.AsDto).ToList(),
AnotherChildModel = new AnotherChildModelDto
{
AnotherChildModelId = model.AnotherChildModelId
}
};
dbContext.Models
.Where(m => SomeCriteria).Select(m => Model.AsDto(m))
.ToInjectable()
.ToList();
更新:对于 EF Core,NenLinq 可以全局配置,
ToInjectable()
可以省略。
builder
.UseSqlServer(connectionString)
.WithLambdaInjection(); // enabling NeinLinq extension
DelegateDecompiler - 比其他的更简洁
[Computed]
public static ModelDto AsDto(Model model)
=> new ModelDto
{
ModelId = model.ModelId,
ModelName = model.ModelName,
ChildModels = model.ChildModels.AsQueryable().Select(ChildModel.AsDto).ToList(),
AnotherChildModel = new AnotherChildModelDto
{
AnotherChildModelId = model.AnotherChildModelId
}
}
dbContext.Models
.Where(m => SomeCriteria).Select(m => Model.AsDto(m))
.Decompile()
.ToList();
所有库都执行相同的操作 - 在 EF Core 处理之前更正表达式树。它们都需要额外的调用来注入它自己的
IQueryProvider
。
无需任何第三方库即可完成。关键是在制作
AsQueryable()
之前先使用Select
。当然,您可以在没有它的情况下让它工作,但您很可能会获取比实际需要更多的列。
对于你的代码来说,它会是这样的:
dbContext.Models.Where(m => SomeCriteria).AsQueryable().Select(Model.AsDto).ToList();
示例:
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using var context = new MyDbContext();
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
var blogPostIds = context.Blogs
.Select(b => new
{
BlogId = b.Id,
PostIds = b.Posts.AsQueryable().Select(Helper.Selector).ToList()
})
.ToList();
public static class Helper
{
public static Expression<Func<Post, int>> Selector
=> x => x.Id;
}
public class MyDbContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseSqlServer("Data Source=(LocalDb)\\MSSQLLocalDB;Initial Catalog=Selector;Integrated Security=SSPI;")
.LogTo(Console.WriteLine, LogLevel.Information);
}
public class Blog
{
public int Id { get; set; }
public string Title { get; set; }
public IEnumerable<Post> Posts { get; set; }
}
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public int BlogId { get; set; }
public Blog Blog { get; set; }
}
ExpressionVisitor
并使用它来编辑外部 AsDto()
方法的表达式树并注入 AsDto()
的内部 AnotherChildModel
方法来实现。这里的基本思想是首先定义 AsDto()
表达式的 curried版本,将
Func<AnotherChildModel, AnotherChildModelDto>
作为附加参数,然后使用 ExpressionVisitor
创建一个修改后的副本,用表达式替换 curried 函数参数AnotherChildModel.AsDto
。由于此方法直接使用表达式树,因此它独立于您正在使用的特定 LINQ 框架。
要了解如何做到这一点,首先定义以下扩展方法,将一些
Expression<Func<T, TResult>>
注入和/或组合到某些包含表达式树中:
public static partial class ExpressionExtensions
{
// Uncurry and compose an Expression<Func<T1, Func<T2, T3>, TResult>> into an Expression<Func<T1, TResult>> by composing with an Expression<Func<T2, T3>>
public static Expression<Func<T1, TResult>> Inject<T1, T2, T3, TResult>(this Expression<Func<T1, Func<T2, T3>, TResult>> outer, Expression<Func<T2, T3>> inner) =>
Expression.Lambda<Func<T1, TResult>>(
new InvokeReplacer((outer.Parameters[1], inner)).Visit(outer.Body),
false, outer.Parameters[0]);
// Compose two Func<Tx, Ty> expressions with compatible generic parameters into a third.
public static Expression<Func<T1, TResult>> Compose<T1, T2, TResult>(this Expression<Func<T2, TResult>> outer, Expression<Func<T1, T2>> inner) =>
Expression.Lambda<Func<T1, TResult>>(
new ParameterReplacer(new [] {(outer.Parameters[0], inner.Body)}).Visit(outer.Body),
false, inner.Parameters[0]);
}
class InvokeReplacer : ExpressionVisitor
{
// Replace an Invoke() with the body of a lambda, replacing the formal paramaters of the lambda with the arguments of the invoke.
// TODO: Handle replacing of functions that are not invoked but just passed as parameters to some external method, e.g.
// collection.Select(map) instead of collection.Select(i => map(i))
readonly Dictionary<Expression, LambdaExpression> funcsToReplace;
public InvokeReplacer(params (Expression func, LambdaExpression replacement) [] funcsToReplace) =>
this.funcsToReplace = funcsToReplace.ToDictionary(p => p.func, p => p.replacement);
protected override Expression VisitInvocation(InvocationExpression invoke) =>
funcsToReplace.TryGetValue(invoke.Expression, out var lambda)
? (invoke.Arguments.Count != lambda.Parameters.Count
? throw new InvalidOperationException("Wrong number of arguments")
: new ParameterReplacer(lambda.Parameters.Zip(invoke.Arguments)).Visit(lambda.Body))
: base.VisitInvocation(invoke);
}
class ParameterReplacer : ExpressionVisitor
{
// Replace formal parameters (e.g. of a lambda body) with some containing expression in scope.
readonly Dictionary<ParameterExpression, Expression> parametersToReplace;
public ParameterReplacer(IEnumerable<(ParameterExpression parameter, Expression replacement)> parametersToReplace) =>
this.parametersToReplace = parametersToReplace.ToDictionary(p => p.parameter, p => p.replacement);
protected override Expression VisitParameter(ParameterExpression p) =>
parametersToReplace.TryGetValue(p, out var e) ? e : base.VisitParameter(p);
}
现在你可以写
Model.AsDto()
如下:
// Lazily create the expression once and cache its value for performance.
static Lazy<Expression<Func<Model, ModelDto>>> AsDtoExpression = new(static () => {
// Create a curried lambda with a Func<AnotherChildModel, AnotherChildModelDto> passed in:
Expression<Func<Model, Func<AnotherChildModel, AnotherChildModelDto>, ModelDto>> asDtoCurried =
(model, map) => new ModelDto
{
// Set the values we can set directly:
ModelId = model.ModelId,
ModelName = model.ModelName,
ChildModels = model.ChildModels.AsQueryable().Select(ChildModel.AsDto).ToList(),
// But we can't call AnotherChildModel.AsDto() directly so sub in map for now.
AnotherChildModelDto = map(model.AnotherChildModel),
};
// Then inject AnotherChildModel.AsDto in place of the curried function:
return asDtoCurried.Inject(AnotherChildModel.AsDto);
});
public static Expression<Func<Model, ModelDto>> AsDto => AsDtoExpression.Value;
演示小提琴#1 这里。
备注:
简单地从
AnotherChildModel.AsDto()
内部调用 Model.Dto()
可能很诱人。虽然这可能适用于 LINQ to Objects,但它不适用于 Entity Framework 或其他 LINQ to Entities 应用程序,原因请参见 “LINQ to Entities 不支持 LINQ 表达式节点类型“Invoke”” - 难住了! .
我纯粹出于性能原因懒惰地构建表达式树并缓存结果。
同样的想法可以用于注入
ChildModel.AsDto()
,消除对.AsQueryable()
的需要:
// Lazily create the expression once and cache its value for performance.
static Lazy<Expression<Func<Model, ModelDto>>> AsDtoExpression = new(static () => {
// Create a curried lambda with a Func<AnotherChildModel, AnotherChildModelDto> passed in:
Expression<Func<Model, Func<ChildModel, ChildModelDto>, Func<AnotherChildModel, AnotherChildModelDto>, ModelDto>> asDtoCurried =
(model, map1, map2) => new ModelDto
{
// Set the values we can set directly:
ModelId = model.ModelId,
ModelName = model.ModelName,
// And sum in maps for the child models' AsDto() methods for now.
ChildModels = model.ChildModels.Select(c => map1(c)).ToList(),
AnotherChildModelDto = map2(model.AnotherChildModel),
};
// Now inject the child AsDto() methods in place of the curried functions.
return asDtoCurried.Inject(ChildModel.AsDto, AnotherChildModel.AsDto);
});
public static partial class ExpressionExtensions
{
public static Expression<Func<T1, TResult>> Inject<T1, T2, T3, T4, T5, TResult>(
this Expression<Func<T1, Func<T2, T3>, Func<T4, T5>, TResult>> outer,
Expression<Func<T2, T3>> inner1, Expression<Func<T4, T5>> inner2) =>
Expression.Lambda<Func<T1, TResult>>(
new InvokeReplacer((outer.Parameters[1], inner1), (outer.Parameters[2], inner2)).Visit(outer.Body),
false,
outer.Parameters[0]);
}
请注意,虽然实现了替换
Select(c => map1(c))
,但未实现替换 Select(map1)
。
演示小提琴 #2 这里。
有关编辑表达式树的更多内容,请参阅翻译表达式树和为什么我要使用ExpressionVisitor?。