使用Cosmos DB .NET SDK V3保存含接口属性文档报错求助
问题现象
使用Cosmos DB .NET SDK V3保存包含接口类型属性的文档时,触发Newtonsoft.Json.JsonSerializationException,错误信息:
Could not create an instance of type GTA__Tests.ItemTests.IMyCommand. Type is an interface or abstract class and cannot be instantiated. Path 'Commands[0]', line 1, position 103.
代码定义
接口:
public interface IMyCommand { [JsonProperty(PropertyName = "$type")] string CommandType { get; } }
实现类:
public class MyCommand1 : IMyCommand { [JsonProperty("$type")] public string CommandType => nameof(MyCommand1); } public class MyCommand2 : IMyCommand { [JsonProperty("$type")] public string CommandType => nameof(MyCommand2); } // 分区键为CompanyId public class MyClass { [JsonProperty("id")] public Guid Id { get; set; } public string CompanyId { get; set; } public List<IMyCommand> Commands { get; set; } public MyClass(string partitionKey) { CompanyId = partitionKey; } }
测试代码:
// 使用Cosmos DB .NET SDK V3 public class Item_Tests { private string url; private string key; private string dbName; private string containerName; private string partitionKey = "Test"; CosmosClient client; Container db; [SetUp] public void SetUp() { var config = Config.GetConfig(); url = config["CosmosLive:Url"]; key = config["CosmosLive:Key"]; dbName = config["CosmosLive:DbName"]; containerName = config["CosmosLive:ContainerName"]; client = new CosmosClient(url, key); db = client.GetContainer(dbName, containerName); } [Test] public async Task SaveAMyClassDocumentToCosmosDb_ReturnsOK() { var myclass = new MyClass(partitionKey) { Id = Guid.NewGuid(), Commands = new List<IMyCommand> { new MyCommand1(), new MyCommand2() } }; var result = await db.UpsertItemAsync<MyClass>(myclass, new PartitionKey(partitionKey)); Assert.IsTrue(result.StatusCode is System.Net.HttpStatusCode.Created); } }
问题原因
Cosmos DB SDK V3默认使用Newtonsoft.Json进行序列化与反序列化。调用UpsertItemAsync时,SDK不仅会将对象序列化写入Cosmos,还会把Cosmos返回的文档反序列化为MyClass类型。由于MyClass.Commands是List<IMyCommand>(接口类型集合),反序列化时Newtonsoft.Json无法自动推断JSON对象对应的具体实现类(MyCommand1/MyCommand2),即使定义了$type字段,默认配置也不会启用该字段的类型识别逻辑。
解决方案
方案一:全局配置类型序列化
通过自定义Cosmos序列化器,开启Newtonsoft.Json的TypeNameHandling功能,让其自动识别$type字段完成多态反序列化。
- 创建自定义序列化器:
public class CustomCosmosSerializer : CosmosJsonDotNetSerializer { public CustomCosmosSerializer() : base(CreateSerializerSettings()) { } private static JsonSerializerSettings CreateSerializerSettings() { var settings = new JsonSerializerSettings(); // 自动识别类型,序列化时写入$type字段,反序列化时根据该字段实例化对应类 settings.TypeNameHandling = TypeNameHandling.Auto; // 仅写入类型名称,避免冗余的程序集信息 settings.TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple; // 可添加其他序列化配置,比如忽略空值 settings.NullValueHandling = NullValueHandling.Ignore; return settings; } }
- 修改
CosmosClient初始化逻辑:
[SetUp] public void SetUp() { var config = Config.GetConfig(); url = config["CosmosLive:Url"]; key = config["CosmosLive:Key"]; dbName = config["CosmosLive:DbName"]; containerName = config["CosmosLive:ContainerName"]; var clientOptions = new CosmosClientOptions { Serializer = new CustomCosmosSerializer() }; client = new CosmosClient(url, key, clientOptions); db = client.GetContainer(dbName, containerName); }
说明:开启TypeNameHandling.Auto后,Newtonsoft.Json会自动在多态类型的JSON中添加$type字段,反序列化时据此实例化对应实现类。此时自定义的CommandType属性可以保留或移除,若保留建议修改字段名(比如改为CommandType)避免与自动生成的$type冲突。
方案二:自定义接口转换器
若不想全局修改序列化配置,可为IMyCommand接口添加自定义转换器,根据定义的CommandType字段手动映射具体实现类。
- 创建转换器:
public class MyCommandConverter : JsonConverter<IMyCommand> { public override IMyCommand ReadJson(JsonReader reader, Type objectType, IMyCommand existingValue, bool hasExistingValue, JsonSerializer serializer) { var jObject = JObject.Load(reader); var commandType = jObject["$type"].Value<string>(); return commandType switch { nameof(MyCommand1) => jObject.ToObject<MyCommand1>(serializer), nameof(MyCommand2) => jObject.ToObject<MyCommand2>(serializer), _ => throw new JsonSerializationException($"未知命令类型:{commandType}") }; } public override void WriteJson(JsonWriter writer, IMyCommand value, JsonSerializer serializer) { serializer.Serialize(writer, value); } }
- 为接口添加转换器特性:
[JsonConverter(typeof(MyCommandConverter))] public interface IMyCommand { [JsonProperty(PropertyName = "$type")] string CommandType { get; } }
说明:该方案仅针对IMyCommand接口生效,反序列化时转换器会读取$type字段值,实例化对应的实现类,灵活性更高。
内容的提问来源于stack exchange,提问作者Paul Stanley

