0

Using .Net 6, Azure.Cosmos 3.33

============= Some extra context, only to be thorough ==============

the question is really about the several ways of querying items in CosmosDb 3, but to avoid misunderstandings here is a full disclaimer of the underlying infrastructure :

   public interface IWithKey<out TK>
   {
      public TK Id { get; }
   }

   public interface IWithPartitionKey<out TK>
   {
      public TK PartitionKey { get; }
   }

   public interface ICosmosDbEntity<out TK, PK> : IWithKey<TK>, IWithPartitionKey<PK> where TK : struct
   {
   }

   public abstract class CosmosDbEntity<TK, PK> : ICosmosDbEntity<TK, PK> where TK : struct
   {
      [JsonPropertyName("id")] public TK Id { get; protected set; }

      [JsonIgnore] public virtual PK PartitionKey { get; } = default!;

      protected CosmosDbEntity(TK id)
      {
         Id = id;
      }
   }

My actual data class :

public class MyType : CosmosDbEntity<Guid, PartitionKey>
{
   [JsonIgnore]
   //[Newtonsoft.Json.JsonIgnore]
   public override PartitionKey PartitionKey => SomeGuid.AsPartitionKey();

   public Guid SomeGuid { get; }


   public MyType(Guid id, Guid someGuid) : base(id)
   {
      SomeGuid = someGuid;
   }
}

The custom serializer class, designed to use system.Text.Json instead of Newtonsoft's Json.Net :

   public class CosmosNetSerializer : CosmosSerializer
   {
      private readonly JsonSerializerOptions? _serializerOptions;

      public CosmosNetSerializer() => _serializerOptions = null;

      public CosmosNetSerializer(JsonSerializerOptions serializerOptions) =>
         this._serializerOptions = serializerOptions;

      public override T FromStream<T>(Stream stream)
      {
         using (stream)
         {
            if (typeof(Stream).IsAssignableFrom(typeof(T)))
            {
               return (T)(object)stream;
            }

            return JsonSerializer.DeserializeAsync<T>(stream, _serializerOptions).GetAwaiter().GetResult();
         }
      }

      public override Stream ToStream<T>(T input)
      {
         var outputStream = new MemoryStream();

         JsonSerializer.SerializeAsync<T>(outputStream, input, _serializerOptions).GetAwaiter().GetResult();

         outputStream.Position = 0;
         return outputStream;
      }
   }

And how the Cosmos client gets instantiated :

 var options = new CosmosClientOptions
 {
    ConnectionMode = //...,

    // JsonSerializerDefaults.Web normally makes fields comparison camel-case
    Serializer = new CosmosNetSerializer(new(JsonSerializerDefaults.Web))
 };

 // Cosmos version 3.33
 return Microsoft.Azure.Cosmos.CosmosClient
    .CreateAndInitializeAsync(connectionStrings.CosmosDb,
       credentials, listOfContainers, options)
    .GetAwaiter()
    .GetResult();

============= end of context ==============

Now, consider those several ways of querying items in my Azure Cosmos db :

     Guid id = ...;
     string partitionKey = ...;

1. ReadItemAsync (with partition key) => OK

     var response = container.ReadItemAsync<MyType>(id.ToString(),
        new PartitionKey(partitionKey)).Result;

     var item = response?.Resource;
     Assert.NotNull(item);
     

2. GetItemLinqQueryable (without partition key) => NOT OK

     var item = container.GetItemLinqQueryable<MyType>(true)
        .Where(m => m.Id == id)
        .AsEnumerable()
        .FirstOrDefault();

     Assert.NotNull(item);

3. GetItemLinqQueryable (without 'Where') + DeleteItemAsync (with partition key) => OK

        var items = container.GetItemLinqQueryable<MyType>(true)
          .ToList();

        foreach (var item in items)
        {
           container.DeleteItemAsync<MyType>(item.Id.ToString(), new PartitionKey(partitionKey)).Wait();
        }

4. With iterator (without partition key) => OK

     var items = container.GetItemLinqQueryable<MyType>(true)
        .Where(m => m.Id == input.Id) // <-- the clause is still here!
        .ToFeedIterator();

     while (items.HasMoreResults)
     {
        var item = items.ReadNextAsync().Result;
        Assert.NotNull(item);
     }

5. : GetItemLinqQueryable (with partition key) => NOT OK

     var options = new QueryRequestOptions
     {
        PartitionKey = new PartitionKey(partitionKey)
     };

     var item = container.GetItemLinqQueryable<MyType>(
        true, 
        null, 
        options // <-- there IS a partition key!
     )
        .Where(m => m.Id == input.Id);
        .FirstOrDefault();

     Assert.NotNull(item);

6. GetItemQueryIterator (without partition key) => OK

     var query = container.GetItemQueryIterator<MyType>(
        $"select * from t where t.id='{itemId.ToString()}'");

     while (query.HasMoreResults)
     {
         var items = await query.ReadNextAsync();
         var item = items.FirstOrDefault();
     }

Problem :

#1, #3, #4, #6 work, but #2 and #5 fail. In #2 and #5, item is null. Why can method #2 or #5 not find the item?

Troubleshooting

At first I thought it might be cause by my custom CosmosSerializer (maybe the id was not compared properly -- despite the fact that my serializer does not touch it, it only works with another special field) but #3 seems to prove but that's not it, as it works with te id too.

