Can a LINQ query with a where clause on the key to an OData service be done with filter query option instead of a canonical URL?

huangapple go评论46阅读模式
英文:

Can a LINQ query with a where clause on the key to an OData service be done with filter query option instead of a canonical URL?

问题

问题

我正在尝试从OData V4服务中查询数据。这是通过使用Visual Studio的OData Connected Service扩展生成的C#客户端完成的。查询是通过一个包含实体的键字段的LINQ表达式进行的。

查询结果是一个DataServiceQueryException("在处理此请求时发生错误"),并包含来自Microsoft.OData.Client命名空间的内部DataServiceClientException("未找到")。请参见下面的完整堆栈跟踪。

分析

使用Fiddler,我可以看到发送的请求使用规范URL(也称为按键请求)。如果标准值与任何现有数据不匹配,响应将具有代码404未找到。这个代码似乎引起了异常。

当where子句更改为包括非键字段时,请求将使用$filter查询选项发送。在这种情况下,如果标准值与任何现有数据不匹配,响应将具有代码200 OK。这不会引发异常,并将LINQ查询的结果返回为空。

另一个解决方法是不使用LINQ,而是明确指定应使用筛选查询选项。

与OData参考服务TripPin的比较显示,在这种情况下,404响应似乎不是正确的响应。相反,TripPin返回204无内容。尽管OData规范有一些迹象表明这似乎是正确的响应,但我找不到明确的说明。无论如何,由于我无法控制OData服务并更改其行为,这一点没有意义。

复制详细信息

不幸的是,所涉及的OData服务不是公开可用的。可能可以模拟这样的服务或找到显示相同行为的公共服务。自从我找到了解决方案以来,我还没有研究过这一点。

尽管如此,以下是导致异常的代码:

static void GetData()
{
  Uri odataUri = new Uri("https://the-odata-service", UriKind.Absolute);
  // Resources是由OData Connected Service扩展生成的类
  // 并扩展了Microsoft.OData.Client.DataServiceContext
  Resources context = new Resources(odataUri);

  var entity = context.Entities.Where(x => x.Key == 1).SingleOrDefault();
}

生成这个请求和响应:

GET https://the-odata-service/entities(1) HTTP/1.1

HTTP/1.1 404 Not Found

异常:

未处理的异常。Microsoft.OData.Client.DataServiceQueryException:在处理此请求时发生错误。
 ---> Microsoft.OData.Client.DataServiceClientException:未找到
   at Microsoft.OData.Client.QueryResult.ExecuteQuery()
   at Microsoft.OData.Client.DataServiceRequest.Execute[TElement](DataServiceContext context, QueryComponents queryComponents)
   --- 内部异常堆栈跟踪的结尾 ---
   at Microsoft.OData.Client.DataServiceRequest.Execute[TElement](DataServiceContext context, QueryComponents queryComponents)
   at Microsoft.OData.Client.DataServiceQuery`1.GetEnumerator()
   at System.Linq.Enumerable.TryGetSingle[TSource](IEnumerable`1 source, Boolean& found)
   at System.Linq.Enumerable.SingleOrDefault[TSource](IEnumerable`1 source)
   at Microsoft.OData.Client.DataServiceQueryProvider.ReturnSingleton[TElement](Expression expression)
   at System.Linq.Queryable.SingleOrDefault[TSource](IQueryable`1 source)
   at <在GetData方法中SingleOranull的调用行中的测试程序>

如果我将LINQ更改为:

var entity = context.Entities
  .Where(x => 
    x.Key == 1
    && x.AnotherNonKeyField == "2")
  .SingleOrDefault();

我得到:

GET https://the-odata-service/Entities?$filter=Key%20eq%201%20and%20AnotherNonKeyField%20eq%20'2'&$top=2 HTTP/1.1

HTTP/1.1 200 OK
{
  " @odata.context ":"https://the-odata-service/$metadata#Entities"," value ":[
    
  ]
}

这不会导致异常,但entity为空。

问题

总之,虽然有解决方法,但我更喜欢使用LINQ查询OData服务,而无需添加虚拟标准(这不一定总是可能的)。有没有办法做到这一点?

英文:

The problem

I'm trying to query data from an OData V4 service. This is done with a C# client generated by the OData Connected Service extension for Visual Studio. The query is done with a LINQ expression with a where clause. The where clause contains criteria for the key fields of the entity being queried.

The query results in a DataServiceQueryException ("An error occurred while processing this request") with an inner DataServiceClientException ("NotFound"), both from the Microsoft.OData.Client namespace. See below for the full stack trace.

Analysis

Using Fiddler I can see that the request being sent is using a canonical URL (also called a by-key request). If the criteria values do not match any existing data, the response has the code 404 Not Found. This code seems to cause the exception.

When the where clause is changed to also include non-key fields, the request is sent using a $filter query option. In this case, if the criteria values do not match any existing data, the response has the code 200 OK. This does not cause an exception and returns null as result of the LINQ query.

Another workaround is to not use LINQ and instead specify explicitely that a filter query option should be used.

Comparison with the OData reference service TripPin showed that the 404 response does not seem to be the correct response in this case. TripPin instead returns 204 No Content. While the OData specification has several indications that this seems the correct response in this case, I could not find an explicit statement to that effect. In any case, this point is moot since I don't have control over the OData service and can't change its behavior.

Repro details

Unfortunately, the OData service in question is not publicly available. It may be possible to mock such a service or find a public service that shows the same behavior. I have not looked into this since I found a solution (see my answer).

