Skip to content

Support IOpenApiDocumentProvider (was executing IApiPublisher implementors during application run) #61031

Closed
@captainsafia

Description

@captainsafia

Background and Motivation

Currently, if a user wants to run the startup logic associated with an application without launching an actual application service, they must use the HostFactoryResolver to launch the application and inject a no-op IServer implementation into the DI container. This helps users achieve the desired goal of launching the application without actually listening to HTTP requests and is a helpful vector for entites that want to execute some logic on the state of the application. One of our most popular scenarios for this is the need to launch the application in order to resolve endpoints that should be mapped to an OpenAPI document.

This proposal takes inspiration from Aspire's IDistributedApplicationPublisher interface and infrastructure to provide a more streamlined API for interacting with the application without listening on an HTTP server.

Proposed API

// Assembly: Microsoft.AspNetCore.Http.Abstractions

namespace Microsoft.AspNetCore.Http.Abstractions
{
  public interface IApiPublisher
  {
      Task PublishAsync(ApiPublisherContext context, CancellationToken cancellationToken = default);
  }
}
// Assembly: Microsoft.AspNetCore.Http.Abstractions

namespace Microsoft.AspNetCore.Http.Abstractions
{
  public sealed class ApiPublisherContext
  {
      public string[]? Args { get; init; }
      public IServiceProvider ApplicationServices { get; init; }
  }
}

Although the IApiPublisher feature is not specific to OpenAPI support, the following API addition is proposed to make it easier to interact with the in-memory OpenAPI document from IApiPublisher implementations.

// Assembly: Microsoft.AspNetCore.OpenApi

namespace Microsoft.AspNetCore.OpenApi
{
  public interface IOpenApiDocumentProvider
  {
    Task<OpenApiDocument> GetOpenApiDocumentAsync(IServiceProvider serviceProvider, HttpRequest? httpRequest = null, CancellationToken cancellationToken = default)
  }
}

Usage Examples

namespace Microsoft.AspNetCore.OpenApi;

services.AddKeyedSingleton<IApiPublisher, OpenApiPublisher>("openapi");

internal sealed class OpenApiPublisher(ILogger<OpenApiPublisher> logger) : IApiPublisher
{
    public async Task PublishAsync(ApiPublisherContext context, CancellationToken cancellationToken = default)
    {
        var documentServices = context.ApplicationServices.GetServices<NamedService<OpenApiDocumentService>>();
        foreach (var documentService in documentServices)
        {
            logger.LogInformation("Publishing OpenAPI document '{DocumentName}'", documentService.Name);
            // Resolve the required services using a case-insensitive document name.
            var documentName = documentService.Name.ToLowerInvariant();
            var openApiDocumentService = context.ApplicationServices.GetRequiredKeyedService<IOpenApiDocumentProvider>(documentName);
            var options = context.ApplicationServices.GetRequiredService<IOptionsMonitor<OpenApiOptions>>();
            var namedOption = options.Get(documentName);
            var resolvedOpenApiVersion = namedOption.OpenApiVersion;

            // Get the OpenAPI document using a scoped service provider to support
            // correct scopes for transformers registered on the document.
            using var scopedService = context.ApplicationServices.CreateScope();
            var document = await openApiDocumentService.GetOpenApiDocumentAsync(scopedService.ServiceProvider, null, cancellationToken);

            // Serialize the OpenAPI document to disk.
            var path = $"{documentName}.json";
            using var fileStream = new FileStream(path, FileMode.Create);
            using var writer = new StreamWriter(fileStream);
            var jsonWriter = new OpenApiJsonWriter(writer);
            await document.SerializeAsync(jsonWriter, resolvedOpenApiVersion, cancellationToken);
            await writer.FlushAsync(cancellationToken);

            writer.Dispose();
            fileStream.Dispose();
        }
    }
}
$ dotnet run --publisher openapi
Using launch settings from /Users/captainsafia/git/aspnetcore/api-publishers/src/OpenApi/sample/Properties/launchSettings.json...
Building...
info: Microsoft.AspNetCore.OpenApi.OpenApiPublisher[0]
      Publishing OpenAPI document 'v1'
info: Microsoft.AspNetCore.OpenApi.OpenApiPublisher[0]
      Publishing OpenAPI document 'v2'
info: Microsoft.AspNetCore.OpenApi.OpenApiPublisher[0]
      Publishing OpenAPI document 'controllers'
info: Microsoft.AspNetCore.OpenApi.OpenApiPublisher[0]
      Publishing OpenAPI document 'responses'
info: Microsoft.AspNetCore.OpenApi.OpenApiPublisher[0]
      Publishing OpenAPI document 'forms'
info: Microsoft.AspNetCore.OpenApi.OpenApiPublisher[0]
      Publishing OpenAPI document 'schemas-by-ref'
info: Microsoft.AspNetCore.OpenApi.OpenApiPublisher[0]
      Publishing OpenAPI document 'xml'

Alternative Designs

  • We can consider naming this IApplicationPublisher or IWebApplicationPublisher since none of the behavior is strictly API specific.
  • We can consider adding an ApplicationRunMode enum and property that allows users to inspect whether an application is running in publish or server mode.

Risks

Although users can do this currently with a HostFactoryResolver + no-op IServer implementation, this does make it much easier to launch an ASP.NET Core app without listening on any ports which might expose uninteded behavior in certain scenarios that assume a server is running.

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-approvedAPI was approved in API review, it can be implementedarea-hostingIncludes Hosting

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions