I'd like to create custom JSON format, that would wrap the response in data and would return Content-Type like
vnd.myapi+json
Currently I have created like a wrapper classes that I return in my controllers but it would be nicer if that could be handled under the hood:
public class ApiResult<TValue>
{
[JsonProperty("data")]
public TValue Value { get; set; }
[JsonExtensionData]
public Dictionary<string, object> Metadata { get; } = new Dictionary<string, object>();
public ApiResult(TValue value)
{
Value = value;
}
}
[HttpGet("{id}")]
public async Task<ActionResult<ApiResult<Bike>>> GetByIdAsync(int id)
{
var bike = _dbContext.Bikes.AsNoTracking().SingleOrDefault(e => e.Id == id);
if (bike == null)
{
return NotFound();
}
return new ApiResult(bike);
}
public static class ApiResultExtensions
{
public static ApiResult<T> AddMetadata<T>(this ApiResult<T> result, string key, object value)
{
result.Metadata[key] = value;
return result;
}
}
I'd like to return response like:
{
"data": { ... },
"pagination": { ... },
"someothermetadata": { ... }
}
But the pagination would have to be added somehow to the metadata in my controller's action, of course there's some article about content negotiation here: https://docs.microsoft.com/en-us/aspnet/core/web-api/advanced/formatting?view=aspnetcore-2.1 but still I'd like to be sure I'm on the right track.
If that would be handled under the hood with my custom formatter then how would I add metadata like a pagination to it, to be aside of "data" and not inside of it?
When having a custom formatter I'd like to still have some way to add metadata to it from my controllers or by some mechanism so the format could be extensible.
One advantage or disadvantage with the approach above is that it works with all serializers xml, json, yaml etc. By having custom formatter it would probably work only for json, and I will need to create few different formatters to support all the formats that I want.
Implementing a custom response handler in Web API. Create a new Web API project in Visual Studio and save it with the name of your choice. Now, select the Web API project you have created in the Solution Explorer Window and create a Solution Folder. Create a file named CustomResponseHandler.
ASP.NET Core MVC supports data exchange in Web APIs using input and output formatters. Input formatters are used by Model Binding. Output formatters are used to format responses. The framework provides built-in input and output formatters for JSON and XML.
Okay, after spending some good amount of time with ASP.NET Core there are basically 4 ways I can think of to solve this. The topic itself is quite complex and broad to think of and honestly, I don't think there's a silver bullet or the best practice for this.
For custom Content-Type(let's say you want to implement application/hal+json
), the official way and probably the most elegant way is to create custom output formatter. This way your actions won't know anything about the output format but you still can control the formatting behaviour inside your controllers thanks to dependency injection mechanism and scoped lifetime.
This is the most popular way used by OData official C# libraries and json:api framework for ASP.Net Core. Probably the best way to implement hypermedia formats.
To control your custom output formatter from a controller you either have to create your own "context" to pass data between your controllers and custom formatter and add it to DI container with scoped lifetime:
services.AddScoped<ApiContext>();
This way there will be only one instance of ApiContext
per request. You can inject it to both you controllers and output formatters and pass data between them.
You can also use ActionContextAccessor
and HttpContextAccessor
and access your controller and action inside your custom output formatter. To access controller you have to cast ActionContextAccessor.ActionContext.ActionDescriptor
to ControllerActionDescriptor
. You can then generate links inside your output formatters using IUrlHelper
and action names so the controller will be free from this logic.
IActionContextAccessor
is optional and not added to the container by default, to use it in your project you have to add it to the IoC container.
services.AddSingleton<IActionContextAccessor, ActionContextAccessor>()
Using services inside custom output formatter:
You can't do constructor dependency injection in a formatter class. For example, you can't get a logger by adding a logger parameter to the constructor. To access services, you have to use the context object that gets passed in to your methods.
https://docs.microsoft.com/en-us/aspnet/core/web-api/advanced/custom-formatters?view=aspnetcore-2.0#read-write
Swashbuckle support:
Swashbuckle obviously won't generate a correct response example with this approach and the approach with filters. You will probably have to create your custom document filter.
Example: How to add pagination links:
Usually paging, filtering is solved with specification pattern you will typically have some common model for the specification in your [Get]
actions. You can then identify in your formatter if currently executed action is returning list of elements by it's parameter type or something else:
var specificationParameter = actionContextAccessor.ActionContext.ActionDescriptor.Parameters.SingleOrDefault(p => p.ParameterType == typeof(ISpecification<>));
if (specificationParameter != null)
{
// add pagination links or whatever
var urlHelper = new UrlHelper(actionContextAccessor.ActionContext);
var link = urlHelper.Action(new UrlActionContext()
{
Protocol = httpContext.Request.Scheme,
Host = httpContext.Request.Host.ToUriComponent(),
Values = yourspecification
})
}
Advantages (or not):
Your actions don't define the format, they know nothing about a format or how to generate links and where to put them. They know only of the result type, not the meta-data describing the result.
Re-usable, you can easily add the format to other projects without worrying how to handle it in your actions. Everything related to linking, formatting is handled under the hood. No need for any logic in your actions.
Serialization implementation is up to you, you don't have to use Newtonsoft.JSON, you can use Jil for example.
Disadvantages:
One disadvantage of this approach that it will only work with specific Content-Type. So to support XML we'd need to create another custom output formatter with Content-Type like vnd.myapi+xml
instead of vnd.myapi+json
.
We're not working directly with the action result
Can be more complex to implement
Result filters allow us to define some kind of behaviour that will execute before our action returns. I think of it as some form of post-hook. I don't think it's the right place for wrapping our response.
They can be applied per action or globally to all actions.
Personally, I wouldn't use it for this kind of thing but use it as a supplement for the 3rd option.
Sample result filter wrapping the output:
public class ResultFilter : IResultFilter
{
public void OnResultExecuting(ResultExecutingContext context)
{
if (context.Result is ObjectResult objectResult)
{
objectResult.Value = new ApiResult { Data = objectResult.Value };
}
}
public void OnResultExecuted(ResultExecutedContext context)
{
}
}
You can put the same logic in IActionFilter
and it should work as well:
public class ActionFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
}
public void OnActionExecuted(ActionExecutedContext context)
{
if (context.Result is ObjectResult objectResult)
{
objectResult.Value = new ApiResult { Data = objectResult.Value };
}
}
}
This is the easiest way to wrap your responses especially if you already have the existing project with controllers. So if you care about time, choose this one.
(The way I do it in my question)
This is also used here: https://github.com/nbarbettini/BeautifulRestApi/tree/master/src to implement https://github.com/ionwg/ion-doc/blob/master/index.adoc personally I think this would be better suited in custom output formatter.
This is probably the easiest way but it's also "sealing" your API to that specific format. There are advantages to this approach but there can be some disadvantages too. For example, if you wanted to change the format of your API, you can't do it easily because your actions are coupled with that specific response model, and if you have some logic on that model in your actions, for example, you're adding pagination links for next and prev. You practically have to rewrite all your actions and formatting logic to support that new format. With custom output formatter you can even support both formats depending on the Content-Type header.
Advantages:
ActionResult<T>
(2.1+), you can also add [ProducesResponseType]
attribute to your actions.Disadvantages:
Content-Type
header. It always remains the same for application/json
and application/xml
. (maybe it's advantage?)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.vnd.myapi+json
doesn't give any benefit and implementing custom output formatter just for the name doesn't make sense as formatting is still responsibility of controller's actions.I think this is more like a shortcut for properly handling the output format. I think following the single responsibility principle it should be the job for output formatter as the name suggests it formats the output.
The last thing you can do is a custom middleware, you can resolve IActionResultExecutor
from there and return IActionResult
like you would do in your MVC controllers.
https://github.com/aspnet/Mvc/issues/7238#issuecomment-357391426
You could also resolve IActionContextAccessor
to get access to MVC's action context and cast ActionDescriptor
to ControllerActionDescriptor
if you need to access controller info.
Docs say:
Resource filters work like middleware in that they surround the execution of everything that comes later in the pipeline. But filters differ from middleware in that they're part of MVC, which means that they have access to MVC context and constructs.
But it's not entirely true, because you can access action context and you can return action results which is part of MVC from your middleware.
If you have anything to add, share your own experiences and advantages or disadvantages feel free to comment.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With