Skip to content

Commit 8b77ff5

Browse files
authored
Support resolving OpenAPI server URLs from HttpRequest (#60617)
* Support resolving OpenAPI server URLs from HttpRequest * Try passing optional params everywhere
1 parent 719c33f commit 8b77ff5

File tree

4 files changed

+62
-7
lines changed

4 files changed

+62
-7
lines changed

src/OpenApi/src/Extensions/OpenApiEndpointRouteBuilderExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public static IEndpointConventionBuilder MapOpenApi(this IEndpointRouteBuilder e
5151
}
5252
else
5353
{
54-
var document = await documentService.GetOpenApiDocumentAsync(context.RequestServices, context.RequestAborted);
54+
var document = await documentService.GetOpenApiDocumentAsync(context.RequestServices, context.Request, context.RequestAborted);
5555
var documentOptions = options.Get(lowercasedDocumentName);
5656
using var output = MemoryBufferWriter.Get();
5757
using var writer = Utf8BufferTextWriter.Get(output);

src/OpenApi/src/Services/OpenApiDocumentService.cs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
using Microsoft.AspNetCore.Hosting.Server;
1515
using Microsoft.AspNetCore.Hosting.Server.Features;
1616
using Microsoft.AspNetCore.Http;
17+
using Microsoft.AspNetCore.Http.Extensions;
1718
using Microsoft.AspNetCore.Http.Metadata;
1819
using Microsoft.AspNetCore.Mvc;
1920
using Microsoft.AspNetCore.Mvc.ApiExplorer;
@@ -56,7 +57,7 @@ internal sealed class OpenApiDocumentService(
5657
internal bool TryGetCachedOperationTransformerContext(string descriptionId, [NotNullWhen(true)] out OpenApiOperationTransformerContext? context)
5758
=> _operationTransformerContextCache.TryGetValue(descriptionId, out context);
5859

59-
public async Task<OpenApiDocument> GetOpenApiDocumentAsync(IServiceProvider scopedServiceProvider, CancellationToken cancellationToken = default)
60+
public async Task<OpenApiDocument> GetOpenApiDocumentAsync(IServiceProvider scopedServiceProvider, HttpRequest? httpRequest = null, CancellationToken cancellationToken = default)
6061
{
6162
// Schema and operation transformers are scoped per-request and can be
6263
// pre-allocated to hold the same number of transformers as the associated
@@ -71,7 +72,7 @@ public async Task<OpenApiDocument> GetOpenApiDocumentAsync(IServiceProvider scop
7172
var document = new OpenApiDocument
7273
{
7374
Info = GetOpenApiInfo(),
74-
Servers = GetOpenApiServers()
75+
Servers = GetOpenApiServers(httpRequest)
7576
};
7677
document.Paths = await GetOpenApiPathsAsync(document, scopedServiceProvider, operationTransformers, schemaTransformers, cancellationToken);
7778
document.Tags = document.Tags?.Distinct(OpenApiTagComparer.Instance).ToList();
@@ -198,12 +199,26 @@ internal OpenApiInfo GetOpenApiInfo()
198199
};
199200
}
200201

201-
internal List<OpenApiServer> GetOpenApiServers()
202+
// Resolve server URL from the request to handle reverse proxies.
203+
// If there is active request object, assume a development environment and use the server addresses.
204+
internal List<OpenApiServer> GetOpenApiServers(HttpRequest? httpRequest = null)
205+
{
206+
if (httpRequest is not null)
207+
{
208+
var serverUrl = UriHelper.BuildAbsolute(httpRequest.Scheme, httpRequest.Host, httpRequest.PathBase);
209+
return [new OpenApiServer { Url = serverUrl }];
210+
}
211+
else
212+
{
213+
return GetDevelopmentOpenApiServers();
214+
}
215+
}
216+
private List<OpenApiServer> GetDevelopmentOpenApiServers()
202217
{
203218
if (hostEnvironment.IsDevelopment() &&
204219
server?.Features.Get<IServerAddressesFeature>()?.Addresses is { Count: > 0 } addresses)
205220
{
206-
return addresses.Select(address => new OpenApiServer { Url = address }).ToList();
221+
return [.. addresses.Select(address => new OpenApiServer { Url = address })];
207222
}
208223
return [];
209224
}

src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentService/OpenApiDocumentServiceTests.Servers.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using Microsoft.AspNetCore.Http;
45
using Microsoft.AspNetCore.Mvc.ApiExplorer;
56
using Microsoft.AspNetCore.OpenApi;
67
using Microsoft.Extensions.DependencyInjection;
@@ -10,6 +11,45 @@
1011

1112
public partial class OpenApiDocumentServiceTests
1213
{
14+
[Theory]
15+
[InlineData("Development", "localhost:5001", "", "http", "http://localhost:5001/")]
16+
[InlineData("Development", "example.com", "/api", "https", "https://example.com/api")]
17+
[InlineData("Staging", "localhost:5002", "/v1", "http", "http://localhost:5002/v1")]
18+
[InlineData("Staging", "api.example.com", "/base/path", "https", "https://api.example.com/base/path")]
19+
[InlineData("Development", "localhost", "/", "http", "http://localhost/")]
20+
public void GetOpenApiServers_FavorsHttpContextRequestOverServerAddress(string environment, string host, string pathBase, string scheme, string expectedUri)
21+
{
22+
// Arrange
23+
var hostEnvironment = new HostingEnvironment
24+
{
25+
ApplicationName = "TestApplication",
26+
EnvironmentName = environment
27+
};
28+
var docService = new OpenApiDocumentService(
29+
"v1",
30+
new Mock<IApiDescriptionGroupCollectionProvider>().Object,
31+
hostEnvironment,
32+
GetMockOptionsMonitor(),
33+
new Mock<IKeyedServiceProvider>().Object,
34+
new OpenApiTestServer(["http://localhost:5000"]));
35+
var httpContext = new DefaultHttpContext()
36+
{
37+
Request =
38+
{
39+
Host = new HostString(host),
40+
PathBase = pathBase,
41+
Scheme = scheme
42+
43+
}
44+
};
45+
46+
// Act
47+
var servers = docService.GetOpenApiServers(httpContext.Request);
48+
49+
// Assert
50+
Assert.Contains(expectedUri, servers.Select(s => s.Url));
51+
}
52+
1353
[Fact]
1454
public void GetOpenApiServers_HandlesServerAddressFeatureWithValues()
1555
{

src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentServiceTestsBase.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public static async Task VerifyOpenApiDocument(IEndpointRouteBuilder builder, Op
3434
{
3535
var documentService = CreateDocumentService(builder, openApiOptions);
3636
var scopedService = ((TestServiceProvider)builder.ServiceProvider).CreateScope();
37-
var document = await documentService.GetOpenApiDocumentAsync(scopedService.ServiceProvider, cancellationToken);
37+
var document = await documentService.GetOpenApiDocumentAsync(scopedService.ServiceProvider, null, cancellationToken);
3838
verifyOpenApiDocument(document);
3939
}
4040

@@ -43,7 +43,7 @@ public static async Task VerifyOpenApiDocument(ActionDescriptor action, Action<O
4343
var builder = CreateBuilder();
4444
var documentService = CreateDocumentService(builder, action);
4545
var scopedService = ((TestServiceProvider)builder.ServiceProvider).CreateScope();
46-
var document = await documentService.GetOpenApiDocumentAsync(scopedService.ServiceProvider, cancellationToken);
46+
var document = await documentService.GetOpenApiDocumentAsync(scopedService.ServiceProvider, null, cancellationToken);
4747
verifyOpenApiDocument(document);
4848
}
4949

0 commit comments

Comments
 (0)