11

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://learn.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.

Konrad
  • 6,385
  • 12
  • 53
  • 96
  • I'm not sure if you're saying whether you want a custom data format, i.e. not JSON but something close to JSON (in which case why). Or you are worried about the structure of the data itself (which is just down the model you serialize). Or if you are worried about the formatting of the JSON - i.e. the line breaks/tabs etc (again if so why)? – James Gaunt May 18 '18 at 07:55
  • @JamesGaunt I still want JSON but I want to wrap my result in "data" so I can add some metadata next to it like pagination for instance. This is also useful for implementing HATEOAS, where I'd like to add some links to my data with possible actions. For example something like http://jsonapi.org/ – Konrad May 18 '18 at 07:56
  • 1
    In that case I think your approach is correct. It's not really a mime-type change - it's just how your API is designed. So you need to send an appropriate model the JSON serializer - which is exactly what you are doing. What are you looking to gain by moving this "under the hood"? – James Gaunt May 18 '18 at 07:59
  • 1
    If it is just the mime-type you want to change you can just do this by serializing the JSON yourself and returning a content result with any mime-type you want. – James Gaunt May 18 '18 at 08:00
  • By custom JSON format I meant custom hypermedia format like jsonapi.org etc. – Konrad May 18 '18 at 08:01
  • I wanted to gain some sort of automation so I can configure how my API works from some separate place. – Konrad May 18 '18 at 08:02
  • Or possibly some more cleaner and elegant way to do this. – Konrad May 18 '18 at 08:03
  • 1
    If you just don't want the code in the controller (where it's obvious and clear) - and would rather hide it somewhere sneaky - maybe a ResultFilter. Not sure if it's a good idea though! https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-2.0#result-filters – James Gaunt May 18 '18 at 08:05
  • @JamesGaunt that could work – Konrad May 18 '18 at 08:07
  • Is there some way to add global result filter? – Konrad May 18 '18 at 08:09
  • Oh nvm. `services.AddScoped();` – Konrad May 18 '18 at 08:15
  • I just figured it is `services.AddMvc(opt => opt.Filters.Add(typeof(ApiResultFilter)))` but still it doesn't satisfy me, because when using swashbuckle it doesn't generate correct example format when the result is modified inside the filter. To workaround that I'd probably have to add `ProducesResponseType` but still it doesn't seem elegant. – Konrad May 18 '18 at 08:53

1 Answers1

55

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.


1. Custom output formatters

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://learn.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

2. Result filters

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.

3. Explicitly formatting/wrapping your results in your actions

(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:

  • Works with all Content-Types, the format is an integral part of your API.
  • Swashbuckle works out of the box, when using ActionResult<T> (2.1+), you can also add [ProducesResponseType] attribute to your actions.

Disadvantages:

  • You can't control the format with Content-Type header. It always remains the same for application/json and application/xml. (maybe it's advantage?)
  • 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.
  • Theoretically custom Content-Type like 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.

4. Custom middleware

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.

Konrad
  • 6,385
  • 12
  • 53
  • 96