扩展属性中的 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' 查询不起作用

网络扩展网页内容过滤器在 iOS 中不起作用

为啥我在 vue.js 中的自定义搜索过滤器不起作用?

为啥 style 属性中定义的这个内联 SVG 过滤器不起作用?

Emmet 扩展缩写在具有属性的 Visual Studio Code 中不起作用

引导表过滤不起作用