Polymorphic Model Binding with Generic Type Parameter

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?

Answers:

Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.

Method 1

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.


All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x