Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v15: Add documentName support to IOperationIdHandler and ISchemaIdHandler #17073

Open
wants to merge 9 commits into
base: contrib
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Api.Common.OpenApi;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Extensions;

namespace Umbraco.Cms.Api.Common.Configuration;
Expand All @@ -14,6 +15,7 @@ public class ConfigureUmbracoSwaggerGenOptions : IConfigureOptions<SwaggerGenOpt
{
private readonly IOperationIdSelector _operationIdSelector;
private readonly ISchemaIdSelector _schemaIdSelector;
private readonly ISubTypesSelector _subTypesSelector;

[Obsolete("Use non-obsolete constructor. This will be removed in Umbraco 15.")]
public ConfigureUmbracoSwaggerGenOptions(
Expand All @@ -24,12 +26,21 @@ public ConfigureUmbracoSwaggerGenOptions(
{
}

[Obsolete("Use non-obsolete constructor. This will be removed in Umbraco 16.")]
public ConfigureUmbracoSwaggerGenOptions(
IOperationIdSelector operationIdSelector,
ISchemaIdSelector schemaIdSelector)
: this(operationIdSelector, schemaIdSelector, StaticServiceProvider.Instance.GetRequiredService<ISubTypesSelector>())
{ }

public ConfigureUmbracoSwaggerGenOptions(
IOperationIdSelector operationIdSelector,
ISchemaIdSelector schemaIdSelector,
ISubTypesSelector subTypesSelector)
{
_operationIdSelector = operationIdSelector;
_schemaIdSelector = schemaIdSelector;
_subTypesSelector = subTypesSelector;
}

public void Configure(SwaggerGenOptions swaggerGenOptions)
Expand Down Expand Up @@ -62,6 +73,7 @@ public void Configure(SwaggerGenOptions swaggerGenOptions)
swaggerGenOptions.OrderActionsBy(ActionOrderBy);
swaggerGenOptions.SchemaFilter<EnumSchemaFilter>();
swaggerGenOptions.CustomSchemaIds(_schemaIdSelector.SchemaId);
swaggerGenOptions.SelectSubTypesUsing(_subTypesSelector.SubTypes);
swaggerGenOptions.SupportNonNullableReferenceTypes();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public static IUmbracoBuilder AddUmbracoApiOpenApiUI(this IUmbracoBuilder builde
builder.Services.AddSingleton<IOperationIdHandler, OperationIdHandler>();
builder.Services.AddSingleton<ISchemaIdSelector, SchemaIdSelector>();
builder.Services.AddSingleton<ISchemaIdHandler, SchemaIdHandler>();
builder.Services.AddSingleton<ISubTypesSelector, SubTypesSelector>();
builder.Services.AddSingleton<ISubTypesHandler, SubTypesHandler>();
builder.Services.Configure<UmbracoPipelineOptions>(options => options.AddFilter(new SwaggerRouteTemplatePipelineFilter("UmbracoApiCommon")));

return builder;
Expand Down
5 changes: 5 additions & 0 deletions src/Umbraco.Cms.Api.Common/OpenApi/IOperationIdHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ namespace Umbraco.Cms.Api.Common.OpenApi;

public interface IOperationIdHandler
{
[Obsolete("Use CanHandle(ApiDescription apiDescription, string documentName) instead. Will be removed in v16.")]
bool CanHandle(ApiDescription apiDescription);

#pragma warning disable CS0618 // Type or member is obsolete
bool CanHandle(ApiDescription apiDescription, string documentName) => CanHandle(apiDescription);
#pragma warning restore CS0618 // Type or member is obsolete

string Handle(ApiDescription apiDescription);
}
5 changes: 5 additions & 0 deletions src/Umbraco.Cms.Api.Common/OpenApi/ISchemaIdHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ namespace Umbraco.Cms.Api.Common.OpenApi;

public interface ISchemaIdHandler
{
[Obsolete("Use CanHandle(Type type, string documentName) instead. Will be removed in v16.")]
bool CanHandle(Type type);

#pragma warning disable CS0618 // Type or member is obsolete
bool CanHandle(Type type, string documentName) => CanHandle(type);
#pragma warning restore CS0618 // Type or member is obsolete

string Handle(Type type);
}
8 changes: 8 additions & 0 deletions src/Umbraco.Cms.Api.Common/OpenApi/ISubTypesHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Umbraco.Cms.Api.Common.OpenApi;

public interface ISubTypesHandler
{
bool CanHandle(Type type, string documentName);

IEnumerable<Type> Handle(Type type);
}
6 changes: 6 additions & 0 deletions src/Umbraco.Cms.Api.Common/OpenApi/ISubTypesSelector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Api.Common.OpenApi;

public interface ISubTypesSelector
{
IEnumerable<Type> SubTypes(Type type);
}
27 changes: 27 additions & 0 deletions src/Umbraco.Cms.Api.Common/OpenApi/OpenApiSelectorBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Extensions;

namespace Umbraco.Cms.Api.Common.OpenApi;

public abstract class OpenApiSelectorBase(
IOptions<GlobalSettings> settings,
IHostingEnvironment hostingEnvironment,
IHttpContextAccessor httpContextAccessor)
{
protected string ResolveOpenApiDocumentName()
{
var backOfficePath = settings.Value.GetBackOfficePath(hostingEnvironment);
if (httpContextAccessor.HttpContext?.Request.Path.StartsWithSegments($"{backOfficePath}/swagger/") ?? false)
{
// Split the path into segments
var segments = httpContextAccessor.HttpContext.Request.Path.Value!.TrimStart($"{backOfficePath}/swagger/").Split('/');

// Extract the document name from the path
return segments[0];
}
return string.Empty;
}
}
8 changes: 7 additions & 1 deletion src/Umbraco.Cms.Api.Common/OpenApi/OperationIdHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,24 @@ public class OperationIdHandler : IOperationIdHandler
public OperationIdHandler(IOptions<ApiVersioningOptions> apiVersioningOptions)
=> _apiVersioningOptions = apiVersioningOptions.Value;

[Obsolete("Use CanHandle(ApiDescription apiDescription, string documentName) instead. Will be removed in v16.")]
public bool CanHandle(ApiDescription apiDescription)
=> CanHandle(apiDescription, string.Empty);

public bool CanHandle(ApiDescription apiDescription, string documentName)
{
if (apiDescription.ActionDescriptor is not ControllerActionDescriptor controllerActionDescriptor)
{
return false;
}

return CanHandle(apiDescription, controllerActionDescriptor);
return CanHandle(apiDescription, controllerActionDescriptor, documentName);
}

protected virtual bool CanHandle(ApiDescription apiDescription, ControllerActionDescriptor controllerActionDescriptor)
=> controllerActionDescriptor.ControllerTypeInfo.Namespace?.StartsWith("Umbraco.Cms.Api") is true;
protected virtual bool CanHandle(ApiDescription apiDescription, ControllerActionDescriptor controllerActionDescriptor, string documentName)
=> CanHandle(apiDescription, controllerActionDescriptor);

public virtual string Handle(ApiDescription apiDescription)
=> UmbracoOperationId(apiDescription);
Expand Down
34 changes: 29 additions & 5 deletions src/Umbraco.Cms.Api.Common/OpenApi/OperationIdSelector.cs
Original file line number Diff line number Diff line change
@@ -1,27 +1,51 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Hosting;

namespace Umbraco.Cms.Api.Common.OpenApi;

public class OperationIdSelector : IOperationIdSelector
public class OperationIdSelector : OpenApiSelectorBase, IOperationIdSelector
{
private readonly IEnumerable<IOperationIdHandler> _operationIdHandlers;

[Obsolete("Use non-obsolete constructor. This will be removed in Umbraco 15.")]
public OperationIdSelector()
: this(Enumerable.Empty<IOperationIdHandler>())
{
}
{ }

[Obsolete("Use non obsolete constructor instead. Will be removed in v16.")]
public OperationIdSelector(IEnumerable<IOperationIdHandler> operationIdHandlers)
: this(
StaticServiceProvider.Instance.GetRequiredService<IOptions<GlobalSettings>>(),
StaticServiceProvider.Instance.GetRequiredService<IHostingEnvironment>(),
StaticServiceProvider.Instance.GetRequiredService<IHttpContextAccessor>(),
operationIdHandlers)
{ }

public OperationIdSelector(
IOptions<GlobalSettings> settings,
IHostingEnvironment hostingEnvironment,
IHttpContextAccessor httpContextAccessor,
IEnumerable<IOperationIdHandler> operationIdHandlers)
: base(settings, hostingEnvironment, httpContextAccessor)
=> _operationIdHandlers = operationIdHandlers;

[Obsolete("Use overload that only takes ApiDescription instead. This will be removed in Umbraco 15.")]
public virtual string? OperationId(ApiDescription apiDescription, ApiVersioningOptions apiVersioningOptions) => OperationId(apiDescription);

public virtual string? OperationId(ApiDescription apiDescription)
{
IOperationIdHandler? handler = _operationIdHandlers.FirstOrDefault(h => h.CanHandle(apiDescription));
return handler?.Handle(apiDescription);
var documentName = ResolveOpenApiDocumentName();
if (!string.IsNullOrEmpty(documentName))
{
IOperationIdHandler? handler = _operationIdHandlers.FirstOrDefault(h => h.CanHandle(apiDescription, documentName));
return handler?.Handle(apiDescription);
}
return null;
}
}
6 changes: 6 additions & 0 deletions src/Umbraco.Cms.Api.Common/OpenApi/SchemaIdHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,15 @@ namespace Umbraco.Cms.Api.Common.OpenApi;
// NOTE: Left unsealed on purpose, so it is extendable.
public class SchemaIdHandler : ISchemaIdHandler
{
[Obsolete("Use CanHandle(Type type, string documentName) instead. Will be removed in v16.")]
public virtual bool CanHandle(Type type)
=> type.Namespace?.StartsWith("Umbraco.Cms") is true;

#pragma warning disable CS0618 // Type or member is obsolete
public virtual bool CanHandle(Type type, string documentName)
=> CanHandle(type);
#pragma warning restore CS0618 // Type or member is obsolete

public virtual string Handle(Type type)
=> UmbracoSchemaId(type);

Expand Down
34 changes: 30 additions & 4 deletions src/Umbraco.Cms.Api.Common/OpenApi/SchemaIdSelector.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,41 @@
namespace Umbraco.Cms.Api.Common.OpenApi;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Hosting;

public class SchemaIdSelector : ISchemaIdSelector
namespace Umbraco.Cms.Api.Common.OpenApi;

public class SchemaIdSelector : OpenApiSelectorBase, ISchemaIdSelector
{
private readonly IEnumerable<ISchemaIdHandler> _schemaIdHandlers;

[Obsolete("Use non obsolete constructor instead. Will be removed in v16.")]
public SchemaIdSelector(IEnumerable<ISchemaIdHandler> schemaIdHandlers)
: this(
StaticServiceProvider.Instance.GetRequiredService<IOptions<GlobalSettings>>(),
StaticServiceProvider.Instance.GetRequiredService<IHostingEnvironment>(),
StaticServiceProvider.Instance.GetRequiredService<IHttpContextAccessor>(),
schemaIdHandlers)
{ }

public SchemaIdSelector(
IOptions<GlobalSettings> settings,
IHostingEnvironment hostingEnvironment,
IHttpContextAccessor httpContextAccessor,
IEnumerable<ISchemaIdHandler> schemaIdHandlers)
: base(settings, hostingEnvironment, httpContextAccessor)
=> _schemaIdHandlers = schemaIdHandlers;

public virtual string SchemaId(Type type)
{
ISchemaIdHandler? handler = _schemaIdHandlers.FirstOrDefault(h => h.CanHandle(type));
return handler?.Handle(type) ?? type.Name;
var documentName = ResolveOpenApiDocumentName();
if (!string.IsNullOrEmpty(documentName))
{
ISchemaIdHandler? handler = _schemaIdHandlers.FirstOrDefault(h => h.CanHandle(type, documentName));
return handler?.Handle(type) ?? type.Name;
}
return type.Name;
}
}
15 changes: 15 additions & 0 deletions src/Umbraco.Cms.Api.Common/OpenApi/SubTypesHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Umbraco.Cms.Api.Common.Serialization;

namespace Umbraco.Cms.Api.Common.OpenApi;

public class SubTypesHandler(IUmbracoJsonTypeInfoResolver umbracoJsonTypeInfoResolver) : ISubTypesHandler
{
public virtual bool CanHandle(Type type)
=> type.Namespace?.StartsWith("Umbraco.Cms") is true;

public virtual bool CanHandle(Type type, string documentName)
=> CanHandle(type);

public virtual IEnumerable<Type> Handle(Type type)
=> umbracoJsonTypeInfoResolver.FindSubTypes(type);
}
34 changes: 34 additions & 0 deletions src/Umbraco.Cms.Api.Common/OpenApi/SubTypesSelector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Common.Serialization;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Extensions;

namespace Umbraco.Cms.Api.Common.OpenApi;

public class SubTypesSelector(
IOptions<GlobalSettings> settings,
IHostingEnvironment hostingEnvironment,
IHttpContextAccessor httpContextAccessor,
IEnumerable<ISubTypesHandler> subTypeHandlers,
IUmbracoJsonTypeInfoResolver umbracoJsonTypeInfoResolver)
: OpenApiSelectorBase(settings, hostingEnvironment, httpContextAccessor), ISubTypesSelector
{
public IEnumerable<Type> SubTypes(Type type)
{
var documentName = ResolveOpenApiDocumentName();
if (!string.IsNullOrEmpty(documentName))
{
// Find the first handler that can handle the type / document name combination
ISubTypesHandler? handler = subTypeHandlers.FirstOrDefault(h => h.CanHandle(type, documentName));
if (handler != null)
{
return handler.Handle(type);
}
}

// Default implementation to maintain backwards compatibility
return umbracoJsonTypeInfoResolver.FindSubTypes(type);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ public void Configure(SwaggerGenOptions swaggerGenOptions)
});

swaggerGenOptions.OperationFilter<ResponseHeaderOperationFilter>();
swaggerGenOptions.SelectSubTypesUsing(_umbracoJsonTypeInfoResolver.FindSubTypes);
swaggerGenOptions.UseOneOfForPolymorphism();

// Ensure all types that implements the IOpenApiDiscriminator have a $type property in the OpenApi schema with the default value (The class name) that is expected by the server
Expand Down
Loading