Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
232 views
in Technique[技术] by (71.8m points)

c# - Different status code response depending on model validation errors

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

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)
Waitting for answers

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...