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

6
我目前在自学OData,但遇到了一个我无法解决的情况。要么是我误解了OData规范,要么需要做些什么来使它正常工作。
我已经建立了一个小型的书籍和作者实体模型(EF/CF)。很简单的内容,从作者到书籍有一个一对多的关系:
modelBuilder.Entity<Book>().HasRequired(b => b.Author);
modelBuilder.Entity<Author>().HasMany(a => a.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')

...但我在尝试引用嵌套属性时遇到了问题,无论我怎么做。上面的查询有效,并呈现整个作者实体,但如果我添加&$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));
        } 
    }
}

所以,就像我所说的那样,还有其他需要添加或配置的内容来使关联实体中的引用属性工作吗?

2
好的,进一步调查和阅读OData v4规范教会了我筛选语法应该使用“any”函数,就像这样: http://myBooksDatabase/Authors?$filter=Books/any(b: contains(b/Name, 'Harry Potter'))。这个方法可行,但如果我需要一些书籍属性,比如标题和ISBN怎么办?我仍然无法弄清楚如何指定一个$select语句来限制嵌套属性到我需要的那些。 - Jonas Rembratt
1个回答

12

乔纳斯,我希望你最终解决了问题 :) 对于在家的所有赌徒,乔纳斯已经确定了两个问题:

  1. 如果至少有一个子项满足某些条件,如何选择实体

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

答案1: 使用“任何”函数来过滤作者,以便将其限制为具有包含字符串“哈利波特”在书名中的书的作者

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

REF: 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 Operators
OData定义了两个操作符,用于在集合上评估布尔表达式。这两个操作符都必须以标识集合的导航路径为前缀。lambda操作符的参数是一个lambda变量名称,后跟一个冒号(:)和一个布尔表达式,该布尔表达式使用lambda变量名称来引用由导航路径标识的相关实体的属性。
5.1.1.10.1 any
any操作符将布尔表达式应用于集合的每个成员,并在集合中的任何成员满足表达式时返回true,否则返回false。没有参数的any操作符在集合不为空时返回true。
示例79:所有具有数量大于100的项目的订单 http://host/service/Orders?$filter=Items/any(d:d/Quantity gt 100)
5.1.1.10.2 all
all操作符将布尔表达式应用于集合的每个成员,并在集合中的所有成员均满足表达式时返回true,否则返回false。 示例80:仅具有数量大于100的项目的所有订单 http://host/service/Orders?$filter=Items/all(d:d/Quantity 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进行测试


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接