diff --git a/src/OpenApi/src/Extensions/OpenApiServiceCollectionExtensions.cs b/src/OpenApi/src/Extensions/OpenApiServiceCollectionExtensions.cs index 8c4d98300c7a..6516247a215e 100644 --- a/src/OpenApi/src/Extensions/OpenApiServiceCollectionExtensions.cs +++ b/src/OpenApi/src/Extensions/OpenApiServiceCollectionExtensions.cs @@ -110,6 +110,8 @@ private static IServiceCollection AddOpenApiCore(this IServiceCollection service services.AddEndpointsApiExplorer(); services.AddKeyedSingleton(documentName); services.AddKeyedSingleton(documentName); + services.AddKeyedSingleton(documentName); + // Required for build-time generation services.AddSingleton(); // Required to resolve document names for build-time generation diff --git a/src/OpenApi/src/PublicAPI.Unshipped.txt b/src/OpenApi/src/PublicAPI.Unshipped.txt index 629fbbb86f29..8c0657a70dca 100644 --- a/src/OpenApi/src/PublicAPI.Unshipped.txt +++ b/src/OpenApi/src/PublicAPI.Unshipped.txt @@ -1,4 +1,6 @@ #nullable enable +Microsoft.AspNetCore.OpenApi.IOpenApiDocumentProvider +Microsoft.AspNetCore.OpenApi.IOpenApiDocumentProvider.GetOpenApiDocumentAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! static Microsoft.AspNetCore.Builder.OpenApiEndpointConventionBuilderExtensions.AddOpenApiOperationTransformer(this TBuilder builder, System.Func! transformer) -> TBuilder 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.AspNetCore.OpenApi.OpenApiOperationTransformerContext.Document.get -> Microsoft.OpenApi.Models.OpenApiDocument? diff --git a/src/OpenApi/src/Services/IOpenApiDocumentProvider.cs b/src/OpenApi/src/Services/IOpenApiDocumentProvider.cs new file mode 100644 index 000000000000..afefb05eed7c --- /dev/null +++ b/src/OpenApi/src/Services/IOpenApiDocumentProvider.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.OpenApi.Models; + +namespace Microsoft.AspNetCore.OpenApi; + +/// +/// Represents a provider for OpenAPI documents that can be used by consumers to +/// retrieve generated OpenAPI documents at runtime. +/// +public interface IOpenApiDocumentProvider +{ + /// + /// Gets the OpenAPI document. + /// + /// A token to monitor for cancellation requests. + /// A task that represents the asynchronous operation. The task result contains the OpenAPI document. + /// + /// This method is typically used by consumers to retrieve the OpenAPI document. The generated document + /// may not contain the appropriate servers information since it can be instantiated outside the context + /// of an HTTP request. In these scenarios, the can be modified to + /// include the appropriate servers information. + /// + /// + /// Any OpenAPI transformers registered in the instance associated with + /// this document will be applied to the document before it is returned. + /// + Task GetOpenApiDocumentAsync(CancellationToken cancellationToken = default); +} diff --git a/src/OpenApi/src/Services/OpenApiDocumentService.cs b/src/OpenApi/src/Services/OpenApiDocumentService.cs index a358f56d08a9..f0678a8d155b 100644 --- a/src/OpenApi/src/Services/OpenApiDocumentService.cs +++ b/src/OpenApi/src/Services/OpenApiDocumentService.cs @@ -38,7 +38,7 @@ internal sealed class OpenApiDocumentService( IHostEnvironment hostEnvironment, IOptionsMonitor optionsMonitor, IServiceProvider serviceProvider, - IServer? server = null) + IServer? server = null) : IOpenApiDocumentProvider { private readonly OpenApiOptions _options = optionsMonitor.Get(documentName); private readonly OpenApiSchemaService _componentService = serviceProvider.GetRequiredKeyedService(documentName); @@ -744,4 +744,11 @@ private static Type GetTargetType(ApiDescription description, ApiParameterDescri targetType ??= typeof(string); return targetType; } + + /// + public Task GetOpenApiDocumentAsync(CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + return GetOpenApiDocumentAsync(serviceProvider, httpRequest: null, cancellationToken); + } } diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/OpenApiServiceCollectionExtensionsTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/OpenApiServiceCollectionExtensionsTests.cs index 57e177605258..3faeb8f1de0e 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/OpenApiServiceCollectionExtensionsTests.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/OpenApiServiceCollectionExtensionsTests.cs @@ -4,8 +4,11 @@ using Microsoft.AspNetCore.OpenApi; using Microsoft.Extensions.ApiDescriptions; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Internal; using Microsoft.Extensions.Options; using Microsoft.OpenApi; +using Microsoft.OpenApi.Models; public class OpenApiServiceCollectionExtensions { @@ -189,4 +192,112 @@ public void AddOpenApi_WithDuplicateDocumentNames_UsesLastRegistration_ValidateO Assert.Equal(documentName, namedOption.DocumentName); Assert.Equal(OpenApiSpecVersion.OpenApi2_0, namedOption.OpenApiVersion); } + + [Fact] + public void AddOpenApi_WithDefaultDocumentName_RegistersIOpenApiDocumentProviderInterface() + { + // Arrange + var services = new ServiceCollection(); + // Include dependencies for OpenApiDocumentService + services.AddSingleton(new HostingEnvironment + { + EnvironmentName = Environments.Development, + ApplicationName = "Test Application" + }); + services.AddLogging(); + services.AddRouting(); + + // Act + services.AddOpenApi(); + var serviceProvider = services.BuildServiceProvider(); + + // Assert + var documentProvider = serviceProvider.GetRequiredKeyedService(Microsoft.AspNetCore.OpenApi.OpenApiConstants.DefaultDocumentName); + Assert.NotNull(documentProvider); + Assert.IsType(documentProvider); + } + + [Fact] + public void AddOpenApi_WithCustomDocumentName_RegistersIOpenApiDocumentProviderInterface() + { + // Arrange + var services = new ServiceCollection(); + // Include dependencies for OpenApiDocumentService + services.AddSingleton(new HostingEnvironment + { + EnvironmentName = Environments.Development, + ApplicationName = "Test Application" + }); + services.AddLogging(); + services.AddRouting(); + var documentName = "v1"; + + // Act + services.AddOpenApi(documentName); + var serviceProvider = services.BuildServiceProvider(); + + // Assert + var documentProvider = serviceProvider.GetRequiredKeyedService(documentName.ToLowerInvariant()); + Assert.NotNull(documentProvider); + Assert.IsType(documentProvider); + } + + [Fact] + public async Task GetOpenApiDocumentAsync_ReturnsDocument() + { + // Arrange + var services = new ServiceCollection(); + // Include dependencies for OpenApiDocumentService + services.AddSingleton(new HostingEnvironment + { + EnvironmentName = Environments.Development, + ApplicationName = "Test Application" + }); + services.AddLogging(); + services.AddRouting(); + + var documentName = "v1"; + services.AddOpenApi(documentName); + var serviceProvider = services.BuildServiceProvider(); + var documentProvider = serviceProvider.GetRequiredKeyedService(documentName.ToLowerInvariant()); + + // Act + var document = await documentProvider.GetOpenApiDocumentAsync(); + + // Assert + Assert.NotNull(document); + Assert.IsType(document); + + // Verify basic document structure + Assert.NotNull(document.Info); + Assert.Equal($"Test Application | {documentName.ToLowerInvariant()}", document.Info.Title); + Assert.Equal("1.0.0", document.Info.Version); + } + + [Fact] + public async Task GetOpenApiDocumentAsync_HandlesCancellation() + { + // Arrange + var services = new ServiceCollection(); + services.AddSingleton(new HostingEnvironment + { + EnvironmentName = Environments.Development, + ApplicationName = "Test Application" + }); + services.AddLogging(); + services.AddRouting(); + var documentName = "v1"; + services.AddOpenApi(documentName); + var serviceProvider = services.BuildServiceProvider(); + var documentProvider = serviceProvider.GetRequiredKeyedService(documentName.ToLowerInvariant()); + + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + // Act & Assert + await Assert.ThrowsAsync(async () => + { + await documentProvider.GetOpenApiDocumentAsync(cts.Token); + }); + } }