Obviously I always checked that the item was present before querying it. I set a breakpoint and go see the CosmosDb container, and even check that the Guid is correct.

I tried with PartitionKey.None in Scenario #5 ... didn't help

I tried adding [JsonPropertyName("id")] above the declaration of Id, to be sure that it wasn't a casing issue. But Scenario #4 disproved that casing is the issue anyways! (the .Where(...) adds a WHERE Id=... with a capital 'i' in the query and it still works)

jeancallisti
  • 1,046
  • 1
  • 11
  • 21
  • Try to add your partition key in the query : `var options = new QueryRequestOptions (){ PartitionKey =new PartitionKey(partitionkey) }` And `item = container.GetItemLinqQueryable(true, options)` – Bisjob May 11 '23 at 09:38
  • I never use `query.AsEnumerable()` Always a `ToList()` or `ToFeedIterator()` and the only errors I got were when I didn't provide any partitionKey. If it can helps you – Bisjob May 11 '23 at 09:41
  • Can you please share the full definition of the `Id` property in the code, including any attributes? It often happened to me that a query with the id property involved didn't work at first because the C# property is pascalcase, while in Cosmos is lowercase. Because of this, the generated query looks like `WHERE c.Id = 'something'`, while the correct one would be `WHERE c.id = 'something'`. – FstTesla May 11 '23 at 09:47
  • @Bisjob I was actuallyhoping to save myself from needing the partition key, even if it's more expensive... – jeancallisti May 11 '23 at 09:57
  • @FstTesla this scenario crossed my mind and it would indeed be a possibility (capital 'i' in the C# class, lower-case 'i' in the json data) but I feed this kind of settings to my CosmosSerializer : `JsonSerializerOptions = new(JsonSerializerDefaults.Web)` -- the "Web" setting normally protects from camelCase issues. Plus, again, the Delete works! *How can I see the translated queries to be 100% sure?* – jeancallisti May 11 '23 at 09:59
  • I just added scenario #4 which definitely rules out the casing problem on "Id" – jeancallisti May 11 '23 at 10:07
  • 1
    @jeancallisti I found out that, counterintuitively, the naming convention in the serializer does not participate in how the properties are translated into the query. For this reason, only an explicit `[JsonProperty("id")]` (for Newtonsoft.Json) or `[JsonPropertyName("id")]` (for System.Text.Json) is able to change the property name in the SQL. To see the generated query, you store the `IQueryable` in a local variable (before `.AsEnumerable()`) and simply call `.ToString()` on it. – FstTesla May 11 '23 at 10:09
  • 1
    Indeed the id should be lower case in cosmos. But it's working on your scenario #4 so your id seems to be OK. For the partition key, you still can set it to null : `new partitionKey(null)` – Bisjob May 11 '23 at 15:30
  • I looked at the query. 'Id' indeed has a capital 'i'. But like @Bisjob pointed out, it doesn't prevent scenario #4 from working! Plus, adding `[JsonPropertyName("id")]` only helps with serialization but it doesn't change the capitalization in the Linq query (I just tried). I'll try with a null partition key now. – jeancallisti May 12 '23 at 10:50
  • After reading this ( https://stackoverflow.com/questions/67130216/cosmos-sdk-returns-empty-results-on-a-query-without-partition-specified ) I tried with `PartitionKey.None` still with no luck. – jeancallisti May 12 '23 at 11:02
  • I added scenario 5 ... I have the feeling that it's simply Linq that doesn't work when run synchronously without using the Enumerator. – jeancallisti May 12 '23 at 11:12
  • 1
    indeed the `FirstOrDefault` doesn't seem to work on the query linq in cosmosDb. If you want to request the first item by ID you can do the following : `await Collection.ReadItemAsync(itemId, partitionkey.Value, itemRequestOptions, cancellationToken);` – Bisjob May 13 '23 at 11:11
  • Or you can execute your query in cosmos with `query.ToList()` and then call the `FirstOrDefault()` – Bisjob May 13 '23 at 11:13

1 Answers1

1

The solution/answer has been given by the devs of the Cosmos SDK, directly on their forums.

Here is what they wrote :

  • In regards to the 2nd SO example:

Currently, SDK doesn't support custom serializers in GetItemLinqQueryable .

If you invoke container.GetItemLinqQueryable<MyType>(true).Where(m => m.Id == id).Expression then you can see translated to SQL query.

It translates to : SELECT VALUE root FROM root WHERE (root["Id"] = <some id>).

As you can see, it uses original the property name (Id with a capital 'i'), not the custom name from JsonPropertyName attribute (id in lowercase). It's a known issue and the SDK team working on this.

See related LINQ queries doesn't use custom CosmosSerializer #2685 for more information.

  • In regards to the 5th SO example:

This part of code: .Where(m => m.Id == input.Id).FirstOrDefault(); raises Microsoft.Azure.Cosmos.Linq.DocumentQueryException : 'Method 'FirstOrDefault' is not supported.

Currently, SDK does not directly support FirstOrDefault() method on the GetItemLinqQueryable query.

See LINQ to SQL translation - Azure Cosmos DB for NoSQL | Microsoft Learn for more information.

jeancallisti
  • 1,046
  • 1
  • 11
  • 21