o9d-aspnet icon indicating copy to clipboard operation
o9d-aspnet copied to clipboard

Add support for polymorphism using System.Text.Json attributes

Open benfoster opened this issue 3 years ago • 0 comments

This PR is in response to this conversation with @davidfowl regarding support for polymorphism.

With the current implementation, the parameter and corresponding validator types are defined at startup. If a derived type is provided at runtime, a validator will not be resolved.

The more likely use case for this (and one that we use frequently in our APIs) is where we need to deserialize to the appropriate type using a type discriminator in the request, for example:

{
   "type":"student",
   "name":"J Doe",
   "studentId":"123abc"
}

As of .NET 7.0, System.Text.Json includes support for polymorphic types which is applied using attributes, for example:

    [JsonPolymorphic(TypeDiscriminatorPropertyName = "type", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)]
    [JsonDerivedType(typeof(Student), "student")]
    public class Person
    {
        public string? Name { get; set; }
    }

    public class Student : Person
    {
        public string? StudentId { get; set; }
    }

This PR adds support for these attributes. When a type is decorated with one or more [JsonDerivedType] attributes, we generate a type map containing the derived type and the validator type. This is preferable to generating the validator type by reflection on each request.

When validating, if the type map is populated we resolve the derived validator based on the runtime type, otherwise we fall back to the default behaviour based on the parameter info. The main goal is to avoid incurring any runtime cost for non-polymorphic use cases.

Of course this doesn't support every use case (including the example in the conversation linked above) but I would prefer to let feedback drive this.

If the library was to support polymorphism by other means (e.g. BindAsync) then I believe we would need to change the existing validateable delegate to return a type map rather than a boolean value. Then the corresponding strategies could be updated to support passing derived types as parameters (I'm in favour of them being specified explicitly) e.g:

group.MapPost("/things", ([Validate(derivedTypes: typeof(Student), typeof(Teacher)] DoSomething _) => Results.Ok());

benfoster avatar Dec 24 '22 09:12 benfoster