Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions 14/umbraco-cms/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,7 @@
* [Default Property Editor Schema aliases](tutorials/creating-a-property-editor/default-property-editor-schema-aliases.md)
* [Creating a backoffice API](tutorials/creating-a-backoffice-api/README.md)
* [Documenting your controllers](tutorials/creating-a-backoffice-api/documenting-your-controllers.md)
* [Adding a custom Swagger document](tutorials/creating-a-backoffice-api/adding-a-custom-swagger-document.md)
* [Polymorphic output in the Management API](tutorials/creating-a-backoffice-api/polymorphic-output-in-the-management-api.md)
* [Umbraco schema and operation IDs](tutorials/creating-a-backoffice-api/umbraco-schema-and-operation-ids.md)
* [Implementing Custom Error Pages](tutorials/custom-error-page.md)
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
---
description: Adding a custom Swagger document for a custom Management API
---

# Adding a custom Swagger document

By default, all controllers based on ManagementApiControllerBase will be included in the default Management API Swagger document.

When building custom Management API controllers, sometimes it's preferable to have a dedicated Swagger document for them. Doing so is a three-step process:

1. Register the Swagger document with Swagger UI.
2. Instruct Swagger UI to utilize Umbraco authentication for the Swagger document.
3. Move the controllers to the Swagger document.

The following code exemplifies how to achieve the first two steps;

{% code title="MyItemApiComposer.cs" %}
```csharp
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Api.Management.OpenApi;
using Umbraco.Cms.Core.Composing;

namespace UmbracoDocs.Samples;

public class MyItemApiComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.Services.ConfigureOptions<MyItemApiSwaggerGenOptions>();
}
}

public class MyItemApiSwaggerGenOptions : IConfigureOptions<SwaggerGenOptions>
{
public void Configure(SwaggerGenOptions options)
{
// register the custom Swagger document "my-item-api"
options.SwaggerDoc(
"my-item-api",
new OpenApiInfo { Title = "My item API", Version = "1.0" }
);

// enable Umbraco authentication for the "my-item-api" Swagger document
options.OperationFilter<MyItemApiOperationSecurityFilter>();
}
}

public class MyItemApiOperationSecurityFilter : BackOfficeSecurityRequirementsOperationFilterBase
{
protected override string ApiName => "my-item-api";
}
```
{% endcode %}

With this in place, the last step is to annotate the relevant API controllers with the MapToApi attribute:

{% code title="MyItemApiController.cs" %}
```csharp
[MapToApi("my-item-api")]
public class MyItemApiController : ManagementApiControllerBase
```
{% endcode %}

Now when we visit the Swagger UI, "My item API" has its own Swagger document:

![My item API in Swagger UI](images/my-item-api-swagger-ui.png)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
---
description: How to support polymorphic outputs from custom Management APIs
---

# Polymorphic output in the Management API

For security reasons, the `System.Text.Json` serializer will not serialize types that are not explicitly referenced at compile time.

This can be a challenge when dealing with polymorphic API outputs. As a workaround, the Management API provides two options for enabling polymorphic outputs.

## Polymorphism by interface

This approach requires that all output models implement the same interface - for example:

{% code title="IMyItem.cs" %}
```csharp
public interface IMyItem
{
Guid Id { get; }

string Value { get; set; }
}
```
{% endcode %}

{% code title="MyItem.cs" %}
```csharp
public class MyItem(string value) : IMyItem
{
public Guid Id { get; } = Guid.NewGuid();

public string Value { get; set; } = value;
}
```
{% endcode %}

{% code title="MyOtherItem.cs" %}
```csharp
public class MyOtherItem(string value, int otherValue) : IMyItem
{
public Guid Id { get; } = Guid.NewGuid();

public string Value { get; set; } = value;

public int OtherValue { get; } = otherValue;
}
```
{% endcode %}

The `ProducesResponseType` annotation on the endpoints must also be updated to use the interface:

{% code title="MyItemApiController.cs" %}
```csharp
...
[ProducesResponseType<PagedViewModel<IMyItem>>(StatusCodes.Status200OK)]
public IActionResult GetAllItems(int skip = 0, int take = 10)
...
[ProducesResponseType<IMyItem>(StatusCodes.Status200OK)]
public IActionResult GetItem(Guid id)
...
```
{% endcode %}

