net 8.0: migrating from serialization to source generators

177 Views Asked by At

We're in the process of migrating an existing Blazor WASM (hosted on a ASP.NET Core App) to net 8.0 and we're having several issues with trimming (and how it works). One of them is ensuring that the trimmer doesn't remove the types used for interacting with the Web API consumed by the Blazor WASM App. Currently, serializing/deserializing relies on System.Text.Json (reflection base and no trimming). In theory, moving from reflection to source generators shouldn't be that hard. Unfortunately, it seems like this is one of those scenarios where theory doesn't quite match what happens in the real world.

For instance, suppose you've got the following helper generic type:

public sealed record Descriptor<T>(T Value, string Description);

In the project we're migrating, this type is used by several of the API endpoints for returning enum values/string pairs shown by several dropdowns that exist in different pages presented by the Blazor WASM UI.

Now, if I'm not mistaken, to serialize this type through source generation, then I must apply the JsonSerializableAttribute to a new partial JsonSerializerContext derived type. Futhermore, it seems like you can't specify an open generic type, so if you're using this generic type with enums Kind and KindB, then you'll have to at least have this:

[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Serialization)]
[JsonSerializable(typeof(Descriptor<Kind>))]
[JsonSerializable(typeof(Descriptor<KindB>))]
public partial class DescriptorContext: JsonSerializerContext{}

Now, when you have several enum types, this isn't pretty. However, it seems like you also need add entries if you want to serialize collections of each type of descriptor:

[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Serialization)]
[JsonSerializable(typeof(Descriptor<Kind>))]
[JsonSerializable(typeof(IEnumerable<Descriptor<Kind>>))]
[JsonSerializable(typeof(List<Descriptor<Kind>>))]
// removed entries for  KindB
public partial class DescriptorContext: JsonSerializerContext{}

In other words, without these entries, I'm unable to serialize/deserialize IEnumerable<Descriptor<Kind>> or List<Descriptor<Kind>>instances like the ones shown on the next example:

List<Descriptor<Kind>> items = [
    new(Kind.Single, "Single"),
    new(Kind.Colective, "Colective")
];
string serialized = JsonSerializer.Serialize(items, 
                                             DescriptorContext.Default.ListDescriptorKind);
List<Descriptor<Kind>> recovered =
    JsonSerializer.Deserialize(serialized, 
                               DescriptorContext.Default.ListDescriptorKind)!;

foreach( Descriptor<Kind> item in recovered ) {
    Console.WriteLine($"{item.Value} {item.Description}");
}

Is this correct? I mean, is it right to conclude that:

  1. You can't specify an open type
  2. You must specify all the types you want to serialize/deserialize through JsonSerializableAttribute, including closed generics like the one shown on the previous snippet (ex.: List<Descriptor<Kind>>)

I really hope I'm missing something here because if using source generators does require all these lines, then it's really hard to use them in any mid-size project... If you look at this example, I'd say that maintaining the JsonSerializableAttribute list is not simple.

The second issue we're facing is that we don't have the source code for the assembly which has the types used by the Web API that is consumed by the Blazor WASM app. Initially, Json.NET was used for serialization/deserialization and the API has several endpoints where the return type is a derived type of the type specified on the signature of the controller's method (polymorphism):

public async Task<ActionResult<Base>> DoSomething(){
   // always returns an instance of a Base's derived type
}

Initially, serialization was done with Json.NET. However, we've ended up migrating to System.Text.Json when it officially supported inheritance. At the time, we've ended up resorting to custom DefaultJsonTypeInfoResolver so that the discriminator used is compatible with the one generated by default by Json.NET (property name is set to $type and its value is full assembly qualified name). This was required because the several of the types used in the API were also persisted to the database. Here's some demo code used for the DefaultJsonTypeInfoResolver:

public class JsonHierarchyTypeInfoResolver : DefaultJsonTypeInfoResolver {

    public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options) {       
        var jsonTypeInfo = base.GetTypeInfo(type, options);
        var jsonType = jsonTypeInfo.Type;

        if( jsonType == typeof(Equipamento) ) {
            jsonTypeInfo = GetTypeInfoForDevice(jsonTypeInfo);
        }
        // ... remaining code removed
}

private static JsonTypeInfo GetTypeInfoForDevice(JsonTypeInfo jsonTypeInfo) {
        jsonTypeInfo.PolymorphismOptions = new JsonPolymorphismOptions {                                                                           
           TypeDiscriminatorPropertyName = "$type",
           IgnoreUnrecognizedTypeDiscriminators = true,                                                                           
           UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization
         };

        
        foreach(var jsonDerivedType in _derivedDevicesTypes) {
            jsonTypeInfo.PolymorphismOptions.DerivedTypes.Add(
                               new JsonDerivedType(jsonDerivedType, 
                                        jsonDerivedType.AssemblyQualifiedName!));
        }

        return jsonTypeInfo;
    }
    

}

So, the final 2 questions are:

  1. Is it possible to combine this resolver with the one generated by source generators? Or do they only work with polymorphic attributes (ex.: JsonDerivedTypeAttribute)
  2. If no, then does it mean that I'll have to rewrite a new DTO assembly which uses polymorphic attributes in order to reuse the endpoint API based on polymorphism/inheritance (i.e., if I want the API's method signature to return A and then return A derived types)?
0

There are 0 best solutions below