CosmosClient:ReadItemAsync成功,GetItemLinqQueryable失败。

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

CosmosClient : ReadItemAsync succeeds, GetItemLinqQueryable fails

问题

Using .Net 6, Azure.Cosmos 3.33

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

问题实际上是关于在 CosmosDb 3 中查询项的几种方式,但为了避免误解,这里有关于底层基础结构的完整免责声明:

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;
   }
}

我的实际数据类:

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

   public Guid SomeGuid { get; }

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

自定义序列化器类,旨在使用 system.Text.Json 而不是 Newtonsoft 的 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;
   }
}

以及 Cosmos 客户端的实例化方式:

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

   // JsonSerializerDefaults.Web 通常使字段比较小写
   Serializer = new CosmosNetSerializer(new(JsonSerializerDefaults.Web))
};

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

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

现在,考虑一下在我的 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();
}

问题:

#1, #3, #4, #6 可以正常工作,但 #2 和 #5 失败。在 #2 和 #5 中,item 为 null。
为什么方法 #2 或 #5 找不到项目?

故障排除

起初,我以为这可能是由于我的自定义 CosmosSerializer(也许 id 没有正确比较 -- 尽管我的序列化器没有触及它,它只使用另一个特殊字段)导致的,但 #3 似乎证明了这不是问题,因为它也可以使用 id。

显然,在查询之前,我总是检查项目是否存在。我设置了断点并查看 CosmosDb 容器,甚至检查 Guid 是否正确。

在场景 #5 中尝试使用 PartitionKey.None ... 没有帮助

我尝试在 Id 的声明上方添加 [JsonPropertyName("id")],以确保这不是大小写问题。但是 场景 #4 反驳了大小写问题!.Where(...) 在查询中添加了 WHERE Id=...,并且 仍然有效

英文:

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&lt;out TK&gt;
   {
      public TK Id { get; }
   }

   public interface IWithPartitionKey&lt;out TK&gt;
   {
      public TK PartitionKey { get; }
   }

   public interface ICosmosDbEntity&lt;out TK, PK&gt; : IWithKey&lt;TK&gt;, IWithPartitionKey&lt;PK&gt; where TK : struct
   {
   }

   public abstract class CosmosDbEntity&lt;TK, PK&gt; : ICosmosDbEntity&lt;TK, PK&gt; where TK : struct
   {
      [JsonPropertyName(&quot;id&quot;)] 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&lt;Guid, PartitionKey&gt;
{
   [JsonIgnore]
   //[Newtonsoft.Json.JsonIgnore]
   public override PartitionKey PartitionKey =&gt; 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() =&gt; _serializerOptions = null;

      public CosmosNetSerializer(JsonSerializerOptions serializerOptions) =&gt;
         this._serializerOptions = serializerOptions;

      public override T FromStream&lt;T&gt;(Stream stream)
      {
         using (stream)
         {
            if (typeof(Stream).IsAssignableFrom(typeof(T)))
            {
               return (T)(object)stream;
            }

            return JsonSerializer.DeserializeAsync&lt;T&gt;(stream, _serializerOptions).GetAwaiter().GetResult();
         }
      }

      public override Stream ToStream&lt;T&gt;(T input)
      {
         var outputStream = new MemoryStream();

         JsonSerializer.SerializeAsync&lt;T&gt;(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&lt;MyType&gt;(id.ToString(),
        new PartitionKey(partitionKey)).Result;

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

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

     var item = container.GetItemLinqQueryable&lt;MyType&gt;(true)
        .Where(m =&gt; m.Id == id)
        .AsEnumerable()
        .FirstOrDefault();

     Assert.NotNull(item);

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

        var items = container.GetItemLinqQueryable&lt;MyType&gt;(true)
          .ToList();

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

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

     var items = container.GetItemLinqQueryable&lt;MyType&gt;(true)
        .Where(m =&gt; m.Id == input.Id) // &lt;-- 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&lt;MyType&gt;(
        true, 
        null, 
        options // &lt;-- there IS a partition key!
     )
        .Where(m =&gt; m.Id == input.Id);
        .FirstOrDefault();

     Assert.NotNull(item);

6. GetItemQueryIterator (without partition key) => OK

     var query = container.GetItemQueryIterator&lt;MyType&gt;(
        $&quot;select * from t where t.id=&#39;{itemId.ToString()}&#39;&quot;);

     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(&quot;id&quot;)] 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)

答案1

得分: 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&lt;MyType&gt;(true).Where(m =&gt; m.Id == id).Expression then you can see translated to SQL query.

It translates to: SELECT VALUE root FROM root WHERE (root[&quot;Id&quot;] = &lt;some id&gt;).

As you can see, it uses the original 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 is 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 =&gt; 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.

英文:

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&lt;MyType&gt;(true).Where(m =&gt; m.Id == id).Expression then you can see translated to SQL query.

It translates to : SELECT VALUE root FROM root WHERE (root[&quot;Id&quot;] = &lt;some id&gt;).

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 =&gt; m.Id == input.Id).FirstOrDefault(); raises Microsoft.Azure.Cosmos.Linq.DocumentQueryException : &#39;Method &#39;FirstOrDefault&#39; 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.

huangapple
  • 本文由 发表于 2023年5月11日 17:21:44
  • 转载请务必保留本文链接:https://go.coder-hub.com/76226063.html
匿名

发表评论

匿名网友

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

确定