C# REST API - how to extend the model state error with an error code

Viewed 396

I'm creating an ASP.net Core Web API (.net 5) to serve data to a Single Page Application (SPA). I'm using DataAnnotations (System.ComponentModel.DataAnnotations) to carry out model state validation in a very standard way in my Controller. I want to ensure my error responses come back in a very consistent manner (and are translatable in the front end!).

MVC Controller

Example controller is as follows:

[ApiController, Route("[controller]")]
public class AgencyController : Controller
{
    private readonly IOptions<ApiBehaviorOptions> _apiBehaviorOptions;

    public AgencyController(IOptions<ApiBehaviorOptions> apiBehaviorOptions)
    {
        _apiBehaviorOptions = apiBehaviorOptions;
    }

    [HttpPost]
    [ProducesResponseType(typeof(void), (int) HttpStatusCode.OK)]
    [ProducesResponseType(typeof(ProblemDetails), (int) HttpStatusCode.BadRequest)]
    public IActionResult Create([FromBody] ExampleCreateModel createModel)
    {
        // If the email already exists, add the custom error.
        var emailExists = EmailAlreadyExists(createModel.Email);
        if (emailExists)
        {
            ModelState.AddModelError("Email", "Email already exists");
            return _apiBehaviorOptions.Value.InvalidModelStateResponseFactory(ControllerContext);
        }

        // Return some relevant status...
        return Ok();
    }

    // Real verification implementation would go here....
    private bool EmailAlreadyExists(string email) => true;
}

This sample code above demonstrates a controller that takes takes the POCO model (shown below) and if it passes the attribute validation, carries out additional inline validation to ensure the email address doesn't already exist (pseudo code for the email exists check).

Sample Model

public class ExampleCreateModel
{
    [Required] 
    public string Name { get; set; } = "";

    [Required]
    [EmailAddress(ErrorMessage = "Invalid email format")]
    public string Email { get; set; } = "";
}

Error Response Format

Validation failure on the model shown above yields the standard error object from the MVC app in this format (sample):
{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "traceId": "00-6b30709ed71b7347afd41e0ed58e1ccb-e3ede2ff7591a24d-00",
  "errors": {
    "Email": [
      "Email already exists"
    ]
  }
}

this format is consistent for both regular attribute validation and the custom inline validation in the code above

My Question

The advantage of using the built in errors is consistency (and follows a standard pattern). BUT one down side for me is that I'd like to return error codes as well as text as part of the error object, so the UI can translate them but it doesn't seem to easily support that i.e. an "Email already exists" error could be a 3001 error and the UI could show a 3001 in various languages.

Is there any standard way to use the existing DataAnnotation attributes to include additional information? Such that the POCO model would become something like this:

public class ExampleCreateModel
{
    [Required(ErrorCode = 3000)] 
    public string Name { get; set; } = "";

    [Required]
    [EmailAddress(ErrorMessage = "Invalid email format", ErrorCode = 3001)]
    public string Email { get; set; } = "";
}

Resulting in an error object similar to this:

{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "traceId": "00-6b30709ed71b7347afd41e0ed58e1ccb-e3ede2ff7591a24d-00",
  "errors": {
    "Email": [
      { "message": "Email already exists", "errorCode": 3002 }
    ]
  }
}

Just to restate for clarity - my aim here is to give the UI an opportunity to easily translate error code, while achieving error consistency using the out-of-the-box error response.

Thanks in advance for any pointers!

2 Answers

Is there any standard way to use the existing DataAnnotation attributes to include additional information?

No.

I think that the simplest (but pretty hacky) approach could be to include the error code in the message string itself and then extract it client-side:

public class ExampleCreateModel
{
    // ...
    [Required]
    [EmailAddress(ErrorMessage = "ErrorCode:3001 - Invalid email format")]
    public string Email { get; set; } = "";
}

You could even go one step further and put a serialized JSON object in there to make it more structured (but pay attention to escaping). Of course you should centralize the logic of creating such 'custom' error messages into some kind of static utilty class in order to make it consistent throughout your application.

Related