Web API上的OData-如何查询嵌套属性



我目前正在自学OData,但遇到了无法解决的问题。要么是我误解了OData规范,要么我需要做点什么来让它发挥作用。

我建立了一个图书和作者实体(EF/CF)的小模型。从作者到书籍的一对多关系非常简单的东西:

modelBuilder.Entity<Book>().HasRequired(b => b.Author);
modelBuilder.Entity<Author>().HasMany(a => a.Books);

现在,当查询Authors时,我希望能够展开Books属性并对其(嵌套)属性进行筛选。例如,如果我问"哈利波特的书是谁写的",比如。。。

http://myBooksDatabase/Authors?$expand=Books&$filter=contains(Books/Name,'Harry Potter')&$select=Name

我得到这个错误响应:

{
    error: {
    code: ""
    message: "The query specified in the URI is not valid. The parent value for a property access of a property 'Name' is not a single value. Property access can only be applied to a single value."
    innererror: {
        message: "The parent value for a property access of a property 'Name' is not a single value. Property access can only be applied to a single value."
        type: "Microsoft.OData.Core.ODataException"
        stacktrace: " at Microsoft.OData.Core.UriParser.Parsers.EndPathBinder.BindEndPath(EndPathToken endPathToken) at Microsoft.OData.Core.UriParser.Parsers.MetadataBinder.BindEndPath(EndPathToken endPathToken) at Microsoft.OData.Core.UriParser.Parsers.MetadataBinder.Bind(QueryToken token) at Microsoft.OData.Core.UriParser.Parsers.MetadataBinder.BindFunctionParameter(FunctionParameterToken token) at Microsoft.OData.Core.UriParser.Parsers.MetadataBinder.Bind(QueryToken token) at Microsoft.OData.Core.UriParser.Parsers.FunctionCallBinder.<BindFunctionCall>b__8(FunctionParameterToken ar) at System.Linq.Enumerable.WhereSelectEnumerableIterator`2.MoveNext() at System.Collections.Generic.List`1..ctor(IEnumerable`1 collection) at Microsoft.OData.Core.UriParser.Parsers.FunctionCallBinder.BindFunctionCall(FunctionCallToken functionCallToken) at Microsoft.OData.Core.UriParser.Parsers.MetadataBinder.BindFunctionCall(FunctionCallToken functionCallToken) at Microsoft.OData.Core.UriParser.Parsers.MetadataBinder.Bind(QueryToken token) at Microsoft.OData.Core.UriParser.Parsers.FilterBinder.BindFilter(QueryToken filter) at Microsoft.OData.Core.UriParser.ODataQueryOptionParser.ParseFilterImplementation(String filter, ODataUriParserConfiguration configuration, IEdmType elementType, IEdmNavigationSource navigationSource) at Microsoft.OData.Core.UriParser.ODataQueryOptionParser.ParseFilter() at System.Web.OData.Query.FilterQueryOption.get_FilterClause() at System.Web.OData.Query.Validators.FilterQueryValidator.Validate(FilterQueryOption filterQueryOption, ODataValidationSettings settings) at System.Web.OData.Query.FilterQueryOption.Validate(ODataValidationSettings validationSettings) at System.Web.OData.Query.Validators.ODataQueryValidator.Validate(ODataQueryOptions options, ODataValidationSettings validationSettings) at System.Web.OData.Query.ODataQueryOptions.Validate(ODataValidationSettings validationSettings) at System.Web.OData.EnableQueryAttribute.ValidateQuery(HttpRequestMessage request, ODataQueryOptions queryOptions) at System.Web.OData.EnableQueryAttribute.ExecuteQuery(Object response, HttpRequestMessage request, HttpActionDescriptor actionDescriptor) at System.Web.OData.EnableQueryAttribute.OnActionExecuted(HttpActionExecutedContext actionExecutedContext)"
        }-
    }-
}

我意识到我可以通过查询Books实体来获得。。。

http://myBooksDatabase/Books?$expand=Author&$filter=contains(Name,'Harry')

但我遇到的问题来自于当我尝试引用嵌套属性时,无论我怎么做。上面的查询有效,并显示了整个Author实体,但如果我添加&$select=Author/Name,我会得到以下响应:

