Search code examples
c#asp.net-coremodelstatemodel-validation

Is there any way to add error code to ModelState error


I'm looking for a way to add error code alongside the error message to ModelState. for example

ModelState.AddModelError("ErrorKey", new { Code = 4001, Message = "Some error message" });

For some bad requests client should do an action and comparing error message is not an ideal solution for making a decision. ModelState.AddModelError method only accepts two parameters, an error key and a message. Is there a way to achieve this or something similar?


Solution

  • I found a way to add the error code to ValidationProblemDetails:

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

    ValidationProblemDetails has an Error property that is IDictionary<string, string[]> and replace this property with our version to add code error.

    public class ValidationError
    {
        public int Code { get; set; }
    
        public string Message { get; set; }
    }
    

    Constructor of ValidationProblemDetails accepts ModelStateDictionary and need to convert it to list of ValidationError:

    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 = 100, 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;
    }
    

    Create custom ProblemDetailsFactory to create CustomValidationProblemDetails when we want to return bad request response:

    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 at the end register the factory:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
    
        services.AddTransient<ProblemDetailsFactory, CustomProblemDetailsFactory>();
    }
    

    Read the Extending ProblemDetails - Add error code to ValidationProblemDetails for more detail.