0

I have a controller which looks like this:

[HttpPost]
public async Task<ActionResult> CreateAsync(TodoItems item)
{
    await item.AddNewItemAsync(item.Id, item.Name);

    return Ok(new ApiOkResponse(item, $"An item has been added."));
}
[HttpGet("{id}")]
public async Task<ActionResult> GetById(int id)
{
    TodoItems items = new();
    if (!items.TryGetProduct(id, out var item))
    {
        throw new NotFoundException($"The item with id {id} not found");
    }
    return Ok(new ApiOkResponse(item));
}

Here in the controller, in the CreateAsync action, I have tried to return the object wrapped with messages and data.

And in the Get action I have thrown a NotFoundException.

To wrap the return responses, along with exception handling I have tried two different ways:

One way is by inheriting the ObjectResultExecutor, which looks so simple and plain, which I have implemented from.

public class ResponseEnvelopeResultExecutor : ObjectResultExecutor
{
    public ResponseEnvelopeResultExecutor(OutputFormatterSelector formatterSelector, IHttpResponseStreamWriterFactory writerFactory,
        ILoggerFactory loggerFactory, IOptions<MvcOptions> mvcOptions) : base(formatterSelector, writerFactory, loggerFactory, mvcOptions)
    {
    }

    public override Task ExecuteAsync(ActionContext context, ObjectResult result)
    {
        var response = (ApiOkResponse)(result.Value);

        TypeCode typeCode = Type.GetTypeCode(result.Value.GetType());
        if (typeCode == TypeCode.Object)
            result.Value = response;

        return base.ExecuteAsync(context, result);
    }
}

I have also implemented the same thing using custom middleware, where there are plenty of findings on the stack overflow.

  1. How can I wrap Web API responses(in .net core) for consistency?
  2. How to read ASP.NET Core Response.Body?

My implementation using Middleware:

using (var memoryStream = new MemoryStream())
{
    //set the current response to the memorystream.
    httpContext.Response.Body = memoryStream;

    await _next(httpContext);

    //reset the body 
    httpContext.Response.Body = currentBody;
    memoryStream.Seek(0, SeekOrigin.Begin);

    var readToEnd = new StreamReader(memoryStream).ReadToEnd();

    var objResult = JsonConvert.DeserializeObject<ApiResponse>(readToEnd);

    _logger.Information($"The Response return from the middleware is " +
        $"{JsonConvert.SerializeObject(objResult)}");

    await httpContext.Response.WriteAsync(JsonConvert.SerializeObject(objResult));
}

Both ways seem to work fine currently, but the first approach looks clean to me. But I am still not sure which way is good? Can anyone explain to me the difference between both cases and what are its drawbacks?

And what could be the possible changes that need to be considered in the future using both techniques?

I have been reading this answer, wherein point 3 (disadvantages) for Explicitly formatting/wrapping your results in your actions, says,

Your actions are responsible for returning the correctly formatted response. Something like: return new ApiResponse(obj); or you can create extension method and call it like obj.ToResponse() but you always have to think about the correct response format.

In my case, I have always wrap my response format. After reading that document I found that I implemented it in the same way which states the drawbacks of both techniques.

What are the proper ways of doing this formatting in dot-net-core5?

I am also using the same custom wrapper to decorate the Exception as well.

public class ApiExceptionFilterAttribute : ExceptionFilterAttribute
{

    private readonly IDictionary<Type, Action<ExceptionContext>> _exceptionHandlers;

    public ApiExceptionFilterAttribute()
    {
        // Register known exception types and handlers.
        _exceptionHandlers = new Dictionary<Type, Action<ExceptionContext>>
        {
            { typeof(NotFoundException), HandleNotFoundException },
            { typeof(UnauthorizedAccessException), HandleUnauthorizedAccessException },
            { typeof(ForbiddenAccessException), HandleForbiddenAccessException },
        };
    }

    public override void OnException(ExceptionContext context)
    {
        HandleException(context);

        base.OnException(context);
    }

    private void HandleException(ExceptionContext context)
    {
        Type type = context.Exception.GetType();
        if (_exceptionHandlers.ContainsKey(type))
        {
            _exceptionHandlers[type].Invoke(context);
            return;
        }

        if (!context.ModelState.IsValid)
        {
            HandleInvalidModelStateException(context);
            return;
        }

        HandleUnknownException(context);
    }

    private void HandleInvalidModelStateException(ExceptionContext context)
    {

        var errorList = (from item in context.ModelState.Values
                         from error in item.Errors
                         select error.ErrorMessage).ToList();

        var details = new ApiOkResponse(StatusCodes.Status400BadRequest, String.Join(" ; ", errorList));
        context.Result = new BadRequestObjectResult(details);

        context.ExceptionHandled = true;
    }

    private void HandleNotFoundException(ExceptionContext context)
    {
        var exception = context.Exception as NotFoundException;

        var details = new ApiOkResponse(StatusCodes.Status404NotFound, exception.Message);

        context.Result = new NotFoundObjectResult(details);
        context.ExceptionHandled = true;
    }

    private void HandleUnauthorizedAccessException(ExceptionContext context)
    {
        var details = new ApiOkResponse(StatusCodes.Status401Unauthorized, "Unauthorized");

        context.Result = new ObjectResult(details)
        {
            StatusCode = StatusCodes.Status401Unauthorized
        };

        context.ExceptionHandled = true;
    }

