Skip to content

Commit 2c0bb03

Browse files
authored
Add IOpenApiDocumentProvider interface and implementation (#61463)
* Add IOpenApiDocumentProvider interface and implementation * Address feedback
1 parent 2c02209 commit 2c0bb03

File tree

5 files changed

+153
-1
lines changed

5 files changed

+153
-1
lines changed

src/OpenApi/src/Extensions/OpenApiServiceCollectionExtensions.cs

+2
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ private static IServiceCollection AddOpenApiCore(this IServiceCollection service
110110
services.AddEndpointsApiExplorer();
111111
services.AddKeyedSingleton<OpenApiSchemaService>(documentName);
112112
services.AddKeyedSingleton<OpenApiDocumentService>(documentName);
113+
services.AddKeyedSingleton<IOpenApiDocumentProvider, OpenApiDocumentService>(documentName);
114+
113115
// Required for build-time generation
114116
services.AddSingleton<IDocumentProvider, OpenApiDocumentProvider>();
115117
// Required to resolve document names for build-time generation

src/OpenApi/src/PublicAPI.Unshipped.txt

+2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
#nullable enable
2+
Microsoft.AspNetCore.OpenApi.IOpenApiDocumentProvider
3+
Microsoft.AspNetCore.OpenApi.IOpenApiDocumentProvider.GetOpenApiDocumentAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Microsoft.OpenApi.Models.OpenApiDocument!>!
24
static Microsoft.AspNetCore.Builder.OpenApiEndpointConventionBuilderExtensions.AddOpenApiOperationTransformer<TBuilder>(this TBuilder builder, System.Func<Microsoft.OpenApi.Models.OpenApiOperation!, Microsoft.AspNetCore.OpenApi.OpenApiOperationTransformerContext!, System.Threading.CancellationToken, System.Threading.Tasks.Task!>! transformer) -> TBuilder
35
Microsoft.AspNetCore.OpenApi.OpenApiDocumentTransformerContext.GetOrCreateSchemaAsync(System.Type! type, Microsoft.AspNetCore.Mvc.ApiExplorer.ApiParameterDescription? parameterDescription = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Microsoft.OpenApi.Models.OpenApiSchema!>!
46
Microsoft.AspNetCore.OpenApi.OpenApiOperationTransformerContext.Document.get -> Microsoft.OpenApi.Models.OpenApiDocument?
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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.OpenApi.Models;
5+
6+
namespace Microsoft.AspNetCore.OpenApi;
7+
8+
/// <summary>
9+
/// Represents a provider for OpenAPI documents that can be used by consumers to
10+
/// retrieve generated OpenAPI documents at runtime.
11+
/// </summary>
12+
public interface IOpenApiDocumentProvider
13+
{
14+
/// <summary>
15+
/// Gets the OpenAPI document.
16+
/// </summary>
17+
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
18+
/// <returns>A task that represents the asynchronous operation. The task result contains the OpenAPI document.</returns>
19+
/// <remarks>
20+
/// This method is typically used by consumers to retrieve the OpenAPI document. The generated document
21+
/// may not contain the appropriate servers information since it can be instantiated outside the context
22+
/// of an HTTP request. In these scenarios, the <see cref="OpenApiDocument"/> can be modified to
23+
/// include the appropriate servers information.
24+
/// </remarks>
25+
/// <remarks>
26+
/// Any OpenAPI transformers registered in the <see cref="OpenApiOptions"/> instance associated with
27+
/// this document will be applied to the document before it is returned.
28+
/// </remarks>
29+
Task<OpenApiDocument> GetOpenApiDocumentAsync(CancellationToken cancellationToken = default);
30+
}

src/OpenApi/src/Services/OpenApiDocumentService.cs

+8-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ internal sealed class OpenApiDocumentService(
3838
IHostEnvironment hostEnvironment,
3939
IOptionsMonitor<OpenApiOptions> optionsMonitor,
4040
IServiceProvider serviceProvider,
41-
IServer? server = null)
41+
IServer? server = null) : IOpenApiDocumentProvider
4242
{
4343
private readonly OpenApiOptions _options = optionsMonitor.Get(documentName);
4444
private readonly OpenApiSchemaService _componentService = serviceProvider.GetRequiredKeyedService<OpenApiSchemaService>(documentName);
@@ -744,4 +744,11 @@ private static Type GetTargetType(ApiDescription description, ApiParameterDescri
744744
targetType ??= typeof(string);
745745
return targetType;
746746
}
747+
748+
/// <inheritdoc />
749+
public Task<OpenApiDocument> GetOpenApiDocumentAsync(CancellationToken cancellationToken = default)
750+
{
751+
cancellationToken.ThrowIfCancellationRequested();
752+
return GetOpenApiDocumentAsync(serviceProvider, httpRequest: null, cancellationToken);
753+
}
747754
}

src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/OpenApiServiceCollectionExtensionsTests.cs

+111
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44
using Microsoft.AspNetCore.OpenApi;
55
using Microsoft.Extensions.ApiDescriptions;
66
using Microsoft.Extensions.DependencyInjection;
7+
using Microsoft.Extensions.Hosting;
8+
using Microsoft.Extensions.Hosting.Internal;
79
using Microsoft.Extensions.Options;
810
using Microsoft.OpenApi;
11+
using Microsoft.OpenApi.Models;
912

1013
public class OpenApiServiceCollectionExtensions
1114
{
@@ -189,4 +192,112 @@ public void AddOpenApi_WithDuplicateDocumentNames_UsesLastRegistration_ValidateO
189192
Assert.Equal(documentName, namedOption.DocumentName);
190193
Assert.Equal(OpenApiSpecVersion.OpenApi2_0, namedOption.OpenApiVersion);
191194
}
195+
196+
[Fact]
197+
public void AddOpenApi_WithDefaultDocumentName_RegistersIOpenApiDocumentProviderInterface()
198+
{
199+
// Arrange
200+
var services = new ServiceCollection();
201+
// Include dependencies for OpenApiDocumentService
202+
services.AddSingleton<IHostEnvironment>(new HostingEnvironment
203+
{
204+
EnvironmentName = Environments.Development,
205+
ApplicationName = "Test Application"
206+
});
207+
services.AddLogging();
208+
services.AddRouting();
209+
210+
// Act
211+
services.AddOpenApi();
212+
var serviceProvider = services.BuildServiceProvider();
213+
214+
// Assert
215+
var documentProvider = serviceProvider.GetRequiredKeyedService<IOpenApiDocumentProvider>(Microsoft.AspNetCore.OpenApi.OpenApiConstants.DefaultDocumentName);
216+
Assert.NotNull(documentProvider);
217+
Assert.IsType<OpenApiDocumentService>(documentProvider);
218+
}
219+
220+
[Fact]
221+
public void AddOpenApi_WithCustomDocumentName_RegistersIOpenApiDocumentProviderInterface()
222+
{
223+
// Arrange
224+
var services = new ServiceCollection();
225+
// Include dependencies for OpenApiDocumentService
226+
services.AddSingleton<IHostEnvironment>(new HostingEnvironment
227+
{
228+
EnvironmentName = Environments.Development,
229+
ApplicationName = "Test Application"
230+
});
231+
services.AddLogging();
232+
services.AddRouting();
233+
var documentName = "v1";
234+
235+
// Act
236+
services.AddOpenApi(documentName);
237+
var serviceProvider = services.BuildServiceProvider();
238+
239+
// Assert
240+
var documentProvider = serviceProvider.GetRequiredKeyedService<IOpenApiDocumentProvider>(documentName.ToLowerInvariant());
241+
Assert.NotNull(documentProvider);
242+
Assert.IsType<OpenApiDocumentService>(documentProvider);
243+
}
244+
245+
[Fact]
246+
public async Task GetOpenApiDocumentAsync_ReturnsDocument()
247+
{
248+
// Arrange
249+
var services = new ServiceCollection();
250+
// Include dependencies for OpenApiDocumentService
251+
services.AddSingleton<IHostEnvironment>(new HostingEnvironment
252+
{
253+
EnvironmentName = Environments.Development,
254+
ApplicationName = "Test Application"
255+
});
256+
services.AddLogging();
257+
services.AddRouting();
258+
259+
var documentName = "v1";
260+
services.AddOpenApi(documentName);
261+
var serviceProvider = services.BuildServiceProvider();
262+
var documentProvider = serviceProvider.GetRequiredKeyedService<IOpenApiDocumentProvider>(documentName.ToLowerInvariant());
263+
264+
// Act
265+
var document = await documentProvider.GetOpenApiDocumentAsync();
266+
267+
// Assert
268+
Assert.NotNull(document);
269+
Assert.IsType<OpenApiDocument>(document);
270+
271+
// Verify basic document structure
272+
Assert.NotNull(document.Info);
273+
Assert.Equal($"Test Application | {documentName.ToLowerInvariant()}", document.Info.Title);
274+
Assert.Equal("1.0.0", document.Info.Version);
275+
}
276+
277+
[Fact]
278+
public async Task GetOpenApiDocumentAsync_HandlesCancellation()
279+
{
280+
// Arrange
281+
var services = new ServiceCollection();
282+
services.AddSingleton<IHostEnvironment>(new HostingEnvironment
283+
{
284+
EnvironmentName = Environments.Development,
285+
ApplicationName = "Test Application"
286+
});
287+
services.AddLogging();
288+
services.AddRouting();
289+
var documentName = "v1";
290+
services.AddOpenApi(documentName);
291+
var serviceProvider = services.BuildServiceProvider();
292+
var documentProvider = serviceProvider.GetRequiredKeyedService<IOpenApiDocumentProvider>(documentName.ToLowerInvariant());
293+
294+
using var cts = new CancellationTokenSource();
295+
cts.Cancel();
296+
297+
// Act & Assert
298+
await Assert.ThrowsAsync<OperationCanceledException>(async () =>
299+
{
300+
await documentProvider.GetOpenApiDocumentAsync(cts.Token);
301+
});
302+
}
192303
}

0 commit comments

Comments
 (0)