From 794f1ee322f449e3cf586c15bedfc2d3c515596d Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Fri, 11 Apr 2025 15:59:57 -0700 Subject: [PATCH 1/2] Add IOpenApiDocumentProvider interface and implementation --- .../OpenApiServiceCollectionExtensions.cs | 2 + src/OpenApi/src/PublicAPI.Unshipped.txt | 2 + .../src/Services/IOpenApiDocumentProvider.cs | 30 +++++++ .../src/Services/OpenApiDocumentService.cs | 6 +- ...OpenApiServiceCollectionExtensionsTests.cs | 86 +++++++++++++++++++ 5 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 src/OpenApi/src/Services/IOpenApiDocumentProvider.cs 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..c4fa08e1e60f 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,8 @@ private static Type GetTargetType(ApiDescription description, ApiParameterDescri targetType ??= typeof(string); return targetType; } + + /// + public Task GetOpenApiDocumentAsync(CancellationToken cancellationToken = default) + => GetOpenApiDocumentAsync(serviceProvider, 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..d2b94a94a05e 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/OpenApiServiceCollectionExtensionsTests.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/OpenApiServiceCollectionExtensionsTests.cs @@ -2,10 +2,15 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.OpenApi; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.ApiDescriptions; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +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 +194,85 @@ 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); + } } From a1645cb96ce81d097e4d89b3d08f3908a4649dfc Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Tue, 15 Apr 2025 14:25:59 -0700 Subject: [PATCH 2/2] Address feedback --- .../src/Services/OpenApiDocumentService.cs | 5 +++- ...OpenApiServiceCollectionExtensionsTests.cs | 29 +++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/OpenApi/src/Services/OpenApiDocumentService.cs b/src/OpenApi/src/Services/OpenApiDocumentService.cs index c4fa08e1e60f..f0678a8d155b 100644 --- a/src/OpenApi/src/Services/OpenApiDocumentService.cs +++ b/src/OpenApi/src/Services/OpenApiDocumentService.cs @@ -747,5 +747,8 @@ private static Type GetTargetType(ApiDescription description, ApiParameterDescri /// public Task GetOpenApiDocumentAsync(CancellationToken cancellationToken = default) - => GetOpenApiDocumentAsync(serviceProvider, null, cancellationToken); + { + 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 d2b94a94a05e..3faeb8f1de0e 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/OpenApiServiceCollectionExtensionsTests.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/OpenApiServiceCollectionExtensionsTests.cs @@ -2,10 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.OpenApi; -using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.ApiDescriptions; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting.Internal; using Microsoft.Extensions.Options; @@ -275,4 +273,31 @@ public async Task GetOpenApiDocumentAsync_ReturnsDocument() 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); + }); + } }