Polymorphic model binding in AspNet Core WebApi?
Asked Answered
S

2

10

anyone has a working example of a custom model binding with polymorphic model binding? I'm trying this example (which is for Mvc not Api projects) with a web api project but it's not working for API projects. I think some steps are missing in terms of populating the ValueProvider but I can't find any resources related to this (AspNet Core 3.1).

My attempt so far:

Dtos:

public abstract class Device
{
    public string Kind { get; set; }
}

public class Laptop : Device
{
    public string CPUIndex { get; set; }
}

public class SmartPhone : Device
{
    public string ScreenSize { get; set; }
}

Custom model binder implementation:

public class DeviceModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context.Metadata.ModelType != typeof(Device))
        {
            return null;
        }

        var subclasses = new[] { typeof(Laptop), typeof(SmartPhone), };

        var binders = new Dictionary<Type, (ModelMetadata, IModelBinder)>();
        foreach (var type in subclasses)
        {
            var modelMetadata = context.MetadataProvider.GetMetadataForType(type);
            binders[type] = (modelMetadata, context.CreateBinder(modelMetadata));
        }

        return new DeviceModelBinder(binders);
    }
}

public class DeviceModelBinder : IModelBinder
{
    private Dictionary<Type, (ModelMetadata, IModelBinder)> binders;

    public DeviceModelBinder(Dictionary<Type, (ModelMetadata, IModelBinder)> binders)
    {
        this.binders = binders;
    }

    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var modelKindName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, nameof(Device.Kind));
        var modelTypeValue = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);

        IModelBinder modelBinder;
        ModelMetadata modelMetadata;
        if (modelTypeValue.FirstValue == "Laptop")
        {
            (modelMetadata, modelBinder) = binders[typeof(Laptop)];
        }
        else if (modelTypeValue.FirstValue == "SmartPhone")
        {
            (modelMetadata, modelBinder) = binders[typeof(SmartPhone)];
        }
        else
        {
            bindingContext.Result = ModelBindingResult.Failed();
            return;
        }

        var newBindingContext = DefaultModelBindingContext.CreateBindingContext(
            bindingContext.ActionContext,
            bindingContext.ValueProvider,
            modelMetadata,
            bindingInfo: null,
            bindingContext.ModelName);

        await modelBinder.BindModelAsync(newBindingContext);
        bindingContext.Result = newBindingContext.Result;

        if (newBindingContext.Result.IsModelSet)
        {
            // Setting the ValidationState ensures properties on derived types are correctly 
            bindingContext.ValidationState[newBindingContext.Result] = new ValidationStateEntry
            {
                Metadata = modelMetadata,
            };
        }
    }
}

I register the model binder provider like so:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers(o => o.ModelBinderProviders.Insert(0, new DeviceModelBinderProvider()));
    }

Then my controller:

[ApiController]
[Route("test")]
public class TestController : ControllerBase
{
    [HttpPost]
    public IActionResult Test(Device dto)
    {
        var x = dto;
        return Ok();
    }
}

I'm posting a json request body like:

{
    "ScreenSize": "1",
    "Kind": "SmartPhone"
}

Really fedup with the documentation on this as there's too much magic going on. My fallback is to manually parse the HttpContent from the request and deserialise. But I'm hoping to use the model binder approach like in the example. The only two strange things I'm seeing are, the bindingContext.ModelName is empty and bindingContext.ValueProvider only has a route value provider containing action and controller keys. So, it looks like the body is not even parsed into the value provider.

Striking answered 18/5, 2020 at 9:0 Comment(0)
S
3

Formatters, which is what's used when JSON data, do not interact with the rest of the model binding\value provider subsystem. For this scenario, you'd have to write a converter for the JSON library that you're using.

More information:

Related info

Scallion answered 11/8, 2020 at 15:14 Comment(1)
Can you explain what do you mean by json formatter does not interact with the rest of the model binding? Do you mean that the json request is not getting parsed correctly?Striking
D
1

I have tried exact same code you posted and its working for me.

Here is image of its value.

enter image description here

and here is screenshot of postman request.

enter image description here

CURL request from postman.

curl --location --request POST 'https://localhost:44332/test' \
--header 'Content-Type: application/json' \
--form 'ScreenSize=1' \
--form 'Kind=SmartPhone'

And startup.cs as image below. enter image description here

Deploy answered 18/5, 2020 at 9:42 Comment(11)
Which version of dotnet core are you using? Could you upload the whole project somewhere so I can try and run it?Striking
.Net Core 3.1, I think you might be missing content-type header in your request.Deploy
Can you also let me know the build number of the runtime please? I had the content type header. Wait why are you also sending data in form?Striking
Can you please show me the ful curl request from postman and also your full Startup class? I tried on two different machines but it's not working on either. Apprecate it if you can share with me the full project.Striking
I have added curl request and startup in answer.Deploy
Thanks very much for your help! So, the thing is you are posting as form data not json in request body. But at least this gives me an idea. It's just not using the correct value provider for whatever reason.Striking
Check this information: "formatters, which is what's used when JSON data, do not interact with the rest of the model binding \ value provider subsystem. For this scenario, you'd have to write a converter for the JSON library that you're using". Source and more information here: [Polymorphic model binding in AspNetCore 3.1 Api ](github.com/dotnet/aspnetcore/issues/21939)Scallion
That's why it doesn't work when you don't use form dataScallion
@richardsonwtr, Ohh man, that was the real reason for not working. I suggest you to add this comment as an answer and it should be the one accepted. Please also include these links in your answer learn.microsoft.com/en-us/dotnet/standard/serialization/… & newtonsoft.com/json/help/html/CustomJsonConverter.htmDeploy
@Deploy answer added. Thank you all for your contribution. The community thanks you all for your effort. I was only able to discover the root of the problem by reading your anwers/comments.Scallion
@Scallion and with your comment here I also learn the real cause, this will be helpful and this is what I love about this community. We help other, we get help from others and we gain knowledge at the same time. +1 to you efforts too.Deploy

© 2022 - 2024 — McMap. All rights reserved.