From 1288df8a5566564aa40b49390dbf563e7a7cee3b Mon Sep 17 00:00:00 2001 From: Sander ten Brinke Date: Wed, 27 Nov 2024 15:24:57 +0100 Subject: [PATCH 1/6] Fix analyser error --- .../OpenApiEndpointRouteBuilderExtensionsTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/OpenApiEndpointRouteBuilderExtensionsTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/OpenApiEndpointRouteBuilderExtensionsTests.cs index 5ef1079759e9..0d27ca89315f 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/OpenApiEndpointRouteBuilderExtensionsTests.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/OpenApiEndpointRouteBuilderExtensionsTests.cs @@ -59,7 +59,7 @@ public async Task MapOpenApi_ReturnsRenderedDocument() context.Response.Body = responseBodyStream; context.RequestServices = serviceProvider; context.Request.RouteValues.Add("documentName", "v1"); - var endpoint = builder.DataSources.First().Endpoints.First(); + var endpoint = builder.DataSources.First().Endpoints[0]; // Act var requestDelegate = endpoint.RequestDelegate; @@ -89,7 +89,7 @@ public async Task MapOpenApi_ReturnsDefaultDocumentIfNoNameProvided(string expec var responseBodyStream = new MemoryStream(); context.Response.Body = responseBodyStream; context.RequestServices = serviceProvider; - var endpoint = builder.DataSources.First().Endpoints.First(); + var endpoint = builder.DataSources.First().Endpoints[0]; // Act var requestDelegate = endpoint.RequestDelegate; @@ -121,7 +121,7 @@ public async Task MapOpenApi_Returns404ForUnresolvedDocument() context.Response.Body = responseBodyStream; context.RequestServices = serviceProvider; context.Request.RouteValues.Add("documentName", "v2"); - var endpoint = builder.DataSources.First().Endpoints.First(); + var endpoint = builder.DataSources.First().Endpoints[0]; // Act var requestDelegate = endpoint.RequestDelegate; @@ -150,7 +150,7 @@ public async Task MapOpenApi_ReturnsDocumentIfNameProvidedInQuery(string expecte context.Response.Body = responseBodyStream; context.RequestServices = serviceProvider; context.Request.QueryString = new QueryString($"?documentName={documentName}"); - var endpoint = builder.DataSources.First().Endpoints.First(); + var endpoint = builder.DataSources.First().Endpoints[0]; // Act var requestDelegate = endpoint.RequestDelegate; From e16335d74adfe6b9d26200de71fde4a43a7b85e2 Mon Sep 17 00:00:00 2001 From: Sander ten Brinke Date: Wed, 27 Nov 2024 15:26:21 +0100 Subject: [PATCH 2/6] Wrote unit test to verify when bug is fixed --- ...nApiEndpointRouteBuilderExtensionsTests.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/OpenApiEndpointRouteBuilderExtensionsTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/OpenApiEndpointRouteBuilderExtensionsTests.cs index 0d27ca89315f..ab4549f2b5ed 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/OpenApiEndpointRouteBuilderExtensionsTests.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/OpenApiEndpointRouteBuilderExtensionsTests.cs @@ -132,6 +132,30 @@ public async Task MapOpenApi_Returns404ForUnresolvedDocument() Assert.Equal("No OpenAPI document with the name 'v2' was found.", Encoding.UTF8.GetString(responseBodyStream.ToArray())); } + [Theory] + [InlineData("CaseSensitive", "casesensitive")] + [InlineData("casesensitive", "CaseSensitive")] + public async Task MapOpenApi_ReturnsDocumentWhenPathIsCaseSensitive(string registeredDocumentName, string requestedDocumentName) + { + // Arrange + var serviceProvider = CreateServiceProvider(registeredDocumentName); + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(serviceProvider)); + builder.MapOpenApi("/openapi/{documentName}.json"); + var context = new DefaultHttpContext(); + var responseBodyStream = new MemoryStream(); + context.Response.Body = responseBodyStream; + context.RequestServices = serviceProvider; + context.Request.RouteValues.Add("documentName", requestedDocumentName); + var endpoint = builder.DataSources.First().Endpoints[0]; + + // Act + var requestDelegate = endpoint.RequestDelegate; + await requestDelegate(context); + + // Assert + Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); + } + [Theory] [InlineData("/openapi.json", "application/json;charset=utf-8", false)] [InlineData("/openapi.yaml", "text/plain+yaml;charset=utf-8", true)] From 78ef1b065fbd5a5ae6c3f5cc15aa0d758ed19d79 Mon Sep 17 00:00:00 2001 From: Sander ten Brinke Date: Wed, 27 Nov 2024 15:44:12 +0100 Subject: [PATCH 3/6] Register document names in a case-insensitive manner --- .../OpenApiEndpointRouteBuilderExtensions.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/OpenApi/src/Extensions/OpenApiEndpointRouteBuilderExtensions.cs b/src/OpenApi/src/Extensions/OpenApiEndpointRouteBuilderExtensions.cs index def54f8c8d95..cb6cc5e6b774 100644 --- a/src/OpenApi/src/Extensions/OpenApiEndpointRouteBuilderExtensions.cs +++ b/src/OpenApi/src/Extensions/OpenApiEndpointRouteBuilderExtensions.cs @@ -30,16 +30,22 @@ public static IEndpointConventionBuilder MapOpenApi(this IEndpointRouteBuilder e var options = endpoints.ServiceProvider.GetRequiredService>(); return endpoints.MapGet(pattern, async (HttpContext context, string documentName = OpenApiConstants.DefaultDocumentName) => { + // We need to retrieve the document name in a case-insensitive manner + // to support case-insensitive document name resolution. + // Keyed Services are case-sensitive by default, which doesn't work well for document names in ASP.NET Core + // as routing in ASP.NET Core is case-insensitive by default. + var lowercasedDocumentName = documentName.ToLowerInvariant(); + // It would be ideal to use the `HttpResponseStreamWriter` to // asynchronously write to the response stream here but Microsoft.OpenApi // does not yet support async APIs on their writers. // See https://github.com/microsoft/OpenAPI.NET/issues/421 for more info. - var documentService = context.RequestServices.GetKeyedService(documentName); + var documentService = context.RequestServices.GetKeyedService(lowercasedDocumentName); if (documentService is null) { context.Response.StatusCode = StatusCodes.Status404NotFound; context.Response.ContentType = "text/plain;charset=utf-8"; - await context.Response.WriteAsync($"No OpenAPI document with the name '{documentName}' was found."); + await context.Response.WriteAsync($"No OpenAPI document with the name '{lowercasedDocumentName}' was found."); } else { From ac47b75db8d37b477a58078b14abdaa3ae5ec70a Mon Sep 17 00:00:00 2001 From: Sander ten Brinke Date: Wed, 27 Nov 2024 15:46:54 +0100 Subject: [PATCH 4/6] Change OpenAPI document name resolution to be case-insensitive --- .../OpenApiServiceCollectionExtensions.cs | 43 +++++++++++-------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/src/OpenApi/src/Extensions/OpenApiServiceCollectionExtensions.cs b/src/OpenApi/src/Extensions/OpenApiServiceCollectionExtensions.cs index a5baa31a3c3c..c47f82b180d1 100644 --- a/src/OpenApi/src/Extensions/OpenApiServiceCollectionExtensions.cs +++ b/src/OpenApi/src/Extensions/OpenApiServiceCollectionExtensions.cs @@ -57,13 +57,35 @@ public static IServiceCollection AddOpenApi(this IServiceCollection services, st ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(configureOptions); - services.AddOpenApiCore(documentName); - services.Configure(documentName, options => + // We need to store the document name in a case-insensitive manner + // to support case-insensitive document name resolution. + // Keyed Services are case-sensitive by default, which doesn't work well for document names in ASP.NET Core + // as routing in ASP.NET Core is case-insensitive by default. + var lowercasedDocumentName = documentName.ToLowerInvariant(); + + AddOpenApiCore(services, lowercasedDocumentName); + services.Configure(lowercasedDocumentName, options => { - options.DocumentName = documentName; + options.DocumentName = lowercasedDocumentName; configureOptions(options); }); return services; + + // The reason this method is a local function is to prevent case-sensitive document names being passed into this method (from other methods) in the future. + static IServiceCollection AddOpenApiCore(IServiceCollection services, string documentName) + { + services.AddEndpointsApiExplorer(); + services.AddKeyedSingleton(documentName); + services.AddKeyedSingleton(documentName); + services.AddKeyedSingleton(documentName); + // Required for build-time generation + services.AddSingleton(); + // Required to resolve document names for build-time generation + services.AddSingleton(new NamedService(documentName)); + // Required to support JSON serializations + services.TryAddEnumerable(ServiceDescriptor.Singleton, OpenApiSchemaJsonOptions>()); + return services; + } } /// @@ -99,19 +121,4 @@ public static IServiceCollection AddOpenApi(this IServiceCollection services, Ac /// public static IServiceCollection AddOpenApi(this IServiceCollection services) => services.AddOpenApi(OpenApiConstants.DefaultDocumentName); - - private static IServiceCollection AddOpenApiCore(this IServiceCollection services, string documentName) - { - services.AddEndpointsApiExplorer(); - services.AddKeyedSingleton(documentName); - services.AddKeyedSingleton(documentName); - services.AddKeyedSingleton(documentName); - // Required for build-time generation - services.AddSingleton(); - // Required to resolve document names for build-time generation - services.AddSingleton(new NamedService(documentName)); - // Required to support JSON serializations - services.TryAddEnumerable(ServiceDescriptor.Singleton, OpenApiSchemaJsonOptions>()); - return services; - } } From 6cb7e078f093660fc33f575ced7c46d6fe695119 Mon Sep 17 00:00:00 2001 From: Sander ten Brinke Date: Mon, 16 Dec 2024 20:56:35 +0000 Subject: [PATCH 5/6] Restore AddOpenApiCore to its own method --- .../OpenApiServiceCollectionExtensions.cs | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/src/OpenApi/src/Extensions/OpenApiServiceCollectionExtensions.cs b/src/OpenApi/src/Extensions/OpenApiServiceCollectionExtensions.cs index c47f82b180d1..864ed223a5d6 100644 --- a/src/OpenApi/src/Extensions/OpenApiServiceCollectionExtensions.cs +++ b/src/OpenApi/src/Extensions/OpenApiServiceCollectionExtensions.cs @@ -63,29 +63,13 @@ public static IServiceCollection AddOpenApi(this IServiceCollection services, st // as routing in ASP.NET Core is case-insensitive by default. var lowercasedDocumentName = documentName.ToLowerInvariant(); - AddOpenApiCore(services, lowercasedDocumentName); + services.AddOpenApiCore(services, lowercasedDocumentName); services.Configure(lowercasedDocumentName, options => { options.DocumentName = lowercasedDocumentName; configureOptions(options); }); return services; - - // The reason this method is a local function is to prevent case-sensitive document names being passed into this method (from other methods) in the future. - static IServiceCollection AddOpenApiCore(IServiceCollection services, string documentName) - { - services.AddEndpointsApiExplorer(); - services.AddKeyedSingleton(documentName); - services.AddKeyedSingleton(documentName); - services.AddKeyedSingleton(documentName); - // Required for build-time generation - services.AddSingleton(); - // Required to resolve document names for build-time generation - services.AddSingleton(new NamedService(documentName)); - // Required to support JSON serializations - services.TryAddEnumerable(ServiceDescriptor.Singleton, OpenApiSchemaJsonOptions>()); - return services; - } } /// @@ -121,4 +105,19 @@ public static IServiceCollection AddOpenApi(this IServiceCollection services, Ac /// public static IServiceCollection AddOpenApi(this IServiceCollection services) => services.AddOpenApi(OpenApiConstants.DefaultDocumentName); + + static IServiceCollection AddOpenApiCore(IServiceCollection services, string documentName) + { + services.AddEndpointsApiExplorer(); + services.AddKeyedSingleton(documentName); + services.AddKeyedSingleton(documentName); + services.AddKeyedSingleton(documentName); + // Required for build-time generation + services.AddSingleton(); + // Required to resolve document names for build-time generation + services.AddSingleton(new NamedService(documentName)); + // Required to support JSON serializations + services.TryAddEnumerable(ServiceDescriptor.Singleton, OpenApiSchemaJsonOptions>()); + return services; + } } From bc40d7ac8a4acbbe0e7722f508f5264b6504bfce Mon Sep 17 00:00:00 2001 From: Sander ten Brinke Date: Mon, 16 Dec 2024 21:01:56 +0000 Subject: [PATCH 6/6] Fix typo in extension method --- .../src/Extensions/OpenApiServiceCollectionExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OpenApi/src/Extensions/OpenApiServiceCollectionExtensions.cs b/src/OpenApi/src/Extensions/OpenApiServiceCollectionExtensions.cs index 864ed223a5d6..5e848bdc5851 100644 --- a/src/OpenApi/src/Extensions/OpenApiServiceCollectionExtensions.cs +++ b/src/OpenApi/src/Extensions/OpenApiServiceCollectionExtensions.cs @@ -63,7 +63,7 @@ public static IServiceCollection AddOpenApi(this IServiceCollection services, st // as routing in ASP.NET Core is case-insensitive by default. var lowercasedDocumentName = documentName.ToLowerInvariant(); - services.AddOpenApiCore(services, lowercasedDocumentName); + services.AddOpenApiCore(lowercasedDocumentName); services.Configure(lowercasedDocumentName, options => { options.DocumentName = lowercasedDocumentName; @@ -106,7 +106,7 @@ public static IServiceCollection AddOpenApi(this IServiceCollection services, Ac public static IServiceCollection AddOpenApi(this IServiceCollection services) => services.AddOpenApi(OpenApiConstants.DefaultDocumentName); - static IServiceCollection AddOpenApiCore(IServiceCollection services, string documentName) + private static IServiceCollection AddOpenApiCore(this IServiceCollection services, string documentName) { services.AddEndpointsApiExplorer(); services.AddKeyedSingleton(documentName);