Skip to content

Commit

Permalink
Change OpenAPI document name resolution to be case-insensitive (#59199)
Browse files Browse the repository at this point in the history
* Fix analyser error

* Wrote unit test to verify when bug is fixed

* Register document names in a case-insensitive manner

* Change OpenAPI document name resolution to be case-insensitive

* Restore AddOpenApiCore to its own method

* Fix typo in extension method
  • Loading branch information
sander1095 authored Dec 16, 2024
1 parent bc2efb7 commit 09d7789
Show file tree
Hide file tree
Showing 3 changed files with 45 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,22 @@ public static IEndpointConventionBuilder MapOpenApi(this IEndpointRouteBuilder e
var options = endpoints.ServiceProvider.GetRequiredService<IOptionsMonitor<OpenApiOptions>>();
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<OpenApiDocumentService>(documentName);
var documentService = context.RequestServices.GetKeyedService<OpenApiDocumentService>(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
{
Expand Down
12 changes: 9 additions & 3 deletions src/OpenApi/src/Extensions/OpenApiServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,16 @@ public static IServiceCollection AddOpenApi(this IServiceCollection services, st
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(configureOptions);

services.AddOpenApiCore(documentName);
services.Configure<OpenApiOptions>(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<OpenApiOptions>(lowercasedDocumentName, options =>
{
options.DocumentName = documentName;
options.DocumentName = lowercasedDocumentName;
configureOptions(options);
});
return services;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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)]
Expand All @@ -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;
Expand Down

0 comments on commit 09d7789

Please sign in to comment.