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?
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.