diff --git a/Directory.Packages.props b/Directory.Packages.props index f46c178e87..e55472577b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -14,9 +14,15 @@ - + + 6.0.0 + 8.0.0 + - + + 6.0.0 + 8.0.0 + diff --git a/src/System.CommandLine.Hosting/HostApplicationBuilderAction.cs b/src/System.CommandLine.Hosting/HostApplicationBuilderAction.cs new file mode 100644 index 0000000000..dc2ad73749 --- /dev/null +++ b/src/System.CommandLine.Hosting/HostApplicationBuilderAction.cs @@ -0,0 +1,150 @@ +#if NET8_0_OR_GREATER +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace System.CommandLine.Hosting; + +public class HostApplicationBuilderAction() : HostingAction() +{ + private new readonly Func? _createHostBuilder; + public new Action? ConfigureHost { get; set; } + + protected override IHostBuilder CreateHostBuiderCore(string[] args) + { + var hostBuilder = _createHostBuilder?.Invoke(args) ?? + new HostApplicationBuilder(args); + return new HostApplicationBuilderWrapper(hostBuilder); + } + + protected override void ConfigureHostBuilder(IHostBuilder hostBuilder) + { + base.ConfigureHostBuilder(hostBuilder); + ConfigureHost?.Invoke(GetHostApplicationBuilder(hostBuilder)); + } + + private static HostApplicationBuilder GetHostApplicationBuilder( + IHostBuilder hostBuilder + ) + { + return (HostApplicationBuilder)hostBuilder + .Properties[typeof(HostApplicationBuilder)]; + } + + private class HostApplicationBuilderWrapper( + HostApplicationBuilder hostApplicationBuilder + ) : IHostBuilder + { + private Action? _useServiceProviderFactoryAction; + private object? _configureServiceProviderBuilderAction; + + public HostBuilderContext Context { get; } = new( + ((IHostApplicationBuilder)hostApplicationBuilder).Properties + ) + { + Configuration = hostApplicationBuilder.Configuration, + HostingEnvironment = hostApplicationBuilder.Environment, + Properties = + { { typeof(HostApplicationBuilder), hostApplicationBuilder } } + }; + + public IDictionary Properties => + ((IHostApplicationBuilder)hostApplicationBuilder).Properties; + + public IHost Build() + { + _useServiceProviderFactoryAction?.Invoke(); + return hostApplicationBuilder.Build(); + } + + public IHostBuilder ConfigureHostConfiguration( + Action configureDelegate + ) + { + configureDelegate?.Invoke(hostApplicationBuilder.Configuration); + return this; + } + + public IHostBuilder ConfigureAppConfiguration( + Action configureDelegate + ) + { + SynchronizeContext(); + configureDelegate?.Invoke( + Context, + hostApplicationBuilder.Configuration + ); + SynchronizeContext(); + return this; + } + + public IHostBuilder ConfigureServices( + Action configureDelegate + ) + { + SynchronizeContext(); + configureDelegate?.Invoke(Context, hostApplicationBuilder.Services); + SynchronizeContext(); + return this; + } + + IHostBuilder IHostBuilder.UseServiceProviderFactory( + IServiceProviderFactory factory + ) + { + _useServiceProviderFactoryAction = () => + { + Action? configureDelegate = null; + if (_configureServiceProviderBuilderAction is Action configureDelegateWithContext) + { + configureDelegate = builder => + { + SynchronizeContext(); + configureDelegateWithContext(Context, builder); + SynchronizeContext(); + }; + } + hostApplicationBuilder.ConfigureContainer(factory, configureDelegate); + }; + return this; + } + + IHostBuilder IHostBuilder.UseServiceProviderFactory( + Func> factory + ) + { + _useServiceProviderFactoryAction = () => + { + Action? configureDelegate = null; + if (_configureServiceProviderBuilderAction is Action configureDelegateWithContext) + { + configureDelegate = builder => + { + SynchronizeContext(); + configureDelegateWithContext(Context, builder); + SynchronizeContext(); + }; + } + var factoryInstance = factory(Context); + hostApplicationBuilder.ConfigureContainer(factoryInstance, configureDelegate); + }; + return this; + } + + IHostBuilder IHostBuilder.ConfigureContainer( + Action configureDelegate + ) + { + _configureServiceProviderBuilderAction = configureDelegate; + return this; + } + + private void SynchronizeContext() + { + Context.Configuration = hostApplicationBuilder.Configuration; + Context.HostingEnvironment = hostApplicationBuilder.Environment; + } + } +} +#endif \ No newline at end of file diff --git a/src/System.CommandLine.Hosting/HostConfigurationDirective.cs b/src/System.CommandLine.Hosting/HostConfigurationDirective.cs new file mode 100644 index 0000000000..e316bbef72 --- /dev/null +++ b/src/System.CommandLine.Hosting/HostConfigurationDirective.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Linq; + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; + +using System.CommandLine.Parsing; + +namespace System.CommandLine.Hosting; + +public class HostConfigurationDirective() : Directive(Name) +{ + public new const string Name = "config"; + + internal static void ConfigureHostBuilder(IHostBuilder hostBuilder) + { + var parseResult = hostBuilder.GetParseResult(); + if (parseResult.Configuration.RootCommand is RootCommand rootCommand && + rootCommand.Directives.FirstOrDefault(IsConfigDirective) + is Directive configDirective && + parseResult.GetResult(configDirective) + is DirectiveResult configResult + ) + { + var configKvps = configResult.Values.Select(GetKeyValuePair) + .ToList(); + hostBuilder.ConfigureHostConfiguration( + (config) => config.AddInMemoryCollection(configKvps) + ); + } + + static bool IsConfigDirective(Directive directive) => + string.Equals(directive.Name, Name, StringComparison.OrdinalIgnoreCase); + + [Diagnostics.CodeAnalysis.SuppressMessage( + "Style", + "IDE0057: Use range operator", + Justification = ".NET Standard 2.0" + )] + static KeyValuePair GetKeyValuePair(string configDirective) + { + ReadOnlySpan kvpSpan = configDirective.AsSpan(); + int eqlIdx = kvpSpan.IndexOf('='); + string key; + string? value = default; + if (eqlIdx < 0) + key = kvpSpan.Trim().ToString(); + else + { + key = kvpSpan.Slice(0, eqlIdx).Trim().ToString(); + value = kvpSpan.Slice(eqlIdx + 1).Trim().ToString(); + } + return new KeyValuePair(key, value); + } + } +} diff --git a/src/System.CommandLine.Hosting/HostingAction.cs b/src/System.CommandLine.Hosting/HostingAction.cs index 3518ea4775..34bc7b1afa 100644 --- a/src/System.CommandLine.Hosting/HostingAction.cs +++ b/src/System.CommandLine.Hosting/HostingAction.cs @@ -1,120 +1,81 @@ -using System.Collections.Generic; -using System.CommandLine.Binding; -using System.CommandLine.Invocation; -using System.CommandLine.NamingConventionBinder; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -namespace System.CommandLine.Hosting -{ - // It's a wrapper, that configures the host, starts it and then runs the actual action. - internal sealed class HostingAction : BindingHandler - { - internal const string HostingDirectiveName = "config"; +using System.CommandLine.Invocation; - private readonly Func _hostBuilderFactory; - private readonly Action _configureHost; - private readonly AsynchronousCommandLineAction _actualAction; +namespace System.CommandLine.Hosting; - internal static void SetHandlers(Command command, Func hostBuilderFactory, Action configureHost) - { - command.Action = new HostingAction(hostBuilderFactory, configureHost, (AsynchronousCommandLineAction)command.Action); - command.TreatUnmatchedTokensAsErrors = false; // to pass unmatched Tokens to host builder factory - - foreach (Command subCommand in command.Subcommands) - { - SetHandlers(subCommand, hostBuilderFactory, configureHost); - } - } +public class HostingAction() : AsynchronousCommandLineAction() +{ + protected readonly Func? _createHostBuilder; + internal Action? ConfigureHost { get; set; } + public Action? ConfigureServices { get; set; } - private HostingAction(Func hostBuilderFactory, Action configureHost, AsynchronousCommandLineAction actualAction) + protected virtual IHostBuilder CreateHostBuiderCore(string[] args) + { + var hostBuilder = _createHostBuilder?.Invoke(args) ?? + new HostBuilder(); + return hostBuilder; + } + + protected virtual void ConfigureHostBuilder(IHostBuilder hostBuilder) + { + ConfigureHost?.Invoke(hostBuilder); + if (ConfigureServices is not null) { - _hostBuilderFactory = hostBuilderFactory; - _configureHost = configureHost; - _actualAction = actualAction; + hostBuilder.ConfigureServices(ConfigureServices); } + } - public override BindingContext GetBindingContext(ParseResult parseResult) - => _actualAction is BindingHandler bindingHandler - ? bindingHandler.GetBindingContext(parseResult) - : base.GetBindingContext(parseResult); - - public override async Task InvokeAsync(ParseResult parseResult, CancellationToken cancellationToken = default) + public override async Task InvokeAsync( + ParseResult parseResult, + CancellationToken cancellationToken = default + ) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(parseResult); +#else + _ = parseResult ?? throw new ArgumentNullException(nameof(parseResult)); +#endif + + string[] unmatchedTokens = parseResult.UnmatchedTokens?.ToArray() ?? []; + IHostBuilder hostBuilder = CreateHostBuiderCore(unmatchedTokens); + hostBuilder.Properties[typeof(ParseResult)] = parseResult; + + // As long as done before first await + // ProcessTerminationTimeout can be set to null + // so that .NET Generic Host can control console lifetime instead. + parseResult.Configuration.ProcessTerminationTimeout = null; + hostBuilder.UseConsoleLifetime(); + + hostBuilder.ConfigureServices(static (context, services) => { - var argsRemaining = parseResult.UnmatchedTokens; - var hostBuilder = _hostBuilderFactory?.Invoke(argsRemaining.ToArray()) - ?? new HostBuilder(); - hostBuilder.Properties[typeof(ParseResult)] = parseResult; - - if (parseResult.Configuration.RootCommand is RootCommand root && - root.Directives.SingleOrDefault(d => d.Name == HostingDirectiveName) is { } directive) - { - if (parseResult.GetResult(directive) is { } directiveResult) - { - hostBuilder.ConfigureHostConfiguration(config => - { - var kvpSeparator = new[] { '=' }; + var parseResult = context.GetParseResult(); + var hostingAction = parseResult.GetHostingAction(); + services.AddSingleton(parseResult); + services.AddSingleton(parseResult.Configuration); + services.AddHostedService(); + // TODO: add IHostingActionInvocation singleton + }); - config.AddInMemoryCollection(directiveResult.Values.Select(s => - { - var parts = s.Split(kvpSeparator, count: 2); - var key = parts[0]; - var value = parts.Length > 1 ? parts[1] : null; - return new KeyValuePair(key, value); - }).ToList()); - }); - } - } + ConfigureHostBuilder(hostBuilder); - var bindingContext = GetBindingContext(parseResult); - int registeredBefore = 0; - hostBuilder.UseInvocationLifetime(); - hostBuilder.ConfigureServices(services => - { - services.AddSingleton(parseResult); - services.AddSingleton(bindingContext); + using var host = hostBuilder.Build(); + await host.StartAsync(cancellationToken) + .ConfigureAwait(continueOnCapturedContext: false); - registeredBefore = services.Count; - }); + var appRunningTask = host.WaitForShutdownAsync(cancellationToken); - if (_configureHost is not null) - { - _configureHost.Invoke(hostBuilder); + // TODO: Retrieve ExecuteTask from HostingActionService to get result + Task invocationTask = Task.FromResult(0); - hostBuilder.ConfigureServices(services => - { - // "_configureHost" just registered types that might be needed in BindingContext - for (int i = registeredBefore; i < services.Count; i++) - { - Type captured = services[i].ServiceType; - bindingContext.AddService(captured, c => c.GetService().Services.GetService(captured)); - } - }); - } + await appRunningTask.ConfigureAwait(continueOnCapturedContext: false); - using var host = hostBuilder.Build(); - - bindingContext.AddService(typeof(IHost), _ => host); - - await host.StartAsync(cancellationToken); - - try - { - if (_actualAction is not null) - { - return await _actualAction.InvokeAsync(parseResult, cancellationToken); - } - - return 0; - } - finally - { - await host.StopAsync(cancellationToken); - } - } + return await invocationTask + .ConfigureAwait(continueOnCapturedContext: false); } } \ No newline at end of file diff --git a/src/System.CommandLine.Hosting/HostingActionService.cs b/src/System.CommandLine.Hosting/HostingActionService.cs new file mode 100644 index 0000000000..069fb46909 --- /dev/null +++ b/src/System.CommandLine.Hosting/HostingActionService.cs @@ -0,0 +1,65 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; + +namespace System.CommandLine.Hosting; + +internal class HostingActionService( + IHostApplicationLifetime lifetime, + IHostingActionInvocation invocation + ) : BackgroundService() +{ + public new Task? ExecuteTask => base.ExecuteTask as Task; + + protected override Task ExecuteAsync(CancellationToken stoppingToken) + { + return WaitForStartAndInvokeAsync(stoppingToken); + } + + private async Task WaitForApplicationStarted(CancellationToken cancelToken) + { + TaskCompletionSource appStarted = new(); + using var startedReg = lifetime.ApplicationStarted + .Register(SetTaskComplete, appStarted); + using var preStartCancelReg = cancelToken + .Register(SetTaskCanceled, appStarted); + + await appStarted.Task + .ConfigureAwait(continueOnCapturedContext: false); + + static void SetTaskComplete(object? state) + { + var tcs = (TaskCompletionSource)state!; + tcs.TrySetResult(default); + } + + static void SetTaskCanceled(object? state) + { + var tcs = (TaskCompletionSource)state!; + tcs.TrySetCanceled( + CancellationToken.None + ); + } + } + + private async Task WaitForStartAndInvokeAsync(CancellationToken cancelToken) + { + await WaitForApplicationStarted(cancelToken) + .ConfigureAwait(continueOnCapturedContext: false); + try + { + int result = await invocation.InvokeAsync(cancelToken) + .ConfigureAwait(continueOnCapturedContext: false); + return result; + } + finally + { + // If the application is not already shut down or shutting down, + // make sure that application is shutting down now. + if (!lifetime.ApplicationStopping.IsCancellationRequested) + { + lifetime.StopApplication(); + } + } + } +} \ No newline at end of file diff --git a/src/System.CommandLine.Hosting/HostingExtensions.cs b/src/System.CommandLine.Hosting/HostingExtensions.cs index 1266704011..2057060f3d 100644 --- a/src/System.CommandLine.Hosting/HostingExtensions.cs +++ b/src/System.CommandLine.Hosting/HostingExtensions.cs @@ -1,97 +1,407 @@ -using System.CommandLine.Binding; -using System.CommandLine.Invocation; -using System.CommandLine.NamingConventionBinder; -using CommandHandler = System.CommandLine.NamingConventionBinder.CommandHandler; - -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Options; + +using System.CommandLine.Parsing; namespace System.CommandLine.Hosting { public static class HostingExtensions { - public static CommandLineConfiguration UseHost( - this CommandLineConfiguration config, - Func hostBuilderFactory, - Action configureHost = null) + public static ParseResult GetParseResult(this IHostBuilder hostBuilder) { - if (config.RootCommand is RootCommand root) - { - root.Add(new Directive(HostingAction.HostingDirectiveName)); - } + _ = hostBuilder ?? throw new ArgumentNullException(nameof(hostBuilder)); - HostingAction.SetHandlers(config.RootCommand, hostBuilderFactory, configureHost); + if (hostBuilder.Properties.TryGetValue(typeof(ParseResult), out var ctxObj) && + ctxObj is ParseResult invocationContext) + return invocationContext; - return config; + throw new InvalidOperationException("Host builder has no command-line parse result registered to it."); } - public static CommandLineConfiguration UseHost( - this CommandLineConfiguration config, - Action configureHost = null - ) => UseHost(config, null, configureHost); + public static ParseResult GetParseResult(this HostBuilderContext context) + { + _ = context ?? throw new ArgumentNullException(nameof(context)); - public static IHostBuilder UseInvocationLifetime(this IHostBuilder host, Action configureOptions = null) + if (context.Properties.TryGetValue(typeof(ParseResult), out var ctxObj) && + ctxObj is ParseResult invocationContext) + return invocationContext; + + throw new InvalidOperationException("Host builder context has no command-line parse result registered to it."); + } + + internal static HostingAction GetHostingAction(this ParseResult parseResult) { - return host.ConfigureServices(services => + if (!parseResult.TryGetHostingAction(out var hostingAction)) + throw new InvalidOperationException("Command-line parse result is not for a command with an associated .NET Generic Host command-line action."); +#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER + return hostingAction; +#else + return hostingAction!; +#endif + } + + internal static bool TryGetHostingAction( + this ParseResult parseResult, +#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER + [Diagnostics.CodeAnalysis.NotNullWhen(returnValue: true)] +#endif + out HostingAction? hostingAction + ) + { + hostingAction = parseResult.CommandResult.Command.Action + as HostingAction; + return hostingAction is not null; + } + + private static void ConfigureHostBuilderOnSymbolValidation( + this Symbol symbol, + Action< + IHostBuilder, + SymbolResult + > configureHostBuilderAction + ) + { + switch (symbol) { - services.AddSingleton(); - if (configureOptions is Action) - services.Configure(configureOptions); - }); + case Argument argument: + argument.Validators.Add(SymbolResultAction); + break; + case Option option: + option.Validators.Add(SymbolResultAction); + break; + case Command command: + command.Validators.Add(SymbolResultAction); + break; + default: + throw new InvalidOperationException(); + } + + void SymbolResultAction(SymbolResult symbolResult) + { + CommandResult? commandResult = null; + // Find nearest parent symbol result starting from current, + // stop when a CommandResult is found + for (SymbolResult? parentResult = symbolResult; + parentResult is not null && + (commandResult = parentResult as CommandResult) is null; + parentResult = parentResult.Parent) + { } + + // No CommandResult was found, strange but nothing to do here. + if (commandResult is null) return; + + // CommandResult was found, but Command action is not a + // .NET Generic Host action, so nothing to do in that case + if (commandResult.Command.Action is not HostingAction hostingAction) + { return; } + + // .NET Generic Host action identitfied, + // register Model Binder configuration action + hostingAction.ConfigureHost += hostBuilder => + configureHostBuilderAction(hostBuilder, symbolResult); + } } - public static OptionsBuilder BindCommandLine( - this OptionsBuilder optionsBuilder) - where TOptions : class + private static void ConfigureSymbolOptionsServices( + this Symbol symbol, + Action optionsInstanceApplyValueAction, + Action< + HostBuilderContext, + IServiceCollection, + Action + > configureServicesAction + ) where TOptions : class { - if (optionsBuilder is null) - throw new ArgumentNullException(nameof(optionsBuilder)); - return optionsBuilder.Configure((opts, serviceProvider) => + symbol.ConfigureHostBuilderOnSymbolValidation(ConfigureHostBuilder); + + void ConfigureHostBuilder( + IHostBuilder hostBuilder, + SymbolResult symbolResult + ) { - var modelBinder = serviceProvider - .GetService>() - ?? new ModelBinder(); - var bindingContext = serviceProvider.GetRequiredService(); - modelBinder.UpdateInstance(opts, bindingContext); - }); + hostBuilder.ConfigureServices( + (context, services) => + ConfigureServices(context, services, symbolResult) + ); + } + + void ConfigureServices( + HostBuilderContext context, + IServiceCollection services, + SymbolResult symbolResult + ) + { + configureServicesAction(context, services, options => + { + TValue? symbolValue = symbolResult switch + { + ArgumentResult { Argument: Argument argument } => + symbolResult.GetValue(argument), + OptionResult { Option: Option option } => + symbolResult.GetValue(option), + _ => throw new InvalidOperationException() + }; + optionsInstanceApplyValueAction(options, symbolValue); + }); + } } - public static Command UseCommandHandler(this Command command) - where THandler : CommandLineAction + public static Option ConfigureOptionsInstance( + this Option option, + string? optionsName, + Action optionsInstanceApplyValueAction + ) where TOptions : class { - command.Action = CommandHandler.Create(typeof(THandler).GetMethod(nameof(AsynchronousCommandLineAction.InvokeAsync))); +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(option); +#else + _ = option ?? throw new ArgumentNullException(nameof(option)); +#endif + option.ConfigureSymbolOptionsServices( + optionsInstanceApplyValueAction, + (_, services, configureAction) => services + .Configure(optionsName, configureAction) + ); + return option; + } - return command; + public static Argument ConfigureOptionsInstance( + this Argument argument, + string? optionsName, + Action optionsInstanceApplyValueAction + ) where TOptions : class + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(argument); +#else + _ = argument ?? throw new ArgumentNullException(nameof(argument)); +#endif + argument.ConfigureSymbolOptionsServices( + optionsInstanceApplyValueAction, + (_, services, configureAction) => services + .Configure(optionsName, configureAction) + ); + return argument; } - public static ParseResult GetParseResult(this IHostBuilder hostBuilder) + public static Option ConfigureOptionsInstance( + this Option option, + Action optionsInstanceApplyValueAction + ) where TOptions : class { - _ = hostBuilder ?? throw new ArgumentNullException(nameof(hostBuilder)); +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(option); +#else + _ = option ?? throw new ArgumentNullException(nameof(option)); +#endif + option.ConfigureSymbolOptionsServices( + optionsInstanceApplyValueAction, + (_, services, configureAction) => services + .Configure(configureAction) + ); + return option; + } - if (hostBuilder.Properties.TryGetValue(typeof(ParseResult), out var ctxObj) && - ctxObj is ParseResult invocationContext) - return invocationContext; + public static Argument ConfigureOptionsInstance( + this Argument argument, + Action optionsInstanceApplyValueAction + ) where TOptions : class + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(argument); +#else + _ = argument ?? throw new ArgumentNullException(nameof(argument)); +#endif + argument.ConfigureSymbolOptionsServices( + optionsInstanceApplyValueAction, + (_, services, configureAction) => services + .Configure(configureAction) + ); + return argument; + } - throw new InvalidOperationException("Host builder has no Invocation Context registered to it."); + public static Option ConfigureAllOptionsInstances( + this Option option, + Action optionsInstanceApplyValueAction + ) where TOptions : class + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(option); +#else + _ = option ?? throw new ArgumentNullException(nameof(option)); +#endif + option.ConfigureSymbolOptionsServices( + optionsInstanceApplyValueAction, + (_, services, configureAction) => services + .ConfigureAll(configureAction) + ); + return option; } - public static ParseResult GetParseResult(this HostBuilderContext context) + public static Argument ConfigureAllOptionsInstances( + this Argument argument, + Action optionsInstanceApplyValueAction + ) where TOptions : class { - _ = context ?? throw new ArgumentNullException(nameof(context)); +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(argument); +#else + _ = argument ?? throw new ArgumentNullException(nameof(argument)); +#endif + argument.ConfigureSymbolOptionsServices( + optionsInstanceApplyValueAction, + (_, services, configureAction) => services + .ConfigureAll(configureAction) + ); + return argument; + } - if (context.Properties.TryGetValue(typeof(ParseResult), out var ctxObj) && - ctxObj is ParseResult invocationContext) - return invocationContext; + public static Option PostConfigureOptionsInstance( + this Option option, + string? optionsName, + Action optionsInstanceApplyValueAction + ) where TOptions : class + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(option); +#else + _ = option ?? throw new ArgumentNullException(nameof(option)); +#endif + option.ConfigureSymbolOptionsServices( + optionsInstanceApplyValueAction, + (_, services, configureAction) => services + .PostConfigure(optionsName, configureAction) + ); + return option; + } + + public static Argument PostConfigureOptionsInstance( + this Argument argument, + string? optionsName, + Action optionsInstanceApplyValueAction + ) where TOptions : class + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(argument); +#else + _ = argument ?? throw new ArgumentNullException(nameof(argument)); +#endif + argument.ConfigureSymbolOptionsServices( + optionsInstanceApplyValueAction, + (_, services, configureAction) => services + .PostConfigure(optionsName, configureAction) + ); + return argument; + } + + public static Option PostConfigureOptionsInstance( + this Option option, + Action optionsInstanceApplyValueAction + ) where TOptions : class + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(option); +#else + _ = option ?? throw new ArgumentNullException(nameof(option)); +#endif + option.ConfigureSymbolOptionsServices( + optionsInstanceApplyValueAction, + (_, services, configureAction) => services + .PostConfigure(configureAction) + ); + return option; + } + + public static Argument PostConfigureOptionsInstance( + this Argument argument, + Action optionsInstanceApplyValueAction + ) where TOptions : class + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(argument); +#else + _ = argument ?? throw new ArgumentNullException(nameof(argument)); +#endif + argument.ConfigureSymbolOptionsServices( + optionsInstanceApplyValueAction, + (_, services, configureAction) => services + .PostConfigure(configureAction) + ); + return argument; + } - throw new InvalidOperationException("Host builder has no Invocation Context registered to it."); + public static Option PostConfigureAllOptionsInstances( + this Option option, + Action optionsInstanceApplyValueAction + ) where TOptions : class + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(option); +#else + _ = option ?? throw new ArgumentNullException(nameof(option)); +#endif + option.ConfigureSymbolOptionsServices( + optionsInstanceApplyValueAction, + (_, services, configureAction) => services + .PostConfigureAll(configureAction) + ); + return option; + } + + public static Argument PostConfigureAllOptionsInstances( + this Argument argument, + Action optionsInstanceApplyValueAction + ) where TOptions : class + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(argument); +#else + _ = argument ?? throw new ArgumentNullException(nameof(argument)); +#endif + argument.ConfigureSymbolOptionsServices( + optionsInstanceApplyValueAction, + (_, services, configureAction) => services + .PostConfigureAll(configureAction) + ); + return argument; + } + + public static Command ConfigureHostBuilder( + this Command command, + Action hostBuilderAction + ) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(command); +#else + _ = command ?? throw new ArgumentNullException(nameof(command)); +#endif + command.ConfigureHostBuilderOnSymbolValidation( + (hostBuilder, symbolResult) => hostBuilderAction?.Invoke( + hostBuilder, + (CommandResult)symbolResult + ) + ); + return command; } - public static IHost GetHost(this ParseResult parseResult) + public static RootCommand ConfigureHostBuilder( + this RootCommand rootCommand, + Action hostBuilderAction + ) { - _ = parseResult ?? throw new ArgumentNullException(paramName: nameof(parseResult)); - var hostModelBinder = new ModelBinder(); - return (IHost)hostModelBinder.CreateInstance(parseResult.GetBindingContext()); +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(rootCommand); +#else + _ = rootCommand ?? throw new ArgumentNullException(nameof(rootCommand)); +#endif + rootCommand.ConfigureHostBuilderOnSymbolValidation( + (hostBuilder, symbolResult) => hostBuilderAction?.Invoke( + hostBuilder, + (CommandResult)symbolResult + ) + ); + return rootCommand; } } } diff --git a/src/System.CommandLine.Hosting/IHostingActionInvocation.cs b/src/System.CommandLine.Hosting/IHostingActionInvocation.cs new file mode 100644 index 0000000000..201bfa078c --- /dev/null +++ b/src/System.CommandLine.Hosting/IHostingActionInvocation.cs @@ -0,0 +1,9 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace System.CommandLine.Hosting; + +public interface IHostingActionInvocation +{ + Task InvokeAsync(CancellationToken cancelToken = default); +} diff --git a/src/System.CommandLine.Hosting/InvocationLifetime.cs b/src/System.CommandLine.Hosting/InvocationLifetime.cs deleted file mode 100644 index be974776ac..0000000000 --- a/src/System.CommandLine.Hosting/InvocationLifetime.cs +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System.Threading; -using System.Threading.Tasks; - -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; - -#if NETSTANDARD2_0 -using IHostEnvironment = Microsoft.Extensions.Hosting.IHostingEnvironment; -using IHostApplicationLifetime = Microsoft.Extensions.Hosting.IApplicationLifetime; -#endif - -namespace System.CommandLine.Hosting -{ - public class InvocationLifetime : IHostLifetime - { - private CancellationTokenRegistration invokeCancelReg; - private CancellationTokenRegistration appStartedReg; - private CancellationTokenRegistration appStoppingReg; - - public InvocationLifetime( - IOptions options, - IHostEnvironment environment, - IHostApplicationLifetime applicationLifetime, - ILoggerFactory loggerFactory = null) - { - Options = options?.Value ?? throw new ArgumentNullException(nameof(options)); - Environment = environment - ?? throw new ArgumentNullException(nameof(environment)); - ApplicationLifetime = applicationLifetime - ?? throw new ArgumentNullException(nameof(applicationLifetime)); - - Logger = (loggerFactory ?? NullLoggerFactory.Instance) - .CreateLogger("Microsoft.Hosting.Lifetime"); - } - - public InvocationLifetimeOptions Options { get; } - private ILogger Logger { get; } - public IHostEnvironment Environment { get; } - public IHostApplicationLifetime ApplicationLifetime { get; } - - public Task WaitForStartAsync(CancellationToken cancellationToken) - { - if (!Options.SuppressStatusMessages) - { - appStartedReg = ApplicationLifetime.ApplicationStarted.Register(state => - { - ((InvocationLifetime)state).OnApplicationStarted(); - }, this); - appStoppingReg = ApplicationLifetime.ApplicationStopping.Register(state => - { - ((InvocationLifetime)state).OnApplicationStopping(); - }, this); - } - - // The token comes from HostingAction.InvokeAsync - // and it's the invocation cancellation token. - invokeCancelReg = cancellationToken.Register(state => - { - ((InvocationLifetime)state).OnInvocationCancelled(); - }, this); - - return Task.CompletedTask; - } - - public Task StopAsync(CancellationToken cancellationToken) - { - // There's nothing to do here - return Task.CompletedTask; - } - - private void OnInvocationCancelled() - { - ApplicationLifetime.StopApplication(); - } - - private void OnApplicationStarted() - { - Logger.LogInformation("Application started. Press Ctrl+C to shut down."); - Logger.LogInformation("Hosting environment: {envName}", Environment.EnvironmentName); - Logger.LogInformation("Content root path: {contentRoot}", Environment.ContentRootPath); - } - - private void OnApplicationStopping() - { - Logger.LogInformation("Application is shutting down..."); - } - - public void Dispose() - { - invokeCancelReg.Dispose(); - appStartedReg.Dispose(); - appStoppingReg.Dispose(); - } - } -} diff --git a/src/System.CommandLine.Hosting/InvocationLifetimeOptions.cs b/src/System.CommandLine.Hosting/InvocationLifetimeOptions.cs deleted file mode 100644 index 7d8f1c1946..0000000000 --- a/src/System.CommandLine.Hosting/InvocationLifetimeOptions.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Microsoft.Extensions.Hosting; - -namespace System.CommandLine.Hosting -{ - public class InvocationLifetimeOptions : ConsoleLifetimeOptions - { - } -} diff --git a/src/System.CommandLine.Hosting/System.CommandLine.Hosting.csproj b/src/System.CommandLine.Hosting/System.CommandLine.Hosting.csproj index f8e403ebf6..e5642b918a 100644 --- a/src/System.CommandLine.Hosting/System.CommandLine.Hosting.csproj +++ b/src/System.CommandLine.Hosting/System.CommandLine.Hosting.csproj @@ -1,9 +1,11 @@ + 12 true netstandard2.0;netstandard2.1;$(TargetFrameworkForNETSDK) This package provides support for using System.CommandLine with Microsoft.Extensions.Hosting. + enable @@ -20,7 +22,6 @@ -