Skip to content

Commit 2f2b731

Browse files
committed
Support resolving OpenAPI server URLs from HttpRequest (#60617)
* Support resolving OpenAPI server URLs from HttpRequest * Try passing optional params everywhere
1 parent eeb8b14 commit 2f2b731

File tree

4 files changed

+63
-8
lines changed

4 files changed

+63
-8
lines changed

src/OpenApi/src/Extensions/OpenApiEndpointRouteBuilderExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public static IEndpointConventionBuilder MapOpenApi(this IEndpointRouteBuilder e
4343
}
4444
else
4545
{
46-
var document = await documentService.GetOpenApiDocumentAsync(context.RequestServices, context.RequestAborted);
46+
var document = await documentService.GetOpenApiDocumentAsync(context.RequestServices, context.Request, context.RequestAborted);
4747
var documentOptions = options.Get(documentName);
4848
using var output = MemoryBufferWriter.Get();
4949
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;
@@ -55,7 +56,7 @@ internal sealed class OpenApiDocumentService(
5556
internal bool TryGetCachedOperationTransformerContext(string descriptionId, [NotNullWhen(true)] out OpenApiOperationTransformerContext? context)
5657
=> _operationTransformerContextCache.TryGetValue(descriptionId, out context);
5758

58-
public async Task<OpenApiDocument> GetOpenApiDocumentAsync(IServiceProvider scopedServiceProvider, CancellationToken cancellationToken = default)
59+
public async Task<OpenApiDocument> GetOpenApiDocumentAsync(IServiceProvider scopedServiceProvider, HttpRequest? httpRequest = null, CancellationToken cancellationToken = default)
5960
{
6061
// For good hygiene, operation-level tags must also appear in the document-level
6162
// tags collection. This set captures all tags that have been seen so far.
@@ -74,7 +75,7 @@ public async Task<OpenApiDocument> GetOpenApiDocumentAsync(IServiceProvider scop
7475
{
7576
Info = GetOpenApiInfo(),
7677
Paths = await GetOpenApiPathsAsync(capturedTags, scopedServiceProvider, operationTransformers, schemaTransformers, cancellationToken),
77-
Servers = GetOpenApiServers(),
78+
Servers = GetOpenApiServers(httpRequest),
7879
Tags = [.. capturedTags]
7980
};
8081
try
@@ -192,12 +193,26 @@ internal OpenApiInfo GetOpenApiInfo()
192193
};
193194
}
194195

195-
internal List<OpenApiServer> GetOpenApiServers()
196+
// Resolve server URL from the request to handle reverse proxies.
197+
// If there is active request object, assume a development environment and use the server addresses.
198+
internal List<OpenApiServer> GetOpenApiServers(HttpRequest? httpRequest = null)
199+
{
200+
if (httpRequest is not null)
201+
{
202+
var serverUrl = UriHelper.BuildAbsolute(httpRequest.Scheme, httpRequest.Host, httpRequest.PathBase);
203+
return [new OpenApiServer { Url = serverUrl }];
204+
}
205+
else
206+
{
207+
return GetDevelopmentOpenApiServers();
208+
}
209+
}
210+
private List<OpenApiServer> GetDevelopmentOpenApiServers()
196211
{
197212
if (hostEnvironment.IsDevelopment() &&
198213
server?.Features.Get<IServerAddressesFeature>()?.Addresses is { Count: > 0 } addresses)
199214
{
200-
return addresses.Select(address => new OpenApiServer { Url = address }).ToList();
215+
return [.. addresses.Select(address => new OpenApiServer { Url = address })];
201216
}
202217
return [];
203218
}

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: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,16 @@ public static async Task VerifyOpenApiDocument(IEndpointRouteBuilder builder, Op
3535
{
3636
var documentService = CreateDocumentService(builder, openApiOptions);
3737
var scopedService = ((TestServiceProvider)builder.ServiceProvider).CreateScope();
38-
var document = await documentService.GetOpenApiDocumentAsync(scopedService.ServiceProvider, cancellationToken);
38+
var document = await documentService.GetOpenApiDocumentAsync(scopedService.ServiceProvider, null, cancellationToken);
3939
verifyOpenApiDocument(document);
4040
}
4141

42-
public static async Task VerifyOpenApiDocument(ActionDescriptor action, Action<OpenApiDocument> verifyOpenApiDocument)
42+
public static async Task VerifyOpenApiDocument(ActionDescriptor action, Action<OpenApiDocument> verifyOpenApiDocument, CancellationToken cancellationToken = default)
4343
{
4444
var builder = CreateBuilder();
4545
var documentService = CreateDocumentService(builder, action);
4646
var scopedService = ((TestServiceProvider)builder.ServiceProvider).CreateScope();
47-
var document = await documentService.GetOpenApiDocumentAsync(scopedService.ServiceProvider);
47+
var document = await documentService.GetOpenApiDocumentAsync(scopedService.ServiceProvider, null);
4848
verifyOpenApiDocument(document);
4949
}
5050

0 commit comments

Comments
 (0)