Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom model binder for a webapi method with more than one parameter

WHAT I HAVE

I have an api controller (ASP.NET Core MVC) with the following method:

[HttpPost]
[Route("delete")]
public Task<ActionResult> SomeAction(Guid[] ids,  UserToken userToken, CancellationToken cancellationToken)
{
   ....
}

I have a custom model binder and binder provider:

public class UserTokenBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (context.Metadata.ModelType == typeof(UserToken))
        {
            return new BinderTypeModelBinder(typeof(UserTokenBinder));
        }

        return null;
    }
}

public class UserTokenBinder: IModelBinder
{
    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var token = await bindingContext.ActionContext.HttpContext.User.ToUserTokenAsync(CancellationToken.None);

        bindingContext.Result = ModelBindingResult.Success(token ?? UserToken.UnidentifiedUser);
    }
}

Added the binder provider to services:

services.AddMvc(options =>
{                    
    options.ModelBinderProviders.Insert(0, new UserTokenBinderProvider());
});

THE ISSUE

While server is loading I'm getting the following exception (InvalidOperationException):

... 'SomeAction' has more than one parameter that was specified or inferred as bound from request body. Only one parameter per action may be bound from body. Inspect the following parameters, and use 'FromQueryAttribute' to specify bound from query, 'FromRouteAttribute' to specify bound from route, and 'FromBodyAttribute' for parameters to be bound from body: Guid[] ids, UserToken userToken

It seems that MVC disregards the custom binder I have for the UserToken type and tries to bind it using default methods. Any ideas why?

EDIT After receiving an answer here, an issue was opened to amend ASP.NET Core documentation.

like image 383
Aryéh Radlé Avatar asked Jul 04 '19 08:07

Aryéh Radlé


1 Answers

The presence of the [ApiController] attribute introduces Binding source parameter inference for action parameters. At startup, an action model convention runs against all detected controller actions and infers the binding sources. For complex types, such as your Guid[] and UserToken parameters, this inference chooses the request body as the source - it's as though you'd added [FromBody] to both of those parameters yourself, like this:

public Task<ActionResult> SomeAction(
    [FromBody] Guid[] ids,
    [FromBody] UserToken userToken,
    CancellationToken cancellationToken)

In your question, you state:

It seems that MVC disregards the custom binder I have for the UserToken type and tries to bind it using default methods.

This isn't quite what's going on here. It's not trying to bind anything yet - it's just trying to configure the binding sources at startup, before model binding can even occur. You've correctly instructed MVC to use your custom model binder, but the action model convention I mentioned above doesn't know anything about the IModelBinderProvider you've added. Even if it did, the actual association between the model binder provider and the type (UserToken) isn't known until the GetBinder method runs, which only happens when model binding is needed; not at startup when the application model is being configured.

If you were to update your UserToken class to include a [ModelBinder] attribute, it would all work (you could even remove UserTokenBinderProvider):

[ModelBinder(typeof(UserTokenBinderProvider))]
public class UserToken { }

The big downside to this approach is that your UserToken class would be dependent on an MVC attribute, which might not be something you want. So, is there something better?

Now, you might be wondering why I didn't show [FromBody] for the CancellationToken parameter above. Does this mean that CancellationToken gets special treatment? Yes, it does. A BindingSourceMetadataProvider is added to the MvcOptions instance that specifies its binding source as BindingSource.Special. When the action model convention runs and attempts to infer the binding source, it sees that the binding source is already set and leaves it alone.

To resolve your issue, add a BindingSourceMetadataProvider for your UserToken type and use BindingSource.Special, like this:

services.AddMvc(options =>
{                    
    options.ModelBinderProviders.Insert(0, new UserTokenBinderProvider());
    options.ModelMetadataDetailsProviders.Add(
        new BindingSourceMetadataProvider(typeof(UserToken), BindingSource.Special));
});
like image 98
Kirk Larkin Avatar answered Nov 06 '22 07:11

Kirk Larkin