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.