diff --git a/src/Umbraco.Cms.Api.Common/Configuration/ConfigureUmbracoSwaggerGenOptions.cs b/src/Umbraco.Cms.Api.Common/Configuration/ConfigureUmbracoSwaggerGenOptions.cs index 572a74717926..d379b9601962 100644 --- a/src/Umbraco.Cms.Api.Common/Configuration/ConfigureUmbracoSwaggerGenOptions.cs +++ b/src/Umbraco.Cms.Api.Common/Configuration/ConfigureUmbracoSwaggerGenOptions.cs @@ -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; @@ -14,6 +15,7 @@ public class ConfigureUmbracoSwaggerGenOptions : IConfigureOptions()) + { } + + public ConfigureUmbracoSwaggerGenOptions( + IOperationIdSelector operationIdSelector, + ISchemaIdSelector schemaIdSelector, + ISubTypesSelector subTypesSelector) { _operationIdSelector = operationIdSelector; _schemaIdSelector = schemaIdSelector; + _subTypesSelector = subTypesSelector; } public void Configure(SwaggerGenOptions swaggerGenOptions) @@ -62,6 +73,7 @@ public void Configure(SwaggerGenOptions swaggerGenOptions) swaggerGenOptions.OrderActionsBy(ActionOrderBy); swaggerGenOptions.SchemaFilter(); swaggerGenOptions.CustomSchemaIds(_schemaIdSelector.SchemaId); + swaggerGenOptions.SelectSubTypesUsing(_subTypesSelector.SubTypes); swaggerGenOptions.SupportNonNullableReferenceTypes(); } diff --git a/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderApiExtensions.cs b/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderApiExtensions.cs index 49fe1f233e19..fcd7d9c6ec6d 100644 --- a/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderApiExtensions.cs +++ b/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderApiExtensions.cs @@ -23,6 +23,8 @@ public static IUmbracoBuilder AddUmbracoApiOpenApiUI(this IUmbracoBuilder builde builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.Configure(options => options.AddFilter(new SwaggerRouteTemplatePipelineFilter("UmbracoApiCommon"))); return builder; diff --git a/src/Umbraco.Cms.Api.Common/OpenApi/IOperationIdHandler.cs b/src/Umbraco.Cms.Api.Common/OpenApi/IOperationIdHandler.cs index e966c3b2e2c8..8855c81c0cbe 100644 --- a/src/Umbraco.Cms.Api.Common/OpenApi/IOperationIdHandler.cs +++ b/src/Umbraco.Cms.Api.Common/OpenApi/IOperationIdHandler.cs @@ -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); } diff --git a/src/Umbraco.Cms.Api.Common/OpenApi/ISchemaIdHandler.cs b/src/Umbraco.Cms.Api.Common/OpenApi/ISchemaIdHandler.cs index 81881f121a35..2df4ef8de4d8 100644 --- a/src/Umbraco.Cms.Api.Common/OpenApi/ISchemaIdHandler.cs +++ b/src/Umbraco.Cms.Api.Common/OpenApi/ISchemaIdHandler.cs @@ -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); } diff --git a/src/Umbraco.Cms.Api.Common/OpenApi/ISubTypesHandler.cs b/src/Umbraco.Cms.Api.Common/OpenApi/ISubTypesHandler.cs new file mode 100644 index 000000000000..72466789e607 --- /dev/null +++ b/src/Umbraco.Cms.Api.Common/OpenApi/ISubTypesHandler.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Cms.Api.Common.OpenApi; + +public interface ISubTypesHandler +{ + bool CanHandle(Type type, string documentName); + + IEnumerable Handle(Type type); +} diff --git a/src/Umbraco.Cms.Api.Common/OpenApi/ISubTypesSelector.cs b/src/Umbraco.Cms.Api.Common/OpenApi/ISubTypesSelector.cs new file mode 100644 index 000000000000..e358361eb393 --- /dev/null +++ b/src/Umbraco.Cms.Api.Common/OpenApi/ISubTypesSelector.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Common.OpenApi; + +public interface ISubTypesSelector +{ + IEnumerable SubTypes(Type type); +} diff --git a/src/Umbraco.Cms.Api.Common/OpenApi/OpenApiSelectorBase.cs b/src/Umbraco.Cms.Api.Common/OpenApi/OpenApiSelectorBase.cs new file mode 100644 index 000000000000..ba023849ecfb --- /dev/null +++ b/src/Umbraco.Cms.Api.Common/OpenApi/OpenApiSelectorBase.cs @@ -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 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; + } +} diff --git a/src/Umbraco.Cms.Api.Common/OpenApi/OperationIdHandler.cs b/src/Umbraco.Cms.Api.Common/OpenApi/OperationIdHandler.cs index 8fe6c7dff4fc..0d67c9d912e8 100644 --- a/src/Umbraco.Cms.Api.Common/OpenApi/OperationIdHandler.cs +++ b/src/Umbraco.Cms.Api.Common/OpenApi/OperationIdHandler.cs @@ -14,18 +14,24 @@ public class OperationIdHandler : IOperationIdHandler public OperationIdHandler(IOptions 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); diff --git a/src/Umbraco.Cms.Api.Common/OpenApi/OperationIdSelector.cs b/src/Umbraco.Cms.Api.Common/OpenApi/OperationIdSelector.cs index 3c00a126be9e..55402599d982 100644 --- a/src/Umbraco.Cms.Api.Common/OpenApi/OperationIdSelector.cs +++ b/src/Umbraco.Cms.Api.Common/OpenApi/OperationIdSelector.cs @@ -1,19 +1,38 @@ 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 _operationIdHandlers; [Obsolete("Use non-obsolete constructor. This will be removed in Umbraco 15.")] public OperationIdSelector() : this(Enumerable.Empty()) - { - } + { } + [Obsolete("Use non obsolete constructor instead. Will be removed in v16.")] public OperationIdSelector(IEnumerable operationIdHandlers) + : this( + StaticServiceProvider.Instance.GetRequiredService>(), + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService(), + operationIdHandlers) + { } + + public OperationIdSelector( + IOptions settings, + IHostingEnvironment hostingEnvironment, + IHttpContextAccessor httpContextAccessor, + IEnumerable operationIdHandlers) + : base(settings, hostingEnvironment, httpContextAccessor) => _operationIdHandlers = operationIdHandlers; [Obsolete("Use overload that only takes ApiDescription instead. This will be removed in Umbraco 15.")] @@ -21,7 +40,12 @@ public OperationIdSelector(IEnumerable operationIdHandlers) 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; } } diff --git a/src/Umbraco.Cms.Api.Common/OpenApi/SchemaIdHandler.cs b/src/Umbraco.Cms.Api.Common/OpenApi/SchemaIdHandler.cs index c08e0be19ac9..2f3e1c50f4e0 100644 --- a/src/Umbraco.Cms.Api.Common/OpenApi/SchemaIdHandler.cs +++ b/src/Umbraco.Cms.Api.Common/OpenApi/SchemaIdHandler.cs @@ -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); diff --git a/src/Umbraco.Cms.Api.Common/OpenApi/SchemaIdSelector.cs b/src/Umbraco.Cms.Api.Common/OpenApi/SchemaIdSelector.cs index 4ff28c795903..5cdbb0f11475 100644 --- a/src/Umbraco.Cms.Api.Common/OpenApi/SchemaIdSelector.cs +++ b/src/Umbraco.Cms.Api.Common/OpenApi/SchemaIdSelector.cs @@ -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 _schemaIdHandlers; + [Obsolete("Use non obsolete constructor instead. Will be removed in v16.")] public SchemaIdSelector(IEnumerable schemaIdHandlers) + : this( + StaticServiceProvider.Instance.GetRequiredService>(), + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService(), + schemaIdHandlers) + { } + + public SchemaIdSelector( + IOptions settings, + IHostingEnvironment hostingEnvironment, + IHttpContextAccessor httpContextAccessor, + IEnumerable 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; } } diff --git a/src/Umbraco.Cms.Api.Common/OpenApi/SubTypesHandler.cs b/src/Umbraco.Cms.Api.Common/OpenApi/SubTypesHandler.cs new file mode 100644 index 000000000000..5744a0075f03 --- /dev/null +++ b/src/Umbraco.Cms.Api.Common/OpenApi/SubTypesHandler.cs @@ -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 Handle(Type type) + => umbracoJsonTypeInfoResolver.FindSubTypes(type); +} diff --git a/src/Umbraco.Cms.Api.Common/OpenApi/SubTypesSelector.cs b/src/Umbraco.Cms.Api.Common/OpenApi/SubTypesSelector.cs new file mode 100644 index 000000000000..9ba628fb8329 --- /dev/null +++ b/src/Umbraco.Cms.Api.Common/OpenApi/SubTypesSelector.cs @@ -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 settings, + IHostingEnvironment hostingEnvironment, + IHttpContextAccessor httpContextAccessor, + IEnumerable subTypeHandlers, + IUmbracoJsonTypeInfoResolver umbracoJsonTypeInfoResolver) + : OpenApiSelectorBase(settings, hostingEnvironment, httpContextAccessor), ISubTypesSelector +{ + public IEnumerable 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); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Configuration/ConfigureUmbracoManagementApiSwaggerGenOptions.cs b/src/Umbraco.Cms.Api.Management/Configuration/ConfigureUmbracoManagementApiSwaggerGenOptions.cs index 6b67c805af25..74862e3babbf 100644 --- a/src/Umbraco.Cms.Api.Management/Configuration/ConfigureUmbracoManagementApiSwaggerGenOptions.cs +++ b/src/Umbraco.Cms.Api.Management/Configuration/ConfigureUmbracoManagementApiSwaggerGenOptions.cs @@ -31,7 +31,6 @@ public void Configure(SwaggerGenOptions swaggerGenOptions) }); swaggerGenOptions.OperationFilter(); - 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