.Net 6, C#10
This is my classes hierarchy:
public interface IWithKey {
public Guid Id {get; }
}
public interface IType : IWithKey {
public string Payload { get; }
// a bunch of other complex fields (strings, Guids, nested objects...)
}
public class MyBaseType : IWithKey {
[JsonInclude]
public Guid Id {get; protected set; }
public MyBaseType(Guid id) {
Id = id;
}
}
public class MyType : MyBaseType, IType {
public Guid Id { get; private set; }
public string Payload { get; private set; }
// a bunch of other complex fields (strings, Guids, nested objects...)
public MyType(Guid id, string payload, /* all the other fields... */)
: base(id)
{
Payload = payload;
// all the other fields...
}
}
I have a custom Cosmos serializer. It exists solely to let me use system.Text.Json instead of Newtonsoft's Json.Net (as explained here: https://github.com/Azure/azure-cosmos-dotnet-v3/issues/202 )
using System.Text.Json;
using Microsoft.Azure.Cosmos;
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();
// A BREAKPOINT HERE SHOWS THAT 'input' has all the fields, including Id
JsonSerializer.SerializeAsync<T>(outputStream, input, _serializerOptions).GetAwaiter().GetResult();
// A BREAKPOINT HERE AND A LOOK INSIDE 'outputStream' shows that ALL the fields have been serialized EXCEPT 'Id' or 'id.
outputStream.Position = 0;
return outputStream;
}
}
}
Not that it matters, but the serializer is instantiated this way :
var options = new(JsonSerializerDefaults.Web);
var cosmosOptions = new CosmosClientOptions
{
ConnectionMode = //...,
Serializer = new CosmosNetSerializer(options)
};
var cosmosClient = CosmosClient
.CreateAndInitializeAsync(connectionString, containersList, cosmosOptions);
The serialization is triggered by inserting into Cosmos :
var container = cosmosClient.GetContainer("myDatabase", "myContainer");
var item = GetItem();
await container.CreateItemAsync(item, "partitionKey", null);
Please note the subtlety : It lies in how the item is obtained. It's not of type MyType
but of type IType
:
public IType GetItem() {
return new MyType(id : Guid.NewGuid(), payload: "YYYY", /* all the other fields... */)
}
Problem:
Look again at the comments in the serializer's code.
input
is a full-blown object of type IType
(passed as IType
but the breakpoint shows that C# understands that, in effect, it's a MyType
).
It does have a field "Id" with a valid value, and a TON of other custom fields. Some of them with custom converters, some with nested objects, etc.
Absolutely everything gets serialized, I can see it in outputStream
...
...Except for 'Id
' or 'id
', which goes missing.
Why?
The issue was caused by the class hierarchy. Id
was the only field coming from an underlying class, and System.Text.Json requires extra wotk to handle serialization of class hierarchies.
I changed this :
SerializeAsync<T>
to this :
SerializeAsync<object>
...as suggested in some posts that explain how to handle class hierarchies in .Net6 with System.Text.Json.
If I had been using .Net7 then there exist "better" solutions where you indicate the class hierarchy with decorators above each class definition.