Skip to content

Add IOpenApiDocumentProvider interface and implementation #61463

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ private static IServiceCollection AddOpenApiCore(this IServiceCollection service
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
Expand Down
2 changes: 2 additions & 0 deletions src/OpenApi/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -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?
Expand Down
30 changes: 30 additions & 0 deletions src/OpenApi/src/Services/IOpenApiDocumentProvider.cs
Original file line number Diff line number Diff line change
@@ -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);
}
9 changes: 8 additions & 1 deletion src/OpenApi/src/Services/OpenApiDocumentService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -744,4 +744,11 @@ private static Type GetTargetType(ApiDescription description, ApiParameterDescri
targetType ??= typeof(string);
return targetType;
}

/// <inheritdoc />
public Task<OpenApiDocument> GetOpenApiDocumentAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
return GetOpenApiDocumentAsync(serviceProvider, httpRequest: null, cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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<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);
});
}
}
Loading