{
    error: {
    code: ""
    message: "The query specified in the URI is not valid. Found a path with multiple navigation properties or a bad complex property path in a select clause. Please reword your query such that each level of select or expand only contains either TypeSegments or Properties."
    innererror: {
        message: "Found a path with multiple navigation properties or a bad complex property path in a select clause. Please reword your query such that each level of select or expand only contains either TypeSegments or Properties."
        type: "Microsoft.OData.Core.ODataException"
        stacktrace: " at Microsoft.OData.Core.UriParser.Visitors.SelectPropertyVisitor.ProcessTokenAsPath(NonSystemToken tokenIn) at Microsoft.OData.Core.UriParser.Visitors.SelectPropertyVisitor.Visit(NonSystemToken tokenIn) at Microsoft.OData.Core.UriParser.Syntactic.NonSystemToken.Accept(IPathSegmentTokenVisitor visitor) at Microsoft.OData.Core.UriParser.Parsers.SelectBinder.Bind(SelectToken tokenIn) at Microsoft.OData.Core.UriParser.Parsers.SelectExpandBinder.Bind(ExpandToken tokenIn) at Microsoft.OData.Core.UriParser.Parsers.SelectExpandSemanticBinder.Bind(IEdmStructuredType elementType, IEdmNavigationSource navigationSource, ExpandToken expandToken, SelectToken selectToken, ODataUriParserConfiguration configuration) at Microsoft.OData.Core.UriParser.ODataQueryOptionParser.ParseSelectAndExpandImplementation(String select, String expand, ODataUriParserConfiguration configuration, IEdmStructuredType elementType, IEdmNavigationSource navigationSource) at Microsoft.OData.Core.UriParser.ODataQueryOptionParser.ParseSelectAndExpand() at System.Web.OData.Query.Validators.SelectExpandQueryValidator.Validate(SelectExpandQueryOption selectExpandQueryOption, ODataValidationSettings validationSettings) at System.Web.OData.Query.SelectExpandQueryOption.Validate(ODataValidationSettings validationSettings) at System.Web.OData.Query.Validators.ODataQueryValidator.Validate(ODataQueryOptions options, ODataValidationSettings validationSettings) at System.Web.OData.Query.ODataQueryOptions.Validate(ODataValidationSettings validationSettings) at System.Web.OData.EnableQueryAttribute.ValidateQuery(HttpRequestMessage request, ODataQueryOptions queryOptions) at System.Web.OData.EnableQueryAttribute.ExecuteQuery(Object response, HttpRequestMessage request, HttpActionDescriptor actionDescriptor) at System.Web.OData.EnableQueryAttribute.OnActionExecuted(HttpActionExecutedContext actionExecutedContext)"
        }-
    }-
}

这是我为作者和图书设计的两个OData控制器:

namespace My.OData.Controllers
{
    public class AuthorsController : ODataController
    {
        // GET /Author
        [EnableQuery]
        public IQueryable<Author> Get()
        {
            return MediaContext.Singleton.Authors;
        }
        // GET /Authors(<key>)
        [EnableQuery]
        public SingleResult<Author> Get([FromODataUri] Guid key)
        {
            var result = MediaContext.Singleton.Authors.Where(b => b.Id == key);
            return SingleResult.Create(result);
        }
        // GET /Authors(<key>)/Books
        [EnableQuery]
        public IQueryable<Book> GetBooks([FromODataUri] Guid key)
        {
            return MediaContext.Singleton.Authors.Where(a => a.Id == key).SelectMany(author => author.Books);
        } 
    }
    public class BooksController : ODataController
    {
        // GET /Books
        [EnableQuery]
        public IQueryable<Book> Get()
        {
            return MediaContext.Singleton.Books;
        }
        // GET /Books(<key>)
        [EnableQuery]
        public SingleResult<Book> Get([FromODataUri] Guid key)
        {
            var result = MediaContext.Singleton.Books.Where(b => b.Id == key);
            return SingleResult.Create(result);
        }
        // GET /Books(<key>)/Author
        [EnableQuery]
        public SingleResult<Author> GetAuthor([FromODataUri] Guid key)
        {
            return SingleResult.Create(MediaContext.Singleton.Books.Where(b => b.Id == key).Select(b => b.Author));
        } 
    }
}

那么,正如我所说,我是否还需要添加或配置其他内容来使相关实体中的引用属性发挥作用?

