Skip to content

ControllerBase.ModelBinderFactory isn't actually used to do model binding #21724

@Shazwazza

Description

@Shazwazza

As a framework/library developer I find it difficult to apply 'MvcOptions' to only my controllers or areas since the documented and preferred way to do this is to apply MvcOptions globally. Ideally you could apply MvcOptions at an "Area" level (but that's a topic for another day). So in order to apply various MvcOptions to only my controllers I need to resort to using base classes and attributes which isn't very pretty. However I've found that ModelBinding and IInputFormatter's extensibility are particularly limited especially for binding the Body of a request.

For example, I don't want to apply this (AddNewtonsoftJson) globally, I just want the NewtonsoftJsonInputFormatter applied for my own controllers for body model binding.

services.AddMvc()
                .AddNewtonsoftJson(options =>
                {
                    options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
                });

But this is very difficult to make possible because:

  • It is not possible to create an IModelBinderProvider to target specific controllers. This is because the current ControllerActionDescriptor does not flow to the ModelBinderProviderContext even though a ControllerActionDescriptor is available when this whole process takes place within ControllerBinderDelegateProvider. If a model binder had access to the ControllerActionDescriptor then it would be possible to create a model binder specifically for certain controllers/assemblies/etc...
  • It is not possible to assign a custom IModelBinderFactory to the ControllerBase.ModelBinderFactory in it's ctor to be used for the actual model binding process. This was my last resort and I assumed this would work but actually assigning ControllerBase.ModelBinderFactory doesn't actually do anything apart from affect how ControllerBase.TryUpdateModelAsync works which isn't used for the actual model binding process (also mentioned here TryUpdateModelAsync not working in ASP․NET Core 3.1 #17876 and TryUpdateModelAsync() doesn't work for ASP.NET Core MVC Web API aspnet/Mvc#5659 (comment))

I know that I can assign [ModelBinder] attributes to my action parameters but I don't want to do that in a variety of cases. I know I can create a custom IModelBinderProvider and inspect the type being bound and only return a result for my own types, but that is also not what I need since I need controller specific model binders, not just model specific.

It seems like this area of MVC is very limiting but doesn't need to be since all of the info necessary to make this flexible is already there. I definitely would have thought that at least assigning an instance to ControllerBase.ModelBinderFactory that it would actually be used in the model binding process.

I have figured out how to do this though but it's quite a process to do:

  • Create a custom IModelBinder that you want to use to bind
  • Create a custom IApplicationModelProvider very similar to ApiBehaviorApplicationModelProvider
    • In my case I use a custom attribute assigned to my controllers similar to how the [ApiController] is used in ApiBehaviorApplicationModelProvider
  • Ensure that the InferParameterBindingInfoConvention convention is applied like this which will assign BindingSource.Body as the BindingSource to any complex parameter
  • Create a new IActionModelConvention and apply that convention after InferParameterBindingInfoConvention. The source can look like:
     public class MyBodyModelBinderConvention : IActionModelConvention
     {
     	public void Apply(ActionModel action)
     	{
     		foreach (var p in action.Parameters
     			.Where(p => p.BindingInfo?.BindingSource == BindingSource.Body))
     		{
     			p.BindingInfo.BinderType = typeof(MyBodyModelBinder);
     		}
     	}
     }
  • Configure DI:
    • services.TryAddSingleton<MyBodyModelBinder>();
    • services.TryAddEnumerable(ServiceDescriptor.Transient<IApplicationModelProvider, MyApplicationModelProvider>());

That's quite a process :) It's fine now that I've got it but for simplicity, I would expect that ControllerBase.ModelBinderFactory is just used to all the model binding including binding the body.

Further technical details

  • ASP.NET Core version: 3.1
  • The IDE: VS 2019 enterprise preview

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-mvcIncludes: MVC, Actions and Controllers, Localization, CORS, most templates

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions