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 { diff --git a/src/OpenApi/src/Extensions/OpenApiServiceCollectionExtensions.cs b/src/OpenApi/src/Extensions/OpenApiServiceCollectionExtensions.cs index a5baa31a3c3c..5e848bdc5851 100644 --- a/src/OpenApi/src/Extensions/OpenApiServiceCollectionExtensions.cs +++ b/src/OpenApi/src/Extensions/OpenApiServiceCollectionExtensions.cs @@ -57,10 +57,16 @@ 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(); + + services.AddOpenApiCore(lowercasedDocumentName); + services.Configure(lowercasedDocumentName, options => { - options.DocumentName = documentName; + options.DocumentName = lowercasedDocumentName; configureOptions(options); }); return services; 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..ab4549f2b5ed 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; @@ -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)] @@ -150,7 +174,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;