From e527909d36dc7a74ec5fb65ea40ed228241ba302 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 3 Jun 2026 15:19:03 -0400 Subject: [PATCH 1/4] feat: adds support for additional operations emission in OpenAPI 3.2.0 --- .../Extensions/ApiDescriptionExtensions.cs | 15 +++++-- src/OpenApi/src/Services/OpenApiConstants.cs | 17 -------- .../src/Services/OpenApiDocumentService.cs | 13 +++--- .../ApiDescriptionExtensionsTests.cs | 23 ++++++++--- ...ment_documentName=controllers.verified.txt | 31 ++++++++++++++ ...ment_documentName=controllers.verified.txt | 31 ++++++++++++++ ...ment_documentName=controllers.verified.txt | 31 ++++++++++++++ ...ifyOpenApiDocumentIsInvariant.verified.txt | 31 ++++++++++++++ ...OpenApiDocumentServiceTests.QueryMethod.cs | 41 +++++++++++++++++++ 9 files changed, 200 insertions(+), 33 deletions(-) diff --git a/src/OpenApi/src/Extensions/ApiDescriptionExtensions.cs b/src/OpenApi/src/Extensions/ApiDescriptionExtensions.cs index 55e8d3ed765b..a312f3cbda22 100644 --- a/src/OpenApi/src/Extensions/ApiDescriptionExtensions.cs +++ b/src/OpenApi/src/Extensions/ApiDescriptionExtensions.cs @@ -17,10 +17,16 @@ internal static class ApiDescriptionExtensions /// /// The ApiDescription to resolve an HttpMethod from. /// The associated with the given , if known. - public static HttpMethod? GetHttpMethod(this ApiDescription apiDescription) => - apiDescription.HttpMethod?.ToUpperInvariant() switch + public static HttpMethod? GetHttpMethod(this ApiDescription apiDescription) + { + var httpMethod = apiDescription.HttpMethod?.ToUpperInvariant(); + if (string.IsNullOrWhiteSpace(httpMethod)) + { + return null; + } + + return httpMethod switch { - // Only add methods documented in the OpenAPI spec: https://spec.openapis.org/oas/v3.2.0.html#path-item-object "GET" => HttpMethod.Get, "POST" => HttpMethod.Post, "PUT" => HttpMethod.Put, @@ -30,8 +36,9 @@ internal static class ApiDescriptionExtensions "OPTIONS" => HttpMethod.Options, "TRACE" => HttpMethod.Trace, "QUERY" => HttpMethod.Query, - _ => null, + _ => new HttpMethod(httpMethod), }; + } /// /// Maps the relative path included in the ApiDescription to the path diff --git a/src/OpenApi/src/Services/OpenApiConstants.cs b/src/OpenApi/src/Services/OpenApiConstants.cs index 47e1d67e53bc..6756b0ec5335 100644 --- a/src/OpenApi/src/Services/OpenApiConstants.cs +++ b/src/OpenApi/src/Services/OpenApiConstants.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Net.Http; - namespace Microsoft.AspNetCore.OpenApi; internal static class OpenApiConstants @@ -19,21 +17,6 @@ internal static class OpenApiConstants internal const string RefPrefix = "#"; internal const string NullableProperty = "x-is-nullable-property"; internal const string DefaultOpenApiResponseKey = "default"; - // Since there's a finite set of HTTP methods that can be included in a given - // OpenApiPaths, we can pre-allocate an array of these methods and use a direct - // lookup on the OpenApiPaths dictionary to avoid allocating an enumerator - // over the KeyValuePairs in OpenApiPaths. - internal static readonly HttpMethod[] HttpMethods = [ - HttpMethod.Get, - HttpMethod.Post, - HttpMethod.Put, - HttpMethod.Delete, - HttpMethod.Options, - HttpMethod.Head, - HttpMethod.Patch, - HttpMethod.Trace, - HttpMethod.Query - ]; // Represents primitive types that should never be represented as // a schema reference and always inlined. internal static readonly List PrimitiveTypes = diff --git a/src/OpenApi/src/Services/OpenApiDocumentService.cs b/src/OpenApi/src/Services/OpenApiDocumentService.cs index c45b76e82b67..720854705419 100644 --- a/src/OpenApi/src/Services/OpenApiDocumentService.cs +++ b/src/OpenApi/src/Services/OpenApiDocumentService.cs @@ -163,14 +163,13 @@ internal async Task ForEachOperationAsync( { foreach (var pathItem in document.Paths.Values) { - for (var i = 0; i < OpenApiConstants.HttpMethods.Length; i++) + if (pathItem.Operations is null) { - var httpMethod = OpenApiConstants.HttpMethods[i]; - if (pathItem.Operations is null || !pathItem.Operations.TryGetValue(httpMethod, out var operation)) - { - continue; - } + continue; + } + foreach (var operation in pathItem.Operations.Values) + { if (operation.Metadata is { } annotations && annotations.TryGetValue(OpenApiConstants.DescriptionId, out var descriptionId) && descriptionId is string descriptionIdString && @@ -294,7 +293,7 @@ private async Task> GetOperationsAsync( if (description.GetHttpMethod() is not { } method) { - // Skip unsupported HTTP methods + // Skip descriptions with no HTTP method. continue; } diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/ApiDescriptionExtensionsTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/ApiDescriptionExtensionsTests.cs index 91952fa9c278..a1ffd82d3247 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/ApiDescriptionExtensionsTests.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/ApiDescriptionExtensionsTests.cs @@ -91,18 +91,31 @@ public void GetHttpMethod_ReturnsHttpMethodForApiDescription(string httpMethod, } [Fact] - public void GetHttpMethod_ReturnsNullForUnsupportedMethod() + public void GetHttpMethod_ReturnsCustomHttpMethodForUnsupportedMethod() { - // Arrange - Test that unsupported HTTP methods return null var apiDescription = new ApiDescription { - HttpMethod = "UNSUPPORTED" + HttpMethod = "foo" + }; + + var result = apiDescription.GetHttpMethod(); + + Assert.Equal(new HttpMethod("FOO"), result); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void GetHttpMethod_ReturnsNullWhenApiDescriptionHasNoHttpMethod(string httpMethod) + { + var apiDescription = new ApiDescription + { + HttpMethod = httpMethod }; - // Act var result = apiDescription.GetHttpMethod(); - // Assert Assert.Null(result); } } diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_0/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=controllers.verified.txt b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_0/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=controllers.verified.txt index 767a26b50d05..6e101884883f 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_0/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=controllers.verified.txt +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_0/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=controllers.verified.txt @@ -156,6 +156,37 @@ } } }, + "/unsupported": { + "x-oai-additionalOperations": { + "FOO": { + "tags": [ + "Test" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/CurrentWeather" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CurrentWeather" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CurrentWeather" + } + } + } + } + } + } + } + }, "/query-with-body": { "x-oai-additionalOperations": { "QUERY": { diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_1/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=controllers.verified.txt b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_1/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=controllers.verified.txt index b2d056690a11..c91d1790090c 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_1/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=controllers.verified.txt +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_1/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=controllers.verified.txt @@ -156,6 +156,37 @@ } } }, + "/unsupported": { + "x-oai-additionalOperations": { + "FOO": { + "tags": [ + "Test" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/CurrentWeather" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CurrentWeather" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CurrentWeather" + } + } + } + } + } + } + } + }, "/query-with-body": { "x-oai-additionalOperations": { "QUERY": { diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_2/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=controllers.verified.txt b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_2/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=controllers.verified.txt index 569657ee3707..cbd1509f948a 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_2/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=controllers.verified.txt +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_2/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=controllers.verified.txt @@ -154,6 +154,37 @@ } } }, + "/unsupported": { + "additionalOperations": { + "FOO": { + "tags": [ + "Test" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/CurrentWeather" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CurrentWeather" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CurrentWeather" + } + } + } + } + } + } + } + }, "/query-with-body": { "query": { "tags": [ diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentLocalizationTests.VerifyOpenApiDocumentIsInvariant.verified.txt b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentLocalizationTests.VerifyOpenApiDocumentIsInvariant.verified.txt index a4b193822a4e..6b19265f6e69 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentLocalizationTests.VerifyOpenApiDocumentIsInvariant.verified.txt +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentLocalizationTests.VerifyOpenApiDocumentIsInvariant.verified.txt @@ -1654,6 +1654,37 @@ } } }, + "/unsupported": { + "x-oai-additionalOperations": { + "FOO": { + "tags": [ + "Test" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/CurrentWeather" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CurrentWeather" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CurrentWeather" + } + } + } + } + } + } + } + }, "/query-with-body": { "x-oai-additionalOperations": { "QUERY": { diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentService/OpenApiDocumentServiceTests.QueryMethod.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentService/OpenApiDocumentServiceTests.QueryMethod.cs index 4ee5327fe3d0..48f893e808bf 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentService/OpenApiDocumentServiceTests.QueryMethod.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentService/OpenApiDocumentServiceTests.QueryMethod.cs @@ -5,6 +5,8 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.DependencyInjection; public partial class OpenApiDocumentServiceTests : OpenApiDocumentServiceTestBase { @@ -71,7 +73,46 @@ await VerifyOpenApiDocument(builder, document => }); } + [Fact] + public async Task CustomMethod_AppearsInDocumentForMvcAction() + { + var action = CreateActionDescriptor(nameof(ActionWithCustomMethod)); + + await VerifyOpenApiDocument(action, document => + { + var path = document.Paths["/api/custom"]; + Assert.True(path.Operations.ContainsKey(new HttpMethod("FOO"))); + Assert.DoesNotContain(HttpMethod.Post, path.Operations.Keys); + }); + } + + [Fact] + public async Task CustomMethod_IsVisitedByForEachOperationAsync() + { + var builder = CreateBuilder(); + var action = CreateActionDescriptor(nameof(ActionWithCustomMethod)); + var documentService = CreateDocumentService(builder, action); + var scopedService = builder.ServiceProvider.CreateScope(); + var document = await documentService.GetOpenApiDocumentAsync(scopedService.ServiceProvider); + + await documentService.ForEachOperationAsync(document, (operation, context, cancellationToken) => + { + operation.Description = context.Description.HttpMethod; + return Task.CompletedTask; + }, CancellationToken.None); + + var operation = document.Paths["/api/custom"].Operations[new HttpMethod("FOO")]; + Assert.Equal("FOO", operation.Description); + } + #nullable enable private record TodoItem(int Id, string Title, bool Completed); #nullable restore + + [Route("/api/custom")] + [HttpFoo] + private ActionResult ActionWithCustomMethod() + => new OkObjectResult(new TodoItem(100, "Title", true)); + + private sealed class HttpFooAttribute() : HttpMethodAttribute(["FOO"]); } From 42543d25b8a49bce8422beaaa41a8bfd836dbd1d Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 3 Jun 2026 15:24:42 -0400 Subject: [PATCH 2/4] fix: do not capitalize additional operations --- .../Extensions/ApiDescriptionExtensions.cs | 4 +-- .../ApiDescriptionExtensionsTests.cs | 31 ++++++++++++------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/OpenApi/src/Extensions/ApiDescriptionExtensions.cs b/src/OpenApi/src/Extensions/ApiDescriptionExtensions.cs index a312f3cbda22..7d95a9a4874d 100644 --- a/src/OpenApi/src/Extensions/ApiDescriptionExtensions.cs +++ b/src/OpenApi/src/Extensions/ApiDescriptionExtensions.cs @@ -19,13 +19,13 @@ internal static class ApiDescriptionExtensions /// The associated with the given , if known. public static HttpMethod? GetHttpMethod(this ApiDescription apiDescription) { - var httpMethod = apiDescription.HttpMethod?.ToUpperInvariant(); + var httpMethod = apiDescription.HttpMethod; if (string.IsNullOrWhiteSpace(httpMethod)) { return null; } - return httpMethod switch + return httpMethod.ToUpperInvariant() switch { "GET" => HttpMethod.Get, "POST" => HttpMethod.Post, diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/ApiDescriptionExtensionsTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/ApiDescriptionExtensionsTests.cs index a1ffd82d3247..d578c93a612c 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/ApiDescriptionExtensionsTests.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/ApiDescriptionExtensionsTests.cs @@ -58,7 +58,7 @@ public void MapRelativePathToItemPath_WithRoutePattern_HandlesRoutesThatStartWit public static class HttpMethodTestData { - public static IEnumerable TestCases => new List + public static IEnumerable KnownMethods => new List { new object[] { "GET", HttpMethod.Get }, new object[] { "POST", HttpMethod.Post }, @@ -69,38 +69,47 @@ public static class HttpMethodTestData new object[] { "OPTIONS", HttpMethod.Options }, new object[] { "TRACE", HttpMethod.Trace }, new object[] { "QUERY", HttpMethod.Query }, - new object[] { "gEt", HttpMethod.Get }, // Test case-insensitivity + new object[] { "gEt", HttpMethod.Get }, + new object[] { "pOsT", HttpMethod.Post }, + new object[] { "QuErY", HttpMethod.Query }, + }; + + public static IEnumerable UnsupportedMethods => new List + { + new object[] { "foo" }, + new object[] { "Foo" }, + new object[] { "FOO" }, + new object[] { "customMethod" }, }; } [Theory] - [MemberData(nameof(HttpMethodTestData.TestCases), MemberType = typeof(HttpMethodTestData))] - public void GetHttpMethod_ReturnsHttpMethodForApiDescription(string httpMethod, HttpMethod expectedHttpMethod) + [MemberData(nameof(HttpMethodTestData.KnownMethods), MemberType = typeof(HttpMethodTestData))] + public void GetHttpMethod_ReturnsKnownHttpMethodForApiDescription(string httpMethod, HttpMethod expectedHttpMethod) { - // Arrange var apiDescription = new ApiDescription { HttpMethod = httpMethod }; - // Act var result = apiDescription.GetHttpMethod(); - // Assert Assert.Equal(expectedHttpMethod, result); + Assert.Equal(expectedHttpMethod.Method, result?.Method); } - [Fact] - public void GetHttpMethod_ReturnsCustomHttpMethodForUnsupportedMethod() + [Theory] + [MemberData(nameof(HttpMethodTestData.UnsupportedMethods), MemberType = typeof(HttpMethodTestData))] + public void GetHttpMethod_PreservesOriginalCasingForUnsupportedMethod(string httpMethod) { var apiDescription = new ApiDescription { - HttpMethod = "foo" + HttpMethod = httpMethod }; var result = apiDescription.GetHttpMethod(); - Assert.Equal(new HttpMethod("FOO"), result); + Assert.Equal(httpMethod, result?.Method); } [Theory] From 83d6e58ab341ec3e46dc5cb375299206c5cfd758 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 3 Jun 2026 15:29:38 -0400 Subject: [PATCH 3/4] chore: addresses initial review comments --- .../Extensions/ApiDescriptionExtensions.cs | 23 ++++++++++++++++--- .../ApiDescriptionExtensionsTests.cs | 22 ++++++++++++++++++ ...OpenApiDocumentServiceTests.QueryMethod.cs | 2 +- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/OpenApi/src/Extensions/ApiDescriptionExtensions.cs b/src/OpenApi/src/Extensions/ApiDescriptionExtensions.cs index 7d95a9a4874d..f8d6a1e0dcdb 100644 --- a/src/OpenApi/src/Extensions/ApiDescriptionExtensions.cs +++ b/src/OpenApi/src/Extensions/ApiDescriptionExtensions.cs @@ -16,7 +16,10 @@ internal static class ApiDescriptionExtensions /// Maps the HTTP method of the ApiDescription to the HttpMethod. /// /// The ApiDescription to resolve an HttpMethod from. - /// The associated with the given , if known. + /// + /// The associated with the given , including custom methods + /// when the provided HTTP method token is valid. Returns when the HTTP method is missing or invalid. + /// public static HttpMethod? GetHttpMethod(this ApiDescription apiDescription) { var httpMethod = apiDescription.HttpMethod; @@ -25,7 +28,9 @@ internal static class ApiDescriptionExtensions return null; } - return httpMethod.ToUpperInvariant() switch + var normalizedHttpMethod = httpMethod.Trim(); + + return normalizedHttpMethod.ToUpperInvariant() switch { "GET" => HttpMethod.Get, "POST" => HttpMethod.Post, @@ -36,8 +41,20 @@ internal static class ApiDescriptionExtensions "OPTIONS" => HttpMethod.Options, "TRACE" => HttpMethod.Trace, "QUERY" => HttpMethod.Query, - _ => new HttpMethod(httpMethod), + _ => TryCreateHttpMethod(httpMethod), }; + + static HttpMethod? TryCreateHttpMethod(string httpMethod) + { + try + { + return new HttpMethod(httpMethod); + } + catch (FormatException) + { + return null; + } + } } /// diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/ApiDescriptionExtensionsTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/ApiDescriptionExtensionsTests.cs index d578c93a612c..0891becdf315 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/ApiDescriptionExtensionsTests.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/ApiDescriptionExtensionsTests.cs @@ -72,6 +72,7 @@ public static class HttpMethodTestData new object[] { "gEt", HttpMethod.Get }, new object[] { "pOsT", HttpMethod.Post }, new object[] { "QuErY", HttpMethod.Query }, + new object[] { " GET ", HttpMethod.Get }, }; public static IEnumerable UnsupportedMethods => new List @@ -81,6 +82,13 @@ public static class HttpMethodTestData new object[] { "FOO" }, new object[] { "customMethod" }, }; + + public static IEnumerable InvalidMethods => new List + { + new object[] { "FOO " }, + new object[] { " FOO" }, + new object[] { "FOO BAR" }, + }; } [Theory] @@ -112,6 +120,20 @@ public void GetHttpMethod_PreservesOriginalCasingForUnsupportedMethod(string htt Assert.Equal(httpMethod, result?.Method); } + [Theory] + [MemberData(nameof(HttpMethodTestData.InvalidMethods), MemberType = typeof(HttpMethodTestData))] + public void GetHttpMethod_ReturnsNullForInvalidHttpMethodToken(string httpMethod) + { + var apiDescription = new ApiDescription + { + HttpMethod = httpMethod + }; + + var result = apiDescription.GetHttpMethod(); + + Assert.Null(result); + } + [Theory] [InlineData(null)] [InlineData("")] diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentService/OpenApiDocumentServiceTests.QueryMethod.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentService/OpenApiDocumentServiceTests.QueryMethod.cs index 48f893e808bf..bfa583b535cb 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentService/OpenApiDocumentServiceTests.QueryMethod.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentService/OpenApiDocumentServiceTests.QueryMethod.cs @@ -92,7 +92,7 @@ public async Task CustomMethod_IsVisitedByForEachOperationAsync() var builder = CreateBuilder(); var action = CreateActionDescriptor(nameof(ActionWithCustomMethod)); var documentService = CreateDocumentService(builder, action); - var scopedService = builder.ServiceProvider.CreateScope(); + using var scopedService = builder.ServiceProvider.CreateScope(); var document = await documentService.GetOpenApiDocumentAsync(scopedService.ServiceProvider); await documentService.ForEachOperationAsync(document, (operation, context, cancellationToken) => From b3f78bc22d476d7366e730176e32bf11e34ab6a6 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 3 Jun 2026 15:34:17 -0400 Subject: [PATCH 4/4] chore: addresses another set of comments --- .../src/Extensions/ApiDescriptionExtensions.cs | 2 +- .../src/Services/OpenApiDocumentService.cs | 2 +- .../Extensions/ApiDescriptionExtensionsTests.cs | 16 ++++++++-------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/OpenApi/src/Extensions/ApiDescriptionExtensions.cs b/src/OpenApi/src/Extensions/ApiDescriptionExtensions.cs index f8d6a1e0dcdb..6758d349fbaf 100644 --- a/src/OpenApi/src/Extensions/ApiDescriptionExtensions.cs +++ b/src/OpenApi/src/Extensions/ApiDescriptionExtensions.cs @@ -41,7 +41,7 @@ internal static class ApiDescriptionExtensions "OPTIONS" => HttpMethod.Options, "TRACE" => HttpMethod.Trace, "QUERY" => HttpMethod.Query, - _ => TryCreateHttpMethod(httpMethod), + _ => TryCreateHttpMethod(normalizedHttpMethod), }; static HttpMethod? TryCreateHttpMethod(string httpMethod) diff --git a/src/OpenApi/src/Services/OpenApiDocumentService.cs b/src/OpenApi/src/Services/OpenApiDocumentService.cs index 720854705419..2ac9af88a658 100644 --- a/src/OpenApi/src/Services/OpenApiDocumentService.cs +++ b/src/OpenApi/src/Services/OpenApiDocumentService.cs @@ -293,7 +293,7 @@ private async Task> GetOperationsAsync( if (description.GetHttpMethod() is not { } method) { - // Skip descriptions with no HTTP method. + // Skip descriptions with a missing or invalid HTTP method. continue; } diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/ApiDescriptionExtensionsTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/ApiDescriptionExtensionsTests.cs index 0891becdf315..93a16e579da6 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/ApiDescriptionExtensionsTests.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/ApiDescriptionExtensionsTests.cs @@ -77,16 +77,16 @@ public static class HttpMethodTestData public static IEnumerable UnsupportedMethods => new List { - new object[] { "foo" }, - new object[] { "Foo" }, - new object[] { "FOO" }, - new object[] { "customMethod" }, + new object[] { "foo", "foo" }, + new object[] { "Foo", "Foo" }, + new object[] { "FOO", "FOO" }, + new object[] { "customMethod", "customMethod" }, + new object[] { " FOO ", "FOO" }, + new object[] { " FooBar ", "FooBar" }, }; public static IEnumerable InvalidMethods => new List { - new object[] { "FOO " }, - new object[] { " FOO" }, new object[] { "FOO BAR" }, }; } @@ -108,7 +108,7 @@ public void GetHttpMethod_ReturnsKnownHttpMethodForApiDescription(string httpMet [Theory] [MemberData(nameof(HttpMethodTestData.UnsupportedMethods), MemberType = typeof(HttpMethodTestData))] - public void GetHttpMethod_PreservesOriginalCasingForUnsupportedMethod(string httpMethod) + public void GetHttpMethod_PreservesUnsupportedMethodCasingAfterTrimming(string httpMethod, string expectedHttpMethod) { var apiDescription = new ApiDescription { @@ -117,7 +117,7 @@ public void GetHttpMethod_PreservesOriginalCasingForUnsupportedMethod(string htt var result = apiDescription.GetHttpMethod(); - Assert.Equal(httpMethod, result?.Method); + Assert.Equal(expectedHttpMethod, result?.Method); } [Theory]