    private void HandleForbiddenAccessException(ExceptionContext context)
    {
        var details = new ApiOkResponse(StatusCodes.Status403Forbidden, "Forbidden");

        context.Result = new ObjectResult(details)
        {
            StatusCode = StatusCodes.Status403Forbidden
        };

        context.ExceptionHandled = true;
    }

    private void HandleUnknownException(ExceptionContext context)
    {
        var details = new ApiOkResponse(StatusCodes.Status500InternalServerError, "An error occurred while processing your request.");

        context.Result = new ObjectResult(details)
        {
            StatusCode = StatusCodes.Status500InternalServerError
        };

        context.ExceptionHandled = true;
    }
}

I want to have my responses return in this json format:

{
    "statusCode": 200,
    "requestId": "6b801f87-e985-4092-938e-3723e4c6bb50",
    "message": "An item has been added.",
    "result": {
        "id": 0,
        "name": "d"
    }
}

If there is any exception message it should go into message fields.

Rasik
  • 1,961
  • 3
  • 35
  • 72
  • `To wrap the object` why wrap the object? ASP.NET Core will return a 500 error, which *is* the correct response in this case. There's nothing `OK` about an exception. Although a `KeyNotFoundException` should probably be replaced with the proper response: 404 - Not Found. Returning the wrong response **breaks** HTTP and your application. An OK is something that can be cached. If you return OK instead of an error, that response can easily get cached by clients and intermediate proxies. Even if you fix the problem, by adding a new entity, clients will still read that `yeaNAH` response from cache – Panagiotis Kanavos Jul 08 '21 at 07:41
  • 1
    Maybe you can replace `throw new KeyNotFoundException...` by `return NotFound()`. – vernou Jul 08 '21 at 07:44
  • In any case, therer's no reason to write a converter. All classes derived from [ObjectResult](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.objectresult?view=aspnetcore-5.0) accept an `Object` parameter which will be properly serialized by ASP.NET Core itself. If you want to pass extra information, just pass the object, eg `return new NotFoundResult(new {Key=id, Message="Where my key?"})` or `return NotFound(new {Key = ...});` – Panagiotis Kanavos Jul 08 '21 at 07:47
  • if I replace it with `return NotFound()` I won't have control over the responses, dot net will add the response by itself. I want to have the custom messages and format for the responses. – Rasik Jul 08 '21 at 07:48
  • @aakash what do you want to control? Because what you try to do is far more likely to *break* the response than control it. The format of the response is negotiated by the **client**. If the client asks for JSON, they expect JSON. If they ask XML, they expect XML. If you return JSON to a client that asked for XML, you'll break them. – Panagiotis Kanavos Jul 08 '21 at 07:52
  • Isn't there a way to give the client what they expect in a format which i defined for the responses? `{ "statusCode": 200, "requestId": "6b801f87-e985-4092-938e-3723e4c6bb50", "message": "An item has been added.", "result": { "id": 0, "name": "d" } }` – Rasik Jul 08 '21 at 07:54
  • `I want to have my responses return in this json format:` what the point of the duplicate status code?????? Is the API going to lie and return a 200 status code but a 400 status code? Or 200 and 201? Bsides, there are already standards for these things. ASP.NET Core already takes care of them. A validation error will include error specifics out of the box. You can return [a standardized ProblemDetails payload](https://learn.microsoft.com/en-us/aspnet/core/web-api/handle-errors?view=aspnetcore-5.0#pd) in case of error. – Panagiotis Kanavos Jul 08 '21 at 07:58
  • @aakash there is a way to return a **standard** [IETF Problem Details](https://datatracker.ietf.org/doc/html/rfc7807) response instead of a custom one only you own code would recognize. It's described in [Handle errors in ASP.NET Core web APIs](https://learn.microsoft.com/en-us/aspnet/core/web-api/handle-errors?view=aspnetcore-5.0) shows ). Essentially, you just `return Problem(...)` – Panagiotis Kanavos Jul 08 '21 at 08:00
  • @aakash for an invalid key though, you could have ASP.NET Core generate [a fully filled automatic 400 response](https://learn.microsoft.com/en-us/aspnet/core/web-api/?view=aspnetcore-5.0#automatic-http-400-responses) if you used [ASP.NET Corer's validation](https://learn.microsoft.com/en-us/aspnet/core/mvc/models/validation?view=aspnetcore-5.0). The automatic response would contain a TraceID, one entry for every validation etc. I think you can just add `[Range(1,int.MaxValue)]` to the `id` parameter. – Panagiotis Kanavos Jul 08 '21 at 08:10
  • So you are saying about the exception or the error handling. What about wrapping the responses when I have to return something like this, `return Ok(new ApiOkResponse(item, $"An item has been added."));` – Rasik Jul 08 '21 at 08:13
  • and for model validation, I am using fluent validation. – Rasik Jul 08 '21 at 08:14
  • @aakash in a microservice world it's impossible to have each service implement its own schema, tracing and error responses, which is why standards like Swagger (now OpenAPI) and OpenTelemetry have emerged. There are standard ways to track an activity across multiple services. Don't roll yourr won – Panagiotis Kanavos Jul 08 '21 at 08:15
  • @aakash I'm saying, don't roll your own. Use the standard ways. I use Fluent validation as well, it works with ASP.NET Core just fine. Your action *doesn't* use Fluent though, because it doesn't use a model. It uses raw parameters. In this case a range or null check attribute will work. – Panagiotis Kanavos Jul 08 '21 at 08:16
  • hmm, so you are saying wrapping the responses using custom wrapper class is not a good practice? – Rasik Jul 08 '21 at 08:19

0 Answers0