Nevertheless, here is the code that causes the exception:

static void GetData()
{
  Uri odataUri = new Uri("https://the-odata-service", UriKind.Absolute);
  // Resources is a class generated by the OData Connected Service extension
  // and extends Microsoft.OData.Client.DataServiceContext
  Resources context = new Resources(odataUri);

  var entity = context.Entities.Where(x => x.Key == 1).SingleOrDefault();
}

Producing this request and response:

GET https://the-odata-service/entities(1) HTTP/1.1

HTTP/1.1 404 Not Found

The exception:

Unhandled exception. Microsoft.OData.Client.DataServiceQueryException: An error occurred while processing this request.
 ---> Microsoft.OData.Client.DataServiceClientException: NotFound
   at Microsoft.OData.Client.QueryResult.ExecuteQuery()
   at Microsoft.OData.Client.DataServiceRequest.Execute[TElement](DataServiceContext context, QueryComponents queryComponents)
   --- End of inner exception stack trace ---
   at Microsoft.OData.Client.DataServiceRequest.Execute[TElement](DataServiceContext context, QueryComponents queryComponents)
   at Microsoft.OData.Client.DataServiceQuery`1.GetEnumerator()
   at System.Linq.Enumerable.TryGetSingle[TSource](IEnumerable`1 source, Boolean& found)
   at System.Linq.Enumerable.SingleOrDefault[TSource](IEnumerable`1 source)
   at Microsoft.OData.Client.DataServiceQueryProvider.ReturnSingleton[TElement](Expression expression)
   at System.Linq.Queryable.SingleOrDefault[TSource](IQueryable`1 source)
   at <my test program in the GetData method in the line of the SingleOrDefault call>

If I change the LINQ to

var entity = context.Entities
  .Where(x => 
    x.Key == 1
    && x.AnotherNonKeyField == "2")
  .SingleOrDefault();

I get

GET https://the-odata-service/Entities?$filter=Key%20eq%201%20and%20AnotherNonKeyField%20eq%20'2'&$top=2 HTTP/1.1

HTTP/1.1 200 OK
{
  "@odata.context":"https://the-odata-service/$metadata#Entities","value":[
    
  ]
}

which does not result in an exception, but entity being null.

The question

To sum up, while there are workarounds, I would prefer if I could query the odata service with LINQ and without having to add dummy criteria (which would not always be possible). Is there a way to do that?

答案1

得分: 0

TLDR

DataServiceContextKeyComparisonGeneratesFilterQuery 属性可用于生成 $filter 查询选项。

更多背景信息

我花了一些时间研究这个问题,涉及 LINQ 和生成的客户端。事后看来,显然 Microsoft OData 客户端库是一个更好的起点,因为它引发了异常。但是,当你可以疯狂地搜索和调试几个小时时,谁有时间去阅读堆栈跟踪呢,叹气?

最终,我找到了 问题#851 DataServiceQuery在Where子句仅比较ID时发出“按键”请求,如果未找到实体,则引发异常而不是空结果拉取请求#1762 启用Where子句生成关键谓词的$filter查询选项。特别是后者更好地解释了 KeyComparisonGeneratesFilterQuery 属性的目的以及如何使用它,而不是文档。

有了这个,上面的代码可以像这样修复:

static void GetData()
{
  Uri odataUri = new Uri("https://the-odata-service", UriKind.Absolute);
  // Resources 是由 OData Connected Service 扩展生成的类
  // 并扩展了 Microsoft.OData.Client.DataServiceContext
  Resources context = new Resources(odataUri);
  context.KeyComparisonGeneratesFilterQuery = true;

  var entity = context.Entities.Where(x => x.Key == 1).SingleOrDefault();
}

这将生成

GET https://the-odata-service/Entities?$filter=Key%20eq%201&$top=2 HTTP/1.1

HTTP/1.1 200 OK
{
  "@odata.context":"https://the-odata-service/$metadata#Entities","value":[
    
  ]
}
英文:

TLDR

The KeyComparisonGeneratesFilterQuery property of the DataServiceContext can be used to generate a $filter query option.

Some more background

I spent some time researching this issue in context of LINQ and the client that was generated. In hindsight, it is obvious that the Microsoft OData Client library would have been a better place to start, since it throws the exception. But who has time to read a stack trace when instead you can furiously google and debug for a few hours *sigh* ?

Eventually I found my way to issue #851 DataServiceQuery makes a "by key" request when Where clause compares just the ID, causing exception instead of empty result if the entity is not found. and pull request #1762 Enable Where clause to generate $filter query options for key predicates. Especially the later does a much better job of explaining the purpose and how to use the KeyComparisonGeneratesFilterQuery property than the documentation.

With that, the above code can be fixed like this:

static void GetData()
{
  Uri odataUri = new Uri("https://the-odata-service", UriKind.Absolute);
  // Resources is a class generated by the OData Connected Service extension
  // and extends Microsoft.OData.Client.DataServiceContext
  Resources context = new Resources(odataUri);
  context.KeyComparisonGeneratesFilterQuery = true;

  var entity = context.Entities.Where(x => x.Key == 1).SingleOrDefault();
}

Which produces

GET https://the-odata-service/Entities?$filter=Key%20eq%201&$top=2 HTTP/1.1

HTTP/1.1 200 OK
{
  "@odata.context":"https://the-odata-service/$metadata#Entities","value":[
    
  ]
}

huangapple
  • 本文由 发表于 2023年2月19日 22:44:12
  • 转载请务必保留本文链接:https://go.coder-hub.com/75500923.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定