Skip to content

Commit d498542

Browse files
authored
Set OpenApiServers object in document (#56470)
* Set readOnly status for properties * Set supported servers from IServerAddressFeature * Revert "Set readOnly status for properties" This reverts commit dbe3bae. * Address feedback * Add explicit check for Development environment * Add test for feature with multiple values
1 parent d4e43fc commit d498542

File tree

4 files changed

+160
-5
lines changed

4 files changed

+160
-5
lines changed

src/OpenApi/src/Services/OpenApiDocumentService.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
using System.IO.Pipelines;
1010
using System.Linq;
1111
using System.Reflection;
12+
using Microsoft.AspNetCore.Hosting.Server;
13+
using Microsoft.AspNetCore.Hosting.Server.Features;
1214
using Microsoft.AspNetCore.Http;
1315
using Microsoft.AspNetCore.Http.Metadata;
1416
using Microsoft.AspNetCore.Mvc;
@@ -29,7 +31,8 @@ internal sealed class OpenApiDocumentService(
2931
IApiDescriptionGroupCollectionProvider apiDescriptionGroupCollectionProvider,
3032
IHostEnvironment hostEnvironment,
3133
IOptionsMonitor<OpenApiOptions> optionsMonitor,
32-
IServiceProvider serviceProvider)
34+
IServiceProvider serviceProvider,
35+
IServer? server = null)
3336
{
3437
private readonly OpenApiOptions _options = optionsMonitor.Get(documentName);
3538
private readonly OpenApiSchemaService _componentService = serviceProvider.GetRequiredKeyedService<OpenApiSchemaService>(documentName);
@@ -58,6 +61,7 @@ public async Task<OpenApiDocument> GetOpenApiDocumentAsync(CancellationToken can
5861
{
5962
Info = GetOpenApiInfo(),
6063
Paths = await GetOpenApiPathsAsync(capturedTags, cancellationToken),
64+
Servers = GetOpenApiServers(),
6165
Tags = [.. capturedTags]
6266
};
6367
await ApplyTransformersAsync(document, cancellationToken);
@@ -92,6 +96,16 @@ internal OpenApiInfo GetOpenApiInfo()
9296
};
9397
}
9498

99+
internal List<OpenApiServer> GetOpenApiServers()
100+
{
101+
if (hostEnvironment.IsDevelopment() &&
102+
server?.Features.Get<IServerAddressesFeature>()?.Addresses is { Count: > 0 } addresses)
103+
{
104+
return addresses.Select(address => new OpenApiServer { Url = address }).ToList();
105+
}
106+
return [];
107+
}
108+
95109
/// <summary>
96110
/// Gets the OpenApiPaths for the document based on the ApiDescriptions.
97111
/// </summary>

src/OpenApi/test/Services/OpenApiDocumentService/OpenApiDocumentServiceTests.Info.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ public void GetOpenApiInfo_RespectsHostEnvironmentName()
2323
new Mock<IApiDescriptionGroupCollectionProvider>().Object,
2424
hostEnvironment,
2525
new Mock<IOptionsMonitor<OpenApiOptions>>().Object,
26-
new Mock<IKeyedServiceProvider>().Object);
26+
new Mock<IKeyedServiceProvider>().Object,
27+
new OpenApiTestServer());
2728

2829
// Act
2930
var info = docService.GetOpenApiInfo();
@@ -45,7 +46,8 @@ public void GetOpenApiInfo_RespectsDocumentName()
4546
new Mock<IApiDescriptionGroupCollectionProvider>().Object,
4647
hostEnvironment,
4748
new Mock<IOptionsMonitor<OpenApiOptions>>().Object,
48-
new Mock<IKeyedServiceProvider>().Object);
49+
new Mock<IKeyedServiceProvider>().Object,
50+
new OpenApiTestServer());
4951