## Polymorphism by annotation

This approach requires that all output models implement a common base class. The base class will define all its derived types by annotation - for example:

{% code title="MyItemBase.cs" %}
```csharp
[JsonDerivedType(typeof(MyItem), nameof(MyItem))]
[JsonDerivedType(typeof(MyOtherItem), nameof(MyOtherItem))]
public abstract class MyItemBase(string value)
{
public Guid Id { get; } = Guid.NewGuid();

public string Value { get; set; } = value;
}
```
{% endcode %}

{% code title="MyItem.cs" %}
```csharp
public class MyItem(string value) : MyItemBase(value)
{
}

```
{% endcode %}

{% code title="MyOtherItem.cs" %}
```csharp
public class MyOtherItem(string value, int otherValue) : MyItemBase(value)
{
public int OtherValue { get; } = otherValue;
}

```
{% endcode %}

The `ProducesResponseType` annotation on the endpoints must also be updated to use the base class:


{% code title="MyItemApiController.cs" %}
```csharp
...
[ProducesResponseType<PagedViewModel<MyItemBase>>(StatusCodes.Status200OK)]
public IActionResult GetAllItems(int skip = 0, int take = 10)
...
[ProducesResponseType<MyItemBase>(StatusCodes.Status200OK)]
public IActionResult GetItem(Guid id)
...
```
{% endcode %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
---
description: How to apply the Umbraco schema and operation IDs for custom Management APIs
---

# Umbraco schema and operation IDs

All core Management APIs have a custom scheme for their generated OpenAPI schema and operation IDs.

This scheme is strictly opt-in to avoid affecting custom APIs by default. In this article, we'll see how to opt-in to the scheme.

{% hint style="info" %}
If you are happy with your APIs' default schema and operation IDs, nothing is likely gained by using the Umbraco ones.
{% endhint %}

## Schema IDs

Schema IDs are handled by `ISchemaIdHandler` implementations. To opt-in to the Umbraco schema IDs, we base our implementation on the core handler:

{% code title="SampleSchemaIdHandler.cs" %}
```csharp
using Umbraco.Cms.Api.Common.OpenApi;

namespace UmbracoDocs.Samples;

// this schema ID handler extends the Umbraco schema IDs
// to all types in the UmbracoDocs.Samples namespace
public class SampleSchemaIdHandler : SchemaIdHandler
{
public override bool CanHandle(Type type)
=> type.Namespace == "UmbracoDocs.Samples";
}
```
{% endcode %}

Then, we implement a composer to register the new schema ID handler:

{% code title="SampleSchemaIdComposer.cs" %}
```csharp
using Umbraco.Cms.Api.Common.OpenApi;
using Umbraco.Cms.Core.Composing;

namespace UmbracoDocs.Samples;

public class SampleSchemaIdComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
=> builder.Services.AddSingleton<ISchemaIdHandler, SampleSchemaIdHandler>();
}
```
{% endcode %}

## Operation IDs

Operation IDs follow the same pattern as schema IDs. The only difference is that the `IOperationIdHandler` operates at the API level, not at the type level.

Again, to opt-in to the Umbraco operation IDs, we base our implementation on the core handler:

{% code title="SampleOperationIdHandler.cs" %}
```csharp
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Common.OpenApi;

namespace UmbracoDocs.Samples;

// this operation ID handler extends the Umbraco operation IDs
// to all API controllers in the UmbracoDocs.Samples namespace
public class SampleOperationIdHandler : OperationIdHandler
{
public SampleOperationIdHandler(IOptions<ApiVersioningOptions> apiVersioningOptions)
: base(apiVersioningOptions)
{
}

protected override bool CanHandle(ApiDescription apiDescription, ControllerActionDescriptor controllerActionDescriptor)
=> controllerActionDescriptor.ControllerTypeInfo.Namespace == "UmbracoDocs.Samples";
}
```
{% endcode %}

Then, we implement a composer to register the new operation ID handler:

{% code title="SampleOperationIdComposer.cs" %}
```csharp
using Umbraco.Cms.Api.Common.OpenApi;
using Umbraco.Cms.Core.Composing;

namespace UmbracoDocs.Samples;

public class SampleOperationIdComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
=> builder.Services.AddSingleton<IOperationIdHandler, SampleOperationIdHandler>();
}
```
{% endcode %}