Skip to content

Instantly share code, notes, and snippets.

@markadrake
Last active April 23, 2026 14:15
Show Gist options
  • Select an option

  • Save markadrake/c2fda38477f65f22ed311d127dcd62db to your computer and use it in GitHub Desktop.

Select an option

Save markadrake/c2fda38477f65f22ed311d127dcd62db to your computer and use it in GitHub Desktop.
Add Template Information to the Umbraco v17 Content Delivery API

Registers a custom content response builder in DeliveryApiComposer.cs, returning responses with:

"template": {
  "id": "template-guid",
  "name": "Template name",
  "alias": "templateAlias"
}

id uses Umbraco’s GUID Key, matching the Delivery API’s existing id convention. If content has no assigned template, template is null.

I also added the JSON polymorphism resolver and OpenAPI schema filter so the runtime response and typed swagger model both know about the new property.

namespace Humble.Umbraco.DeliveryApi;
public sealed record ApiTemplate(Guid Id, string? Name, string Alias);
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Extensions;
namespace Humble.Umbraco.DeliveryApi;
public sealed class DeliveryApiComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.Services.AddUnique<IApiContentResponseBuilder, HumbleApiContentResponseBuilder>(ServiceLifetime.Singleton);
builder.Services.Configure<JsonOptions>("Delivery", options =>
{
options.JsonSerializerOptions.TypeInfoResolver = new HumbleDeliveryApiJsonTypeResolver();
});
builder.Services.PostConfigure<SwaggerGenOptions>(options =>
{
options.DocumentFilter<HumbleDeliveryApiSchemaFilter>();
});
}
}
using System.Text.Json.Serialization;
using Umbraco.Cms.Core.Models.DeliveryApi;
namespace Humble.Umbraco.DeliveryApi;
public sealed class HumbleApiContentResponse : ApiContentResponse
{
public HumbleApiContentResponse(
Guid id,
string name,
string contentType,
DateTime createDate,
DateTime updateDate,
IApiContentRoute route,
IDictionary<string, object?> properties,
IDictionary<string, IApiContentRoute> cultures,
ApiTemplate? template)
: base(id, name, contentType, createDate, updateDate, route, properties, cultures)
{
Template = template;
}
[JsonPropertyOrder(90)]
public ApiTemplate? Template { get; }
}
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
namespace Humble.Umbraco.DeliveryApi;
public sealed class HumbleApiContentResponseBuilder : ApiContentResponseBuilder
{
private readonly IFileService _fileService;
public HumbleApiContentResponseBuilder(
IApiContentNameProvider apiContentNameProvider,
IApiContentRouteBuilder apiContentRouteBuilder,
IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor,
IVariationContextAccessor variationContextAccessor,
IFileService fileService)
: base(apiContentNameProvider, apiContentRouteBuilder, outputExpansionStrategyAccessor, variationContextAccessor)
{
_fileService = fileService;
}
protected override IApiContentResponse Create(
IPublishedContent content,
string name,
IApiContentRoute route,
IDictionary<string, object?> properties)
{
return new HumbleApiContentResponse(
content.Key,
name,
content.ContentType.Alias,
content.CreateDate,
content.CultureDate(VariationContextAccessor),
route,
properties,
GetCultures(content),
GetTemplate(content));
}
private ApiTemplate? GetTemplate(IPublishedContent content)
{
int templateId = content.TemplateId.GetValueOrDefault();
if (templateId <= 0)
{
return null;
}
// Umbraco 17's API content response builder is synchronous, so use the matching v17 template lookup here.
#pragma warning disable CS0618
var template = _fileService.GetTemplate(templateId);
#pragma warning restore CS0618
return template is null
? null
: new ApiTemplate(template.Key, template.Name, template.Alias);
}
}
using System.Text.Json.Serialization.Metadata;
using Umbraco.Cms.Core.Models.DeliveryApi;
using Umbraco.Cms.Infrastructure.Serialization;
namespace Humble.Umbraco.DeliveryApi;
public sealed class HumbleDeliveryApiJsonTypeResolver : ContentJsonTypeResolverBase
{
public override Type[] GetDerivedTypes(JsonTypeInfo jsonTypeInfo)
{
return jsonTypeInfo.Type == typeof(IApiContentResponse)
? [typeof(HumbleApiContentResponse)]
: base.GetDerivedTypes(jsonTypeInfo);
}
}
using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Humble.Umbraco.DeliveryApi;
public sealed class HumbleDeliveryApiSchemaFilter : IDocumentFilter
{
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
IOpenApiSchema templateSchema = context.SchemaGenerator.GenerateSchema(typeof(ApiTemplate), context.SchemaRepository);
foreach ((string schemaId, IOpenApiSchema schema) in context.SchemaRepository.Schemas)
{
if (IsContentResponseBaseSchema(schemaId) && schema is OpenApiSchema openApiSchema)
{
AddTemplateProperty(openApiSchema, templateSchema);
}
}
}
private static bool IsContentResponseBaseSchema(string schemaId)
{
return schemaId is "IApiContentResponse" or "IApiContentResponseBase" or "IApiContentResponseModelBase";
}
private static void AddTemplateProperty(OpenApiSchema schema, IOpenApiSchema templateSchema)
{
schema.Properties ??= new Dictionary<string, IOpenApiSchema>();
schema.Properties["template"] = new OpenApiSchema
{
OneOf =
[
templateSchema,
new OpenApiSchema { Type = JsonSchemaType.Null },
],
ReadOnly = true,
};
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment