From 2a9ec39ee69e397341bed95dcae1fa7fe0f47c32 Mon Sep 17 00:00:00 2001 From: Will Rogers Date: Fri, 26 Aug 2022 14:10:49 -0400 Subject: [PATCH 1/5] Set version to '3.0-alpha' --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index d2045a4..bb8a947 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "2.1-alpha", + "version": "3.0-alpha", "gitCommitIdPrefix": "ci.", "nuGetPackageVersion": { "semVer": 2.0 From 9b1b174af0837b1279892c1e5b41fdef848798ea Mon Sep 17 00:00:00 2001 From: Will Rogers Date: Fri, 26 Aug 2022 14:12:47 -0400 Subject: [PATCH 2/5] Update packages --- XO.Console.Cli.Extensions/XO.Console.Cli.Extensions.csproj | 2 +- XO.Console.Cli.Tests/XO.Console.Cli.Tests.csproj | 4 ++-- XO.Console.Cli/XO.Console.Cli.csproj | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/XO.Console.Cli.Extensions/XO.Console.Cli.Extensions.csproj b/XO.Console.Cli.Extensions/XO.Console.Cli.Extensions.csproj index 22a894e..e103aee 100644 --- a/XO.Console.Cli.Extensions/XO.Console.Cli.Extensions.csproj +++ b/XO.Console.Cli.Extensions/XO.Console.Cli.Extensions.csproj @@ -27,7 +27,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/XO.Console.Cli.Tests/XO.Console.Cli.Tests.csproj b/XO.Console.Cli.Tests/XO.Console.Cli.Tests.csproj index 0bf0f1a..c1dfcd4 100644 --- a/XO.Console.Cli.Tests/XO.Console.Cli.Tests.csproj +++ b/XO.Console.Cli.Tests/XO.Console.Cli.Tests.csproj @@ -10,8 +10,8 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/XO.Console.Cli/XO.Console.Cli.csproj b/XO.Console.Cli/XO.Console.Cli.csproj index 3d80fd5..61e72d3 100644 --- a/XO.Console.Cli/XO.Console.Cli.csproj +++ b/XO.Console.Cli/XO.Console.Cli.csproj @@ -23,7 +23,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive From f24d7be922e18322bfc9be7d6c97961a10e03f0e Mon Sep 17 00:00:00 2001 From: Will Rogers Date: Fri, 26 Aug 2022 14:41:49 -0400 Subject: [PATCH 3/5] Refactor hosting extensions to resolve ICommandApp as a service Resolve the configured application from the dependency injection container, which enables consumers to do the same. * Replace ConfigureCommandApp(IHostBuilder) extension methods with AddCommandApp(IServiceCollection). The new extension methods add ICommandApp as a singleton service and configure CommandAppBuilderOptions. * Call AddCommandApp() from the IHostBuilder convenience extensions that configure, build, and run the application. * Change all 'configure' parameter types to Action for consistency. * Remove IHost extension methods that configure ICommandAppBuilder. You must configure the app with AddCommandApp() or the all-in-one IHostBuilder extension methods. * Resolve ICommandApp from the dependency injection container. --- .../CommandAppBuilderOptions.cs | 12 ++- .../CommandAppFactory.cs | 28 +++++++ .../CommandAppHostBuilderExtensions.cs | 51 ++++++------ .../CommandAppHostExtensions.cs | 61 ++------------ .../CommandAppServiceCollectionExtensions.cs | 83 +++++++++++++++++++ 5 files changed, 157 insertions(+), 78 deletions(-) create mode 100644 XO.Console.Cli.Extensions/CommandAppFactory.cs create mode 100644 XO.Console.Cli.Extensions/CommandAppServiceCollectionExtensions.cs diff --git a/XO.Console.Cli.Extensions/CommandAppBuilderOptions.cs b/XO.Console.Cli.Extensions/CommandAppBuilderOptions.cs index c975dc3..bc439b4 100644 --- a/XO.Console.Cli.Extensions/CommandAppBuilderOptions.cs +++ b/XO.Console.Cli.Extensions/CommandAppBuilderOptions.cs @@ -3,10 +3,20 @@ namespace XO.Console.Cli; /// -/// Configures the via . +/// Configures the . /// public sealed class CommandAppBuilderOptions { + /// + /// A delegate that will be called to create the . + /// + /// + /// Used to configure the factory method that will be called to initialize the . The + /// default factory method is . To configure a default command, set an + /// appropriate factory method; for example, . + /// + public Func CommandAppBuilderFactory { get; set; } = CommandAppBuilder.Create; + /// /// A list of delegates that configure . /// diff --git a/XO.Console.Cli.Extensions/CommandAppFactory.cs b/XO.Console.Cli.Extensions/CommandAppFactory.cs new file mode 100644 index 0000000..d0a7353 --- /dev/null +++ b/XO.Console.Cli.Extensions/CommandAppFactory.cs @@ -0,0 +1,28 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; + +namespace XO.Console.Cli; + +internal static class CommandAppFactory +{ + public static ICommandApp BuildCommandApp(IServiceProvider services) + { + var context = services.GetRequiredService(); + var lifetime = services.GetRequiredService(); + var resolver = new ServiceProviderTypeResolver(services); + + var optionsAccessor = services.GetService>(); + var options = optionsAccessor?.Value ?? new(); + + var builder = options.CommandAppBuilderFactory() + .AddHostingGlobalOptions() + .SetApplicationName(context.HostingEnvironment.ApplicationName) + .UseTypeResolver(resolver); + + foreach (var action in options.ConfigureActions) + action(context, builder); + + return builder.Build(); + } +} diff --git a/XO.Console.Cli.Extensions/CommandAppHostBuilderExtensions.cs b/XO.Console.Cli.Extensions/CommandAppHostBuilderExtensions.cs index 543992b..d39a7ac 100644 --- a/XO.Console.Cli.Extensions/CommandAppHostBuilderExtensions.cs +++ b/XO.Console.Cli.Extensions/CommandAppHostBuilderExtensions.cs @@ -11,24 +11,17 @@ namespace XO.Console.Cli; public static class CommandAppHostBuilderExtensions { /// - /// Configures and adds a delegate to its list of configuration actions. + /// Builds the host, then builds and runs a hosted command-line application. /// - /// The to configure. - /// A delegate that configures . - /// The . - public static IHostBuilder ConfigureCommandApp( - this IHostBuilder builder, - Action configure) - { - return builder - .ConfigureServices((_, services) => - { - services.AddOptions() - .Configure(options => options.ConfigureActions.Add(configure)) - ; - }) - ; - } + /// The to configure. + /// The command-line arguments. + /// A delegate that configures the . + /// A whose result is the command exit code. + public static Task RunCommandAppAsync( + this IHostBuilder hostBuilder, + IReadOnlyList args, + Action? configure = null) + => RunCommandAppAsync(hostBuilder, args, default, configure); /// /// Builds the host, then builds and runs a hosted command-line application with a default command. @@ -36,26 +29,31 @@ public static IHostBuilder ConfigureCommandApp( /// The to configure. /// The command-line arguments. /// A delegate that configures the . + /// The command implementation type. /// A whose result is the command exit code. public static Task RunCommandAppAsync( this IHostBuilder hostBuilder, IReadOnlyList args, - Action? configure = null) + Action? configure = null) where TDefaultCommand : class, ICommand => RunCommandAppAsync(hostBuilder, args, CommandAppBuilder.WithDefaultCommand, configure); /// - /// Builds the host, then builds and runs a hosted command-line application. + /// Builds the host, then builds and runs a hosted command-line application with a default command. /// /// The to configure. /// The command-line arguments. + /// The command implementation delegate. /// A delegate that configures the . + /// A class whose properties describe the command parameters. /// A whose result is the command exit code. - public static Task RunCommandAppAsync( + public static Task RunCommandAppAsync( this IHostBuilder hostBuilder, IReadOnlyList args, - Action? configure = null) - => RunCommandAppAsync(hostBuilder, args, CommandAppBuilder.Create, configure); + Func> executeAsync, + Action? configure = null) + where TParameters : CommandParameters + => RunCommandAppAsync(hostBuilder, args, () => CommandAppBuilder.WithDefaultCommand(executeAsync), configure); /// /// Builds the host, then builds and runs a hosted command-line application. @@ -69,9 +67,12 @@ public static Task RunCommandAppAsync( internal static async Task RunCommandAppAsync( this IHostBuilder hostBuilder, IReadOnlyList args, - Func builderFactory, - Action? configure) + Func? builderFactory, + Action? configure) { + hostBuilder.ConfigureServices( + (_, services) => services.AddCommandApp(builderFactory, configure)); + IHost host; try { @@ -90,7 +91,7 @@ internal static async Task RunCommandAppAsync( int result; try { - result = await host.RunCommandAppAsync(args, builderFactory, configure) + result = await host.RunCommandAppAsync(args) .ConfigureAwait(false); } catch (Exception ex) diff --git a/XO.Console.Cli.Extensions/CommandAppHostExtensions.cs b/XO.Console.Cli.Extensions/CommandAppHostExtensions.cs index be5c507..6b40671 100644 --- a/XO.Console.Cli.Extensions/CommandAppHostExtensions.cs +++ b/XO.Console.Cli.Extensions/CommandAppHostExtensions.cs @@ -2,7 +2,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; namespace XO.Console.Cli; @@ -12,46 +11,17 @@ namespace XO.Console.Cli; public static class CommandAppHostExtensions { /// - /// Builds and runs a hosted command-line application with a default command. + /// Runs a command-line application. /// + /// + /// To configure the command-line application, call before building the host. + /// /// The . /// The command-line arguments. - /// A delegate that configures the . - /// A whose result is the command exit code. - public static Task RunCommandAppAsync( - this IHost host, - IReadOnlyList args, - Action? configure = null) - where TDefaultCommand : class, ICommand - => RunCommandAppAsync(host, args, CommandAppBuilder.WithDefaultCommand, configure); - - /// - /// Builds and runs a hosted command-line application. - /// - /// The . - /// The command-line arguments. - /// A delegate that configures the . - /// A whose result is the command exit code. - public static Task RunCommandAppAsync( - this IHost host, - IReadOnlyList args, - Action? configure = null) - => RunCommandAppAsync(host, args, CommandAppBuilder.Create, configure); - - /// - /// Builds and runs a hosted command-line application. - /// - /// The . - /// The command-line arguments. - /// A delegate that constructs the . - /// A delegate that configures the . /// A whose result is the command exit code. [DebuggerNonUserCode] - internal static async Task RunCommandAppAsync( - this IHost host, - IReadOnlyList args, - Func builderFactory, - Action? configure = null) + public static async Task RunCommandAppAsync(this IHost host, IReadOnlyList args) { using var cancellationSource = new CancellationTokenSource(); var logger = host.Services.GetService>(); @@ -59,25 +29,12 @@ internal static async Task RunCommandAppAsync( int result; try { - var context = host.Services.GetRequiredService(); var lifetime = host.Services.GetRequiredService(); - var resolver = new ServiceProviderTypeResolver(host.Services); - - var builder = builderFactory() - .AddHostingGlobalOptions() - .SetApplicationName(context.HostingEnvironment.ApplicationName) - .UseTypeResolver(resolver); - - var optionsAccessor = host.Services.GetService>(); - if (optionsAccessor?.Value is CommandAppBuilderOptions options) - { - foreach (var action in options.ConfigureActions) - action(context, builder); - } + var app = host.Services.GetService(); - configure?.Invoke(builder); + // if the caller did not configure an application, do nothing as a default + app ??= CommandAppFactory.BuildCommandApp(host.Services); - var app = builder.Build(); var parse = app.Parse(args); await host.StartAsync(token).ConfigureAwait(false); diff --git a/XO.Console.Cli.Extensions/CommandAppServiceCollectionExtensions.cs b/XO.Console.Cli.Extensions/CommandAppServiceCollectionExtensions.cs new file mode 100644 index 0000000..41cb1b4 --- /dev/null +++ b/XO.Console.Cli.Extensions/CommandAppServiceCollectionExtensions.cs @@ -0,0 +1,83 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; + +namespace XO.Console.Cli; + +/// +/// Extension methods for adding -related services to a service collection. +/// +public static class CommandAppServiceCollectionExtensions +{ + /// + /// Adds to the service collection. + /// + /// The to configure. + /// A delegate that configures . + /// The . + public static IServiceCollection AddCommandApp( + this IServiceCollection services, + Action configure) + { + ArgumentNullException.ThrowIfNull(configure); + + return services.AddCommandApp(default, configure); + } + + /// + /// Adds to the service collection with a default command. + /// + /// The to configure. + /// A delegate that configures . + /// The command implementation type. + /// The . + public static IServiceCollection AddCommandApp( + this IServiceCollection services, + Action? configure = null) + where TDefaultCommand : class, ICommand + { + return services.AddCommandApp( + CommandAppBuilder.WithDefaultCommand, + configure); + } + + /// + /// Adds to the service collection with a default command. + /// + /// The to configure. + /// The command implementation delegate. + /// A delegate that configures . + /// A class whose properties describe the command parameters. + /// The . + public static IServiceCollection AddCommandApp( + this IServiceCollection services, + Func> executeAsync, + Action? configure = null) + where TParameters : CommandParameters + { + ArgumentNullException.ThrowIfNull(executeAsync); + + return services.AddCommandApp( + () => CommandAppBuilder.WithDefaultCommand(executeAsync), + configure); + } + + internal static IServiceCollection AddCommandApp( + this IServiceCollection services, + Func? builderFactory, + Action? configure) + { + var optionsBuilder = services.AddOptions(); + + if (builderFactory != null) + optionsBuilder.Configure(options => options.CommandAppBuilderFactory = builderFactory); + + if (configure != null) + optionsBuilder.Configure(options => options.ConfigureActions.Add(configure)); + + services.TryAddSingleton( + static services => CommandAppFactory.BuildCommandApp(services)); + + return services; + } +} From ecac5203f77101cd9c5ac37216f7a4ff12e728a1 Mon Sep 17 00:00:00 2001 From: Will Rogers Date: Fri, 26 Aug 2022 15:19:13 -0400 Subject: [PATCH 4/5] Remove AddHostingGlobalOptions(ICommandAppBuilder) This library provides no support for actually implementing these options, so it does not seem appropriate to provide a method that defines them. --- .../CommandAppFactory.cs | 1 - .../HostingCommandAppBuilderExtensions.cs | 29 ------------------- 2 files changed, 30 deletions(-) delete mode 100644 XO.Console.Cli.Extensions/HostingCommandAppBuilderExtensions.cs diff --git a/XO.Console.Cli.Extensions/CommandAppFactory.cs b/XO.Console.Cli.Extensions/CommandAppFactory.cs index d0a7353..314a23d 100644 --- a/XO.Console.Cli.Extensions/CommandAppFactory.cs +++ b/XO.Console.Cli.Extensions/CommandAppFactory.cs @@ -16,7 +16,6 @@ public static ICommandApp BuildCommandApp(IServiceProvider services) var options = optionsAccessor?.Value ?? new(); var builder = options.CommandAppBuilderFactory() - .AddHostingGlobalOptions() .SetApplicationName(context.HostingEnvironment.ApplicationName) .UseTypeResolver(resolver); diff --git a/XO.Console.Cli.Extensions/HostingCommandAppBuilderExtensions.cs b/XO.Console.Cli.Extensions/HostingCommandAppBuilderExtensions.cs deleted file mode 100644 index db61fc2..0000000 --- a/XO.Console.Cli.Extensions/HostingCommandAppBuilderExtensions.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.Extensions.Hosting; - -namespace XO.Console.Cli; - -/// -/// Extension methods for adding Microsoft.Extensions.Hosting-related features to -/// . -/// -public static class HostingCommandAppBuilderExtensions -{ - /// - /// Adds global options to the for configuring the generic host. - /// - /// - /// The options added by this method do not anything by themselves — they serve only as a source of documentation - /// and "allowlist" entries for command parsing. You must separately configure the - /// to parse and implement their values. - /// - /// The to configure. - /// The . - public static ICommandAppBuilder AddHostingGlobalOptions( - this ICommandAppBuilder builder) - { - return builder - .AddGlobalOption("--configuration", "Adds an additional configuration file", "-c") - .AddGlobalOption("--environment", "Sets the hosting environment") - ; - } -} From da256c48c7ce7cde0338e13a76de0cf2a6e5b916 Mon Sep 17 00:00:00 2001 From: Will Rogers Date: Fri, 26 Aug 2022 15:27:11 -0400 Subject: [PATCH 5/5] Tweaks --- .../CommandAppHostBuilderExtensions.cs | 11 ++++++----- XO.Console.Cli.Extensions/CommandAppHostExtensions.cs | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/XO.Console.Cli.Extensions/CommandAppHostBuilderExtensions.cs b/XO.Console.Cli.Extensions/CommandAppHostBuilderExtensions.cs index d39a7ac..3fd3b95 100644 --- a/XO.Console.Cli.Extensions/CommandAppHostBuilderExtensions.cs +++ b/XO.Console.Cli.Extensions/CommandAppHostBuilderExtensions.cs @@ -108,19 +108,20 @@ await DisposeAndFlush(host, loggerFactory) return result; } - private static async Task DisposeAndFlush(IHost host, ILoggerFactory? loggerFactory) + private static ValueTask DisposeAndFlush(IHost host, ILoggerFactory? loggerFactory) { try { if (host is IAsyncDisposable asyncDisposable) - await asyncDisposable.DisposeAsync() - .ConfigureAwait(false); - else - host.Dispose(); + return asyncDisposable.DisposeAsync(); + + host.Dispose(); + return ValueTask.CompletedTask; } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex.ToString()); + return ValueTask.CompletedTask; } finally { diff --git a/XO.Console.Cli.Extensions/CommandAppHostExtensions.cs b/XO.Console.Cli.Extensions/CommandAppHostExtensions.cs index 6b40671..506cdc4 100644 --- a/XO.Console.Cli.Extensions/CommandAppHostExtensions.cs +++ b/XO.Console.Cli.Extensions/CommandAppHostExtensions.cs @@ -11,7 +11,7 @@ namespace XO.Console.Cli; public static class CommandAppHostExtensions { /// - /// Runs a command-line application. + /// Runs the configured command-line application. /// /// /// To configure the command-line application, call RunCommandAppAsync(this IHost host, IReadOnlyList< } catch (Exception ex) { - logger?.LogCritical(ex, "Command crashed! {Message}", ex.Message); + logger?.LogCritical(ex, "Unhandled exception: {ExceptionMessage}", ex.Message); System.Console.Error.WriteLine(ex.Message); result = 1;