Umbraco Content Delivery API – Extending the API response with extra fields

The default Content Delivery API implementation and features can be found in different API endpoints. These controllers are using services, builders, etc. which are in a deeper a layer in the architecture and therefore controllers are re-using these functions. When I started to explore how to extend the default API responses, the most obvious extension points are the controllers – just inherit from the original controller and create a new one with the extension. In my opinion, this is not the best way because we need to touch all the controllers in this case and publish our own endpoints next to the original endpoints.

Therefore I dug deep in the controllers to find out which underlying class is responsible for the part I need to extend.

The requirements

I would like to add a new section – called system – to the original response. The content of this section should come from a specific node from the content tree. I am expecting a result like the following:

{
"system": {
"shared": {
"addToCartToasterText": "Product added to cart",
"removeFromCartToasterText": "Product removed from cart",
"removeFromCartButtonLabel": "Remove from cart",
"addToCartButtonLabel": "Add to cart",
"offBadgeLabel": "Off",
"productsBaseUrl": [
{
"url": null,
"title": "Search",
"target": null,
"destinationId": "546a41d8-ffbc-4207-b80a-f8744c1b1e68",
"destinationType": "searchPage",
"route": {
"path": "/home/products/",
"startItem": {
"id": "35d1fab4-f7c2-4452-93d3-090d26116e71",
"path": "site1"
}
},
"linkType": "Content"
}
],
"categoryLabel": "Category"
}
},
"name": "...",
"createDate": "...",
"updateDate": "...",
"route": {
...
},
"id": "...",
"contentType": "...",
"properties": {
...
},
"cultures": {
...
}
}
view raw response.json hosted with ❤ by GitHub

The solution

After digging in to the code, I found the implementation which takes care of putting together the JSON response for all the endpoints, and this is the ApiContentResponseBuilder class. Unfortunately, this class is also sealed and the Create method is protected – therefore I can’t reuse the original implementation at all, neither through constructor injection nor by inheritance. Below is the implementation of extending response model with the system property and you can see a copied method from the original builder class, marked with HACK note.

public class ExtendedApiContentResponseBuilder : ApiContentBuilderBase<IApiContentResponse>, IApiContentResponseBuilder
{
private readonly IApiContentRouteBuilder _apiContentRouteBuilder;
private readonly IOutputExpansionStrategyAccessor _outputExpansionStrategyAccessor;
private readonly IApiPublishedContentCache _apiPublishedContentCache;
public ExtendedApiContentResponseBuilder(
IApiContentNameProvider apiContentNameProvider,
IApiContentRouteBuilder apiContentRouteBuilder,
IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor,
IApiPublishedContentCache apiPublishedContentCache)
: base(apiContentNameProvider, apiContentRouteBuilder, outputExpansionStrategyAccessor)
{
_apiContentRouteBuilder = apiContentRouteBuilder;
_outputExpansionStrategyAccessor = outputExpansionStrategyAccessor;
_apiPublishedContentCache = apiPublishedContentCache;
}
protected override IApiContentResponse Create(IPublishedContent content, string name, IApiContentRoute route, IDictionary<string, object?> properties)
{
var cultures = GetCultures(content);
var system = GetSystem();
return new ExtendedApiContentResponse(
content.Key,
name,
content.ContentType.Alias,
content.CreateDate,
content.UpdateDate,
route,
properties,
cultures,
system);
}
private IDictionary<string, object?>? GetSystem()
{
var siteSettings = _apiPublishedContentCache.GetByRoute("/site-data/site-settings");
if (siteSettings == null)
{
return new Dictionary<string, object?>(0);
}
var properties = _outputExpansionStrategyAccessor.TryGetValue(out var outputExpansionStrategy)
? new Dictionary<string, object?> { { "shared", outputExpansionStrategy.MapContentProperties(siteSettings) } }
: new Dictionary<string, object?>();
return properties;
}
/// <summary>
/// HACK: Copied from the original ApiContentResponseBuilder
/// </summary>
private Dictionary<string, IApiContentRoute> GetCultures(IPublishedContent content)
{
var routesByCulture = new Dictionary<string, IApiContentRoute>();
foreach (var publishedCultureInfo in content.Cultures.Values)
{
if (publishedCultureInfo.Culture.IsNullOrWhiteSpace())
{
continue;
}
var cultureRoute = _apiContentRouteBuilder.Build(content, publishedCultureInfo.Culture);
if (cultureRoute == null)
{
continue;
}
routesByCulture[publishedCultureInfo.Culture] = cultureRoute;
}
return routesByCulture;
}
}
public class ExtendedApiContentResponse : ApiContentResponse, IApiContentResponse
{
public ExtendedApiContentResponse(
Guid id,
string name,
string contentType,
DateTime createDate,
DateTime updateDate,
IApiContentRoute route,
IDictionary<string, object?> properties,
IDictionary<string, IApiContentRoute> cultures,
IDictionary<string, object?> system)
: base(id, name, contentType, createDate, updateDate, route, properties, cultures)
{
System = system;
}
[JsonPropertyOrder(0)]
public IDictionary<string, object?> System { get; }
}

