From a01496b9ef2083278bfb401aad2a655bc1b7ed2b Mon Sep 17 00:00:00 2001 From: Marcus Wilhelmson Date: Mon, 15 Sep 2025 22:17:02 +0200 Subject: [PATCH 1/7] Use EndpointMetadata to check for existing MapToApiAttribute at runtime --- .../ConfigureUmbracoSwaggerGenOptions.cs | 29 ++++------- .../Extensions/MapToApiAttributeExtensions.cs | 48 +++++++++++++++++++ .../MethodInfoApiCommonExtensions.cs | 32 ------------- 3 files changed, 57 insertions(+), 52 deletions(-) create mode 100644 src/Umbraco.Cms.Api.Common/Extensions/MapToApiAttributeExtensions.cs delete mode 100644 src/Umbraco.Cms.Api.Common/Extensions/MethodInfoApiCommonExtensions.cs diff --git a/src/Umbraco.Cms.Api.Common/Configuration/ConfigureUmbracoSwaggerGenOptions.cs b/src/Umbraco.Cms.Api.Common/Configuration/ConfigureUmbracoSwaggerGenOptions.cs index e4b60877f124..b19421215e19 100644 --- a/src/Umbraco.Cms.Api.Common/Configuration/ConfigureUmbracoSwaggerGenOptions.cs +++ b/src/Umbraco.Cms.Api.Common/Configuration/ConfigureUmbracoSwaggerGenOptions.cs @@ -11,22 +11,11 @@ namespace Umbraco.Cms.Api.Common.Configuration; -public class ConfigureUmbracoSwaggerGenOptions : IConfigureOptions +public class ConfigureUmbracoSwaggerGenOptions( + IOperationIdSelector operationIdSelector, + ISchemaIdSelector schemaIdSelector, + ISubTypesSelector subTypesSelector) : IConfigureOptions { - private readonly IOperationIdSelector _operationIdSelector; - private readonly ISchemaIdSelector _schemaIdSelector; - private readonly ISubTypesSelector _subTypesSelector; - - public ConfigureUmbracoSwaggerGenOptions( - IOperationIdSelector operationIdSelector, - ISchemaIdSelector schemaIdSelector, - ISubTypesSelector subTypesSelector) - { - _operationIdSelector = operationIdSelector; - _schemaIdSelector = schemaIdSelector; - _subTypesSelector = subTypesSelector; - } - public void Configure(SwaggerGenOptions swaggerGenOptions) { swaggerGenOptions.SwaggerDoc( @@ -38,11 +27,11 @@ public void Configure(SwaggerGenOptions swaggerGenOptions) Description = "All endpoints not defined under specific APIs", }); - swaggerGenOptions.CustomOperationIds(description => _operationIdSelector.OperationId(description)); + swaggerGenOptions.CustomOperationIds(operationIdSelector.OperationId); swaggerGenOptions.DocInclusionPredicate((name, api) => { if (api.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor - && controllerActionDescriptor.MethodInfo.HasMapToApiAttribute(name)) + && controllerActionDescriptor.HasMapToApiAttribute(name)) { return true; } @@ -51,11 +40,11 @@ public void Configure(SwaggerGenOptions swaggerGenOptions) return apiVersionMetadata.Name == name || (string.IsNullOrEmpty(apiVersionMetadata.Name) && name == DefaultApiConfiguration.ApiName); }); - swaggerGenOptions.TagActionsBy(api => new[] { api.GroupName }); + swaggerGenOptions.TagActionsBy(api => [api.GroupName]); swaggerGenOptions.OrderActionsBy(ActionOrderBy); swaggerGenOptions.SchemaFilter(); - swaggerGenOptions.CustomSchemaIds(_schemaIdSelector.SchemaId); - swaggerGenOptions.SelectSubTypesUsing(_subTypesSelector.SubTypes); + swaggerGenOptions.CustomSchemaIds(schemaIdSelector.SchemaId); + swaggerGenOptions.SelectSubTypesUsing(subTypesSelector.SubTypes); swaggerGenOptions.SupportNonNullableReferenceTypes(); } diff --git a/src/Umbraco.Cms.Api.Common/Extensions/MapToApiAttributeExtensions.cs b/src/Umbraco.Cms.Api.Common/Extensions/MapToApiAttributeExtensions.cs new file mode 100644 index 000000000000..6bf73a2e4ab9 --- /dev/null +++ b/src/Umbraco.Cms.Api.Common/Extensions/MapToApiAttributeExtensions.cs @@ -0,0 +1,48 @@ +using System.Reflection; +using Asp.Versioning; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Umbraco.Cms.Api.Common.Attributes; +using Umbraco.Cms.Api.Common.Configuration; + +namespace Umbraco.Extensions; + +public static class MapToApiAttributeExtensions +{ + + public static string? GetMapToApiVersionAttributeValue(this MethodInfo methodInfo) + { + MapToApiVersionAttribute[] mapToApis = methodInfo.GetCustomAttributes(typeof(MapToApiVersionAttribute), inherit: true).Cast().ToArray(); + + return string.Join("|", mapToApis.SelectMany(x => x.Versions)); + } + + public static string? GetMapToApiAttributeValue(this MethodInfo methodInfo) + { + MapToApiAttribute[] mapToApis = [.. (methodInfo.DeclaringType?.GetCustomAttributes(typeof(MapToApiAttribute), inherit: true) ?? []).Cast()]; + + return mapToApis.SingleOrDefault()?.ApiName; + } + + private static string? GetMapToApiAttributeValue(this ActionDescriptor actionDescriptor) + { + IEnumerable mapToApiAttributes = actionDescriptor?.EndpointMetadata?.OfType() ?? []; + + return mapToApiAttributes.SingleOrDefault()?.ApiName; + } + + public static bool HasMapToApiAttribute(this MethodInfo methodInfo, string apiName) + { + var value = methodInfo.GetMapToApiAttributeValue(); + + return value == apiName + || (value is null && apiName == DefaultApiConfiguration.ApiName); + } + + public static bool HasMapToApiAttribute(this ActionDescriptor actionDescriptor, string apiName) + { + var value = actionDescriptor.GetMapToApiAttributeValue(); + + return value == apiName + || (value is null && apiName == DefaultApiConfiguration.ApiName); + } +} diff --git a/src/Umbraco.Cms.Api.Common/Extensions/MethodInfoApiCommonExtensions.cs b/src/Umbraco.Cms.Api.Common/Extensions/MethodInfoApiCommonExtensions.cs deleted file mode 100644 index 9d619f5e1c86..000000000000 --- a/src/Umbraco.Cms.Api.Common/Extensions/MethodInfoApiCommonExtensions.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Reflection; -using Asp.Versioning; -using Umbraco.Cms.Api.Common.Attributes; -using Umbraco.Cms.Api.Common.Configuration; - -namespace Umbraco.Extensions; - -public static class MethodInfoApiCommonExtensions -{ - - public static string? GetMapToApiVersionAttributeValue(this MethodInfo methodInfo) - { - MapToApiVersionAttribute[] mapToApis = methodInfo.GetCustomAttributes(typeof(MapToApiVersionAttribute), inherit: true).Cast().ToArray(); - - return string.Join("|", mapToApis.SelectMany(x=>x.Versions)); - } - - public static string? GetMapToApiAttributeValue(this MethodInfo methodInfo) - { - MapToApiAttribute[] mapToApis = (methodInfo.DeclaringType?.GetCustomAttributes(typeof(MapToApiAttribute), inherit: true) ?? Array.Empty()).Cast().ToArray(); - - return mapToApis.SingleOrDefault()?.ApiName; - } - - public static bool HasMapToApiAttribute(this MethodInfo methodInfo, string apiName) - { - var value = methodInfo.GetMapToApiAttributeValue(); - - return value == apiName - || (value is null && apiName == DefaultApiConfiguration.ApiName); - } -} From a3ec05e3e3931903cde93ce77d611ff7c04814e6 Mon Sep 17 00:00:00 2001 From: Marcus Wilhelmson Date: Mon, 15 Sep 2025 23:04:13 +0200 Subject: [PATCH 2/7] fix api breaking change --- .../ActionDescriptorApiCommonExtensions.cs | 24 +++++++++++++++++++ ...ns.cs => MethodInfoApiCommonExtensions.cs} | 22 +++-------------- 2 files changed, 27 insertions(+), 19 deletions(-) create mode 100644 src/Umbraco.Cms.Api.Common/Extensions/ActionDescriptorApiCommonExtensions.cs rename src/Umbraco.Cms.Api.Common/Extensions/{MapToApiAttributeExtensions.cs => MethodInfoApiCommonExtensions.cs} (52%) diff --git a/src/Umbraco.Cms.Api.Common/Extensions/ActionDescriptorApiCommonExtensions.cs b/src/Umbraco.Cms.Api.Common/Extensions/ActionDescriptorApiCommonExtensions.cs new file mode 100644 index 000000000000..16b0c5c14a51 --- /dev/null +++ b/src/Umbraco.Cms.Api.Common/Extensions/ActionDescriptorApiCommonExtensions.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Mvc.Abstractions; +using Umbraco.Cms.Api.Common.Attributes; +using Umbraco.Cms.Api.Common.Configuration; + +namespace Umbraco.Extensions; + +public static class ActionDescriptorApiCommonExtensions +{ + + private static string? GetMapToApiAttributeValue(this ActionDescriptor actionDescriptor) + { + IEnumerable mapToApiAttributes = actionDescriptor?.EndpointMetadata?.OfType() ?? []; + + return mapToApiAttributes.SingleOrDefault()?.ApiName; + } + + public static bool HasMapToApiAttribute(this ActionDescriptor actionDescriptor, string apiName) + { + var value = actionDescriptor.GetMapToApiAttributeValue(); + + return value == apiName + || (value is null && apiName == DefaultApiConfiguration.ApiName); + } +} diff --git a/src/Umbraco.Cms.Api.Common/Extensions/MapToApiAttributeExtensions.cs b/src/Umbraco.Cms.Api.Common/Extensions/MethodInfoApiCommonExtensions.cs similarity index 52% rename from src/Umbraco.Cms.Api.Common/Extensions/MapToApiAttributeExtensions.cs rename to src/Umbraco.Cms.Api.Common/Extensions/MethodInfoApiCommonExtensions.cs index 6bf73a2e4ab9..771c9016bd40 100644 --- a/src/Umbraco.Cms.Api.Common/Extensions/MapToApiAttributeExtensions.cs +++ b/src/Umbraco.Cms.Api.Common/Extensions/MethodInfoApiCommonExtensions.cs @@ -1,12 +1,11 @@ using System.Reflection; using Asp.Versioning; -using Microsoft.AspNetCore.Mvc.Abstractions; using Umbraco.Cms.Api.Common.Attributes; using Umbraco.Cms.Api.Common.Configuration; namespace Umbraco.Extensions; -public static class MapToApiAttributeExtensions +public static class MethodInfoApiCommonExtensions { public static string? GetMapToApiVersionAttributeValue(this MethodInfo methodInfo) @@ -18,18 +17,11 @@ public static class MapToApiAttributeExtensions public static string? GetMapToApiAttributeValue(this MethodInfo methodInfo) { - MapToApiAttribute[] mapToApis = [.. (methodInfo.DeclaringType?.GetCustomAttributes(typeof(MapToApiAttribute), inherit: true) ?? []).Cast()]; + MapToApiAttribute[] mapToApis = (methodInfo.DeclaringType?.GetCustomAttributes(typeof(MapToApiAttribute), inherit: true) ?? Array.Empty()).Cast().ToArray(); return mapToApis.SingleOrDefault()?.ApiName; } - private static string? GetMapToApiAttributeValue(this ActionDescriptor actionDescriptor) - { - IEnumerable mapToApiAttributes = actionDescriptor?.EndpointMetadata?.OfType() ?? []; - - return mapToApiAttributes.SingleOrDefault()?.ApiName; - } - public static bool HasMapToApiAttribute(this MethodInfo methodInfo, string apiName) { var value = methodInfo.GetMapToApiAttributeValue(); @@ -37,12 +29,4 @@ public static bool HasMapToApiAttribute(this MethodInfo methodInfo, string apiNa return value == apiName || (value is null && apiName == DefaultApiConfiguration.ApiName); } - - public static bool HasMapToApiAttribute(this ActionDescriptor actionDescriptor, string apiName) - { - var value = actionDescriptor.GetMapToApiAttributeValue(); - - return value == apiName - || (value is null && apiName == DefaultApiConfiguration.ApiName); - } -} +} \ No newline at end of file From 0bdd1552a13aabefb3ba6097aa1c1a415ef43753 Mon Sep 17 00:00:00 2001 From: mdubbelv Date: Mon, 15 Sep 2025 23:08:11 +0200 Subject: [PATCH 3/7] revert MethodInfoApiCommonExtensions.cs --- .../Extensions/MethodInfoApiCommonExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Cms.Api.Common/Extensions/MethodInfoApiCommonExtensions.cs b/src/Umbraco.Cms.Api.Common/Extensions/MethodInfoApiCommonExtensions.cs index 771c9016bd40..9d619f5e1c86 100644 --- a/src/Umbraco.Cms.Api.Common/Extensions/MethodInfoApiCommonExtensions.cs +++ b/src/Umbraco.Cms.Api.Common/Extensions/MethodInfoApiCommonExtensions.cs @@ -12,7 +12,7 @@ public static class MethodInfoApiCommonExtensions { MapToApiVersionAttribute[] mapToApis = methodInfo.GetCustomAttributes(typeof(MapToApiVersionAttribute), inherit: true).Cast().ToArray(); - return string.Join("|", mapToApis.SelectMany(x => x.Versions)); + return string.Join("|", mapToApis.SelectMany(x=>x.Versions)); } public static string? GetMapToApiAttributeValue(this MethodInfo methodInfo) @@ -29,4 +29,4 @@ public static bool HasMapToApiAttribute(this MethodInfo methodInfo, string apiNa return value == apiName || (value is null && apiName == DefaultApiConfiguration.ApiName); } -} \ No newline at end of file +} From 2e8a489a3ab79e4079fb1cbcd94c088b949f7385 Mon Sep 17 00:00:00 2001 From: mdubbelv Date: Tue, 16 Sep 2025 09:04:28 +0200 Subject: [PATCH 4/7] remove empty line in ActionDescriptorApiCommonExtensions.cs --- .../Extensions/ActionDescriptorApiCommonExtensions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Cms.Api.Common/Extensions/ActionDescriptorApiCommonExtensions.cs b/src/Umbraco.Cms.Api.Common/Extensions/ActionDescriptorApiCommonExtensions.cs index 16b0c5c14a51..279e8585af1d 100644 --- a/src/Umbraco.Cms.Api.Common/Extensions/ActionDescriptorApiCommonExtensions.cs +++ b/src/Umbraco.Cms.Api.Common/Extensions/ActionDescriptorApiCommonExtensions.cs @@ -6,7 +6,6 @@ namespace Umbraco.Extensions; public static class ActionDescriptorApiCommonExtensions { - private static string? GetMapToApiAttributeValue(this ActionDescriptor actionDescriptor) { IEnumerable mapToApiAttributes = actionDescriptor?.EndpointMetadata?.OfType() ?? []; From 2d99e1aac8e3ff2275d58b2891b7ece12fe88152 Mon Sep 17 00:00:00 2001 From: Marcus Wilhelmson Date: Mon, 29 Sep 2025 08:21:33 +0200 Subject: [PATCH 5/7] Add xml comments to ActionDescriptorApiCommonExtensions --- .../ActionDescriptorApiCommonExtensions.cs | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Cms.Api.Common/Extensions/ActionDescriptorApiCommonExtensions.cs b/src/Umbraco.Cms.Api.Common/Extensions/ActionDescriptorApiCommonExtensions.cs index 16b0c5c14a51..94397a6bac78 100644 --- a/src/Umbraco.Cms.Api.Common/Extensions/ActionDescriptorApiCommonExtensions.cs +++ b/src/Umbraco.Cms.Api.Common/Extensions/ActionDescriptorApiCommonExtensions.cs @@ -4,9 +4,18 @@ namespace Umbraco.Extensions; +/// +/// Provides extension methods for to work with . +/// public static class ActionDescriptorApiCommonExtensions { - + /// + /// Retrieves the value from the 's endpoint metadata, if present. + /// + /// The action descriptor to inspect. + /// + /// The API name specified in the , or null if the attribute is not present. + /// private static string? GetMapToApiAttributeValue(this ActionDescriptor actionDescriptor) { IEnumerable mapToApiAttributes = actionDescriptor?.EndpointMetadata?.OfType() ?? []; @@ -14,6 +23,15 @@ public static class ActionDescriptorApiCommonExtensions return mapToApiAttributes.SingleOrDefault()?.ApiName; } + /// + /// Determines whether the has a with the specified API name. + /// + /// The action descriptor to inspect. + /// The API name to check for. + /// + /// true if the is present and matches the specified API name, + /// or if the attribute is not present and the API name matches the default API name; otherwise, false. + /// public static bool HasMapToApiAttribute(this ActionDescriptor actionDescriptor, string apiName) { var value = actionDescriptor.GetMapToApiAttributeValue(); From 8b92f2fcc9c7cdbbd4982bca6eb86b69d4884093 Mon Sep 17 00:00:00 2001 From: Marcus Wilhelmson Date: Mon, 29 Sep 2025 08:25:37 +0200 Subject: [PATCH 6/7] Revert boy scout refactoring to primary constructur --- .../ConfigureUmbracoSwaggerGenOptions.cs | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Cms.Api.Common/Configuration/ConfigureUmbracoSwaggerGenOptions.cs b/src/Umbraco.Cms.Api.Common/Configuration/ConfigureUmbracoSwaggerGenOptions.cs index b19421215e19..a4cabf51bb3b 100644 --- a/src/Umbraco.Cms.Api.Common/Configuration/ConfigureUmbracoSwaggerGenOptions.cs +++ b/src/Umbraco.Cms.Api.Common/Configuration/ConfigureUmbracoSwaggerGenOptions.cs @@ -11,11 +11,22 @@ namespace Umbraco.Cms.Api.Common.Configuration; -public class ConfigureUmbracoSwaggerGenOptions( - IOperationIdSelector operationIdSelector, - ISchemaIdSelector schemaIdSelector, - ISubTypesSelector subTypesSelector) : IConfigureOptions +public class ConfigureUmbracoSwaggerGenOptions : IConfigureOptions { + private readonly IOperationIdSelector _operationIdSelector; + private readonly ISchemaIdSelector _schemaIdSelector; + private readonly ISubTypesSelector _subTypesSelector; + + public ConfigureUmbracoSwaggerGenOptions( + IOperationIdSelector operationIdSelector, + ISchemaIdSelector schemaIdSelector, + ISubTypesSelector subTypesSelector) + { + _operationIdSelector = operationIdSelector; + _schemaIdSelector = schemaIdSelector; + _subTypesSelector = subTypesSelector; + } + public void Configure(SwaggerGenOptions swaggerGenOptions) { swaggerGenOptions.SwaggerDoc( @@ -27,7 +38,7 @@ public void Configure(SwaggerGenOptions swaggerGenOptions) Description = "All endpoints not defined under specific APIs", }); - swaggerGenOptions.CustomOperationIds(operationIdSelector.OperationId); + swaggerGenOptions.CustomOperationIds(description => _operationIdSelector.OperationId(description)); swaggerGenOptions.DocInclusionPredicate((name, api) => { if (api.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor @@ -40,11 +51,11 @@ public void Configure(SwaggerGenOptions swaggerGenOptions) return apiVersionMetadata.Name == name || (string.IsNullOrEmpty(apiVersionMetadata.Name) && name == DefaultApiConfiguration.ApiName); }); - swaggerGenOptions.TagActionsBy(api => [api.GroupName]); + swaggerGenOptions.TagActionsBy(api => new[] { api.GroupName }); swaggerGenOptions.OrderActionsBy(ActionOrderBy); swaggerGenOptions.SchemaFilter(); - swaggerGenOptions.CustomSchemaIds(schemaIdSelector.SchemaId); - swaggerGenOptions.SelectSubTypesUsing(subTypesSelector.SubTypes); + swaggerGenOptions.CustomSchemaIds(_schemaIdSelector.SchemaId); + swaggerGenOptions.SelectSubTypesUsing(_subTypesSelector.SubTypes); swaggerGenOptions.SupportNonNullableReferenceTypes(); } From 44e7ebd8b819107e147335494862a2aa4b2fbb6c Mon Sep 17 00:00:00 2001 From: Marcus Wilhelmson Date: Mon, 29 Sep 2025 08:33:51 +0200 Subject: [PATCH 7/7] Better xml comments in ActionDescriptorApiCommonExtensions --- .../ActionDescriptorApiCommonExtensions.cs | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Cms.Api.Common/Extensions/ActionDescriptorApiCommonExtensions.cs b/src/Umbraco.Cms.Api.Common/Extensions/ActionDescriptorApiCommonExtensions.cs index 94397a6bac78..070800d35fdc 100644 --- a/src/Umbraco.Cms.Api.Common/Extensions/ActionDescriptorApiCommonExtensions.cs +++ b/src/Umbraco.Cms.Api.Common/Extensions/ActionDescriptorApiCommonExtensions.cs @@ -9,22 +9,9 @@ namespace Umbraco.Extensions; /// public static class ActionDescriptorApiCommonExtensions { - /// - /// Retrieves the value from the 's endpoint metadata, if present. - /// - /// The action descriptor to inspect. - /// - /// The API name specified in the , or null if the attribute is not present. - /// - private static string? GetMapToApiAttributeValue(this ActionDescriptor actionDescriptor) - { - IEnumerable mapToApiAttributes = actionDescriptor?.EndpointMetadata?.OfType() ?? []; - - return mapToApiAttributes.SingleOrDefault()?.ApiName; - } - /// /// Determines whether the has a with the specified API name. + /// The check is made in runtime to support attributes added in runtime. /// /// The action descriptor to inspect. /// The API name to check for. @@ -39,4 +26,11 @@ public static bool HasMapToApiAttribute(this ActionDescriptor actionDescriptor, return value == apiName || (value is null && apiName == DefaultApiConfiguration.ApiName); } -} + + private static string? GetMapToApiAttributeValue(this ActionDescriptor actionDescriptor) + { + IEnumerable mapToApiAttributes = actionDescriptor?.EndpointMetadata?.OfType() ?? []; + + return mapToApiAttributes.SingleOrDefault()?.ApiName; + } +} \ No newline at end of file