A tip, a trick and a hack for the Delivery API
Since the introduction of the Delivery API in Umbraco 12, I have been getting all sorts of great questions about its usage.
Many of those questions have been turned into new Delivery API features and/or improvements to the Delivery API documentation 🚀
In the following I’ll cover three of the most often asked questions that have not been put into the official docs. This GitHub repo contains a demo site with all the code running.
💡 Custom media URLs
By default, the Delivery API will output relative paths for media items.
The reasoning behind is simple; if you know the base URL (domain) of the Delivery API, you know the base URL of the media - they are the same 🙂
In some setups, however, this reasoning does not stand. Perhaps you’re serving images from a CDN, or you quite simply require absolute media URLs in your API output.
Fortunately, the Delivery API uses a provider based approach to media URL generation, which means the default media URL provider can be swapped with a custom implementation. Here’s an example of a provider that outputs absolute media URLs:
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Routing;
namespace UmbracoDeliveryApiExtensions.MediaUrlProvider;
public class AbsolutePathApiUrlProvider : IApiMediaUrlProvider
{
private readonly IPublishedUrlProvider _publishedUrlProvider;
public AbsolutePathApiUrlProvider(IPublishedUrlProvider publishedUrlProvider)
=> _publishedUrlProvider = publishedUrlProvider;
public string GetUrl(IPublishedContent media)
=> media.ItemType is PublishedItemType.Media
? _publishedUrlProvider.GetMediaUrl(media, UrlMode.Absolute)
: throw new ArgumentException(nameof(media));
}
Delivery API media URL providers are swapped by means of dependency injection, which means you’ll need a composer to enable it:
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.DeliveryApi;
namespace UmbracoDeliveryApiExtensions.MediaUrlProvider;
public class AbsolutePathApiUrlProviderComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
=> builder.Services.AddSingleton<IApiMediaUrlProvider, AbsolutePathApiUrlProvider>();
}
Now, when fetching media from the Delivery API, the media URLs are absolute instead of relative:
🪄 Custom content APIs
If the Delivery API falls short in delivering precisely the content you need, a custom content API is often the solution.
To make life easier for the API consumers, it would probably br smart to align the output of custom content APIs with that of the Delivery API.
Fortunately that’s a lot easier than it sounds. In the Delivery API, a single service is responsible for generating API output for any piece of content: The IApiContentResponseBuilder
.
The following API controller example utilizes just that:
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Web.Common.Controllers;
namespace UmbracoDeliveryApiExtensions.Controllers;
public class PostsQueryController : UmbracoApiController
{
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
private readonly IApiContentResponseBuilder _apiContentResponseBuilder;
public PostsQueryController(
IPublishedSnapshotAccessor publishedSnapshotAccessor,
IApiContentResponseBuilder apiContentResponseBuilder)
{
_publishedSnapshotAccessor = publishedSnapshotAccessor;
_apiContentResponseBuilder = apiContentResponseBuilder;
}
public IActionResult ByName(string name)
{
if (name.IsNullOrWhiteSpace())
{
return BadRequest("Please supply a name query");
}
var contentCache = _publishedSnapshotAccessor
.GetRequiredPublishedSnapshot()
.Content ?? throw new InvalidOperationException("Could not obtain the content cache");
var posts = contentCache
.GetAtRoot()
.FirstOrDefault(c => c.ContentType.Alias == "posts");
if (posts is null)
{
return NotFound("The Posts root was not found");
}
var matches = posts
.Children
.Where(post => post.Name.InvariantContains(name));
return Ok(matches.Select(_apiContentResponseBuilder.Build).WhereNotNull());
}
}
As expected, the resulting output for each content item is identical to what the Delivery API produces:
Now, this API output could just as well have been obtained with the built-in content querying and filtering. But for the sake of the example let’s forget that for a moment.
As an interesting side effect of utilizing the IApiContentResponseBuilder
, the custom API automatically supports property expansion and limiting:
🪓 Custom API output
In the example above I used the IApiContentResponseBuilder
to generate a Delivery API output from a custom content API.
If you need to enrich the API output with additional data, the easiest way is to replace this service with a custom implementation.
Luckily, a few subtle additions to Umbraco 13.3 has made this a whole lot simpler than it used to be 😊
The following is a response builder implementation that outputs a fictive server version to the API output:
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
namespace Site.ContentResponseBuilder;
public class ServerVersionResponseBuilder : ApiContentResponseBuilder
{
private static readonly Version ServerVersion = new (1, 2, 345);
public ServerVersionResponseBuilder(
IApiContentNameProvider apiContentNameProvider,
IApiContentRouteBuilder apiContentRouteBuilder,
IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor)
: base(apiContentNameProvider, apiContentRouteBuilder, outputExpansionStrategyAccessor)
{
}
protected override IApiContentResponse Create(
IPublishedContent content,
string name,
IApiContentRoute route,
IDictionary<string, object?> properties)
{
var cultures = GetCultures(content);
return new ServerVersionContentResponse(
content.Key,
name,
content.ContentType.Alias,
content.CreateDate,
content.UpdateDate,
route,
properties,
cultures,
ServerVersion);
}
}
…where ServerVersionContentResponse
is:
using Umbraco.Cms.Core.Models.DeliveryApi;
namespace Site.ContentResponseBuilder;
public class ServerVersionContentResponse : ApiContentResponse
{
public ServerVersionContentResponse(
Guid id,
string name,
string contentType,
DateTime createDate,
DateTime updateDate,
IApiContentRoute route,
IDictionary<string, object?> properties,
IDictionary<string, IApiContentRoute> cultures,
Version serverVersion)
: base(id, name, contentType, createDate, updateDate, route, properties, cultures)
=> ServerVersion = serverVersion;
public Version ServerVersion { get; set; }
}
To start using the response builder, it needs to be registered in the DI. But there’s a twist 🥨
The Delivery API uses System.Text.Json
as JSON serializer, whereas the rest of the Umbraco core uses Newtonsoft.Json
. The reasoning behind is that the upcoming Management API for Umbraco 14 will use System.Text.Json
as well.
As I’m now creating a new response model type for IApiContentResponse
, this type needs registering with System.Text.Json
as an allowed derived type. Otherwise, the serializer will throw up when it attempts to serialize the new response model.
First thing on the to-do list is to create a custom JSON type info resolver, which allows the new response model. Again, Umbraco 13.3 has brought about some changes to make this a relatively simple implementation:
using System.Text.Json.Serialization.Metadata;
using Umbraco.Cms.Api.Delivery.Json;
using Umbraco.Cms.Core.Models.DeliveryApi;
namespace Site.ContentResponseBuilder;
public class ServerVersionJsonTypeInfoResolver : DeliveryApiJsonTypeResolver
{
protected override Type[] GetDerivedTypes(JsonTypeInfo jsonTypeInfo)
=> jsonTypeInfo.Type == typeof(IApiContentResponse)
? [typeof(ServerVersionContentResponse)]
: base.GetDerivedTypes(jsonTypeInfo);
}
And now, at long last, the final bit of code is the composer that ties everything together:
using Umbraco.Cms.Api.Common.DependencyInjection;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.DeliveryApi;
namespace Site.ContentResponseBuilder;
public class ServerVersionResponseBuilderComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.Services
.AddSingleton<IApiContentResponseBuilder, ServerVersionResponseBuilder>();
builder.Services
.AddControllers()
.AddJsonOptions(
Constants.JsonOptionsNames.DeliveryApi,
options => options
.JsonSerializerOptions
.TypeInfoResolver = new ServerVersionJsonTypeInfoResolver());
}
}
With all this in place, the Delivery API finally outputs my server version 🎉
Parting remarks
As stated in the beginning, all of the above relates to questions from the ever awesome Umbraco community. I hope this post answers one or two of your questions as well 🤓
There are of course many more extension points in the Delivery API. Feel free to reach out if you think I missed some really important ones - perhaps a follow-up post will be in order eventually.
Happy coding 💜