31

I'm trying to query Art that has a product of a certain type. Here is my model for Art:

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

From here all I'm doing is the following LINQ query:

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();

I get the following error:

"Method 'Any' is not supported."

I went to the page that the code references for seeing what is supported but I don't see it saying that Any() is not supported, so I'm probably doing something incorrect. Any help is appreciated.

UPDATE

This is really odd to me, so I broke it up to see what was being returned from the two results to better debug the issue to this:

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();

For some reason this works, I'm not a fan of this because since I'm converting it to a list it's running the query twice - but it is at least proof that Any() and Select() should technically work.

Pavel Anikhouski
  • 21,776
  • 12
  • 51
  • 66
gregwhitworth
  • 2,136
  • 4
  • 22
  • 33
  • Hi, are you able to put the details error here? – juvchan Nov 21 '15 at 04:59
  • I added it in, but it won't add much more to this. The source of the error is Microsoft.Azure.Documents.Client – gregwhitworth Nov 21 '15 at 05:03
  • Does the documentation say it's supported? I don't think you should assume it is just because they didn't mention explicitly that it's not. – MarcinJuraszek Nov 21 '15 at 05:08
  • Sure, I'm curious if anyone has another way of accomplishing the same thing without using Any(). – gregwhitworth Nov 21 '15 at 05:13
  • Just to confirm: your error is a compile-time error or run-time error? – juvchan Nov 21 '15 at 05:44
  • It is a runtime error – gregwhitworth Nov 21 '15 at 05:47
  • 3
    I never worked with `Microsoft.Azure.Documents.Linq` but I had some similiar issues with Data.Linq. This smells like you are mixing Linq-to-Sql and Linq-to-Object... What element type is `i` in `i => i.type`? Is the `i.Products.Any` not the _IEnumerable.Any_ call? – ckerth Dec 02 '15 at 17:15
  • 1
    In your update you first run a ToList and then do the nested query. This means that it doesn't get executed against documentdb but rather against your list implementation. It can still mean that the Any isn't supported on NESTED queries in linq to documentdb – Batavia Dec 03 '15 at 10:41

5 Answers5

92

One of the biggest confusion with LINQ queries against IQueryable<T> is that they look exactly the same as queries against IEnumerable<T>. Well, the former is using Expression<Func<..>> whenever the later is using Func<..>, but except if one is using explicit declarations this is not so noticeable and seems unimportant. However, the big difference comes at runtime.

Once the IEnumerable<T> query is successfully compiled, at runtime it just works, which is not the case with IQueryable<T>. A IQueryable<T> query is actually an expression tree which is processed at runtime by the query provider.

From one side this is a big benefit, from the other side, since the query provider is not involved at query compile time (all the methods are provided as extension methods by Queryable class), there is no way to know if the provider supports some construct/method or not until runtime. People that use Linq to Entities know that very well. To make the things harder, there is no clear documentation what the specific query provider supports and more importantly, what it doesn't support (as you noticed from the "what is supported" link you provided).

What's the solution? (and why your second code works)

The trick is to write the maximum possible (i.e.supported by the query provider) query part against the IQueryable<T>, and then switch to IEnumerable<T> and do the rest (remember, once compiled, IEnumerable<T> query just works). The switch is performed by AsEnumerable() call. And that's why your second code is working - because unsupported Any method is no more in the DocumentDb query provider context. Note that ToList call is not needed and the query is not executed twice - in fact this way there is no single query, but two - one in database and one in memory.

So something like this would be sufficient:

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();

Finally, what really is supported by the DocumentDb query provider

It's not quite clear from the documentation, but the answer is: exactly (and only) what is included there. In other words, the only supported query operators (or better say Queryable or Enumerable extension methods) are

  • Select
  • SelectMany
  • Where
  • OrderBy
  • OrderByDescending

As you may see, it's very limited. Forget about join and grouping operators, Any, Contains, Count, First, Last etc. The only good thing is that it's easy memorizable :)

