How do I disable automatic model binding for a specific ASP.NET Core 5.0 Web API action?

Viewed 2506

I have a third-party proprietary application, for which I need to write an API endpoint in my ASP.NET Core 5.0 web API application.

The third party application sends out an HTTP post request, with only binary data in the request body, alongside the content type application/x-www-form-urlencoded or, sometimes, application/octet-stream (kind of random, but the data is the same).

My action handler looks like this:

[Route("~/Validation")]
[ApiController]
public class ValidationController : ControllerBase
{
    [HttpPost("{requestId}")]
    [Consumes(@"application/octet-stream", @"application/x-www-form-urlencoded")]
    [Produces(@"application/octet-stream")]
    public async Task<IActionResult> Validation_Post([FromRoute] string requestId)
    {
        byte[] rawRequestBody = Array.Empty<byte>();
        {
            long streamInitialPos = 0;
            if (Request.Body.CanSeek) // rewind for this read.
            {
                streamInitialPos = Request.Body.Position;
                Request.Body.Seek(0, SeekOrigin.Begin);
            }
            using (var ms = new MemoryStream())
            {
                await Request.Body.CopyToAsync(ms);
                rawRequestBody = ms.ToArray() ?? throw new NullReferenceException();
            }
            if (Request.Body.CanSeek) // rewind to initial position.
                Request.Body.Seek(streamInitialPos, SeekOrigin.Begin);
        }

        // TODO: Handle rawRequestBody data.

        return new FileContentResult(new byte[] { 1 }, @"application/octet-stream")
        {
            EnableRangeProcessing = true,
            LastModified = DateTime.UtcNow
        };
    }

When the third-party application is sending its HTTP post request to my API endpoint, my API application crashes with a System.ArgumentException:

Microsoft.AspNetCore.Server.IIS.Core.IISHttpServer: Error: Connection ID "18374686481282236432", Request ID "80000011-0000-ff00-b63f-84710c7967bb": An unhandled exception was thrown by the application.

System.ArgumentException: The key '[omitted binary data]' is invalid JQuery syntax because it is missing a closing bracket. (Parameter 'key')
   at Microsoft.AspNetCore.Mvc.ModelBinding.JQueryKeyValuePairNormalizer.NormalizeJQueryToMvc(StringBuilder builder, String key)
   at Microsoft.AspNetCore.Mvc.ModelBinding.JQueryKeyValuePairNormalizer.GetValues(IEnumerable`1 originalValues, Int32 valueCount)
   at Microsoft.AspNetCore.Mvc.ModelBinding.JQueryFormValueProviderFactory.AddValueProviderAsync(ValueProviderFactoryContext context)
   at Microsoft.AspNetCore.Mvc.ModelBinding.CompositeValueProvider.CreateAsync(ActionContext actionContext, IList`1 factories)
   at Microsoft.AspNetCore.Mvc.ModelBinding.CompositeValueProvider.TryCreateAsync(ActionContext actionContext, IList`1 factories)
   at Microsoft.AspNetCore.Mvc.Controllers.ControllerBinderDelegateProvider.<>c__DisplayClass0_0.<<CreateBinderDelegate>g__Bind|0>d.MoveNext()
--- End of stack trace from previous location ---
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeInnerFilterAsync>g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|24_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeFilterPipelineAsync()
--- End of stack trace from previous location ---
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at Microsoft.AspNetCore.Server.IIS.Core.IISHttpContextOfT`1.ProcessRequestAsync()

Microsoft.AspNetCore.Hosting.Diagnostics: Information: Request finished HTTP/1.1 POST http://localhost:10891/validation/dummy application/x-www-form-urlencoded 11072 - 500 - - 164.0024ms

The logs are showing that the correct route action is being used.

How do I disable automatic model binding for only this specific action handler?

Reminder: I cannot do any changes to the third-party application. I have to handle what I receive. I know the request content type is wrong. Please don't make any notes in that regard.


Edit: I have found the surface-level cause for this error. When I remove [FromRoute] string requestId from the function signature, the error will not occur. When I reintroduce it, the error occurs again.

Does not work (causes ASP.NET Core internal exception):

public async Task<IActionResult> Validation_Post([FromRoute] string requestId)

Does work:

public async Task<IActionResult> Validation_Post()

However, I need to access the route variable through Request.RouteValues["requestId"].

Anyway, the question still stands: How do I disable automatic model binding for only this specific action handler?

2 Answers

I think you’ve actually figured out the solution already—or, at least, identified all of the key components. Allow me to walk through them so you can assemble them into a solution.

Disabling Model Binding

Model binding only occurs when you have parameters on your action—otherwise there is no model to bind to. This could be an actual model, such as a POCO, or simply a value type, such as the string parameter you have here; the same basic process applies to both. That’s why you don’t receive the error when you remove the parameter—and also why you’ve effectively answered your question already.

Extracting route values without model binding

The only purpose or benefit to having a parameter is for it to engage in model binding. So if you want to disable model binding, there’s no reason to maintain the parameter. Without it, you can still use the Request property to extract request values, including form fields, query string parameters, route variables, request headers, &c. So, in this case, you can still call e.g., Request.RouteValues["requestId"] within your action, as you noted. Making calls directly to the RouteValueDictionary won’t trigger the call to the JQueryKeyValuePairNormalizer class, so you won’t encounter the same exception.

Potential route ambiguity

That said, depending on how your route and route parameters are defined, you may encounter issues if you’re e.g., relying on this parameter to disambiguate between other overloads of your action, so that’s worth being aware of.

The underlying problem

To return to the core error, however, I’d also recommend evaluating your request data on failed requests to ensure there isn’t anything that might be confused with a field name containing a malformed indexer. From what you describe, I wouldn’t expect that to be the case, unless it was somehow trying to parse the binary data as key/value pairs expected of form data, possibly due to the errant content type. Regardless, the line of code that’s throwing the error occurs, specifically, when there is a field containing a [ without a corresponding ]. For example, it should occur with the following query string:

?key[0=5

If something like that is a consistent fixture of your incoming requests, that’s likely the culprit. Obviously, you can’t do anything about it since you don’t control the client, but it’d be useful to isolate so you can offer guidance for future implementations.

Maybe this works to disable model binding?

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
    public void OnResourceExecuting(ResourceExecutingContext context)
    {
        var factories = context.ValueProviderFactories;
        factories.RemoveType<FormValueProviderFactory>();
        factories.RemoveType<FormFileValueProviderFactory>();
        factories.RemoveType<JQueryFormValueProviderFactory>();
    }

    public void OnResourceExecuted(ResourceExecutedContext context)
    {
    }
}
Related