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.
- How can I wrap Web API responses(in .net core) for consistency?
- 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.