[SOLVED] Polymorphic Model Binding with Generic Type Parameter

Issue

I have models like this:

public class Request<T> where T: AbstractPayload
{
    public T Payload { get; set; }

    // common properties
}

public abstract class AbstractPayload
{
    public int Id { get; set; }
}

public class PayloadOne : AbstractPayload
{
    [Required]
    public string Name { get; set; }
}

public class PayloadTwo : AbstractPayload
{
    [Required]
    public int Value { get; set; }
}

How do I read the model values in an endpoint like the one below? I also need to validate the models.

public async Task<IActionResult> MyAction([FromBody] Request<AbstractPayload> request)
{
    // how do I get the values here ???
    Request<PayloadOne> requestOne = ???
    Request<PayloadTwo> requestTwo = ???

    return Ok();
}

Is there a way to do that?

I tried to implement a custom model binder and model binder provider (following the example here ms docs) but I cannot see any information in the binding context. The value provider always returns empty for me.

I’ve also found an example using the NewtonsoftJson JsonConverter, but I use Text.Json.

What is the right way to do things like that? Or should I redesign the models?

Solution

I ended up with the following solution:

1. I implemented a custom JsonConverter

public class JsonPayloadConverter<TPayload> : JsonConverter<TPayload> 
    where TPayload: AbstractPayload {
    
    public override bool CanConvert(Type typeToConvert) => typeof(TPayload).IsAssignableFrom(typeToConvert);

    public override TPayload Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        // do your deserialization here
        var payload =

        return (TPayload)payload;
    }
}

2. I replaced the validation attributes with IValidatableObject implementation

The validation attributes work only for the base class (the AbstractPayload in the above example).

public class PayloadOne : AbstractPayload, IValidatableObject
{
    public string Name { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (string.IsNullOrEmpty(Name))
        {
            yield return new ValidationResult($"The {nameof(Name)} property is required.", new[] { nameof(Name) });
        }
    }
}

3. I implemented a ValidationFilter

I have a validation method in my filter:


private void ValidatePayload(ActionExecutingContext context, IValidatableObject payload)
{
    foreach (var result in payload.Validate(new ValidationContext(payload)))
    {
        foreach (var member in result.MemberNames)
        {
            context.ModelState.TryAddModelError(member, result.ErrorMessage);
        }
    }
}

It works for my case. I use reflection in some parts of my code to get the right items from the context.

Of course, there may be some drawbacks to my solution. If you see any, add a comment, please.

UPDATE:

I have improved the solution during the time, however, the
JsonCoverter has been the right way to go.

Answered By – el peregrino

Answer Checked By – Katrina (BugsFixing Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *