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.
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));
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With