我正在 .NET 中使用 LINQ 编写数据库查询,并且我希望能够不重复我在
Where
方法调用中放入的代码。
我想返回有新鲜
Blogs
的Comments
,并且想同时过滤Blogs
和Comments
(所以,没有没有评论的博客,也没有旧的评论)
这需要我写一些类似的东西:
ctx.Blogs.Include(blog => blog.Comments.Where(comment => comment.Created < dateTime))
.Where(blog => blog.Comments.Any(comment => comment.Created < dateTime))
.Select(b => new BlogEntryDTO(b));
注意,
comment => comment.Created < dateTime)
是如何完全相同的。
(当然,这是一个玩具示例,真正的查询有一个更复杂的过滤器)
我做了显而易见的事情,并尝试将过滤器提取为
Expression
:
public static IQueryable<BlogEntryDTO> GetBlogsExpression(MyContext ctx, DateTime dateTime)
{
Expression<Func<Comment, bool>> inTime = comment => comment.Created < dateTime;
return ctx
.Blogs.Include(blog => blog.Comments.Where(inTime))
.Where(blog => blog.Comments.Any(inTime))
.Select(b => new BlogEntryDTO(b));
}
但这会产生编译时错误:
Cannot resolve method 'Where(Expression<Func<Comment,bool>>)', candidates are:
IEnumerable<Comment> Where<Comment>(this IEnumerable<Comment>, Func<Comment,bool>) (in class Enumerable)
IEnumerable<Comment> Where<Comment>(this IEnumerable<Comment>, Func<Comment,int,bool>) (in class Enumerable)
这听起来像是它想要
Func
,而不是表达,所以我尝试这样做:
public static IQueryable<BlogEntryDTO> GetBlogsFunction(MyContext ctx, DateTime dateTime)
{
Func<Comment, bool> inTime = comment => comment.Created < dateTime;
return ctx
.Blogs.Include(blog => blog.Comments.Where(inTime))
.Where(blog => blog.Comments.Any(inTime))
.Select(b => new BlogEntryDTO(b));
}
可以编译,但会产生运行时错误:
Unhandled exception.
ArgumentException: Expression of type 'Func`[Comment,Boolean]' cannot be used for parameter of type 'Expression`[Func`[Comment,Boolean]]' of method 'IQueryable`[Comment] Where[Comment](IQueryable`1[Comment], Expression`1[Func`2[Comment,Boolean]])' (Parameter 'arg1')
at Dynamic.Utils.ExpressionUtils.ValidateOneArgument(MethodBase method, ExpressionType nodeKind, Expression arguments, ParameterInfo pi, String methodParamName, String argumentParamName, Int32 index)
...
毫不奇怪,它基本上不知道如何将
Func
转换为SQL。
此后我陷入困境。
这主要是在 .NET 中使用 Entity Framework Core 时如何重用Where调用中的逻辑?,但我在评论中被要求重新发布我自己的失败查询,所以我们开始吧。
完整的可运行代码示例:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Linq.Expressions;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
namespace ConsoleApp1;
internal class Program
{
private static void Main(string[] args)
{
using var ctx = MyContext.MakeInMemoryContext();
var blogs = GetBlogsInlined(ctx, DateTime.Today).ToList();
Console.WriteLine(JsonSerializer.Serialize(blogs));
var blogs2 = GetBlogsExpression(ctx, DateTime.Today).ToList();
Console.WriteLine(JsonSerializer.Serialize(blogs2));
var blogs3 = GetBlogsFunction(ctx, DateTime.Today).ToList();
Console.WriteLine(JsonSerializer.Serialize(blogs3));
}
public static IQueryable<BlogEntryDTO> GetBlogsInlined(MyContext ctx, DateTime dateTime)
{
return ctx
.Blogs.Include(blog => blog.Comments.Where(comment => comment.Created < dateTime))
.Where(blog => blog.Comments.Any(comment => comment.Created < dateTime))
.Select(b => new BlogEntryDTO(b));
}
// Compile time error:
// Cannot resolve method 'Where(Expression<Func<Comment,bool>>)', candidates are:
// IEnumerable<Comment> Where<Comment>(this IEnumerable<Comment>, Func<Comment,bool>) (in class Enumerable)
// IEnumerable<Comment> Where<Comment>(this IEnumerable<Comment>, Func<Comment,int,bool>) (in class Enumerable)
public static IQueryable<BlogEntryDTO> GetBlogsExpression(MyContext ctx, DateTime dateTime)
{
Expression<Func<Comment, bool>> inTime = comment => comment.Created < dateTime;
return ctx
.Blogs.Include(blog => blog.Comments.Where(inTime))
.Where(blog => blog.Comments.Any(inTime))
.Select(b => new BlogEntryDTO(b));
}
// Runtime error:
// Unhandled exception.
// ArgumentException: Expression of type 'Func`[Comment,Boolean]' cannot be used for parameter of type 'Expression`[Func`[Comment,Boolean]]' of method 'IQueryable`[Comment] Where[Comment](IQueryable`1[Comment], Expression`1[Func`2[Comment,Boolean]])' (Parameter 'arg1')
// at Dynamic.Utils.ExpressionUtils.ValidateOneArgument(MethodBase method, ExpressionType nodeKind, Expression arguments, ParameterInfo pi, String methodParamName, String argumentParamName, Int32 index)
public static IQueryable<BlogEntryDTO> GetBlogsFunction(MyContext ctx, DateTime dateTime)
{
Func<Comment, bool> inTime = comment => comment.Created < dateTime;
return ctx
.Blogs.Include(blog => blog.Comments.Where(inTime))
.Where(blog => blog.Comments.Any(inTime))
.Select(b => new BlogEntryDTO(b));
}
public class MyContext(DbContextOptions<MyContext> options) : DbContext(options)
{
public DbSet<BlogEntry> Blogs { get; set; }
public DbSet<Comment> Comments { get; set; }
public static MyContext MakeInMemoryContext()
{
var builder = new DbContextOptionsBuilder<MyContext>().UseInMemoryDatabase("context");
var ctx = new MyContext(builder.Options);
ctx.Database.EnsureDeleted();
ctx.Database.EnsureCreated();
ctx.SetupBlogs();
return ctx;
}
private void SetupBlogs()
{
Blogs.AddRange(
[
new BlogEntry
{
Name = "1",
Created = DateTime.Now.AddDays(-3),
Comments =
[
new Comment { Content = "c1", Created = DateTime.Now.AddDays(-2) },
new Comment { Content = "c2", Created = DateTime.Now.AddDays(-1) }
]
},
new BlogEntry
{
Name = "2",
Created = DateTime.Now.AddDays(-2),
Comments = [new Comment { Content = "c3", Created = DateTime.Now.AddDays(-1) }]
},
new BlogEntry { Name = "2", Created = DateTime.Now.AddDays(-1), Comments = [] }
]
);
SaveChanges();
}
}
public class BlogEntry
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public DateTime Created { get; set; }
public virtual ICollection<Comment> Comments { get; set; }
}
public class Comment
{
[Key]
public int Id { get; set; }
public string Content { get; set; }
public DateTime Created { get; set; }
public int BlogEntryId { get; set; }
public virtual BlogEntry BlogEntry { get; set; }
}
public class BlogEntryDTO(BlogEntry blogEntry)
{
public int Id { get; set; } = blogEntry.Id;
public string Name { get; set; } = blogEntry.Name;
public string[] Comments { get; set; } = blogEntry.Comments.Select(c => c.Content).ToArray();
}
}
.csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.6">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
</Project>
表达式树表示“树状数据结构中的代码”。 与委托(代表已编译的方法)不同,语言或运行时中没有内置运算符来组合、组合、柯里化或非柯里化表达式树,因此,如果您想将您的
inTime
表达式包含在其他表达式中,您可以需要通过创建修改后的表达式树来手动执行此操作。
问题的答案我可以重用代码来使用 EF Core 为子属性选择自定义 DTO 对象吗?显示了几种允许进行表达式树编辑的工具,包括内置类型
ExpressionVisitor
。 因此,如果您想将 inTime
表达式注入到一些更复杂的表达式中,请首先创建以下扩展方法(改编自 here):
public static partial class ExpressionExtensions
{
public static Expression<Func<T1, TResult>> InjectInto<T1, T2, T3, TResult>(this Expression<Func<T2, T3>> inner, Expression<Func<T1, Func<T2, T3>, TResult>> outer) =>
outer.Inject(inner);
// 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]);
}
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(params (ParameterExpression parameter, Expression replacement) [] parametersToReplace)
: this(parametersToReplace.AsEnumerable()) { }
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);
}
现在您可以重写您的
GetBlogsExpression()
方法,如下所示:
public static IQueryable<BlogEntryDTO> GetBlogsExpression(MyContext ctx, DateTime dateTime)
{
Expression<Func<Comment, bool>> inTime = comment => comment.Created < dateTime;
return ctx
.Blogs
.Include(inTime.InjectInto((BlogEntry b, Func<Comment, bool> f) => b.Comments.Where(c => f(c)))) // Injecting inTime into b.Comments.Where(f) so use c => f(c) instead
.Where(inTime.InjectInto((BlogEntry b, Func<Comment, bool> f) => b.Comments.Any(c => f(c))))
.Select(b => new BlogEntryDTO(b));
}
备注:
您的
GetBlogsFunction()
失败,因为 EF Core 无法将表达式内的任意 Invoke()
调用(特别是 intime(Comment c)
的调用)转换为对底层数据库引擎的查询。
您的
GetBlogsExpression()
无法编译,因为如前所述,没有构建表达式的内置功能。 当您尝试这样做时,会导致类型推断出错,最终导致编译器消息混乱。
演示小提琴这里。