这是一个非常尴尬的错误,我花了一段时间才把它处理到这几行代码.完整的代码可以被抓取as fiddle,但你必须提供你的Cosmos URL和密钥才能运行.问题是,我们定义了一个public static readonly Author Unknown字段,而类Author是一个记录,其定义如下:

public record Author(
    [Required] string Id,
    [Required] string DisplayName,
    [Required] string MailAddress)
{
    // This is going to be mutated! 👇🏻
    public static readonly Author Unknown = Create(nameof(Unknown));

    public static Author Create(string name)
    {
        return new Author(
            name.ToLowerInvariant(),
            name,
            $"{name}@test.internal");
    }
}

在下一步中,我们有一个类,我们希望从/向Azure Cosmos数据库读取/写入,该类可以简单到

public class DocumentBase
{
    [Newtonsoft.Json.JsonProperty("id")]
    public Guid Id { get; set; } = default!;
    public string PartitionKey { get; set; } = "PartitionKey";

    // Apply static readonly record in default ctor of class
    public Author Modified { get; set; } = Author.Unknown;
}

我们设置中的下一件事是一个Cosmos DB容器,我们可以使用它来读/写我们的文档:

private static async Task<Container> CreateContainer()
{
    var endPoint = "https://{yourCosmosDatabase}.documents.azure.com:443/";
    var key = "{yourCosmosKey}";

    var client = new CosmosClient(endPoint, key);
    var containerId = $"{DateTime.UtcNow:s}-{Guid.NewGuid()}";
    var partitionKey = "/PartitionKey";

    var database = client.GetDatabase("Testing");
    var response = await database.CreateContainerAsync(new ContainerProperties
    {
        Id = containerId,
        PartitionKeyPath = partitionKey,
        DefaultTimeToLive = -1
    });

    return response.Container;
}

以下是更改Author.Unknown中的值的代码:

public static async Task Main()
{
    // Create a container to access Cosmos
    var container = await CreateContainer();

    // Create an instance with individual values
    var defaultValue = new DocumentBase
    {
        Id = Guid.NewGuid(),
        Modified = Author.Create("Modified"),
    };

    // Create a copy of the static readonly record for comparison
    var copyOfUnknown = Author.Unknown with { };
    // Check if the copy and the original values are equal
    // with value comparison thanks to the help of record
    Author.Unknown.ShouldBe(copyOfUnknown);

    // Let Cosmos write that instance down to the database
    // and read back the fresh created value.
    // It will be returned by the function, but we ignore it.
    await container.CreateItemAsync(defaultValue, new PartitionKey(defaultValue.PartitionKey));

    // Make another comparison that fails, because in Author.Unknown
    // you find now Author.Create("Modified")!
    Author.Unknown.ShouldBe(copyOfUnknown);
}

那么这里到底是怎么回事? 我们有一个存储recordpublic static readonly字段,在对来自宇宙的响应进行反序列化后,那个字段发生了Mutations ?这怎么可能呢?

要解决这个问题,我们有多个选项,也许可以显示遇到这个问题所需的配料:

  • 将readonly字段替换为属性getter,该属性getter在每次被调用时返回一个新实例.
  • Do not set DocumentBase的默认ctor中的Modified属性
  • Set使用复制ctor = Author.Unknown with { };在默认ctor DocumentBase中设置Modified属性

然而,这种行为确实出乎意料,如果有人能解释这种行为,我会很高兴.顺便说一下,我们使用了最新的NuGet软件包Microsoft.Azure.Cosmos 3.38.1.

推荐答案

This is a known issue with Json.NET and immutable records.在填充预先分配的实例(如Author.Unknown)时,Json.NET可能会改变(显然)不可变的属性.有关确认,请参阅Json.NET问题Deserialization appears to write over default record properties rather than create new ones #2451[1].

As a workaround,你可以告诉Json.NET忽略编译器生成的属性,并使用私有只读属性进行序列化:

public record Author(
    [property: Newtonsoft.Json.JsonIgnore][Required] string Id,
    [property: Newtonsoft.Json.JsonIgnore][Required] string DisplayName,
    [property: Newtonsoft.Json.JsonIgnore][Required] string MailAddress)
{
    [Newtonsoft.Json.JsonProperty("Id")] string JsonId => Id;
    [Newtonsoft.Json.JsonProperty("DisplayName")] string JsonDisplayName => Id;
    [Newtonsoft.Json.JsonProperty("MailAddress")] string JsonMailAddress => MailAddress;
    
    // Remainder unchanged

工作演示小提琴#1here.

JamesNK建议的另一个解决方法是将所有值为Author的属性标记为ObjectCreationHandling.Replace:

public class DocumentBase
{
    // Apply static readonly record in default ctor of class
    [Newtonsoft.Json.JsonProperty(ObjectCreationHandling = ObjectCreationHandling.Replace)]
    public Author Modified { get; set; } = Author.Unknown;

    // Remainder unchanged

here.第二次世界大战

可能有使用自定义合同解析器自动执行此操作的选项,但我不确定Microsoft.Azure.Cosmos客户端是否提供访问设置以修改使用的合同解析器.


[1] FYI, the reason that Json.NET is able to modify an (apparently) immutable record is that, for positional properties in records, the compiler generates init-only properties. From the docs:

当您使用位置语法进行特性定义时,编译器会创建:

  • 记录声明中提供的每个位置参数的公共自动实现属性.

    • 对于记录类型和只读记录 struct 类型:只初始化属性.
    • 对于记录 struct 类型:读写属性.

你的Author类实际上看起来像:

public class Author
{
   public string Id { get; init; }
   public string DisplayName { get; init; }
   public string MailAddress { get; init; }
}

init-仅限属性can be set by reflection.有关确认,请参阅Init Only Setters:

[init财产访问者]不会针对以下情况提供保护:

  • 对公众成员的反思
  • dynamic的用法
  • ...

由于Json.NET(和其他序列化器)使用公共成员上的反射来创建其序列化契约,因此init-only属性看起来像可设置的属性,因此可以在序列化期间填充.

Csharp相关问答推荐

将多个enum值传递给MAUI中的转换器参数的XML语法是什么?

我可以将Expressc操作设置为在另一个Expressc操作完成后自动发生吗?

当MD5被废弃时,如何在Blazor WASM中使用它?

PredicateBuilder不是循环工作,而是手动工作

.NET最小API映射将T参数列表为[FromQuery]

ASP.NET核心结果.文件vs结果.流

Blazor EventCallback<;MyType<;T>;>;

用C#从Word文档中删除重复的节控件和文本内容控件

为什么无法将对象转换为泛型类型

如何让NLog停止写入冗余信息?

如何在CSharp中将json字符串转换为DataTable?

如果是,我怎么才能让这个加75,如果不是,我怎么才能减go 100?

如何从Entity Framework Core中填充ListIInterface

两个DateTimeOffset之间的差异返回意外的负值

Visual Studio 17.8.0制表符自动完成问题--三缩进

Autofac -动态实例化:手动传递构造函数

ASP.NET Core 8 Web API:如何添加版本控制?

如何为控制器PUT操作绑定对象数组

如何在更新数据库实体时忽略特定字段?

无法将.Net Framework 4.8.1升级到.Net 7