I have a solution for this problem, but my solution seems a bit convoluted. I'll try to explain the problem and my solution with an example from Microsoft's documentation.
In the following example we can find this model binder where an id gets parsed and an Author object is retrieved from a repository or database.
public class AuthorEntityBinder : IModelBinder
{
...
public Task BindModelAsync(ModelBindingContext bindingContext)
{
...
if (!int.TryParse(value, out var id))
{
// Non-integer arguments result in model state errors
bindingContext.ModelState.TryAddModelError(
modelName, "Author Id must be an integer.");
return Task.CompletedTask;
}
// Model will be null if not found, including for
// out of range id values (0, -3, etc.)
var model = _context.Authors.Find(id);
bindingContext.Result = ModelBindingResult.Success(model);
return Task.CompletedTask;
}
}
This is used in the following Controller:
[HttpGet("get/{authorId}")]
public IActionResult Get(Author author)
{
if (author == null)
{
return NotFound();
}
return Ok(author);
}
So this would return a 404 in both situations. When the Author is not found and when the id is not an integer.
We could expand the controller with something like the following:
if (!ModelState.IsValid)
{
return BadRequest()
}
And we could even move this to an ActionFilterAttribute to prevent boilerplate code in our controllers:
public class ValidateModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
context.Result = new BadRequestObjectResult(context.ModelState);
}
}
}
and
[HttpGet("get/{authorId}")]
[ValidateModelAttribute]
public IActionResult Get(Author author)
{ ... }
I'm not sure how the null check could be moved to the ActionFilterAttribute. Probably something to do with context.ActionArguments[modelName] == null
but I don't know how to retrieve the correct modelName. In any case, that's not really my question. The question is what if my repository/business logic during the model binding returns different exceptions for certain situations and I want to return different response codes depending on that.
In the modelBinder can I add the errors with the exceptions:
try
{
var model = _context.Authors.Find(id);
bindingContext.Result = ModelBindingResult.Success(model);
return Task.CompletedTask;
}
catch(CustomExceptionA ex)
{
bindingContext.ModelState.TryAddModelError(modelName, ex, bindingContext.ModelMetaData);
return Task.CompletedTask;
}
catch(CustomExceptionB ex)
{
bindingContext.ModelState.TryAddModelError(modelName, ex, bindingContext.ModelMetaData);
return Task.CompletedTask;
}
Then in the ValidateModelAttribute I can retrieve the first error and type switch on the exception:
public class ValidateModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (context.ModelState.IsValid)
{
return;
}
var error = context.ModelState
.Where(s => !(s.Value.ValidationState is ModelValidationState.Valid))
.SelectMany(s => s.Value.Errors)
.First();
switch(error.Exception)
{
case CustomExceptionA ex:
context.Result = new BadRequestObjectResult(context.ModelState);
break;
case CustomExceptionB ex:
context.Result = new NotFoundObjectResult(context.ModelState);
break;
default:
context.Result = new ConflictObjectResult(context.ModelState);
break;
}
}
}
Would this be the way to go or am I missing a simpler solution? One downside I see from my solution is that the ActionFilterAttribute is executed after all the model binding has finished. Wouldn't it be better to exit the model binding preliminary when my business logic encounters an exception?
question from:
https://stackoverflow.com/questions/65843419/different-status-code-response-depending-on-model-validation-errors