Forráskód Böngészése

Support resolving OpenAPI server URLs from HttpRequest (#60617)

* Support resolving OpenAPI server URLs from HttpRequest

* Try passing optional params everywhere
Safia Abdalla 1 éve
szülő
commit
8b77ff5251

+ 1 - 1
src/OpenApi/src/Extensions/OpenApiEndpointRouteBuilderExtensions.cs

@@ -51,7 +51,7 @@ public static class OpenApiEndpointRouteBuilderExtensions
                 }
                 else
                 {
-                    var document = await documentService.GetOpenApiDocumentAsync(context.RequestServices, context.RequestAborted);
+                    var document = await documentService.GetOpenApiDocumentAsync(context.RequestServices, context.Request, context.RequestAborted);
                     var documentOptions = options.Get(lowercasedDocumentName);
                     using var output = MemoryBufferWriter.Get();
                     using var writer = Utf8BufferTextWriter.Get(output);

+ 19 - 4
src/OpenApi/src/Services/OpenApiDocumentService.cs

@@ -14,6 +14,7 @@ using System.Reflection;
 using Microsoft.AspNetCore.Hosting.Server;
 using Microsoft.AspNetCore.Hosting.Server.Features;
 using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Extensions;
 using Microsoft.AspNetCore.Http.Metadata;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc.ApiExplorer;
@@ -56,7 +57,7 @@ internal sealed class OpenApiDocumentService(
     internal bool TryGetCachedOperationTransformerContext(string descriptionId, [NotNullWhen(true)] out OpenApiOperationTransformerContext? context)
         => _operationTransformerContextCache.TryGetValue(descriptionId, out context);
 
-    public async Task<OpenApiDocument> GetOpenApiDocumentAsync(IServiceProvider scopedServiceProvider, CancellationToken cancellationToken = default)
+    public async Task<OpenApiDocument> GetOpenApiDocumentAsync(IServiceProvider scopedServiceProvider, HttpRequest? httpRequest = null, CancellationToken cancellationToken = default)
     {
         // Schema and operation transformers are scoped per-request and can be
         // pre-allocated to hold the same number of transformers as the associated
@@ -71,7 +72,7 @@ internal sealed class OpenApiDocumentService(
         var document = new OpenApiDocument
         {
             Info = GetOpenApiInfo(),
-            Servers = GetOpenApiServers()
+            Servers = GetOpenApiServers(httpRequest)
         };
         document.Paths = await GetOpenApiPathsAsync(document, scopedServiceProvider, operationTransformers, schemaTransformers, cancellationToken);
         document.Tags = document.Tags?.Distinct(OpenApiTagComparer.Instance).ToList();
@@ -198,12 +199,26 @@ internal sealed class OpenApiDocumentService(
         };
     }
 
-    internal List<OpenApiServer> GetOpenApiServers()
+    // Resolve server URL from the request to handle reverse proxies.
+    // If there is active request object, assume a development environment and use the server addresses.
+    internal List<OpenApiServer> GetOpenApiServers(HttpRequest? httpRequest = null)
+    {
+        if (httpRequest is not null)
+        {
+            var serverUrl = UriHelper.BuildAbsolute(httpRequest.Scheme, httpRequest.Host, httpRequest.PathBase);
+            return [new OpenApiServer { Url = serverUrl }];
+        }
+        else
+        {
+            return GetDevelopmentOpenApiServers();
+        }
+    }
+    private List<OpenApiServer> GetDevelopmentOpenApiServers()
     {
         if (hostEnvironment.IsDevelopment() &&
             server?.Features.Get<IServerAddressesFeature>()?.Addresses is { Count: > 0 } addresses)
         {
-            return addresses.Select(address => new OpenApiServer { Url = address }).ToList();
+            return [.. addresses.Select(address => new OpenApiServer { Url = address })];
         }
         return [];
     }

+ 40 - 0
src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentService/OpenApiDocumentServiceTests.Servers.cs

@@ -1,6 +1,7 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
+using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc.ApiExplorer;
 using Microsoft.AspNetCore.OpenApi;
 using Microsoft.Extensions.DependencyInjection;
@@ -10,6 +11,45 @@ using Moq;
 
 public partial class OpenApiDocumentServiceTests
 {
+    [Theory]
+    [InlineData("Development", "localhost:5001", "", "http", "http://localhost:5001/")]
+    [InlineData("Development", "example.com", "/api", "https", "https://example.com/api")]
+    [InlineData("Staging", "localhost:5002", "/v1", "http", "http://localhost:5002/v1")]
+    [InlineData("Staging", "api.example.com", "/base/path", "https", "https://api.example.com/base/path")]
+    [InlineData("Development", "localhost", "/", "http", "http://localhost/")]
+    public void GetOpenApiServers_FavorsHttpContextRequestOverServerAddress(string environment, string host, string pathBase, string scheme, string expectedUri)
+    {
+        // Arrange
+        var hostEnvironment = new HostingEnvironment
+        {
+            ApplicationName = "TestApplication",
+            EnvironmentName = environment
+        };
+        var docService = new OpenApiDocumentService(
+            "v1",
+            new Mock<IApiDescriptionGroupCollectionProvider>().Object,
+            hostEnvironment,
+            GetMockOptionsMonitor(),
+            new Mock<IKeyedServiceProvider>().Object,
+            new OpenApiTestServer(["http://localhost:5000"]));
+        var httpContext = new DefaultHttpContext()
+        {
+            Request =
+            {
+                Host = new HostString(host),
+                PathBase = pathBase,
+                Scheme = scheme
+
+            }
+        };
+
+        // Act
+        var servers = docService.GetOpenApiServers(httpContext.Request);
+
+        // Assert
+        Assert.Contains(expectedUri, servers.Select(s => s.Url));
+    }
+
     [Fact]
     public void GetOpenApiServers_HandlesServerAddressFeatureWithValues()
     {

+ 2 - 2
src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentServiceTestsBase.cs

@@ -34,7 +34,7 @@ public abstract class OpenApiDocumentServiceTestBase
     {
         var documentService = CreateDocumentService(builder, openApiOptions);
         var scopedService = ((TestServiceProvider)builder.ServiceProvider).CreateScope();
-        var document = await documentService.GetOpenApiDocumentAsync(scopedService.ServiceProvider, cancellationToken);
+        var document = await documentService.GetOpenApiDocumentAsync(scopedService.ServiceProvider, null, cancellationToken);
         verifyOpenApiDocument(document);
     }
 
@@ -43,7 +43,7 @@ public abstract class OpenApiDocumentServiceTestBase
         var builder = CreateBuilder();
         var documentService = CreateDocumentService(builder, action);
         var scopedService = ((TestServiceProvider)builder.ServiceProvider).CreateScope();
-        var document = await documentService.GetOpenApiDocumentAsync(scopedService.ServiceProvider, cancellationToken);
+        var document = await documentService.GetOpenApiDocumentAsync(scopedService.ServiceProvider, null, cancellationToken);
         verifyOpenApiDocument(document);
     }