Killnine
Killnine

Reputation: 5890

Triggering InvalidModelStateResponseFactory when ModelState is invalid

I am writing an API using asp.net core 3.0 and have configured my application with the following behavior for all controllers:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers()
        .ConfigureApiBehaviorOptions(setupAction =>
        {
            setupAction.InvalidModelStateResponseFactory = context =>
            {
                var problemDetails = new ValidationProblemDetails(context.ModelState)
                {
                    Type = "https://courselibrary.com/modelvalidationproblem",
                    Title = "One or more model validation errors occurred.",
                    Status = StatusCodes.Status422UnprocessableEntity,
                    Detail = "See the errors property for details",
                    Instance = context.HttpContext.Request.Path
                };

                problemDetails.Extensions.Add("traceId", context.HttpContext.TraceIdentifier);

                return new UnprocessableEntityObjectResult(problemDetails)
                {
                    ContentTypes = { "application/problem+json" }
                };
            };
        });
    ...
}

This works great with data annotations on my input (ex: [Required]) class's properties. It returns a 422 Unprocessable Entity reponse like this if any annotations fail:

{
    "type": "https://courselibrary.com/modelvalidationproblem",
    "title": "One or more model validation errors occurred.",
    "status": 422,
    "detail": "See the errors property for details",
    "instance": "/api/songbooks/21924d66-dac6-43a5-beee-206de4d35216/songs",
    "traceId": "0HLQFGSFIFL5L:00000001",
    "errors": {
        "Title": [
            "The Title field is required."
        ]
    }
}

However, I am implementing FluentValudation in my controller like this:

if (!newSong.IsValid)
{
    newSong.Validate().AddToModelState(ModelState, null);
    _logger.LogWarning("{method} failed model validation (ModelState: {@modelState}), returning Unprocessable Entity", nameof(Post), ModelState.Values.SelectMany(v => v.Errors));
    return UnprocessableEntity(ModelState);                
}

However, this doesn't trigger the InvalidModelStateResponseFactory like the built-in validation does.

Does anyone know how I can trigger the same sort of event from within my controller to use this convenient handler?

Upvotes: 7

Views: 11480

Answers (1)

Killnine
Killnine

Reputation: 5890

In fact, there is a pretty simple answer. Rather than returning an ActionResult of Ok() or BadRequest() or whatnot, by using this ValidationProblem pattern, you can simply return ValidationProblem(ModelState) and it'll use the factory set up in the Startup.cs Api configuration!

So here's what I can do instead:

if (!newSong.IsValid)
{
    newSong.Validate().AddToModelState(ModelState, null);
    _logger.LogWarning("{method} failed model validation (ModelState: {@modelState}), returning Unprocessable Entity", nameof(Post), ModelState.Values.SelectMany(v => v.Errors));
    return ValidationProblem(ModelState);                
}

In addition, you'll need to override the behavior to pull it out from your startup like this (Thanks to Kevin Dockx for this idea):

public override ActionResult ValidationProblem([ActionResultObjectValue] ModelStateDictionary modelStateDictionary)
    {
        var options = HttpContext.RequestServices.GetRequiredService<IOptions<ApiBehaviorOptions>>();
        return (ActionResult)options.Value.InvalidModelStateResponseFactory(ControllerContext);
    }

Upvotes: 7

Related Questions