ProudMonkey

AutoWrapper Now Supports Problem Details For Your ASP.NET Core APIs

This space is for rent. Contact me at vmsdurano at gmail dot com.

The new version of AutoWrapper v4.0.0-rc comes with a few enhancements and bug fixes. Version 4 is still in RC prerelease state to allow testing it and ensuring that it won't break any existing functionalities. The main feature enhancement for this release is the support of ProblemDetails for defining exception format. This feature was originally requested by Sondre Bjellås via Issue #7. The implementation of this feature is a simplified version of the Hellang.Middleware.ProblemDetails middleware by Kristian Hellang. Kudos to Kristian for providing such a great middleware!

What Problem Details Is?

Taken from Microsoft documentation:

ProblemDetails is a machine-readable format for specifying errors in HTTP API responses based on https://tools.ietf.org/html/rfc7807.

Enabling Problem Details

  1. Install the latest version of AutoWrapper.
Install-Package AutoWrapper.Core -Version 4.0.0-rc  
  1. Then set UseApiProblemDetailsException to true:
app.UseApiResponseAndExceptionWrapper(new AutoWrapperOptions {  
      UseApiProblemDetailsException = true 
});

To see the error details during development, you can set the IsDebug option to true. That's it!

Sample Tests

Here are a few examples how ProblemDetails error is going to look like:

404 Not Found

Request:

[HttpGet("notfound")]
public IActionResult ActionNotFound()  
{
    return NotFound("No record found");
}

Response:

{
    "isError": true,
    "type": "https://httpstatuses.com/404",
    "title": "Not Found",
    "status": 404,
    "detail": "No record found",
    "instance": null,
    "extensions": {}
}

Another example.

Request:

throw new ApiProblemDetailsException($"Record with id: {id} does not exist.", Status404NotFound);  

Response:

{
    "isError": true,
    "type": "https://httpstatuses.com/404",
    "title": "Record with id: 1222 does not exist.",
    "status": 404,
    "detail": null,
    "instance": null,
    "extensions": {}
}
400 Bad Request

Request:

[HttpGet("badreq")]
public IActionResult ActionBadRequest()  
{
    return BadRequest("Something is null.");
}

Response:

{
    "isError": true,
    "type": "https://httpstatuses.com/400",
    "title": "Bad Request",
    "status": 400,
    "detail": "Something is null.",
    "instance": null,
    "extensions": {}
}
422 Unprocessable Entity

Request:

[HttpPost]
public async Task<ApiResponse> Post([FromBody] CreatePersonRequest dto)  
{
    if (!ModelState.IsValid)
    {
        throw new ApiProblemDetailsException(ModelState);
    }
}

Response:

{
    "isError": true,
    "type": "https://httpstatuses.com/422",
    "title": "Unprocessable Entity",
    "status": 422,
    "detail": "Your request parameters didn't validate.",
    "instance": null,
    "extensions": {},
    "validationErrors": [
        {
            "name": "LastName",
            "reason": "'Last Name' must not be empty."
        },
        {
            "name": "FirstName",
            "reason": "'First Name' must not be empty."
        },
        {
            "name": "DateOfBirth",
            "reason": "'Date Of Birth' must not be empty."
        }
    ]
}
500 Internal Server Error

Request:

[HttpGet("unhandled")]
public IActionResult ActionError()  
{
    throw new Exception("Unhanled error occured.");
}

Response:

{
    "isError": true,
    "type": "https://httpstatuses.com/500",
    "title": "Internal Server Error",
    "status": 500,
    "detail": "Unhanled error occured.",
    "instance": null,
    "extensions": {},
    "errors": {
        "message": "Unhanled error occured.",
        "type": "Exception",
        "source": "AutoWrapper_v3._1",
        "raw": "at AutoWrapper_v3._1.API.v1.TestsController.ActionError() in ..removed for brevity..."
    }
}

Another example.

Request:

