I recently wanted to have an ASP.NET Core Web API endpoint, that accepts data via [FromForm] and the payload contains a Polymorphic Type.
For an example consider the following POCO types that the endpoint expects via [FromForm].
[JsonDerivedType(typeof(ClassificationOptions), nameof(ClassificationOptions))]
[JsonDerivedType(typeof(ExtractionOptions), nameof(ExtractionOptions))]
public class AnalyzeOptions
{
public string? CommonOption1 { get; set; } = null;
}
public class ClassificationOptions : AnalyzeOptions
{
public string? ClassificationOption1 { get; set; } = null;
}
public class ExtractionOptions : AnalyzeOptions
{
public string? ExtractionOption1 { get; set; } = null;
}
public class AnalyzeRequestModel
{
public IFormFileCollection Files { get; set; }
public AnalyzeOptions Options { get; set; }
}
And the endpoint looks like below:
[ApiController]
[Route("[controller]")]
public class ValuesController : ControllerBase
{
[HttpPost("FromForm", Name = "PostFromForm")]
public IActionResult PostFromForm([FromForm] AnalyzeRequestModel analyzeRequestModel)
{
// TODO: Process the files and options
return Ok(analyzeRequestModel);
}
}
And I was sending data as below, hoping System.Text.Json will kick in and do the deserialization as expected.
|
Postman |
|
Not deserialized |
But it seems [FromForm] model binding isn't honoring System.Text.Json support for Polymorphic Types which is working perfectly fine with [FromBody].
So in order to get this working, we can write a custom model binder for AnalyzeOptions.
public class AnalyzeOptionsModelBinder : IModelBinder
{
private Dictionary<Type, (ModelMetadata, IModelBinder)> binders;
public AnalyzeOptionsModelBinder(Dictionary<Type, (ModelMetadata, IModelBinder)> binders)
{
this.binders = binders;
}
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
string modelKindName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, "$type");
string? modelTypeValue = bindingContext.ValueProvider.GetValue(modelKindName).FirstValue;
IModelBinder modelBinder;
ModelMetadata modelMetadata;
if (modelTypeValue == nameof(ClassificationOptions))
{
(modelMetadata, modelBinder) = binders[typeof(ClassificationOptions)];
}
else if (modelTypeValue == nameof(ExtractionOptions))
{
(modelMetadata, modelBinder) = binders[typeof(ExtractionOptions)];
}
else
{
bindingContext.Result = ModelBindingResult.Failed();
return;
}
ModelBindingContext 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.Model] = new ValidationStateEntry
{
Metadata = modelMetadata,
};
}
}
}
public class AnalyzeOptionsModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType != typeof(AnalyzeOptions))
{
return null;
}
Type[] subclasses = [typeof(ClassificationOptions), typeof(ExtractionOptions),];
var binders = new Dictionary<Type, (ModelMetadata, IModelBinder)>();
foreach (Type type in subclasses)
{
ModelMetadata modelMetadata = context.MetadataProvider.GetMetadataForType(type);
binders[type] = (modelMetadata, context.CreateBinder(modelMetadata));
}
return new AnalyzeOptionsModelBinder(binders);
}
}
And then register this custom ModelBinderProvider.
builder.Services.AddControllers(options =>
{
// Add the custom model binder to support [FromForm] with Polymorphic types
options.ModelBinderProviders.Insert(0, new AnalyzeOptionsModelBinderProvider());
});
And now if I execute the request again,
|
Custom Model Binder |
Raised an issue in ASP.NET Core repo,
[FromForm]: Support for Polymorphic Types #61378
Hope this helps.
Happy Coding.
Regards,
Jaliya
No comments:
Post a Comment