Extending ProblemDetails - Add error code to ValidationProblemDetails
Tags: netcore, aspnetcore, webapi
Published: 2021-10-03 11:31:05

A while back in one project I faced a situation that required returning custom error code from API to the client. In the client app, the user after completing the registration process had to confirm his phone number. For some users, no text message was sent at that time and they could not do the phone verification step. Later, the user after login encountered an unverified phone number error and had to be redirected to the phone number verification page, but it was difficult for the client to understand when to redirect the user to the page because the server only returned an error message.

Since ASP.NET Core 2.2, I followed [RFC 7807 specification](https://datatracker.ietf.org/doc/html/rfc7807) standard to format errors in web API responses by using [ProblemDetails](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.problemdetails?view=aspnetcore-5.0).

The constructor of `ValidationProblemDetails` class accepts `ModelStateDictionary` and `Dictionary<string, string[]>` that the key field is property name and value is a list of error messages and the dictionary serialized into error field:
```json
{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
    "title": "One or more validation errors occurred.",
    "status": 400,
    "errors": {
        "User": [
            "The user phone number is not verified."
        ]
    }
}
```
I'm going to change error field to this:
```json
{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": [
    {
      "code": 100,
      "message": "The user phone number is not verified."
    }
  ]
}
```
Let's begin by creating custom `ValidationProblemDetails` class:
```csharp
using Microsoft.AspNetCore.Mvc;

public class CustomValidationProblemDetails : ValidationProblemDetails
{
    public CustomValidationProblemDetails()
    {
    }

    [JsonPropertyName("errors")]
    public new IEnumerable<ValidationError> Errors { get; } = new List<ValidationError>();
}
```

`ValidationProblemDetails` has an [Error](https://github.com/dotnet/aspnetcore/blob/cd8e89002761afba120ae1523b4311d129ac23c0/src/Mvc/Mvc.Core/src/ValidationProblemDetails.cs#L90) property that is `IDictionary<string, string[]>` and we replace this property with our version to add code error.
```csharp
public class ValidationError
{
    public int Code { get; set; }

    public string Message { get; set; }
}
```
Constructor of [ValidationProblemDetails](https://github.com/dotnet/aspnetcore/blob/cd8e89002761afba120ae1523b4311d129ac23c0/src/Mvc/Mvc.Core/src/ValidationProblemDetails.cs#L30) accepts `ModelStateDictionary` and we need to convert it to list of `ValidationError`:
```csharp
public CustomValidationProblemDetails(IEnumerable<ValidationError> errors)
{
    Errors = errors;
}

public CustomValidationProblemDetails(ModelStateDictionary modelState)
{
    Errors = ConvertModelStateErrorsToValidationErrors(modelState);
}

private List<ValidationError> ConvertModelStateErrorsToValidationErrors(ModelStateDictionary modelStateDictionary)
{
    List<ValidationError> validationErrors = new();

    foreach (var keyModelStatePair in modelStateDictionary)
    {
        var errors = keyModelStatePair.Value.Errors;
        switch (errors.Count)
        {
            case 0:
                continue;

            case 1:
                validationErrors.Add(new ValidationError { Code = null, Message = errors[0].ErrorMessage });
                break;

            default:
                var errorMessage = string.Join(Environment.NewLine, errors.Select(e => e.ErrorMessage));
                validationErrors.Add(new ValidationError { Message = errorMessage });
                break;
        }
    }

    return validationErrors;
}
```
Now it's time to create custom `ProblemDetailsFactory` to create `CustomValidationProblemDetails` when we want to return bad request response:
```csharp
public class CustomProblemDetailsFactory : ProblemDetailsFactory
{
    public override ProblemDetails CreateProblemDetails(HttpContext httpContext, int? statusCode = null, string title = null,
        string type = null, string detail = null, string instance = null)
    {
        var problemDetails = new ProblemDetails
        {
            Status = statusCode,
            Title = title,
            Type = type,
            Detail = detail,
            Instance = instance,
        };

        return problemDetails;
    }

    public override ValidationProblemDetails CreateValidationProblemDetails(HttpContext httpContext,
        ModelStateDictionary modelStateDictionary, int? statusCode = null, string title = null, string type = null,
        string detail = null, string instance = null)
    {
        statusCode ??= 400;
        type ??= "https://tools.ietf.org/html/rfc7231#section-6.5.1";
        instance ??= httpContext.Request.Path;

        var problemDetails = new CustomValidationProblemDetails(modelStateDictionary)
        {
            Status = statusCode,
            Type = type,
            Instance = instance
        };

        if (title != null)
        {
            // For validation problem details, don't overwrite the default title with null.
            problemDetails.Title = title;
        }

        var traceId = Activity.Current?.Id ?? httpContext?.TraceIdentifier;
        if (traceId != null)
        {
            problemDetails.Extensions["traceId"] = traceId;
        }

        return problemDetails;
    }
}
```
And final step is registering `CustomProblemDetailsFactory` in `ConfigureServices` method of `Startup` class:
```csharp
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddTransient<ProblemDetailsFactory, CustomProblemDetailsFactory>();
}
```
So far we haven't set the `Code` property of `ValidationError` and it's value will be null for data annotation validation or you can set default value when converting `ModelStateDictionary` to list of `ValidationError`:
```csharp
validationErrors.Add(new ValidationError { Code = "100", Message = errors[0].ErrorMessage });
``` 
In part three of [What Every ASP.NET Core Web API project needs - Exception handling middleware](https://dev.to/moesmp/what-every-asp-net-core-web-api-project-needs-part-3-exception-handling-middleware-3nif) series, I have created a domain exception class and also handling domain exception in [ExceptionHandlingMiddleware](https://github.com/mo-esmp/cool-webapi/blob/9ba52fcc407042cd8a8419b92af27170a9fe372b/CoolWebApi/Infrastructure/Middlewares/ApiExceptionHandlingMiddleware.cs#L40) and convert it to bad request response. I added the `Code` property to domain exception class to pass code:
```csharp
public class DomainException : Exception
{
    public DomainException(string message, string code = null)
        : base(message)
    {
        Code = code;
    }

    public string Code { get; }
}
```
Back to my scenario, in the login method, I want to check if the phone number of the user is not confirmed throw a domain exception and catch in middleware and return bad request:
```csharp
public async Task<IActionResult> Login(LoginViewModel model)
{
    var user = await _userManager.FindByEmailAsync(model.Email);
    if (!user?.PhoneConfirmed)
        throw new DomainException("The user phone number is not verified.", Code: "120");
}
```
And finally in exception handling middleware catch exception and convert to `CustomValidationProblemDetails`:
```csharp
public async Task Invoke(HttpContext context)
{
    try
    {
        await _next(context);
    }
    catch (Exception ex)
    {
        await HandleExceptionAsync(context, ex);
    }
}

private async Task HandleExceptionAsync(HttpContext context, Exception ex)
{
    string result;

    if (ex is DomainException e)
    {
        var problemDetails = new CustomValidationProblemDetails(
            new List<ValidationError>
            {
                new() { Code = e.Code, Message = e.Message }
            }
        )
        {
            Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
            Title = "One or more validation errors occurred.",
            Status = (int)HttpStatusCode.BadRequest,
            Instance = context.Request.Path,
        };
        context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
        result = JsonSerializer.Serialize(problemDetails);
    }
...
```

You can find the source code for this walkthrough on [Github](https://github.com/mo-esmp/cool-webapi).