From 0b390ed54c8ad0c12b46c4add29d83fd4f4f7a4a Mon Sep 17 00:00:00 2001 From: TuTiDore Date: Sun, 21 Dec 2025 21:50:49 -0800 Subject: [PATCH 1/4] fix: dotnet 10 openapi --- Common/OpenAPI/OpenApiExtensions.cs | 30 ++++++++++++++++++----------- Common/OpenShockMiddlewareHelper.cs | 6 +++--- Common/OpenShockServiceHelper.cs | 10 ++++++---- 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/Common/OpenAPI/OpenApiExtensions.cs b/Common/OpenAPI/OpenApiExtensions.cs index bed9be8b..d761e5b5 100644 --- a/Common/OpenAPI/OpenApiExtensions.cs +++ b/Common/OpenAPI/OpenApiExtensions.cs @@ -1,4 +1,5 @@ -using Microsoft.OpenApi; +using Asp.Versioning.ApiExplorer; +using Microsoft.OpenApi; namespace OpenShock.Common.OpenAPI; @@ -16,16 +17,23 @@ public static IServiceCollection AddOpenApiExt(this WebApplicationBuil { options.AddPolicy("OpenAPI", policy => policy.Expire(TimeSpan.FromMinutes(10))); }); - builder.Services.AddOpenApi(options => - { - options.OpenApiVersion = OpenApiSpecVersion.OpenApi3_1; - options.AddDocumentTransformer(DocumentDefaults.GetDocumentTransformer(version: "1")); - }); - builder.Services.AddOpenApi("v2", options => + + using (var tempProvider = builder.Services.BuildServiceProvider()) { - options.OpenApiVersion = OpenApiSpecVersion.OpenApi3_1; - options.AddDocumentTransformer(DocumentDefaults.GetDocumentTransformer(version: "2")); - }); + var apiVersionProvider = tempProvider.GetRequiredService(); + + // Configure OpenAPI for each API version + foreach (var description in apiVersionProvider.ApiVersionDescriptions) + { + builder.Services.AddOpenApi(description.GroupName, options => + { + options.OpenApiVersion = OpenApiSpecVersion.OpenApi3_1; + options.AddDocumentTransformer(DocumentDefaults.GetDocumentTransformer( + version: description.ApiVersion.ToString())); + }); + } + } + builder.Services.AddOpenApi("oauth", options => { options.OpenApiVersion = OpenApiSpecVersion.OpenApi3_1; @@ -38,7 +46,7 @@ public static IServiceCollection AddOpenApiExt(this WebApplicationBuil options.ShouldInclude = apiDescription => apiDescription.GroupName is "admin"; options.AddDocumentTransformer(DocumentDefaults.GetDocumentTransformer(version: "1")); }); - + return builder.Services; } } \ No newline at end of file diff --git a/Common/OpenShockMiddlewareHelper.cs b/Common/OpenShockMiddlewareHelper.cs index 85b6299e..2a9b0e23 100644 --- a/Common/OpenShockMiddlewareHelper.cs +++ b/Common/OpenShockMiddlewareHelper.cs @@ -84,9 +84,9 @@ public static async Task UseCommonOpenShockMiddleware(this app.MapScalarApiReference("/scalar/viewer", options => options - .WithOpenApiRoutePattern("/swagger/{documentName}/swagger.json") - .AddDocument("1", "Version 1") - .AddDocument("2", "Version 2") + .WithOpenApiRoutePattern("/openapi/{documentName}.json") + .AddDocument("v1", "Version 1") + .AddDocument("v2", "Version 2") ); app.MapControllers(); diff --git a/Common/OpenShockServiceHelper.cs b/Common/OpenShockServiceHelper.cs index 7e91e8b8..a3ef8f1f 100644 --- a/Common/OpenShockServiceHelper.cs +++ b/Common/OpenShockServiceHelper.cs @@ -143,11 +143,13 @@ public static IServiceCollection AddOpenShockServices(this IServiceCollection se { options.DefaultApiVersion = new ApiVersion(1, 0); options.AssumeDefaultVersionWhenUnspecified = true; - }); - - apiVersioningBuilder.AddApiExplorer(setup => + options.ReportApiVersions = true; + options.ApiVersionReader = new UrlSegmentApiVersionReader(); + }) + .AddMvc() // mvc required for ApiExplorer + .AddApiExplorer(setup => { - setup.GroupNameFormat = "VVV"; + setup.GroupNameFormat = "'v'V"; setup.SubstituteApiVersionInUrl = true; setup.DefaultApiVersion = new ApiVersion(1, 0); setup.AssumeDefaultVersionWhenUnspecified = true; From 1216e8407cd74a03702c4cdefa09eaa9afe07bcc Mon Sep 17 00:00:00 2001 From: TuTiDore Date: Mon, 22 Dec 2025 20:06:11 -0800 Subject: [PATCH 2/4] fix: use same Title --- Common/OpenAPI/DocumentDefaults.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Common/OpenAPI/DocumentDefaults.cs b/Common/OpenAPI/DocumentDefaults.cs index 54764ce6..ada54816 100644 --- a/Common/OpenAPI/DocumentDefaults.cs +++ b/Common/OpenAPI/DocumentDefaults.cs @@ -13,7 +13,7 @@ public static Func Date: Mon, 22 Dec 2025 20:06:39 -0800 Subject: [PATCH 3/4] fix: use better operationId transform --- Common/OpenAPI/OpenApiExtensions.cs | 35 +++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/Common/OpenAPI/OpenApiExtensions.cs b/Common/OpenAPI/OpenApiExtensions.cs index d761e5b5..7bcdabd2 100644 --- a/Common/OpenAPI/OpenApiExtensions.cs +++ b/Common/OpenAPI/OpenApiExtensions.cs @@ -30,6 +30,41 @@ public static IServiceCollection AddOpenApiExt(this WebApplicationBuil options.OpenApiVersion = OpenApiSpecVersion.OpenApi3_1; options.AddDocumentTransformer(DocumentDefaults.GetDocumentTransformer( version: description.ApiVersion.ToString())); + options.CreateSchemaReferenceId = (type) => + { + var defaultName = type.Type.Name; + var cleanName = defaultName + .Replace("[]", "Array") + .Replace("`1", "Of") + .Replace("`2", "OfTwo"); + return cleanName; + }; + options.AddOperationTransformer((operation, context, cancellationToken) => + { + var actionDescriptor = context.Description.ActionDescriptor; + + // Use endpoint name if available + var endpointName = actionDescriptor.EndpointMetadata + .OfType() + .FirstOrDefault()?.EndpointName; + + if (!string.IsNullOrEmpty(endpointName)) + { + operation.OperationId = endpointName; + return Task.CompletedTask; + } + + // For controllers + var controller = actionDescriptor.RouteValues.TryGetValue("controller", out var ctrl) ? ctrl : null; + var action = actionDescriptor.RouteValues.TryGetValue("action", out var act) ? act : null; + + if (!string.IsNullOrEmpty(controller) && !string.IsNullOrEmpty(action)) + { + operation.OperationId = $"{controller}{action}"; + } + + return Task.CompletedTask; + }); }); } } From 217122f45fbe9ad6de63a73590e1d914a90f43d5 Mon Sep 17 00:00:00 2001 From: TuTiDore Date: Mon, 22 Dec 2025 20:07:01 -0800 Subject: [PATCH 4/4] fix: break operationId "DevicePair" into separate endpoints --- API/Controller/Device/Pair.cs | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/API/Controller/Device/Pair.cs b/API/Controller/Device/Pair.cs index 3f3bf01b..d8d79fa1 100644 --- a/API/Controller/Device/Pair.cs +++ b/API/Controller/Device/Pair.cs @@ -22,11 +22,34 @@ public sealed partial class DeviceController [AllowAnonymous] [MapToApiVersion("1")] [HttpGet("pair/{pairCode}", Name = "Pair")] - [HttpGet("~/{version:apiVersion}/pair/{pairCode}", Name = "Pair_DEPRECATED")] // Backwards compatibility + [EndpointName("PairDeviceByCode")] [EnableRateLimiting("auth")] [ProducesResponseType>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] [ProducesResponseType(StatusCodes.Status404NotFound, MediaTypeNames.Application.ProblemJson)] // PairCodeNotFound public async Task Pair([FromRoute] string pairCode) + { + return await PairInternal(pairCode); + } + + /// + /// Pair a device with a pair code. + /// + /// The pair code to pair with. + /// Successfully assigned LCG node + /// No such pair code exists + [AllowAnonymous] + [MapToApiVersion("1")] + [HttpGet("~/{version:apiVersion}/pair/{pairCode}", Name = "PairDeviceByCode_DEPRECATED")] // Backwards compatibility + [EndpointName("PairDeviceByCode_DEPRECATED")] + [EnableRateLimiting("auth")] + [ProducesResponseType>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status404NotFound, MediaTypeNames.Application.ProblemJson)] // PairCodeNotFound + public async Task PairDeprecated([FromRoute] string pairCode) + { + return await PairInternal(pairCode); + } + + public async Task PairInternal([FromRoute] string pairCode) { var devicePairs = _redis.RedisCollection();