diff --git a/src/OpenApi/src/Services/OpenApiDocumentService.cs b/src/OpenApi/src/Services/OpenApiDocumentService.cs index d3adb7f03c95..e92fc43bf073 100644 --- a/src/OpenApi/src/Services/OpenApiDocumentService.cs +++ b/src/OpenApi/src/Services/OpenApiDocumentService.cs @@ -9,6 +9,8 @@ using System.IO.Pipelines; using System.Linq; using System.Reflection; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Mvc; @@ -29,7 +31,8 @@ internal sealed class OpenApiDocumentService( IApiDescriptionGroupCollectionProvider apiDescriptionGroupCollectionProvider, IHostEnvironment hostEnvironment, IOptionsMonitor optionsMonitor, - IServiceProvider serviceProvider) + IServiceProvider serviceProvider, + IServer? server = null) { private readonly OpenApiOptions _options = optionsMonitor.Get(documentName); private readonly OpenApiSchemaService _componentService = serviceProvider.GetRequiredKeyedService(documentName); @@ -58,6 +61,7 @@ public async Task GetOpenApiDocumentAsync(CancellationToken can { Info = GetOpenApiInfo(), Paths = await GetOpenApiPathsAsync(capturedTags, cancellationToken), + Servers = GetOpenApiServers(), Tags = [.. capturedTags] }; await ApplyTransformersAsync(document, cancellationToken); @@ -92,6 +96,16 @@ internal OpenApiInfo GetOpenApiInfo() }; } + internal List GetOpenApiServers() + { + if (hostEnvironment.IsDevelopment() && + server?.Features.Get()?.Addresses is { Count: > 0 } addresses) + { + return addresses.Select(address => new OpenApiServer { Url = address }).ToList(); + } + return []; + } + /// /// Gets the OpenApiPaths for the document based on the ApiDescriptions. /// diff --git a/src/OpenApi/test/Services/OpenApiDocumentService/OpenApiDocumentServiceTests.Info.cs b/src/OpenApi/test/Services/OpenApiDocumentService/OpenApiDocumentServiceTests.Info.cs index 3f201381c0f7..6d4f289ec22e 100644 --- a/src/OpenApi/test/Services/OpenApiDocumentService/OpenApiDocumentServiceTests.Info.cs +++ b/src/OpenApi/test/Services/OpenApiDocumentService/OpenApiDocumentServiceTests.Info.cs @@ -23,7 +23,8 @@ public void GetOpenApiInfo_RespectsHostEnvironmentName() new Mock().Object, hostEnvironment, new Mock>().Object, - new Mock().Object); + new Mock().Object, + new OpenApiTestServer()); // Act var info = docService.GetOpenApiInfo(); @@ -45,7 +46,8 @@ public void GetOpenApiInfo_RespectsDocumentName() new Mock().Object, hostEnvironment, new Mock>().Object, - new Mock().Object); + new Mock().Object, + new OpenApiTestServer()); // Act var info = docService.GetOpenApiInfo(); diff --git a/src/OpenApi/test/Services/OpenApiDocumentService/OpenApiDocumentServiceTests.Servers.cs b/src/OpenApi/test/Services/OpenApiDocumentService/OpenApiDocumentServiceTests.Servers.cs new file mode 100644 index 000000000000..4426e3d0c04f --- /dev/null +++ b/src/OpenApi/test/Services/OpenApiDocumentService/OpenApiDocumentServiceTests.Servers.cs @@ -0,0 +1,109 @@ +// 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.Mvc.ApiExplorer; +using Microsoft.AspNetCore.OpenApi; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting.Internal; +using Microsoft.Extensions.Options; +using Moq; + +public partial class OpenApiDocumentServiceTests +{ + [Fact] + public void GetOpenApiServers_HandlesServerAddressFeatureWithValues() + { + // Arrange + var hostEnvironment = new HostingEnvironment + { + ApplicationName = "TestApplication", + EnvironmentName = "Development" + }; + var docService = new OpenApiDocumentService( + "v1", + new Mock().Object, + hostEnvironment, + new Mock>().Object, + new Mock().Object, + new OpenApiTestServer(["http://localhost:5000"])); + + // Act + var servers = docService.GetOpenApiServers(); + + // Assert + Assert.Contains("http://localhost:5000", servers.Select(s => s.Url)); + } + + [Fact] + public void GetOpenApiServers_HandlesServerAddressFeatureWithMultipleValues() + { + // Arrange + var hostEnvironment = new HostingEnvironment + { + ApplicationName = "TestApplication", + EnvironmentName = "Development" + }; + var docService = new OpenApiDocumentService( + "v1", + new Mock().Object, + hostEnvironment, + new Mock>().Object, + new Mock().Object, + new OpenApiTestServer(["http://localhost:5000", "http://localhost:5002"])); + + // Act + var servers = docService.GetOpenApiServers(); + + // Assert + Assert.Contains("http://localhost:5000", servers.Select(s => s.Url)); + Assert.Contains("http://localhost:5002", servers.Select(s => s.Url)); + } + + [Fact] + public void GetOpenApiServers_HandlesNonDevelopmentEnvironment() + { + // Arrange + var hostEnvironment = new HostingEnvironment + { + ApplicationName = "TestApplication", + EnvironmentName = "Production" + }; + var docService = new OpenApiDocumentService( + "v1", + new Mock().Object, + hostEnvironment, + new Mock>().Object, + new Mock().Object, + new OpenApiTestServer(["http://localhost:5000"])); + + // Act + var servers = docService.GetOpenApiServers(); + + // Assert + Assert.Empty(servers); + } + + [Fact] + public void GetOpenApiServers_HandlesServerAddressFeatureWithNoValues() + { + // Arrange + var hostEnvironment = new HostingEnvironment + { + ApplicationName = "TestApplication", + EnvironmentName = "Development" + }; + var docService = new OpenApiDocumentService( + "v2", + new Mock().Object, + hostEnvironment, + new Mock>().Object, + new Mock().Object, + new OpenApiTestServer()); + + // Act + var servers = docService.GetOpenApiServers(); + + // Assert + Assert.Empty(servers); + } +} diff --git a/src/OpenApi/test/Services/OpenApiDocumentServiceTestsBase.cs b/src/OpenApi/test/Services/OpenApiDocumentServiceTestsBase.cs index 9b72ee79e8df..ebad522b54f7 100644 --- a/src/OpenApi/test/Services/OpenApiDocumentServiceTestsBase.cs +++ b/src/OpenApi/test/Services/OpenApiDocumentServiceTestsBase.cs @@ -3,6 +3,9 @@ using System.Reflection; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ActionConstraints; @@ -75,7 +78,7 @@ internal static OpenApiDocumentService CreateDocumentService(ActionDescriptor ac var schemaService = new OpenApiSchemaService("Test", Options.Create(new Microsoft.AspNetCore.Http.Json.JsonOptions()), builder.ServiceProvider, openApiOptions.Object); ((TestServiceProvider)builder.ServiceProvider).TestSchemaService = schemaService; - var documentService = new OpenApiDocumentService("Test", apiDescriptionGroupCollectionProvider, hostEnvironment, openApiOptions.Object, builder.ServiceProvider); + var documentService = new OpenApiDocumentService("Test", apiDescriptionGroupCollectionProvider, hostEnvironment, openApiOptions.Object, builder.ServiceProvider, new OpenApiTestServer()); ((TestServiceProvider)builder.ServiceProvider).TestDocumentService = documentService; return documentService; @@ -101,7 +104,7 @@ internal static OpenApiDocumentService CreateDocumentService(IEndpointRouteBuild var schemaService = new OpenApiSchemaService("Test", Options.Create(new Microsoft.AspNetCore.Http.Json.JsonOptions()), builder.ServiceProvider, options.Object); ((TestServiceProvider)builder.ServiceProvider).TestSchemaService = schemaService; - var documentService = new OpenApiDocumentService("Test", apiDescriptionGroupCollectionProvider, hostEnvironment, options.Object, builder.ServiceProvider); + var documentService = new OpenApiDocumentService("Test", apiDescriptionGroupCollectionProvider, hostEnvironment, options.Object, builder.ServiceProvider, new OpenApiTestServer()); ((TestServiceProvider)builder.ServiceProvider).TestDocumentService = documentService; return documentService; @@ -276,4 +279,31 @@ public object GetService(Type serviceType) return _serviceProvider.GetService(serviceType); } } + + internal class OpenApiTestServer(string[] addresses = null) : IServer + { + public IFeatureCollection Features => GenerateFeatures(); + + public void Dispose() + { + return; + } + + internal virtual IFeatureCollection GenerateFeatures() + { + var features = new FeatureCollection(); + features.Set(new TestServerAddressesFeature { Addresses = addresses }); + return features; + } + + public Task StartAsync(IHttpApplication application, CancellationToken cancellationToken) where TContext : notnull => Task.CompletedTask; + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + } + + private class TestServerAddressesFeature : IServerAddressesFeature + { + public ICollection Addresses { get; set; } + public bool PreferHostingUrls { get; set; } + } }