Description
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
orIWebApplicationPublisher
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.