Quellcode durchsuchen

Add IOpenApiDocumentProvider interface and implementation (#61463)

* Add IOpenApiDocumentProvider interface and implementation

* Address feedback
Safia Abdalla vor 10 Monaten
Ursprung
Commit
2c0bb03d82

+ 2 - 0
src/OpenApi/src/Extensions/OpenApiServiceCollectionExtensions.cs

@@ -110,6 +110,8 @@ public static class OpenApiServiceCollectionExtensions
         services.AddEndpointsApiExplorer();
         services.AddKeyedSingleton<OpenApiSchemaService>(documentName);
         services.AddKeyedSingleton<OpenApiDocumentService>(documentName);
+        services.AddKeyedSingleton<IOpenApiDocumentProvider, OpenApiDocumentService>(documentName);
+
         // Required for build-time generation
         services.AddSingleton<IDocumentProvider, OpenApiDocumentProvider>();
         // Required to resolve document names for build-time generation

+ 2 - 0
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<Microsoft.OpenApi.Models.OpenApiDocument!>!
 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
 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!>!
 Microsoft.AspNetCore.OpenApi.OpenApiOperationTransformerContext.Document.get -> Microsoft.OpenApi.Models.OpenApiDocument?

+ 30 - 0
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;
+
+/// <summary>
+/// Represents a provider for OpenAPI documents that can be used by consumers to
+/// retrieve generated OpenAPI documents at runtime.
+/// </summary>
+public interface IOpenApiDocumentProvider
+{
+    /// <summary>
+    /// Gets the OpenAPI document.
+    /// </summary>
+    /// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
+    /// <returns>A task that represents the asynchronous operation. The task result contains the OpenAPI document.</returns>
+    /// <remarks>
+    /// 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 <see cref="OpenApiDocument"/> can be modified to
+    /// include the appropriate servers information.
+    /// </remarks>
+    /// <remarks>
+    /// Any OpenAPI transformers registered in the <see cref="OpenApiOptions"/> instance associated with
+    /// this document will be applied to the document before it is returned.
+    /// </remarks>
+    Task<OpenApiDocument> GetOpenApiDocumentAsync(CancellationToken cancellationToken = default);
+}

+ 8 - 1
src/OpenApi/src/Services/OpenApiDocumentService.cs

@@ -38,7 +38,7 @@ internal sealed class OpenApiDocumentService(
     IHostEnvironment hostEnvironment,
     IOptionsMonitor<OpenApiOptions> optionsMonitor,
     IServiceProvider serviceProvider,
-    IServer? server = null)
+    IServer? server = null) : IOpenApiDocumentProvider
 {
     private readonly OpenApiOptions _options = optionsMonitor.Get(documentName);
     private readonly OpenApiSchemaService _componentService = serviceProvider.GetRequiredKeyedService<OpenApiSchemaService>(documentName);
@@ -744,4 +744,11 @@ internal sealed class OpenApiDocumentService(
         targetType ??= typeof(string);
         return targetType;
     }
+
+    /// <inheritdoc />
+    public Task<OpenApiDocument> GetOpenApiDocumentAsync(CancellationToken cancellationToken = default)
+    {
+        cancellationToken.ThrowIfCancellationRequested();
+        return GetOpenApiDocumentAsync(serviceProvider, httpRequest: null, cancellationToken);
+    }
 }

+ 111 - 0
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 class OpenApiServiceCollectionExtensions
         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<IHostEnvironment>(new HostingEnvironment
+        {
+            EnvironmentName = Environments.Development,
+            ApplicationName = "Test Application"
+        });
+        services.AddLogging();
+        services.AddRouting();
+
+        // Act
+        services.AddOpenApi();
+        var serviceProvider = services.BuildServiceProvider();
+
+        // Assert
+        var documentProvider = serviceProvider.GetRequiredKeyedService<IOpenApiDocumentProvider>(Microsoft.AspNetCore.OpenApi.OpenApiConstants.DefaultDocumentName);
+        Assert.NotNull(documentProvider);
+        Assert.IsType<OpenApiDocumentService>(documentProvider);
+    }
+
+    [Fact]
+    public void AddOpenApi_WithCustomDocumentName_RegistersIOpenApiDocumentProviderInterface()
+    {
+        // Arrange
+        var services = new ServiceCollection();
+        // Include dependencies for OpenApiDocumentService
+        services.AddSingleton<IHostEnvironment>(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<IOpenApiDocumentProvider>(documentName.ToLowerInvariant());
+        Assert.NotNull(documentProvider);
+        Assert.IsType<OpenApiDocumentService>(documentProvider);
+    }
+
+    [Fact]
+    public async Task GetOpenApiDocumentAsync_ReturnsDocument()
+    {
+        // Arrange
+        var services = new ServiceCollection();
+        // Include dependencies for OpenApiDocumentService
+        services.AddSingleton<IHostEnvironment>(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<IOpenApiDocumentProvider>(documentName.ToLowerInvariant());
+
+        // Act
+        var document = await documentProvider.GetOpenApiDocumentAsync();
+
+        // Assert
+        Assert.NotNull(document);
+        Assert.IsType<OpenApiDocument>(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<IHostEnvironment>(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<IOpenApiDocumentProvider>(documentName.ToLowerInvariant());
+
+        using var cts = new CancellationTokenSource();
+        cts.Cancel();
+
+        // Act & Assert
+        await Assert.ThrowsAsync<OperationCanceledException>(async () =>
+        {
+            await documentProvider.GetOpenApiDocumentAsync(cts.Token);
+        });
+    }
 }