使用Cosmos SDK和Newtonsoft时static readonly record被篡改问题
问题原因分析
你遇到的问题核心是Newtonsoft.Json(Azure Cosmos SDK v3.x默认序列化器)的反序列化行为,结合C# record的特性与static readonly实例的复用导致的。
触发场景的代码回顾
首先看核心定义:Author是一个record类型,包含全局static readonly实例:
public record Author( [Required] string Id, [Required] string DisplayName, [Required] string MailAddress) { public static readonly Author Unknown = Create(nameof(Unknown)); public static Author Create(string name) { return new Author( name.ToLowerInvariant(), name, $"{name}@test.internal"); } }
DocumentBase的默认构造直接复用这个全局实例:
public class DocumentBase { [Newtonsoft.Json.JsonProperty("id")] public Guid Id { get; set; } = default!; public string PartitionKey { get; set; } = "PartitionKey"; public Author Modified { get; set; } = Author.Unknown; }
写入并反序列化的逻辑:
public static async Task Main() { var container = await CreateContainer(); var defaultValue = new DocumentBase { Id = Guid.NewGuid(), Modified = Author.Create("Modified"), }; var copyOfUnknown = Author.Unknown with { }; Author.Unknown.ShouldBe(copyOfUnknown); await container.CreateItemAsync(defaultValue, new PartitionKey(defaultValue.PartitionKey)); // 此处对比失败,Unknown已被篡改 Author.Unknown.ShouldBe(copyOfUnknown); }
具体触发过程
- 全局实例的引用复用:
DocumentBase默认构造中,Modified直接指向Author.Unknown的内存引用——所有默认构造的DocumentBase对象,这个字段都共享同一个全局实例。 - 反序列化的实例修改:调用
CreateItemAsync后,Cosmos返回写入的文档,SDK自动反序列化为DocumentBase对象:- 反序列化器先调用
DocumentBase无参构造,Modified被设置为Author.Unknown的引用。 - 接着处理
Modified的JSON数据时,Newtonsoft.Json为了性能,会直接通过反射修改已有实例的属性值——即使record的属性是init修饰的,反射可以绕过这个不可变限制。
- 反序列化器先调用
- 全局实例被永久篡改:因为
Author.Unknown是static readonly的全局实例,它的属性被修改后,所有引用它的地方都会看到篡改后的值。
规避方案的原理
- 改用属性getter返回新实例:每次获取
Unknown都生成新的record实例,DocumentBase的Modified指向局部实例,反序列化修改的是局部实例,不影响全局。 - 不在默认构造设置Modified:反序列化时会直接创建新的
Author实例赋值,不会复用全局的Unknown。 - 用
with {}创建副本:Author.Unknown with {}会生成新的record实例(值复制),Modified指向副本,反序列化修改副本,原全局实例不受影响。
额外建议
如果使用.NET 6+,可以将Cosmos SDK的序列化器切换为System.Text.Json,它对record的init属性处理更严格,默认会创建新实例而非修改已有实例,能从根源避免这类问题。
内容的提问来源于stack exchange,提问作者Oliver
相关产品推荐
相关产品推荐

