C# LINQ .Any 不适用于 DocumentDb CreateDocumentQuery

C# LINQ .Any not working on DocumentDb CreateDocumentQuery

我正在尝试查询具有特定类型产品的 Art。这是我的艺术模型:

  public string Title { get; set; }
  public string Description { get; set; }
  public List<Product> Products { get; set; }
  public string PaintedLocation { get; set; }

从这里我所做的就是以下 LINQ 查询:

List<Art> items = DocumentDbHelper.Client.CreateDocumentQuery<Art>(collection.DocumentsLink)
                               .Where(i => i.type == "art")
                               .Where(i => i.Products.Any(p => p.Name == productType))
                               .AsEnumerable()
                               .ToList();

我收到以下错误:

"Method 'Any' is not supported."

我转到了代码引用的页面以查看 what is supported,但我没有看到它说不支持 Any(),所以我可能做错了什么。感谢任何帮助。

更新

这对我来说真的很奇怪,所以我将其分解以查看从两个结果返回的内容,以便更好地调试问题:

List<Art> items = DocumentDbHelper.Client.CreateDocumentQuery<Art>(collection.DocumentsLink)
                       .Where(i => i.Id.Contains("art"))
                       .AsEnumerable()
                       .ToList();

items = items.Where(i => i.Products.Any(p => p.Name == productType))
             .AsEnumerable()
             .ToList();

出于某种原因,这行得通,我不喜欢这个,因为我将其转换为列表,所以 运行 查询了两次 - 但它至少证明了 Any() 和Select() 在技术上应该可行。

我正在使用针对 .Net 4.6 的最新 Azure DocumentDB nuget。

<package id="Microsoft.Azure.DocumentDB" version="1.5.0" targetFramework="net46" />

这是对我来说工作正常的示例代码。

using System.Collections.Generic;
using System.Linq;
using Microsoft.Azure.Documents.Client;
using Microsoft.Azure.Documents.Linq;

var book = client.CreateDocumentQuery<Book>(collectionLink)
                    .Where(b => b.Title == "War and Peace")
                    .Where(b => b.Publishers.Any(p => p.IsNormalized()))
                    .AsEnumerable().FirstOrDefault();
public class Book
{
    [JsonProperty("title")]
    public string Title { get; set; }

    public Author Author { get; set; }

    public int Price { get; set; }

    public List<string> Publishers { get; set; }

}

public class Author
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

您应该尝试使用 IEnumerable.Contains link here

DbHelper.Client.CreateDocumentQuery<Art>(collection.DocumentsLink)
   .Where(i => i.type == "art")
   .Where(i => i.Products
       .Select(p => p.Name).Contains(productType))
                               .AsEnumerable()
                               .ToList();

针对 IQueryable<T> 的 LINQ 查询最大的混淆之一是它们看起来与针对 IEnumerable<T> 的查询完全相同。好吧,前者在后者使用 Func<..> 时使用 Expression<Func<..>>,但除非有人使用显式声明,否则这不是那么引人注目并且似乎不重要。然而,最大的不同在于运行时。

一旦 IEnumerable<T> 查询被成功编译,在运行时它就可以正常工作,而 IQueryable<T> 则不是这样。 IQueryable<T> 查询实际上是一个表达式树,由查询提供程序在运行时处理。

一方面,这是一个很大的好处,另一方面,因为在查询编译时不涉及查询提供程序(所有方法都作为扩展方法由 Queryable class ),在运行时之前无法知道提供者是否支持某些 construct/method。使用 Linq to Entities 的人非常了解这一点。使事情变得更难的是,没有明确的文档说明特定查询提供程序支持什么,更重要的是,它不支持什么(正如您从提供的“支持的内容”link 中注意到的那样)。

有什么解决办法? (以及为什么你的第二个代码有效)

诀窍是针对 IQueryable<T> 编写最大可能的(查询提供者 i.e.supported)查询部分,然后切换到 IEnumerable<T> 并完成其余部分(记住,一旦编译,IEnumerable<T> 查询就可以了)。切换由 AsEnumerable() 调用执行。这就是您的第二个代码起作用的原因 - 因为不受支持的 Any 方法不再出现在 DocumentDb 查询提供程序上下文中。请注意,不需要 ToList 调用并且查询不会执行两次 - 事实上,这种方式没有单个查询,而是两个 - 一个在数据库中,一个在内存中。

所以像这样就足够了:

List<Art> items = DocumentDbHelper.Client.CreateDocumentQuery<Art>(collection.DocumentsLink)
                               .Where(i => i.type == "art")
                               .AsEnumerable() // The context switch!
                               .Where(i => i.Products.Any(p => p.Name == productType))
                               .ToList();

最后,DocumentDb 查询提供程序真正支持什么

文档中的内容不是很清楚,但答案是:完全(且仅)包含在那里的内容。换句话说,唯一受支持的查询运算符(或者更好地说 QueryableEnumerable 扩展方法)是

  • Select
  • Select很多
  • 在哪里
  • 排序方式
  • 降序排列

如您所见,它非常有限。忘掉连接和分组运算符,AnyContainsCountFirstLast 等。唯一的好处是它很容易记住 :)

我怎么知道的?好吧,像往常一样,当文档中的内容不清楚时,要么使用试错法,要么使用反编译器。显然在这种情况下前者不适用,所以我使用了后者。如果你好奇,使用你最喜欢的反编译器并检查Microsoft.Azure.Documents.Client.dll.

内部classDocumentQueryEvaluator的代码

你为什么不试试这个?

 List<Art> items =  DocumentDbHelper.Client.CreateDocument(collection.DocumentsLink)
                           .Where(i => i.type == "art" && i.Products.Any(p => p.Name == productType))
                           .AsEnumerable()
                           .ToList();

目前最高效的解决方案是使用 SQL 语法,因为它允许文档数据库使用集合的索引。
例子:

SELECT a 
  FROM a
  JOIN p in a.Products
 WHERE ARRAY_CONTAINS(a.Id, 'art') 
   AND p.Name = 'My Product Type'

缺点是您可能会得到不唯一的结果,并且必须在客户端区分结果。

要将此问题纳入 DocumentDB,将有助于对以下项目进行投票: https://feedback.azure.com/forums/263030-documentdb/suggestions/14829654-support-sub-query-functions-like-exists-not-exist