[HttpGet("invalidcast")]
public IActionResult ActionInvalidCast()  
{
    try { int num = Convert.ToInt32("123a"); return Ok(); }
    catch { throw; }
}

Response:

{
    "isError": true,
    "type": "https://httpstatuses.com/500",
    "title": "Internal Server Error",
    "status": 500,
    "detail": "Input string was not in a correct format.",
    "instance": null,
    "extensions": {},
    "errors": {
        "message": "Input string was not in a correct format.",
        "type": "FormatException",
        "source": "System.Private.CoreLib",
        "raw": "at System.Number.ThrowOverflowOrFormatException(ParsingStatus status, TypeCode type)..removed for brevity..."
    }
}
Custom

You could also throw your own custom defined object that derives from Microsoft.AspNetCore.Mvc.ProblemDetails class. Consider the following example taken from Hellang.Middleware.ProblemDetails middleware sample:

Request:

[HttpGet("problem/error")]
public IActionResult Result()  
{
    var problem = new OutOfCreditProblemDetails
    {
        Type = "https://example.com/probs/out-of-credit",
        Title = "You do not have enough credit.",
        Detail = "Your current balance is 30, but that costs 50.",
        Instance = "/account/12345/msgs/abc",
        Balance = 30.0m,
        Accounts = { "/account/12345", "/account/67890" }
    };

    return BadRequest(problem);
}

public class OutOfCreditProblemDetails : Microsoft.AspNetCore.Mvc.ProblemDetails  
{
    public OutOfCreditProblemDetails()
    {
        Accounts = new List<string>();
    }
    public decimal Balance { get; set; }

    public ICollection<string> Accounts { get; }
}

Response:

{
    "balance": 30.0,
    "accounts": [
        "/account/12345",
        "/account/67890"
    ],
    "type": "https://example.com/probs/out-of-credit",
    "title": "You do not have enough credit.",
    "status": 400,
    "detail": "Your current balance is 30, but that costs 50.",
    "instance": "/account/12345/msgs/abc",
    "extensions": {}
}

Fixes and Enhancements

  • Added a new Option called UseCustomExceptionFormat to enable using your own custom exception format or using ProblemDetails error format. This means that if you don't want to use the ApiException or ApiProblemDetailsException formats built-in to AutoWrapper, then you can simply set UseCustomExceptionFormat = true. For example, if you want to use Hellang.Middleware.ProblemDetails middleware to take advantage of its detailed features, you can define the AutoWrapper like in the following:
app.UseApiResponseAndExceptionWrapper(new AutoWrapperOptions {  
         UseCustomExceptionFormat = true 
    })
   .UseProblemDetails();
  • [Issue #22] Changed the FormatRequestAsync() method implementation to use CopyAsync() instead of ReadAsync(). Also added validation check to only allow reading the request body from Http POST, PUT and PATCH verbs.
  • [Issue #26] Changed ExceptionMessage datatype from string to object so it can support child JSON attributes. For example:
{
    "isError": true,
    "responseException": {
        "exceptionMessage": {
            "type": "https://tools.ietf.org/html/rfc7231#section-6.5.13",
            "title": "Unsupported Media Type",
            "status": 415,
            "traceId": "|e96629a6-4b34e7a7f65431af."
        }
    }
}
  • [Issue #33] Ignored wrapping the response when StatusCode is 204 NoContent.
  • [Issue #32] Added output validation to avoid re-enconding the JSON value.
  • [Issue #32] Added output validation for number, decimal and boolean types to avoid automatically converting them to string. For example:
[HttpGet("number")]
public int SpitNumber(int schiffnr)  
{
    return 29;
}
  • Starting this version, only .NET Core 3.0 and above will be supported to take advantage of C# new features
  • Refactor code and clean up.

Big thanks to everyone who have supported it! I truly appreciate it. :)

Feel free to request an issue on github if you find bugs or request a new feature. Your valuable feedback is much appreciated to better improve this project. If you find this useful, please give it a star to show your support on this project!

Buy Me A Coffee