Manual model validation is explicit controller logic that inspects ModelState after model binding and validation, then chooses the HTTP response. It becomes necessary when automatic [ApiController] validation is disabled, because actions still execute with invalid input. section 6.2.3
A practical implementation follows this sequence:
- Keep [ApiController] enabled, but scope the exception to the action that needs custom behavior instead of suppressing automatic validation globally. section 6.2.3
- Apply a ManualValidationFilter attribute to the selected action; the filter removes ModelStateInvalidFilter from that action's filter collection. section 6.2.3
- Check ModelState.IsValid inside the action, construct ValidationProblemDetails when validation fails, and return the response appropriate to the invalid input. section 6.2.3 section 6.3.4
- Process the request normally when ModelState is valid. section 6.3.4
Board-game API validation
A DomainsController action can use [ManualValidationFilter] and inspect ModelState itself. For an invalid model, it can return HTTP 403 when Id is not 3 and Name is not "Wargames", or HTTP 400 for other validation failures. A valid model continues through the normal action path. section 6.3.4
Listing 6.7 ManualValidationFilterAttribute
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Infrastructure;
namespace MyBGList.Attributes
{
public class ManualValidationFilterAttribute
: Attribute, IActionModelConvention
{
public void Apply(ActionModel action)
{
for (var i = 0; i < action.Filters.Count; i++)
{
if (action.Filters[i] is ModelStateInvalidFilter
|| action.Filters[i].GetType().Name ==
"ModelStateInvalidFilterFactory")
{
action.Filters.RemoveAt(i);
break;
}
}
}
}
}
Inspect the filter implementation that removes the automatic ModelStateInvalidFilter through IActionModelConvention. section 6.2.3
[HttpGet(Name = "GetDomains")]
[ResponseCache(Location = ResponseCacheLocation.Any, Duration = 60)]
public async Task<ActionResult<RestDTO<Domain[]>>> Get( #1
[FromQuery] RequestDTO<DomainDTO> input)
{
if (!ModelState.IsValid) #2
{
var details = new ValidationProblemDetails(ModelState);
details.Extensions["traceId"] =
System.Diagnostics.Activity.Current?.Id
?? HttpContext.TraceIdentifier;
if (ModelState.Keys.Any(k => k == "PageSize"))
{
details.Type =
"https://tools.ietf.org/html/rfc7231#section-6.6.2";
details.Status = StatusCodes.Status501NotImplemented;
return new ObjectResult(details) {
StatusCode = StatusCodes.Status501NotImplemented
};
}
else
{
details.Type =
"https://tools.ietf.org/html/rfc7231#section-6.5.1";
details.Status = StatusCodes.Status400BadRequest;
return new BadRequestObjectResult(details);
}
}
// ... code omitted ... #3
}
Inspect the action-level ModelState check, ValidationProblemDetails construction, trace identifier, and conditional response handling. section 6.2.3