How do I know that? Well, as usual when something is unclear from the documentation, one either use trial and error or decompiler. Apparently in this case the former is not applicable, so I've used the later. If you are curious, use your favorite decompiler and check the code of the internal class DocumentQueryEvaluator inside the Microsoft.Azure.Documents.Client.dll.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
  • 2
    Similar thing happened to me using unit tests against real environment. The unit tests was mocking the context using an IEnumerable<> to hold the data so .Any() works on the unit test. Published it to test and boom: exception. – Renato Carriço Dec 04 '15 at 15:01
  • 3
    Thank you so much - this was indeed the issue and I greatly appreciate you not only providing the solution but the reasoning behind it, "Teach a man to fish.." and all that. Thanks!! – gregwhitworth Dec 05 '15 at 04:47
  • 9
    Just want to add, please note there are a bunch of new LINQ operators supported in DocumentDB including Math, String, Spatial and Array operators (including Contains). Details here: https://azure.microsoft.com/en-us/blog/azure-documentdb-s-linq-provider-just-got-better/ – Aravind Krishna R. Dec 08 '15 at 00:22
  • That can cause high RU usage – nrofis Dec 04 '18 at 09:53
  • @nrofis It is specifically mentioned in the answer that you should do as much as possible (read filtering) on `IQueryable` before switching to LINQ to Entities. So what you re saying is a fact, but what is the alternative/solution then? – Ivan Stoev Dec 04 '18 at 10:24
  • Is using a filter on IEnumerable essentially filtering client-side? – Crhistian Ramirez Feb 21 '19 at 00:33
  • 1
    @CrhistianRamirez Correct. – Ivan Stoev Feb 21 '19 at 00:38
  • "Finally, what really is supported by the DocumentDb query provider" Take is also supported – osexpert Mar 25 '20 at 13:07
  • In CosmosDB, 'contains' is now also supported. see https://learn.microsoft.com/en-us/azure/cosmos-db/sql/sql-query-linq-to-sql. So if you want to query against a local array with ID's you can use localarray.contains(doc.property) – Bram Vaessen Sep 13 '22 at 20:34
6

I am using the latest Azure DocumentDB nuget targetting .Net 4.6.

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

Here's the sample code which is working fine for me.

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; }
}
juvchan
  • 6,113
  • 2
  • 22
  • 35
  • I'm using all of the same stuff, .NET 46, DB version 1.5 and even your example switching ToList() to FirstOrDefault() does not work and throws the aggregate exception that Any() is not supported. I agree that it should work, and said above that if I just fetch the first Where(), and then apply the Any() it does work, it seems to be the chaining. Any ideas? – gregwhitworth Nov 22 '15 at 19:06
  • What is the Visual Studio version and the Azure SDK installed on your platform? – juvchan Nov 22 '15 at 22:01
  • I am running Visual Studio 2015 Enterprise. The specific SDK is kind of hard to nail down because it's a bunch of pieces, but most point to Windows Azure Tools for Visual Studio 2012 or 2013. I'm installing 2015 now to see if that helps. – gregwhitworth Nov 29 '15 at 06:00
  • I'm curious how you made that query work, as the ".Any" is still not supported by DocumentDb – luizs81 Jun 11 '18 at 07:59
  • Can confirm this is working for me as well. I'm on .NET Framework 4.7 and using the most recent stable version of Microsoft.Azure.DocumentDB v2.2.2 Make sure you're updating it in any project you're using it. That tripped me up initially – Crhistian Ramirez Feb 21 '19 at 15:38
2

You should try using 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();
mrtig
  • 2,217
  • 16
  • 26
  • I am getting the same error - now just with Select() not being supported. This is very strange. – gregwhitworth Nov 22 '15 at 05:14
  • Interesting. What happens if you change the select so that it generates an anonymous type first, then use .Any? – mrtig Nov 22 '15 at 05:27
  • I did this same thing, just not generating it as anonymous type, see my update in the initial question. This seems to work. I guess I should also state that Name on the product is a string. But technically, that shouldn't be of much consequence since the follow up query does work, it's the chaining that seems to cause the issue. – gregwhitworth Nov 22 '15 at 06:00
1

The most performant solution currently is to use the SQL syntax since that allows document DB to use the collection's index.
example:

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

The disadvantage is that you might get non-unique results and have to distinct the result client-side.

To get this issue into DocumentDB, it would help to vote on the following item: https://feedback.azure.com/forums/263030-documentdb/suggestions/14829654-support-sub-query-functions-like-exists-not-exist

TJ Galama
  • 457
  • 3
  • 12
-1

Why don't you try this one?

 List<Art> items =  DocumentDbHelper.Client.CreateDocument(collection.DocumentsLink)
                           .Where(i => i.type == "art" && i.Products.Any(p => p.Name == productType))
                           .AsEnumerable()
                           .ToList();
Jose Ortega
  • 1,002
  • 13
  • 23
  • 1
    Sorry to comment on such an old response, but you missed the point, where Any() cannot be used within the DocumentDb-query provider. – confuse May 07 '18 at 13:46