简体   繁体   中英

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

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.

My recommendation would be a Middleware library called Hellang.Middleware.ProblemDetails. It's well-documented, popular, and has been around a while. The GitHub repo is here: https://github.com/khellang/Middleware/tree/master/src/ProblemDetails

Or just Google "Hellang problemdetails" and you'll see loads of articles about how to use it to do exactly what you're asking for.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM