Management API + NSwag = win

Management API + NSwag = win

A few months ago I wrote about building external integrations for Umbraco using the Management API.

I’m still very excited about the Management API and its potential for building custom integrations, so in this post I’ll put it to the test: I’ll recreate the Clean Starter Kit from Paul Seal from scratch, using nothing but the Management API 😯

The complete code is available in this GitHub repo. It consists of:

  • A blank Umbraco site with an API user configured.
  • A console application that performs the starter kit recreation.

To try the whole thing out, first start the site:

cd UmbracoManagementApiCleanStarterKit\src\Site
dotnet run

…and then run the console application:

cd UmbracoManagementApiCleanStarterKit\src\Builder
dotnet run

Once it’s done, you can log into the backoffice to see the result with:

  • Username: admin@localhost
  • Password: SuperSecret123

The Clean Starter Kit, recreated by the Management API

So many moving parts!

The Clean Starter Kit showcases a lot of Umbraco features. To recreate it, I need to create:

  • Data types
  • Document types and element types (with an elaborate composition setup, I might add)
  • Media
  • Templates
  • Translations

…and that’s just to get to a point where I can start building documents, which in itself contains a lot of complexity - including:

  • Navigation structures
  • Cross-linking pages
  • Picking media
  • Building various block List structures
  • Publishing

Yep. It’s a mouthful 😖

Wanted: An API client

In the previous post I built my own Management API client - partly to showcase it, partly because I only used a fraction of the API. With all these moving parts in play, I definitively won’t be building my own API client this time around 😅

I’ll use .NET for this post because that’s where I’m the most comfortable hacking. So… enter the NSwag OpenAPI toolchain.

Long story short (and I quote): “The NSwag project provides tools to generate OpenAPI specifications from existing ASP.NET Web API controllers and client code from these OpenAPI specifications.”

There are several ways to use NSwag. I think the simplest way here is to use the NSwag code generation NuGet package from a console application:

using NJsonSchema.CodeGeneration.CSharp;
using NSwag;
using NSwag.CodeGeneration.CSharp;

// the Umbraco host (change this to fit your custom setup)
const string host = "https://localhost:44302";

// fetch the OpenAPI spec for the Management API
using var client = new HttpClient();
var json = await client.GetStringAsync($"{host}/umbraco/swagger/management/swagger.json?urls.primaryName=Umbraco%20Management%20API");

// parse the OpenAPI spec
var document = await OpenApiDocument.FromJsonAsync(json);

// settings for the generated API client code
var settings = new CSharpClientGeneratorSettings
{
    ClassName = "ApiClient", 
    CSharpGeneratorSettings = 
    {
        Namespace = "Umbraco.Management.Api",
        JsonLibrary = CSharpJsonLibrary.SystemTextJson
    }
};

// generate the API client and write the code to disk
var generator = new CSharpClientGenerator(document, settings);	
var code = generator.GenerateFile();
File.WriteAllText("../../../ApiClient.Generated.cs", code);

The generated output is a partial class called ApiClient. It’s partial because it allows for tweaking certain aspects, and I’ll use that to enforce a JSON naming policy that works with the Management API.

Also, the output seems to be missing a FileResponse class for certain export/download endpoints in the Management API. Since I won’t be needing any of those, I can make things play nice by adding an empty replacement class 😜

using System.Text.Json;

namespace Umbraco.Management.Api;

public partial class ApiClient
{
    static partial void UpdateJsonSerializerSettings(JsonSerializerOptions settings)
        => settings.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;

    // this class is missing from the generated code ... just add an empty class, we won't need it
    public class FileResponse
    {
    }
}

Utilizing the API client

The API client needs to be authorized, in order to perform operations against the Management API. Luckily, the API client essentially wraps a HttpClient, which means I can authorize the API client myself 👍

I have built a token service to obtain (and ensure reuse of) access tokens from the Management API. It uses IdentityModel to do the heavy lifting for OpenId Connect. With this in hand, an authorized API client can be obtained with a few lines of code:

using IdentityModel.Client;
using Umbraco.Management.Api;

namespace Builder;

public class MyClass
{
    private readonly ApiTokenService _apiTokenService;
    private readonly IHttpClientFactory _httpClientFactory;

    public MyClass(ApiTokenService apiTokenService, IHttpClientFactory httpClientFactory)
    {
        _apiTokenService = apiTokenService;
        _httpClientFactory = httpClientFactory;
    }

