Skip to content

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

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

Closed
captainsafia opened this issue Mar 20, 2025 · 5 comments
Labels
api-approved API was approved in API review, it can be implemented area-hosting Includes Hosting

Comments

@captainsafia
Copy link
Member

captainsafia commented Mar 20, 2025

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.

@captainsafia captainsafia added the api-suggestion Early API idea and discussion, it is NOT ready for implementation label Mar 20, 2025
@ghost ghost added the area-web-frameworks label Mar 20, 2025
@captainsafia captainsafia added api-ready-for-review API is ready for formal API review - https://github.com/dotnet/apireviews and removed area-web-frameworks labels Mar 20, 2025
Copy link
Contributor

Thank you for submitting this for API review. This will be reviewed by @dotnet/aspnet-api-review at the next meeting of the ASP.NET Core API Review group. Please ensure you take a look at the API review process documentation and ensure that:

  • The PR contains changes to the reference-assembly that describe the API change. Or, you have included a snippet of reference-assembly-style code that illustrates the API change.
  • The PR describes the impact to users, both positive (useful new APIs) and negative (breaking changes).
  • Someone is assigned to "champion" this change in the meeting, and they understand the impact and design of the change.

@halter73
Copy link
Member

namespace Microsoft.AspNetCore.Http.Abstractions

Nit: This should just be Microsoft.AspNetCore.Http.

without listening on an HTTP server.

Would this just prevent the IServer/GenericWebHostService from starting, or would it essentially prevent the entire host and any hosted services from starting?


Also, could this also be used to implement things like EF Core tooling?

I'm not saying we'd actually do that since dotnet ef already exists, but I wonder if we should pick a more generic name and assembly if "publishers" can do more than publish OpenAPI documents. An IApiPublisher interface in Microsoft.AspNetCore.Http seems too OpenAPI specific considering what this is capable of.

Given the shape of the API, I wonder if we could add an IPublisher interface to the Microsoft.Extensions.Hosting.Abstractions assembly and support it in all of our "generic" hosts including the ones returned by HostApplicationBuilder and new HostBuilder() putting it on equal footing with IHostedService.

@captainsafia
Copy link
Member Author

Would this just prevent the IServer/GenericWebHostService from starting, or would it essentially prevent the entire host and any hosted services from starting?

The former. This just prevents the IServer/GenericWebHostService from starting. The actual mechanism that discovers and executes the IApiPublisher implementations is a hosted service itself.

Also, could this also be used to implement things like EF Core tooling?

I'm not totally familiar with how the EF Core tooling works but this can be used to replace anything that relies on HostFactoryResolver + no-op IServer implementation for its core behavior at the moment.

An IApiPublisher interface in Microsoft.AspNetCore.Http seems too OpenAPI specific considering what this is capable of.

FWIW, I put the type in the abstractions assembly in the shared framework instead of the Microsoft.AspNetCore.OpenApi package since it's generally applicable.

Given the shape of the API, I wonder if we could add an IPublisher interface to the Microsoft.Extensions.Hosting.Abstractions assembly and support it in all of our "generic" hosts including the ones returned by HostApplicationBuilder and new HostBuilder() putting it on equal footing with IHostedService.

I haven't gone this far down with abstracting this API. I can see this make sense for certain builders where there is a "live" mode for the application (like running an HTTP server and listening on ports) and a "static" mode but I haven't reasoned through how that shape would look for all host types.

@captainsafia captainsafia added area-hosting Includes Hosting and removed api-suggestion Early API idea and discussion, it is NOT ready for implementation labels Mar 21, 2025
@halter73
Copy link
Member

halter73 commented Apr 7, 2025

API Review Notes:

  • More discussion about whether the publisher could move to the generic Host.cs
    • Still unresolved, but also considered moving it into the OpenApi package using hosted services.
  • Why does ApiPublisherContext.Services exist? Can't you constructor inject it?
  • Do we need ApiPublisherContext?
  • What about taking the HttpRequest as an extension method to IOpenApiDocumentProvider
    namespace Microsoft.AspNetCore.OpenApi;
    
    public interface IOpenApiDocumentProvider
    {
      Task<OpenApiDocument> GetOpenApiDocumentAsync(string serverUri = null, CancellationToken cancellationToken = default)
    }
    
    public static class OpenApiDocumentProviderExtensions
    {
      Task<OpenApiDocument> GetOpenApiDocumentAsync(this IOpenApiDocumentProvider documentProvider, HttpRequest httpRequest, CancellationToken cancellationToken = default)
    }
  • It turns out, we don't need to take the serverUri at all

API Approved! (No publisher!)

// Assembly: Microsoft.AspNetCore.OpenApi

namespace Microsoft.AspNetCore.OpenApi;

public interface IOpenApiDocumentProvider
{
    Task<OpenApiDocument> GetOpenApiDocumentAsync(CancellationToken cancellationToken = default)
}

@halter73 halter73 added api-approved API was approved in API review, it can be implemented and removed api-ready-for-review API is ready for formal API review - https://github.com/dotnet/apireviews labels Apr 7, 2025
@halter73 halter73 changed the title Support executing IApiPublisher implementors during application run Support ~executing IApiPublisher implementors during application run~ IOpenApiDocumentProvider Apr 7, 2025
@halter73 halter73 changed the title Support ~executing IApiPublisher implementors during application run~ IOpenApiDocumentProvider Support IOpenApiDocumentProvider (was executing IApiPublisher implementors during application run) Apr 7, 2025
@MackinnonBuck
Copy link
Member

Closing as this was addressed in #61463

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-approved API was approved in API review, it can be implemented area-hosting Includes Hosting
Projects
None yet
Development

No branches or pull requests

3 participants