英文:
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<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)
答案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<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 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 => 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<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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论