扩展属性中的 Odata 过滤不起作用
Posted
技术标签:
【中文标题】扩展属性中的 Odata 过滤不起作用【英文标题】:Odata filtering in expanded property does not work 【发布时间】:2021-05-21 06:31:59 【问题描述】:我们创建了一个 .NET Core API,它使用 Odata 来过滤、选择和扩展数据。数据存储在 Microsoft SQL Server 数据库中,并通过 EntityFramework Core 检索(代码优先)。我们使用 Linq 投影,因此 Odata 过滤器直接应用于查询,但在以下情况下会出错:
检索结果列表时,例如作者用书籍扩展,一切正常。在扩展书籍中过滤时会出错,例如:https://localhost:44316/odata/authors?$expand=Books($filter=Id eq 1)
System.InvalidOperationException: The LINQ expression 'DbSet<Book>()
.Where(b0 => EF.Property<Nullable<int>>(EntityShaperExpression:
EntityType: Author
ValueBufferExpression:
ProjectionBindingExpression: EmptyProjectionMember
IsNullable: False
, "Id") != null && object.Equals(
objA: (object)EF.Property<Nullable<int>>(EntityShaperExpression:
EntityType: Author
ValueBufferExpression:
ProjectionBindingExpression: EmptyProjectionMember
IsNullable: False
, "Id"),
objB: (object)EF.Property<Nullable<int>>(b0, "AuthorId")))
.Where(b0 => b0
.ToDto().Id == __TypedProperty_1)' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'. See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.
at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.<VisitMethodCall>g__CheckTranslated|15_0(ShapedQueryExpression translated, <>c__DisplayClass15_0& )
at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.TranslateSubquery(Expression expression)
at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.Visit(Expression expression)
at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.VisitMemberAssignment(MemberAssignment memberAssignment)
at System.Linq.Expressions.ExpressionVisitor.VisitMemberBinding(MemberBinding node)
at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.VisitMemberInit(MemberInitExpression memberInitExpression)
at System.Linq.Expressions.MemberInitExpression.Accept(ExpressionVisitor visitor)
at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.Visit(Expression expression)
at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.VisitMemberAssignment(MemberAssignment memberAssignment)
at System.Linq.Expressions.ExpressionVisitor.VisitMemberBinding(MemberBinding node)
at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.VisitMemberInit(MemberInitExpression memberInitExpression)
at System.Linq.Expressions.MemberInitExpression.Accept(ExpressionVisitor visitor)
at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.Visit(Expression expression)
at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.Translate(SelectExpression selectExpression, Expression expression)
at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.TranslateSelect(ShapedQueryExpression source, LambdaExpression selector)
at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
at Microsoft.EntityFrameworkCore.Query.QueryCompilationContext.CreateQueryExecutor[TResult](Expression query)
at Microsoft.EntityFrameworkCore.Storage.Database.CompileQuery[TResult](Expression query, Boolean async)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.CompileQueryCore[TResult](IDatabase database, Expression query, IModel model, Boolean async)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.<>c__DisplayClass12_0`1.<ExecuteAsync>b__0()
at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func`1 compiler)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.ExecuteAsync[TResult](Expression query, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.ExecuteAsync[TResult](Expression expression, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1.GetAsyncEnumerator(CancellationToken cancellationToken)
at Microsoft.AspNetCore.Mvc.Infrastructure.AsyncEnumerableReader.ReadInternal[T](Object value)
at Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor.ExecuteAsyncEnumerable(ActionContext context, ObjectResult result, Object asyncEnumerable, Func`2 reader)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResultFilterAsync>g__Awaited|29_0[TFilter,TFilterAsync](ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResultExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNext[TFilter,TFilterAsync](State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeResultFilters()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
我已经在 odata github 页面上创建了一个issue,但这只是部分解决了我们的问题,我们对第二条评论没有任何反应。
这是我们用来从数据库模型映射到 DTO 的代码。
public static IQueryable<AuthorDTO> ProjectTo(IQueryable<Author> source)
return source?.Select(ProjectToAuthorDto());
private static Expression<Func<Author, AuthorDTO>> ProjectToAuthorDto()
return author => new AuthorDTO
Firstname = author.Firstname,
Lastname = author.Lastname,
Id = author.Id,
Books = author.Books.Select(book => book.ToDto())
;
public static BookDTO ToDto(this Book book)
return ProjectToBookDto().Compile().Invoke(book);
private static Expression<Func<Book, BookDTO>> ProjectToBookDto()
return book => new BookDTO
AuthorId = book.AuthorId,
Author = book.Author.ToDto(),
Id = book.Id,
ISBN = book.ISBN,
Title = book.Title
;
当我执行内联映射时,一切正常(见下图),但这不是解决方案,因为映射需要可重用。
此问题仅在 Odata 与 Linq 投影结合使用时才会出现。当我们删除 Odata 包时,一切都按预期返回。此外,当我们在返回结果之前执行查询时(通过添加 .ToList()),我们确实得到了预期的结果,但是,odata 过滤器并未应用于查询。我们在 .NET Core 3.1 和 .NET 5 中都有这个问题。 我在this repo 中为我们的问题创建了一个极其简化的最小版本。
我们的想法已经用尽,不知道下一步该尝试什么。我希望任何人都知道让过滤器工作。
提前致谢!
编辑
我按照 Svyatoslav Danyliv 的建议重新设计了助手。
public static IQueryable<AuthorDTO> ProjectTo(IQueryable<Author> source)
return source?.Select(item => item.ToDto());
[Computed]
public static AuthorDTO ToDto(this Author author)
return new AuthorDTO
Firstname = author.Firstname,
Lastname = author.Lastname,
Id = author.Id,
Books = author.Books.Select(book => book.ToDto())
;
[Computed]
public static BookDTO ToDto(this Book book)
return new BookDTO
AuthorId = book.AuthorId,
//Author = book.Author.ToDto(),
Id = book.Id,
ISBN = book.ISBN,
Title = book.Title
;
并通过以下方式调用它:
// Convert to DTO
IQueryable<AuthorDTO> result = CustomMapper.ProjectTo(authors);
return Ok(result.Decompile());
错误消失了,但现在结果被截断了:
我还在 SQL Server Profiler 中看到,当我应用 $filter 时,查询不再执行。
当我使用.DecompileAsync()
时,出现以下错误:
System.InvalidOperationException: The LINQ expression '$it' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'. See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.
at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.Visit(Expression expression)
at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.VisitMemberAssignment(MemberAssignment memberAssignment)
at System.Linq.Expressions.ExpressionVisitor.VisitMemberBinding(MemberBinding node)
at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.VisitMemberInit(MemberInitExpression memberInitExpression)
at System.Linq.Expressions.MemberInitExpression.Accept(ExpressionVisitor visitor)
at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.Visit(Expression expression)
at System.Linq.Expressions.ExpressionVisitor.VisitLambda[T](Expression`1 node)
at System.Linq.Expressions.Expression`1.Accept(ExpressionVisitor visitor)
at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.Visit(Expression expression)
at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.Visit(Expression expression)
at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.VisitConditional(ConditionalExpression conditionalExpression)
at System.Linq.Expressions.ConditionalExpression.Accept(ExpressionVisitor visitor)
at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.Visit(Expression expression)
at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.VisitMemberAssignment(MemberAssignment memberAssignment)
at System.Linq.Expressions.ExpressionVisitor.VisitMemberBinding(MemberBinding node)
at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.VisitMemberInit(MemberInitExpression memberInitExpression)
at System.Linq.Expressions.MemberInitExpression.Accept(ExpressionVisitor visitor)
at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.Visit(Expression expression)
at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.VisitMemberAssignment(MemberAssignment memberAssignment)
at System.Linq.Expressions.ExpressionVisitor.VisitMemberBinding(MemberBinding node)
at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.VisitMemberInit(MemberInitExpression memberInitExpression)
at System.Linq.Expressions.MemberInitExpression.Accept(ExpressionVisitor visitor)
at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.Visit(Expression expression)
at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.Translate(SelectExpression selectExpression, Expression expression)
at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.TranslateSelect(ShapedQueryExpression source, LambdaExpression selector)
at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
at Microsoft.EntityFrameworkCore.Query.QueryCompilationContext.CreateQueryExecutor[TResult](Expression query)
at Microsoft.EntityFrameworkCore.Storage.Database.CompileQuery[TResult](Expression query, Boolean async)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.CompileQueryCore[TResult](IDatabase database, Expression query, IModel model, Boolean async)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.<>c__DisplayClass12_0`1.<ExecuteAsync>b__0()
at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func`1 compiler)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.ExecuteAsync[TResult](Expression query, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.ExecuteAsync[TResult](Expression expression, CancellationToken cancellationToken)
at DelegateDecompiler.EntityFrameworkCore.AsyncDecompiledQueryProvider.ExecuteAsync[TResult](Expression expression, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1.GetAsyncEnumerator(CancellationToken cancellationToken)
at Microsoft.AspNetCore.Mvc.Infrastructure.AsyncEnumerableReader.ReadInternal[T](Object value)
at Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor.ExecuteAsyncEnumerable(ActionContext context, ObjectResult result, Object asyncEnumerable, Func`2 reader)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResultFilterAsync>g__Awaited|29_0[TFilter,TFilterAsync](ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResultExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNext[TFilter,TFilterAsync](State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeResultFilters()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
有什么想法吗?再次感谢!
编辑 2
Svyatoslav Danyliv 建议使用下面的代码并修复了上面的错误!
[EnableQuery(HandleNullPropagation = HandleNullPropagationOption.False]
感谢您的努力!
最终(工作)代码:
public static IQueryable<AuthorDTO> ProjectTo(IQueryable<Author> source)
return source?.Select(item => item.ToDto());
[Computed]
public static AuthorDTO ToDto(this Author author)
return new AuthorDTO
Firstname = author.Firstname,
Lastname = author.Lastname,
Id = author.Id,
Books = author.Books.Select(book => book.ToDto())
;
[Computed]
public static BookDTO ToDto(this Book book)
return new BookDTO
AuthorId = book.AuthorId,
//Author = book.Author.ToDto(),
Id = book.Id,
ISBN = book.ISBN,
Title = book.Title
;
然后调用它:
IQueryable<AuthorDTO> result = CustomMapper.ProjectTo(authors);
return Ok(result.DecompileAsync());
【问题讨论】:
【参考方案1】:嗯,在使用表达式树时,这是一个常见的错误。您不能以这种方式使用ToDto
。 LINQ 翻译器必须查看表达式树而不是编译的 lambda。
据我所知,有两个库可以帮助您实现此结果:
https://github.com/hazzik/DelegateDecompiler
https://github.com/axelheer/nein-linq
然后你必须重写你的辅助方法(使用 DelegateDecompiler):
[Computed]
public static BookDTO ToDto(this Book book)
return new BookDTO
AuthorId = book.AuthorId,
Author = book.Author.ToDto(),
Id = book.Id,
ISBN = book.ISBN,
Title = book.Title
;
[Computed]
public static AuthorDTO ToDto(this Author author)
...
并按照文档中的说明使用 .Decompile()
或 .DecompileAsync()
性能和查询翻译注意事项
EnableQuery
属性应该初始化为:
[EnableQuery(HandleNullPropagation = HandleNullPropagationOption.False,
HandleReferenceNavigationPropertyExpandFilter = false)]
另外,在 OData 中出现 bug 期间,查询将选择两次展开的项目:有过滤器和没有过滤器。这是当前 OData 库的一个限制。可能应该在他们的存储库中创建问题。
【讨论】:
感谢您的建议!我安装了 DelegateDecompiler 并重新设计了 ToDto 函数。错误消失了,但是 odata 结果被截断了。 截断是什么意思?生成了哪个 SQL? 请查看我编辑的问题。不再生成 SQL。 也许你需要DecompileAsync
?
你有问题。 EF Core 无法翻译 OData 生成的查询 - 微软不懂微软,这总是让我感到惊讶。以上是关于扩展属性中的 Odata 过滤不起作用的主要内容,如果未能解决你的问题,请参考以下文章
WebApi OData:$filter 'any' 或 'all' 查询不起作用
为啥 style 属性中定义的这个内联 SVG 过滤器不起作用?