Search code examples
c#asp.net-coreasp.net-web-apiasp.net-core-webapi.net-5

Best way to implement the ProblemDetails in Asp.Net Core API in clean way


I need to use ProblemDetails for the validation errors. It's working as expected. But there is a big problem here, I have to write a similar code in all the action methods and I think it's not a good idea.

public async Task<ActionResult<SampleResponse>> Post([FromBody] SampleRequest getRateApiRequest)
{
    try
    {
        if (ModelState.IsValid == false)
        {
            ProblemDetails problemDetails = new ProblemDetails();
            problemDetails.Detail = "Detail";
            problemDetails.Instance = "Instance";
            problemDetails.Status = StatusCodes.Status400BadRequest;
            problemDetails.Title = "Title";
            problemDetails.Type = "Type";

            List<FieldCodeMessage> codeMessages = new List<FieldCodeMessage>();
            foreach (var modelState in ModelState)
            {
                if (modelState.Value.ValidationState == Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid)
                {
                    MemberInfo property = typeof(TradeBookingRequestAPI).GetProperty(modelState.Key);
                    var attribute = property.GetCustomAttributes(typeof(DisplayNameAttribute), true).Cast<DisplayNameAttribute>().Single();
                    string displayName = attribute.DisplayName;
                    switch (modelState.Key)
                    {
                        case "Property1":
                            codeMessages.Add(new FieldCodeMessage(field: displayName, code: "01", message: modelState.Value.Errors.Select(a => a.ErrorMessage).FirstOrDefault()));
                            break;
                        case "Property2":
                            codeMessages.Add(new FieldCodeMessage(field: displayName, code: "02", message: modelState.Value.Errors.Select(a => a.ErrorMessage).FirstOrDefault()));
                            break;
                        case "Property3":
                            codeMessages.Add(new FieldCodeMessage(field: displayName, code: "03", message: modelState.Value.Errors.Select(a => a.ErrorMessage).FirstOrDefault()));
                            break;
                        case "Property4":
                            codeMessages.Add(new FieldCodeMessage(field: displayName, code: "04", message: modelState.Value.Errors.Select(a => a.ErrorMessage).FirstOrDefault()));
                            break;
                        case "Property5":
                            codeMessages.Add(new FieldCodeMessage(field: displayName, code: "05", message: modelState.Value.Errors.Select(a => a.ErrorMessage).FirstOrDefault()));
                            break;
                        case "Property6":
                            codeMessages.Add(new FieldCodeMessage(field: displayName, code: "06", message: modelState.Value.Errors.Select(a => a.ErrorMessage).FirstOrDefault()));
                            break;
                    }
                }
            }

            problemDetails.Extensions.Add("Invalid Fields", codeMessages);

            return BadRequest(problemDetails);
        }
    }
    catch (Exception)
    {
        ...
    }
}

So Is there a way to handle this in a centralized place like middleware or something else.

Expected response:

{
    "type": "Type",
    "title": "Title",
    "status": 400,
    "detail": "Detail",
    "instance": "Instance",
    "Invalid Fields": [
        {
            "field": "Proprty 1",
            "code": "01",
            "message": "Invalid Proprty 1"
        },
        {
            "field": "Property 2",
            "code": "02",
            "message": "Invalid Property 2"
        }
    ]
}

I have extened ValidationAttribute to implement the validation logic for all properties, Below is implementation for Property1.

protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
{
    try
    {
        if (value != null)
        {
            propertyDisplayName = validationContext.DisplayName;
            long property1 = (Int64)value;
            Match match = Regex.Match($"{property1}", @"^\d+$", RegexOptions.IgnoreCase);

            if (!string.IsNullOrWhiteSpace($"{property1}") && match.Success)
            {
                return ValidationResult.Success;
            }
            else
            {
                return new ValidationResult($"Invalid {propertyDisplayName}");
            }

        }
        else
        {
            return new ValidationResult($"Invalid {propertyDisplayName}");
        }
    }
    catch (Exception ex)
    {
        ...
    }
}

If there is a way to handle this scenario in the extended ValidationAttribute classes also, that will also work for me.

Note: Target framework is .Net5


Solution

  • I could able to solve the issue by using the below code in ConfigureServices method of Startup.cs.

    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Latest).ConfigureApiBehaviorOptions(options =>
    {
        options.InvalidModelStateResponseFactory = c =>
        {
            ProblemDetails problemDetails = new ProblemDetails();
            problemDetails.Status = StatusCodes.Status400BadRequest;
            problemDetails.Title = "One or more validation errors occurred.";
            problemDetails.Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1";
            
            List<FieldCodeMessage> codeMessages = new List<FieldCodeMessage>();
            foreach (var modelState in c.ModelState)
            {
                if (modelState.Value.ValidationState == Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid)
                {
                    string[] errorMessageCode = modelState.Value.Errors.Select(a => a.ErrorMessage).FirstOrDefault().Split(':');
                    string code = errorMessageCode[0];
                    string message = errorMessageCode[1];
    
                    codeMessages.Add(new FieldCodeMessage(field: modelState.Key, code: code, message: message));
                }
            }
    
            problemDetails.Extensions.Add("Invalid Fields", codeMessages);
    
            return new BadRequestObjectResult(problemDetails);
        };
    });
    

    I had to use one trick to pass the error code along with the message by using the : delimiter like this in IsValid method of the extended ValidationAttribute.

    return new ValidationResult("01:Proprty 1");
    

    If anyone has a better approach or suggestions, please add a comment. I would be happy to know.