Controller Scoped Model Binding in ASP.NET Core
Want to avoid [FromBody] attributes everywhere? Don’t want to use [ApiController] strict conventions? Don’t want to apply IInputFormatter’s globally?
ASP.NET Core MVC is super flexible but it very much caters towards configuring everything at a global level. Perhaps you are building a framework or library or a CMS in .NET Core? In which case you generally want to be as unobtrusive as possible so mucking around with global MVC configuration isn’t really acceptable. The traditional way of dealing with this is by applying configuration directly to controllers which generally means using controller base classes and attributes. This isn’t super pretty but it works in almost all cases from applying authorization/resource/action/exception/result filters to api conventions. However this doesn’t work for model binding.
Model binding vs formatters
Model binding comes in 2 flavors: formatters for deserializing the request body (like JSON) into models and value providers for getting data from other places like form body, query string, headers, etc… Both of these things internally in MVC use model binders though typically the language used for binding the request body are called formatters. The problem with formatters (which are of type IInputFormatter) is that they are only applied at the global level as part of MvcOptions which are in turn passed along to a special model binder called BodyModelBinder. Working with IInputFormatter at the controller level is almost impossible.
There seems to be a couple options that look like you might be able to apply a custom IInputFormatter to a specific controller:
- Create a custom IModelBinderProvider – this unfortunately will not work because the ModelBinderProviderContext doesn’t provide the ControllerActionDescriptor executing so you cannot apply this provider to certain controllers/actions (though this should be possible).
- Assign a custom IModelBinderFactory to the controller explicitly by assigning ControllerBase.ModelBinderFactory in the controllers constructor – this unfortunately doesn’t work because the ControllerBase.ModelBinderFactory isn’t used for body model binding
So how does [ApiController] attribute work?
The [ApiController] attribute does quite a lot of things and configures your controller in a very opinionated way. It almost does what I want and it somehow magically does this
[FromBody] is inferred for complex type parameters
That’s great! It’s what I want to do but I don’t want to use the [ApiController] attribute since it applies too many conventions and the only way to toggle these …. is again at the global level :/ This also still doesn’t solve the problem of applying a specific IInputFormatter to be used for the model binding but it’s a step in the right direction.
The way that the [ApiController] attribute works is by using MVC’s “application model” which is done by implementing IApplicationModelProvider.
A custom IApplicationModelProvider
Taking some inspiration from the way [ApiController] attribute works we can have a look at the source of the application model that makes this happen: ApiBehaviorApplicationModelProvider. This basically assigns a bunch of IActionModelConvention’s: ApiVisibilityConvention, ClientErrorResultFilterConvention, InvalidModelStateFilterConvention, ConsumesConstraintForFormFileParameterConvention, ApiConventionApplicationModelConvention, and InferParameterBindingInfoConvention. The last one InferParameterBindingInfoConvention is the important one that magically makes complex type parameters bind from the request body like JSON like good old WebApi used to do.
So we can make our own application model to target our own controllers and use a custom IActionModelConvention to apply a custom body model binder:
public class MyApplicationModelProvider : IApplicationModelProvider
{
public MyApplicationModelProvider(IModelMetadataProvider modelMetadataProvider)
{
ActionModelConventions = new List<IActionModelConvention>()
{
// Ensure complex models are bound from request body
new InferParameterBindingInfoConvention(modelMetadataProvider),
// Apply custom IInputFormatter to the request body
new MyModelBinderConvention()
};
}
public List<IActionModelConvention> ActionModelConventions { get; }
public int Order => 0;
public void OnProvidersExecuted(ApplicationModelProviderContext context)
{
}
public void OnProvidersExecuting(ApplicationModelProviderContext context)
{
foreach (var controller in context.Result.Controllers)
{
// apply conventions to all actions if attributed with [MyController]
if (IsMyController(controller))
foreach (var action in controller.Actions)
foreach (var convention in ActionModelConventions)
convention.Apply(action);
}
}
// returns true if the controller is attributed with [MyController]
private bool IsMyController(ControllerModel controller)
=> controller.Attributes.OfType<MyControllerAttribute>().Any();
}
And the custom convention:
public class MyModelBinderConvention : IActionModelConvention
{
public void Apply(ActionModel action)
{
foreach (var p in action.Parameters
// the InferParameterBindingInfoConvention must execute first,
// which assigns this BindingSource, so if that is assigned
// we can then assign a custom BinderType to be used.
.Where(p => p.BindingInfo?.BindingSource == BindingSource.Body))
{
p.BindingInfo.BinderType = typeof(MyModelBinder);
}
}
}
Based on the above application model conventions, any controller attributed with our custom [MyController] attribute will have these conventions applied to all of it’s actions. With the above, any complex model that will be bound from the request body will use the IModelBinder type: MyModelBinder, so here’s how that implementation could look:
// inherit from BodyModelBinder - it does a bunch of magic like caching
// that we don't want to miss out on
public class MyModelBinder : BodyModelBinder
{
// TODO: You can inject other dependencies to pass to GetInputFormatter
public MyModelBinder(IHttpRequestStreamReaderFactory readerFactory)
: base(GetInputFormatter(), readerFactory)
{
}
private static IInputFormatter[] GetInputFormatter()
{
return new IInputFormatter[]
{
// TODO: Return any IInputFormatter you want
new MyInputFormatter()
};
}
}
The last thing to do is wire it up in DI:
services.TryAddSingleton<MyModelBinder>();
services.TryAddEnumerable(
ServiceDescriptor.Transient<IApplicationModelProvider,
MyApplicationModelProvider>());
That’s a reasonable amount of plumbing!
It could certainly be simpler to configure a body model binder at the controller level but at least there’s actually a way to do it. For a single controller this is quite a lot of work but for a lot of controllers the MVC “application mode” is quite brilliant! … it just took a lot of source code reading to figure that out :)