By using the outputExpansionStrategy.MapContentProperties(siteSettings) method, I don’t need to care about the field and item serialization for the response, it’ll output the desired result, defined above.

While testing the implementation, I found that it works well with API endpoints where a single item needs to be returned, but the /umbraco/delivery/api/v1/content was failing with a JSON serialization issue related to polymorphic types. This is because the Umbraco implementation is setting the PolymorphismOptions.UnknownDerivedTypeHandling to FailSerialization. In the implementation below, I set it to FallBackToBaseType, to able to respond with the original response for the failed API endpoint.

public class FallbackDeliveryApiJsonTypeResolver : DeliveryApiJsonTypeResolver
{
public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options)
{
var typeInfo = base.GetTypeInfo(type, options);
if (typeInfo.PolymorphismOptions != null)
{
typeInfo.PolymorphismOptions.UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType;
}
return typeInfo;
}
}

This solution is not the best because retrieving the system item still runs for the query API for each item. A better way would be to decide the retrieval of the system item in the ExtendedApiContentResponseBuilder based on a parameter, but I could not find any (I’ll update the post if I do).

To finalize the implementation, here is the startup file at the end.

public class Startup
{
...
public void ConfigureServices(IServiceCollection services)
{
...
services.AddControllers().AddJsonOptions(Constants.JsonOptionsNames.DeliveryApi, options =>
{
// To resolve System.Text.Json polymorphic type resolving issue for the query controller
options.JsonSerializerOptions.TypeInfoResolver = new FallbackDeliveryApiJsonTypeResolver();
});
// Overwrite the default ApiContentResponseBuilder to add "system" section
services.AddSingleton<IApiContentResponseBuilder, ExtendedApiContentResponseBuilder>();
}
}
view raw Startup.cs hosted with ❤ by GitHub

2 thoughts on “Umbraco Content Delivery API – Extending the API response with extra fields

  1. Thanks for sharing this. I’ve tried implementing my own `IApiContentResponseBuilder` but when registering it Umbraco just will not start and hangs. No error, it just continues loading. I’ve tried to register it via my `startup.cs` file and in an `IComposer` with the same result.

    Like

  2. In your `FallbackDeliveryApiJsonTypeResolver`, can you not do this to resolve the issue with polymorphic types?

    “`
    public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options)
    {
    JsonTypeInfo jsonTypeInfo = base.GetTypeInfo(type, options);

    if (jsonTypeInfo.Type == typeof(IApiContentResponse))
    {
    jsonTypeInfo.PolymorphismOptions?.DerivedTypes.Add(new JsonDerivedType(typeof(ExtendedApiContentResponse )));
    }

    return jsonTypeInfo;
    }
    “`

    Like

Leave a comment