    public ApiClient GetApiClient()
    {
        var accessToken = _apiTokenService.GetAccessToken()
            ?? throw new InvalidOperationException("Could not get an access token.");

        var httpClient = _httpClientFactory.CreateClient();
        httpClient.SetBearerToken(accessToken);
        return new ApiClient("https://localhost:44302", httpClient);
    }
}

Once authorization is in place, the API client usage is pretty straight-forward. For example, here’s how a multi URL picker data type is created:

var apiClient = GetApiClient();
await apiClient.PostDataTypeAsync(
    new()
    {
        Name = "My Multi URL Picker",
        EditorAlias = "Umbraco.MultiUrlPicker",
        EditorUiAlias = "Umb.PropertyEditorUi.MultiUrlPicker",
        Values =
        [
            new() { Alias = "minNumber", Value = 0 },
            new() { Alias = "maxNumber", Value = 1 }
        ]
    }
);

Just show me the code, will ya?

Well. No. I’m not going to go through the entire implementation here, because there is simply too much code to cover.

Instead, I’ll cover some notable (weird?) parts of the code, and leave the deep dive to you 🤿

The program flow

Some Umbraco entities are atomic and thus relatively easy to manage. This includes translations, templates and usually also media.

Other entities reference one another, which makes them harder to manage. Some of these references are even cyclic in nature. For example, documents are based on document types, which in turn reference data types, and some of these data types reference both documents and document (or element).

All of this led me to the following program flow:

  1. Create translations, templates and media.
  2. Create data types.
  3. Create document and element types (including compositions).
  4. Update data type configurations - specifically block configurations for block list data types.
  5. Create and publish documents.
  6. Update data type configurations again - specifically dynamic roots and allowed types for content pickers (formerly known as MNTP).

Value collections

Documents, elements and media have property values, and data types have configuration values. All of these values are defined as pairs of string aliases and anonymous object values, because they can assume any value - from simple strings to intricate block editor data.

To infer the value type, you need to know what the value alias means to any concrete entity. And this in turn must be inferred from the entity configuration. For example: Given a document type alias, you must know the expected value type for any given property alias.

In other words, a lot of hardcoded mapping 🤠

It’s not an unconditionally bad thing, though. In fact, it’s one of the things that makes Umbraco so awesomely flexible.

At this point, there are a few ways to identify the value types for each entity type:

  • Use Swagger UI to inspect the Management API output for a similarly configured entity, or
  • Investigate the network traffic between the backoffice and the Management API when configuring an entity, or
  • Reverse engineer the Umbraco source code.

I have used a mix of the first two, just to prove that it can be done without diving into the Umbraco code 😜

As it happens, the value types are not included in the OpenAPI spec for the Management API, so I have created models for all non-trivial value types required to recreate the Clean Starter Kit.

Building documents

In the Clean Starter Kit, some documents reference other documents via content pickers. Specifically:

  • All blog posts reference authors and categories.
  • Some block list items reference the blog root.

A document reference requires the ID of the document. This becomes a bit of a chicken-and-the-egg situation, which can only be solved by splitting the process up in two phases:

  1. Create all documents.
  2. Update the documents that reference other documents.

The Management API doesn’t support PATCH operations, so it’s necessary to pass the “whole truth” for the document updates. Therefore, the documents that need updating are initially created as empty documents, since they’re completely overwritten by the update anyway 😉

IDs of created entities

When creating new entities, the Management API yields a HTTP 201 Created response with a Location header pointing at the new entity resource location. But for some reason, the generated API client does not register this.

Fortunately, all entity creation endpoints in the Management API accepts an (optional) ID of the resource to create. When the Location header doesn’t work, this approach comes in quite handy for cross-linking created entities 🔗

The Management API still rocks 🤘

I’m so excited about all this!

While it’s unlikely that you’d ever want to build an entire site from an external application… the fact that you can do it, using nothing but built-in Umbraco core features, is pretty goddamn awesome 👏

The resulting code is not necessarily pretty. For one, it’s heavily based on assumptions about both site structure and general availability of various entities. It is also somewhat repetitive - although I did refactor it to a certain degree, for the sake of my own sanity 🤪

But all the same - it works.

And more to the point: It was all written in a matter of hours. I won’t even begin to ponder how long it’d take to build the same functionality, if I first had to create custom integration APIs on the Umbraco side of things.

I’m a happy hacker 😆

Happy hacking 💜