5052
// Act
5153
var info = docService.GetOpenApiInfo();
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Mvc.ApiExplorer;
5+
using Microsoft.AspNetCore.OpenApi;
6+
using Microsoft.Extensions.DependencyInjection;
7+
using Microsoft.Extensions.Hosting.Internal;
8+
using Microsoft.Extensions.Options;
9+
using Moq;
10+
11+
public partial class OpenApiDocumentServiceTests
12+
{
13+
[Fact]
14+
public void GetOpenApiServers_HandlesServerAddressFeatureWithValues()
15+
{
16+
// Arrange
17+
var hostEnvironment = new HostingEnvironment
18+
{
19+
ApplicationName = "TestApplication",
20+
EnvironmentName = "Development"
21+
};
22+
var docService = new OpenApiDocumentService(
23+
"v1",
24+
new Mock<IApiDescriptionGroupCollectionProvider>().Object,
25+
hostEnvironment,
26+
new Mock<IOptionsMonitor<OpenApiOptions>>().Object,
27+
new Mock<IKeyedServiceProvider>().Object,
28+
new OpenApiTestServer(["http://localhost:5000"]));
29+
30+
// Act
31+
var servers = docService.GetOpenApiServers();
32+
33+
// Assert
34+
Assert.Contains("http://localhost:5000", servers.Select(s => s.Url));
35+
}
36+
37+
[Fact]
38+
public void GetOpenApiServers_HandlesServerAddressFeatureWithMultipleValues()
39+
{
40+
// Arrange
41+
var hostEnvironment = new HostingEnvironment
42+
{
43+
ApplicationName = "TestApplication",
44+
EnvironmentName = "Development"
45+
};
46+
var docService = new OpenApiDocumentService(
47+
"v1",
48+
new Mock<IApiDescriptionGroupCollectionProvider>().Object,
49+
hostEnvironment,
50+
new Mock<IOptionsMonitor<OpenApiOptions>>().Object,
51+
new Mock<IKeyedServiceProvider>().Object,
52+
new OpenApiTestServer(["http://localhost:5000", "http://localhost:5002"]));
53+
54+
// Act
55+
var servers = docService.GetOpenApiServers();
56+
57+
// Assert
58+
Assert.Contains("http://localhost:5000", servers.Select(s => s.Url));
59+
Assert.Contains("http://localhost:5002", servers.Select(s => s.Url));
60+
}
61+
62+
[Fact]
63+
public void GetOpenApiServers_HandlesNonDevelopmentEnvironment()
64+
{
65+
// Arrange
66+
var hostEnvironment = new HostingEnvironment
67+
{
68+
ApplicationName = "TestApplication",
69+
EnvironmentName = "Production"
70+
};
71+
var docService = new OpenApiDocumentService(
72+
"v1",
73+
new Mock<IApiDescriptionGroupCollectionProvider>().Object,
74+
hostEnvironment,
75+
new Mock<IOptionsMonitor<OpenApiOptions>>().Object,
76+
new Mock<IKeyedServiceProvider>().Object,
77+
new OpenApiTestServer(["http://localhost:5000"]));
78+
79+
// Act
80+
var servers = docService.GetOpenApiServers();
81+
82+
// Assert
83+
Assert.Empty(servers);
84+
}
85+
86+
[Fact]
87+
public void GetOpenApiServers_HandlesServerAddressFeatureWithNoValues()
88+
{
89+
// Arrange
90+
var hostEnvironment = new HostingEnvironment
91+
{
92+
ApplicationName = "TestApplication",
93+
EnvironmentName = "Development"
94+
};
95+
var docService = new OpenApiDocumentService(
96+
"v2",
97+
new Mock<IApiDescriptionGroupCollectionProvider>().Object,
98+
hostEnvironment,
99+
new Mock<IOptionsMonitor<OpenApiOptions>>().Object,
100+
new Mock<IKeyedServiceProvider>().Object,
101+
new OpenApiTestServer());
102+
103+
// Act
104+
var servers = docService.GetOpenApiServers();
105+
106+
// Assert
107+
Assert.Empty(servers);
108+
}
109+
}

src/OpenApi/test/Services/OpenApiDocumentServiceTestsBase.cs

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33

44
using System.Reflection;
55
using Microsoft.AspNetCore.Builder;
6+
using Microsoft.AspNetCore.Hosting.Server;
7+
using Microsoft.AspNetCore.Hosting.Server.Features;
8+
using Microsoft.AspNetCore.Http.Features;
69
using Microsoft.AspNetCore.Mvc;
710
using Microsoft.AspNetCore.Mvc.Abstractions;
811
using Microsoft.AspNetCore.Mvc.ActionConstraints;
@@ -75,7 +78,7 @@ internal static OpenApiDocumentService CreateDocumentService(ActionDescriptor ac
7578

7679
var schemaService = new OpenApiSchemaService("Test", Options.Create(new Microsoft.AspNetCore.Http.Json.JsonOptions()), builder.ServiceProvider, openApiOptions.Object);
7780
((TestServiceProvider)builder.ServiceProvider).TestSchemaService = schemaService;
78-
var documentService = new OpenApiDocumentService("Test", apiDescriptionGroupCollectionProvider, hostEnvironment, openApiOptions.Object, builder.ServiceProvider);
81+
var documentService = new OpenApiDocumentService("Test", apiDescriptionGroupCollectionProvider, hostEnvironment, openApiOptions.Object, builder.ServiceProvider, new OpenApiTestServer());
7982
((TestServiceProvider)builder.ServiceProvider).TestDocumentService = documentService;
8083

8184
return documentService;
@@ -101,7 +104,7 @@ internal static OpenApiDocumentService CreateDocumentService(IEndpointRouteBuild
101104

102105
var schemaService = new OpenApiSchemaService("Test", Options.Create(new Microsoft.AspNetCore.Http.Json.JsonOptions()), builder.ServiceProvider, options.Object);
103106
((TestServiceProvider)builder.ServiceProvider).TestSchemaService = schemaService;
104-
var documentService = new OpenApiDocumentService("Test", apiDescriptionGroupCollectionProvider, hostEnvironment, options.Object, builder.ServiceProvider);
107+
var documentService = new OpenApiDocumentService("Test", apiDescriptionGroupCollectionProvider, hostEnvironment, options.Object, builder.ServiceProvider, new OpenApiTestServer());
105108
((TestServiceProvider)builder.ServiceProvider).TestDocumentService = documentService;
106109

107110
return documentService;
@@ -276,4 +279,31 @@ public object GetService(Type serviceType)
276279
return _serviceProvider.GetService(serviceType);
277280
}
278281
}
282+
283+
internal class OpenApiTestServer(string[] addresses = null) : IServer
284+
{
285+
public IFeatureCollection Features => GenerateFeatures();
286+
287+
public void Dispose()
288+
{
289+
return;
290+
}
291+
292+
internal virtual IFeatureCollection GenerateFeatures()
293+
{
294+
var features = new FeatureCollection();
295+
features.Set<IServerAddressesFeature>(new TestServerAddressesFeature { Addresses = addresses });
296+
return features;
297+
}
298+
299+
public Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken) where TContext : notnull => Task.CompletedTask;
300+
301+
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
302+
}
303+
304+
private class TestServerAddressesFeature : IServerAddressesFeature
305+
{
306+
public ICollection<string> Addresses { get; set; }
307+
public bool PreferHostingUrls { get; set; }
308+
}
279309
}

0 commit comments

Comments
 (0)