Jonas,我希望你最终能做到:)对于所有在家的赌客,乔纳斯已经确定了两个问题:

  1. 如果实体的至少一个子项满足标准,如何选择实体

  2. 如何展开子实体,但仅选择展开集中的特定列

答案1: 使用"any"函数将作者筛选为书籍名称中包含字符串"Harry Potter"的作者

http://myBooksDatabase/Authors?$filter=Books/any(b:contains(b/Name,'Harry Potter'))&$select=Name

参考:http://docs.oasis-open.org/odata/odata/v4.0/errata03/os/complete/part2-url-conventions/odata-v4.0-errata03-os-part2-url-conventions-complete.html

5.1.1.10 Lambda算子

OData定义了两个运算符,用于计算集合上的布尔表达式。两者都必须以标识集合的导航路径作为前缀。lambda运算符的参数是一个lambda变量名,后跟冒号(:)和一个布尔表达式,该表达式使用lambda变量名称引用导航路径标识的相关实体的属性。

5.1.1.10.1任何

any运算符将布尔表达式应用于集合的每个成员,如果该表达式对于集合的任何成员都为true,则返回true,否则返回false。如果集合不为空,则不带参数的any运算符将返回true。

示例79:具有数量大于100的任何项目的所有订单http://host/service/Orders?$filter=项目/任何(d:d/数量gt 100)

5.1.1.10.2所有

all运算符将布尔表达式应用于集合的每个成员,如果该表达式对集合的所有成员都为true,则返回true,否则返回false。示例80:只有数量大于100 的项目的所有订单

http://host/service/Orders?$filter=项目/全部(d:d/数量gt 100)

答案2:在Books"$expand"语句中使用嵌套的"$select"来限制应在展开中返回的列

http://myBooksDatabase/Authors?$filter=Books/any(b:contains(b/Name,'Harry Potter'))&$select=Name&$expand=Books($select=Name,ISBN)

也适用于提供的其他示例:

http://myBooksDatabase/Books?$expand=Author($select=Name)&$filter=contains(Name,'Harry')&$select=Name,ISBN

但这两个查询并不完全相同。第一个查询会找到以"哈利波特"为名写过一本书的作者,但$expand会列出作者写过的所有书,即使"哈利波特》"不在名字中。

这并不是一个完整的结果集,只是一个例子来说明这一点,请注意吟游诗人比德尔的故事的名字中不包括字符串哈利波特,但它被返回是因为作者写过其他名字中有哈利·波特

[ 
  { Name: "J K Rowling", Books: [ 
    { Name: "Harry Potter and the Philosopher's Stone", ISBN: "9781408855652" },
    { Name: "The Tales of Beedle the Bard", ISBN: "9780747599876" },
    { Name: "Harry Potter and the Cursed Child - Parts I and II", ISBN: "9780751565355" }
    ] },
  { Name: "Bruce Foster", Books: [
    { Name: "Harry Potter: A Pop-Up Book: Based on the Film Phenomenon", ISBN: "9781608870080" }
    ]}
]

第二个查询将返回数据库中所有名称为"哈利波特"的书籍,而不考虑作者,但将包括作者的姓名:

[ 
  { Name: "Harry Potter and the Philosopher's Stone", ISBN: "9781408855652", Author: { Name: "J K Rowling" } },
  { Name: "Harry Potter and the Cursed Child - Parts I and II", ISBN: "9780751565355", Author: { Name: "J K Rowling" } },
  { Name: "Harry Potter: A Pop-Up Book: Based on the Film Phenomenon", ISBN: "9781608870080", Author: { Name: "Bruce Foster" } }
]

因此,尽管OP建议您可以通过更改要从中选择的主控制器来获得类似的数据,但数据的形状不同,可能包含冗余/复制信息,或者可能会过滤掉您原本期望的行。如果你改变控制器,你将改变数据的结果形状,所以不要匆忙做出这样的决定。

请注意,尽管OData v4规范包括许多不同的选项,用于使用$select$expand,但并非所有这些语法选项都受官方NuGet包提供的ASP.Net Web API实现的支持。。。

我不确定官方的推理,但到目前为止,我还没有因为规范的这种有限实现而处于不利地位(当涉及到嵌套的$expand$select时)

此解决方案中提供的示例已针对包Microsoft.AspNet.OData v5.6.0-5.9.1 进行了测试

最新更新