Simon Vane
Simon Vane

Reputation: 2014

ASP.NET Core: Prevent Automatic HTTP 400 responses for individual action

I like the Automatic HTTP 400 responses functionality new to ASP.NET Core 2.1 and it's working out really well for most cases.

However, in one action I need to do a bit of pre-processing before validation the payload. I have a custom validator that requires two values in the model to perform validation. One of those values is in the path so I would like to set that value on the model from the path then validate.

I don't want to switch the functionality off for all actions with:

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<ApiBehaviorOptions>(options =>
    {
        options.SuppressModelStateInvalidFilter = true;
    });
}

Is there any way I could switch it off just for an individual action?

Edit:

I tried modifying the InvalidModelStateResponseFactory but it didn't solve my problem because I still need to get into the controller action:

services.Configure<ApiBehaviorOptions>(options =>
{
    options.InvalidModelStateResponseFactory = actionContext =>
    {
        var ignore = actionContext.ActionDescriptor.FilterDescriptors.Any(fd => fd.Filter is SuppressModelStateInvalidFilterAttribute);
        if (ignore)
        {
            // Can only return IActionResult so doesn't enter the controller action.
        }

        return new BadRequestObjectResult(actionContext.ModelState);
    };
});

[AttributeUsage(AttributeTargets.Method)]
public class SuppressModelStateInvalidFilterAttribute : FormatFilterAttribute
{
}

Edit:

Here's a link to an issue I raised on the asp.net core repo in case I get anywhere with that - https://github.com/aspnet/Mvc/issues/8575

Upvotes: 7

Views: 4570

Answers (5)

Miroslav Lhoťan
Miroslav Lhoťan

Reputation: 183

I encountered similar problem and came up with this solution.

public class SuppressModelStateInvalidFilterAttribute : ActionFilterAttribute
{
    public SuppressModelStateInvalidFilterAttribute()
    {
        Order = -2500;
    }

    public override Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        context.ModelState.Clear();
        return next.Invoke();
    }
}

Upvotes: 2

MathuSum Mut
MathuSum Mut

Reputation: 2825

Update: you can just use the following code in ConfigureServices in Startup.cs:

services.Configure<ApiBehaviorOptions>(apiBehaviorOptions => {
    apiBehaviorOptions.SuppressModelStateInvalidFilter = true;
});

Based on Simon Vane's answer, I had to modify the attribute for ASP.Net Core 2.2 as follows:

/// <summary>
/// Suppresses the default ApiController behaviour of automatically creating error 400 responses
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class SuppressModelStateInvalidFilterAttribute : Attribute, IActionModelConvention {
    private static readonly Type ModelStateInvalidFilterFactory = typeof(ModelStateInvalidFilter).Assembly.GetType("Microsoft.AspNetCore.Mvc.Infrastructure.ModelStateInvalidFilterFactory");

    public void Apply(ActionModel action) {
        for (var i = 0; i < action.Filters.Count; i++) {
            if (action.Filters[i] is ModelStateInvalidFilter || action.Filters[i].GetType() == ModelStateInvalidFilterFactory) {
                action.Filters.RemoveAt(i);
                break;
            }
        }
    }
}

Upvotes: 12

Simon Vane
Simon Vane

Reputation: 2014

I had a response from Microsoft - https://github.com/aspnet/Mvc/issues/8575

The following worked a charm.

[AttributeUsage(AttributeTargets.Method)]
public class SuppressModelStateInvalidFilterAttribute : Attribute, IActionModelConvention
{
    public void Apply(ActionModel action)
    {
        for (var i = 0; i < action.Filters.Count; i++)
        {
            if (action.Filters[i] is ModelStateInvalidFilter)
            {
                action.Filters.RemoveAt(i);
                break;
            }
        }
    }
}

In my controller I could then make changes to the model before re-validating it (note the ModelState.Clear(), TryValidateModel add to existing model state):

if (model == null)
{
    return BadRequest(ModelState);
}

model.Property = valueFromPath;

ModelState.Clear();
if (TryValidateModel(model) == false)
{
    return BadRequest(ModelState);
}

Upvotes: 5

Dmitry Pavlov
Dmitry Pavlov

Reputation: 28290

You could play with ApiBehaviorOptions.InvalidModelStateResponseFactory property to handle specific cases based on actionContext details:

services.Configure<ApiBehaviorOptions>(options =>
{
    options.InvalidModelStateResponseFactory = actionContext => 
    {
        // Do what you need here for specific cases with `actionContext` 
        // I believe you can cehck the action attributes 
        // if you'd like to make mark / handle specific cases by action attributes. 

        return new BadRequestObjectResult(context.ModelState);
    }
});

Upvotes: 2

Karl-Johan Sj&#246;gren
Karl-Johan Sj&#246;gren

Reputation: 17532

This could probably be solved by implementing your own validator for your specific case. It is covered quite well in the documentation.

https://learn.microsoft.com/en-us/aspnet/core/mvc/models/validation?view=aspnetcore-2.1#custom-validation

Either that or possibly a custom model binder to create your model with all the preprocessing done before it is validated.

Upvotes: 1

Related Questions