Description
My team maintains a bunch of http apis using asp core. We've added a cli interface to each app because tho starting kestrel is the main case, there are several others, for example
- configure backing services (eg, an auth provider)
- run migrations
- self-check: is all necessary configuration available?
Some are only invoked when running on a local dev machine, others are used in deploy pipelines.
The included System.CommandLine.Hosting.HostingExtensions has a UseHost
for an IHostBuilder
, but the host build object I have is an WebApplicationBuilder
. Building the entire host is somewhat expensive, to be sure. But it's useful enough to be worth it, and the things we're running need most of the app anyway.
So I copied the UseHost function and swapped in the WebApplicationBuilder type. Mostly it's the same, except that the host is neither stopped nor started. I want the command handlers to decide whether or not to start the host! My RunWebApi handler does start the host, but my SelfCheck handler only needs the built host's ServicesCollection.
Does it make sense to add this extension method to the library? As I wrote this out, I realized that maybe IHostApplicationBuilder is a better type than WebApplicationBuilder, but the idea stands.
using System.CommandLine.Builder;
using System.CommandLine.Invocation;
namespace System.CommandLine.Hosting;
public static class HostingExtensions
{
private const string ConfigurationDirectiveName = "config";
public static CommandLineBuilder UseHost(this CommandLineBuilder builder,
Func<string[], WebApplicationBuilder> hostBuilderFactory,
Action<WebApplicationBuilder>? configureHost = null) =>
builder.AddMiddleware(async (invocation, next) =>
{
var argsRemaining = invocation.ParseResult.UnparsedTokens.ToArray();
var hostBuilder = hostBuilderFactory.Invoke(argsRemaining);
hostBuilder.Host.Properties[typeof(InvocationContext)] = invocation;
hostBuilder.Host.ConfigureHostConfiguration(config =>
{
config.AddCommandLineDirectives(invocation.ParseResult, ConfigurationDirectiveName);
});
hostBuilder.Host.ConfigureServices(services =>
{
services.AddSingleton(invocation);
services.AddSingleton(invocation.BindingContext);
services.AddSingleton(invocation.Console);
services.AddTransient(_ => invocation.InvocationResult);
services.AddTransient(_ => invocation.ParseResult);
});
hostBuilder.Host.UseInvocationLifetime(invocation);
configureHost?.Invoke(hostBuilder);
using var host = hostBuilder.Build();
invocation.BindingContext.AddService(typeof(WebApplication), _ => host);
// await host.StartAsync(); // no: let cli handlers decide if they want to start the host
await next(invocation);
// await host.StopAsync(); // don't stop what wasn't started
});