I have gone through the MSDN documentation:
I tried creating a scenario where value sent from the swagger to the API, failed to bind to the model, that is expected on the server. Here is the code of the scenario:
OrderController.cs
[HttpPost]
public async Task<IActionResult> CreateAsync(OrderViewModel viewModel)
{
//map and add this model to the db
//and return a 201 status code
}
And the input I sent from the swagger:
{
null
}
This led to the model-binding failure, and I have a result filter where I am handling this situation as follows: FailedValidationResultFilter
public class FailedValidationResultFilter : IResultFilter
{
public void OnResultExecuted(ResultExecutedContext context)
{
}
public void OnResultExecuting(ResultExecutingContext context)
{
//When model-binding fails
var hasModelBindingFailed = context.ModelState.Any(pair => String.IsNullOrEmpty(pair.Key));
if (hasModelBindingFailed)
{
//do something when model-binding fails.
//and return BadRequestObjectResult
}
//When validation attributes fails
var invalidParams = new Dictionary<String, String[]>(context.ModelState.Count);
foreach (var keyModelStatePair in context.ModelState)
{
var key = keyModelStatePair.Key;
var modelErrors = keyModelStatePair.Value.Errors;
if (modelErrors is not null && modelErrors.Count > 0)
{
var errorMessages = modelErrors.Select(error => error.ErrorMessage).ToArray();
invalidParams.Add(key, errorMessages);
}
}
var problemDetails = new ProblemDetails
{
Type = "123",
Title = "Invalid parameters",
Status = StatusCodes.Status400BadRequest,
Detail = "Your request parameters didn't validate.",
Instance = ""
};
problemDetails.Extensions.Add(nameof(invalidParams), invalidParams);
context.Result = new BadRequestObjectResult(problemDetails);
}
}
What I have observed while debugging is this, that whenever model-binding fails for this input, it returns 2 key value pair:
{ "", "Some error message" }
{ "viewModel", "Again some error message" }
So, I am checking if their is a model-state with an empty key, if it is then there is a model-binding error. And I am not sure why, but it just doesn't feel like the right approach to find if model-binding has failed.
Question: what is the correct way to identify if model binding has failed? What could be another input type that can be passed which leads to failure in model-binding and then in the filter, the first property may not be blank/empty as I am expecting it to be?
After doing much test and trial, I hope I have the correct answer. So, let's begin.
When Request Payload is
null
When we send this payload in the request, the model-validation fails generating 2 keys (one of them is an empty string) with the following error-messages:
Key | Error Message |
---|---|
A non-empty request body is required. | |
viewModel | The viewModel field is required. |
When Request Payload is
{
null
}
In this case these 2 keys are generated:
Key | Error Message |
---|---|
$ | 'n' is an invalid start of a property name. Expected a '"'. Path: $ | LineNumber: 1 | BytePositionInLine: 2. |
viewModel | The viewModel field is required. |
Now, I have used the following piece of code to handle both the scenarios:
//When model-binding fails because input is an invalid JSON
if (modelStateDictionary.Any(pair => pair.Key == DollarSign || String.IsNullOrEmpty(pair.Key)))
{
problemDetails.Detail = RequestFailedModelBinding;
context.Result = GetBadRequestObjectResult(problemDetails);
return;
}
Complete code:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using WebApi.ErrorResponse.ViaFilterAndMiddleware.ViewModels;
using static Microsoft.AspNetCore.Http.StatusCodes;
public class MyModelValidationResultFilter : IResultFilter
{
#region Private Constants
private const Char Dot = '.';
private const String DollarSign = "$";
private const String InvalidParameters = "Invalid parameters.";
private const String RequestFailedModelBinding = "Your request failed model-binding.";
private const String RequestPropertyFailedModelBinding = "Your request failed model-binding: '{0}'.";
private const String RequestParametersDidNotValidate = "Your request parameters did not validate.";
private const String MediaTypeApplicationProblemJson = "application/problem+json";
#endregion Private Constants
/// <summary>
///
/// </summary>
/// <param name="context">The result executed context.</param>
public void OnResultExecuted(ResultExecutedContext context)
{
}
/// <summary>
///
/// </summary>
/// <param name="context">The result executing context.</param>
public void OnResultExecuting(ResultExecutingContext context)
{
if (context.ModelState.IsValid)
return;
var modelStateDictionary = context.ModelState;
var problemDetails = new ProblemDetails
{
Title = InvalidParameters,
Status = Status400BadRequest
};
//When model-binding fails because input is an invalid JSON
if (modelStateDictionary.Any(pair => pair.Key == DollarSign || String.IsNullOrEmpty(pair.Key)))
{
problemDetails.Detail = RequestFailedModelBinding;
context.Result = GetBadRequestObjectResult(problemDetails);
return;
}
//When a specific property-binding fails
var keyValuePair = modelStateDictionary.FirstOrDefault(pair => pair.Key.Contains("$."));
if (keyValuePair.Key is not null)
{
var propertyName = keyValuePair.Key.Split(Dot)[1];
problemDetails.Detail =
String.IsNullOrEmpty(propertyName) ? RequestFailedModelBinding : String.Format(RequestPropertyFailedModelBinding, propertyName);
context.Result = GetBadRequestObjectResult(problemDetails);
return;
}
//When one of the input parameters failed model-validation
var invalidParams = new List<InvalidParam>(modelStateDictionary.Count);
foreach (var keyModelStatePair in modelStateDictionary)
{
var key = keyModelStatePair.Key;
var modelErrors = keyModelStatePair.Value.Errors;
if (modelErrors is not null && modelErrors.Count > 0)
{
IEnumerable<InvalidParam> invalidParam;
if (modelErrors.Count == 1)
{
invalidParam = modelErrors.Select(error => new InvalidParam(keyModelStatePair.Key, new[] { error.ErrorMessage }));
}
else
{
var errorMessages = new String[modelErrors.Count];
for (var i = 0; i < modelErrors.Count; i++)
{
errorMessages[i] = modelErrors[i].ErrorMessage;
}
invalidParam = modelErrors.Select(error => new InvalidParam(keyModelStatePair.Key, errorMessages));
}
invalidParams.AddRange(invalidParam);
}
}
problemDetails.Detail = RequestParametersDidNotValidate;
problemDetails.Extensions[nameof(invalidParams)] = invalidParams;
context.Result = GetBadRequestObjectResult(problemDetails);
}
/// <summary>
/// Creates <see cref="BadRequestObjectResult"/> instance.
/// The content-type is set to: 'application/problem+json'
/// </summary>
/// <param name="problemDetails">The problem details instance.</param>
/// <returns>The bad request object result instance.</returns>
private static BadRequestObjectResult GetBadRequestObjectResult(ProblemDetails problemDetails)
{
var result = new BadRequestObjectResult(problemDetails);
result.ContentTypes.Clear();
result.ContentTypes.Add(MediaTypeApplicationProblemJson);
return result;
}
}