Skip to content

Commit

Permalink
Merge pull request #2 from xo-energy/feature-register-service
Browse files Browse the repository at this point in the history
Refactor hosting extensions to resolve ICommandApp as a service
  • Loading branch information
wjrogers authored Aug 29, 2022
2 parents 2af1c88 + da256c4 commit 664006d
Show file tree
Hide file tree
Showing 10 changed files with 168 additions and 118 deletions.
12 changes: 11 additions & 1 deletion XO.Console.Cli.Extensions/CommandAppBuilderOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,20 @@
namespace XO.Console.Cli;

/// <summary>
/// Configures the <see cref="ICommandAppBuilder"/> via <see cref="CommandAppHostBuilderExtensions.ConfigureCommandApp"/>.
/// Configures the <see cref="ICommandAppBuilder"/>.
/// </summary>
public sealed class CommandAppBuilderOptions
{
/// <summary>
/// A delegate that will be called to create the <see cref="ICommandAppBuilder"/>.
/// </summary>
/// <remarks>
/// Used to configure the factory method that will be called to initialize the <see cref="ICommandAppBuilder"/>. The
/// default factory method is <see cref="CommandAppBuilder.Create"/>. To configure a default command, set an
/// appropriate factory method; for example, <see cref="CommandAppBuilder.WithDefaultCommand{TCommand}()"/>.
/// </remarks>
public Func<ICommandAppBuilder> CommandAppBuilderFactory { get; set; } = CommandAppBuilder.Create;

/// <summary>
/// A list of delegates that configure <see cref="ICommandAppBuilder"/>.
/// </summary>
Expand Down
27 changes: 27 additions & 0 deletions XO.Console.Cli.Extensions/CommandAppFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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<HostBuilderContext>();
var lifetime = services.GetRequiredService<IHostApplicationLifetime>();
var resolver = new ServiceProviderTypeResolver(services);

var optionsAccessor = services.GetService<IOptions<CommandAppBuilderOptions>>();
var options = optionsAccessor?.Value ?? new();

var builder = options.CommandAppBuilderFactory()
.SetApplicationName(context.HostingEnvironment.ApplicationName)
.UseTypeResolver(resolver);

foreach (var action in options.ConfigureActions)
action(context, builder);

return builder.Build();
}
}
62 changes: 32 additions & 30 deletions XO.Console.Cli.Extensions/CommandAppHostBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,51 +11,49 @@ namespace XO.Console.Cli;
public static class CommandAppHostBuilderExtensions
{
/// <summary>
/// Configures <see cref="CommandAppBuilderOptions"/> and adds a delegate to its list of configuration actions.
/// Builds the host, then builds and runs a hosted command-line application.
/// </summary>
/// <param name="builder">The <see cref="IHostBuilder"/> to configure.</param>
/// <param name="configure">A delegate that configures <see cref="ICommandAppBuilder"/>.</param>
/// <returns>The <see cref="IHostBuilder"/>.</returns>
public static IHostBuilder ConfigureCommandApp(
this IHostBuilder builder,
Action<HostBuilderContext, ICommandAppBuilder> configure)
{
return builder
.ConfigureServices((_, services) =>
{
services.AddOptions<CommandAppBuilderOptions>()
.Configure(options => options.ConfigureActions.Add(configure))
;
})
;
}
/// <param name="hostBuilder">The <see cref="IHostBuilder"/> to configure.</param>
/// <param name="args">The command-line arguments.</param>
/// <param name="configure">A delegate that configures the <see cref="ICommandAppBuilder"/>.</param>
/// <returns>A <see cref="Task{TResult}"/> whose result is the command exit code.</returns>
public static Task<int> RunCommandAppAsync(
this IHostBuilder hostBuilder,
IReadOnlyList<string> args,
Action<HostBuilderContext, ICommandAppBuilder>? configure = null)
=> RunCommandAppAsync(hostBuilder, args, default, configure);

/// <summary>
/// Builds the host, then builds and runs a hosted command-line application with a default command.
/// </summary>
/// <param name="hostBuilder">The <see cref="IHostBuilder"/> to configure.</param>
/// <param name="args">The command-line arguments.</param>
/// <param name="configure">A delegate that configures the <see cref="ICommandAppBuilder"/>.</param>
/// <typeparam name="TDefaultCommand">The command implementation type.</typeparam>
/// <returns>A <see cref="Task{TResult}"/> whose result is the command exit code.</returns>
public static Task<int> RunCommandAppAsync<TDefaultCommand>(
this IHostBuilder hostBuilder,
IReadOnlyList<string> args,
Action<ICommandAppBuilder>? configure = null)
Action<HostBuilderContext, ICommandAppBuilder>? configure = null)
where TDefaultCommand : class, ICommand
=> RunCommandAppAsync(hostBuilder, args, CommandAppBuilder.WithDefaultCommand<TDefaultCommand>, configure);

/// <summary>
/// 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.
/// </summary>
/// <param name="hostBuilder">The <see cref="IHostBuilder"/> to configure.</param>
/// <param name="args">The command-line arguments.</param>
/// <param name="executeAsync">The command implementation delegate.</param>
/// <param name="configure">A delegate that configures the <see cref="ICommandAppBuilder"/>.</param>
/// <typeparam name="TParameters">A class whose properties describe the command parameters.</typeparam>
/// <returns>A <see cref="Task{TResult}"/> whose result is the command exit code.</returns>
public static Task<int> RunCommandAppAsync(
public static Task<int> RunCommandAppAsync<TParameters>(
this IHostBuilder hostBuilder,
IReadOnlyList<string> args,
Action<ICommandAppBuilder>? configure = null)
=> RunCommandAppAsync(hostBuilder, args, CommandAppBuilder.Create, configure);
Func<ICommandContext, TParameters, CancellationToken, Task<int>> executeAsync,
Action<HostBuilderContext, ICommandAppBuilder>? configure = null)
where TParameters : CommandParameters
=> RunCommandAppAsync(hostBuilder, args, () => CommandAppBuilder.WithDefaultCommand(executeAsync), configure);

/// <summary>
/// Builds the host, then builds and runs a hosted command-line application.
Expand All @@ -69,9 +67,12 @@ public static Task<int> RunCommandAppAsync(
internal static async Task<int> RunCommandAppAsync(
this IHostBuilder hostBuilder,
IReadOnlyList<string> args,
Func<ICommandAppBuilder> builderFactory,
Action<ICommandAppBuilder>? configure)
Func<ICommandAppBuilder>? builderFactory,
Action<HostBuilderContext, ICommandAppBuilder>? configure)
{
hostBuilder.ConfigureServices(
(_, services) => services.AddCommandApp(builderFactory, configure));

IHost host;
try
{
Expand All @@ -90,7 +91,7 @@ internal static async Task<int> RunCommandAppAsync(
int result;
try
{
result = await host.RunCommandAppAsync(args, builderFactory, configure)
result = await host.RunCommandAppAsync(args)
.ConfigureAwait(false);
}
catch (Exception ex)
Expand All @@ -107,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
{
Expand Down
63 changes: 10 additions & 53 deletions XO.Console.Cli.Extensions/CommandAppHostExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace XO.Console.Cli;

Expand All @@ -12,72 +11,30 @@ namespace XO.Console.Cli;
public static class CommandAppHostExtensions
{
/// <summary>
/// Builds and runs a hosted command-line application with a default command.
/// Runs the configured command-line application.
/// </summary>
/// <remarks>
/// To configure the command-line application, call <see
/// cref="o:CommandAppServiceCollectionExtensions.AddCommandApp()"/> before building the host.
/// </remarks>
/// <param name="host">The <see cref="IHost"/>.</param>
/// <param name="args">The command-line arguments.</param>
/// <param name="configure">A delegate that configures the <see cref="ICommandAppBuilder"/>.</param>
/// <returns>A <see cref="Task{TResult}"/> whose result is the command exit code.</returns>
public static Task<int> RunCommandAppAsync<TDefaultCommand>(
this IHost host,
IReadOnlyList<string> args,
Action<ICommandAppBuilder>? configure = null)
where TDefaultCommand : class, ICommand
=> RunCommandAppAsync(host, args, CommandAppBuilder.WithDefaultCommand<TDefaultCommand>, configure);

/// <summary>
/// Builds and runs a hosted command-line application.
/// </summary>
/// <param name="host">The <see cref="IHost"/>.</param>
/// <param name="args">The command-line arguments.</param>
/// <param name="configure">A delegate that configures the <see cref="ICommandAppBuilder"/>.</param>
/// <returns>A <see cref="Task{TResult}"/> whose result is the command exit code.</returns>
public static Task<int> RunCommandAppAsync(
this IHost host,
IReadOnlyList<string> args,
Action<ICommandAppBuilder>? configure = null)
=> RunCommandAppAsync(host, args, CommandAppBuilder.Create, configure);

/// <summary>
/// Builds and runs a hosted command-line application.
/// </summary>
/// <param name="host">The <see cref="IHost"/>.</param>
/// <param name="args">The command-line arguments.</param>
/// <param name="builderFactory">A delegate that constructs the <see cref="ICommandAppBuilder"/>.</param>
/// <param name="configure">A delegate that configures the <see cref="ICommandAppBuilder"/>.</param>
/// <returns>A <see cref="Task{TResult}"/> whose result is the command exit code.</returns>
[DebuggerNonUserCode]
internal static async Task<int> RunCommandAppAsync(
this IHost host,
IReadOnlyList<string> args,
Func<ICommandAppBuilder> builderFactory,
Action<ICommandAppBuilder>? configure = null)
public static async Task<int> RunCommandAppAsync(this IHost host, IReadOnlyList<string> args)
{
using var cancellationSource = new CancellationTokenSource();
var logger = host.Services.GetService<ILogger<ICommandApp>>();
var token = cancellationSource.Token;
int result;
try
{
var context = host.Services.GetRequiredService<HostBuilderContext>();
var lifetime = host.Services.GetRequiredService<IHostApplicationLifetime>();
var resolver = new ServiceProviderTypeResolver(host.Services);

var builder = builderFactory()
.AddHostingGlobalOptions()
.SetApplicationName(context.HostingEnvironment.ApplicationName)
.UseTypeResolver(resolver);

var optionsAccessor = host.Services.GetService<IOptions<CommandAppBuilderOptions>>();
if (optionsAccessor?.Value is CommandAppBuilderOptions options)
{
foreach (var action in options.ConfigureActions)
action(context, builder);
}
var app = host.Services.GetService<ICommandApp>();

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);
Expand All @@ -92,7 +49,7 @@ internal static async Task<int> RunCommandAppAsync(
}
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;
Expand Down
83 changes: 83 additions & 0 deletions XO.Console.Cli.Extensions/CommandAppServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;

namespace XO.Console.Cli;

/// <summary>
/// Extension methods for adding <see cref="ICommandApp"/>-related services to a service collection.
/// </summary>
public static class CommandAppServiceCollectionExtensions
{
/// <summary>
/// Adds <see cref="ICommandApp"/> to the service collection.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to configure.</param>
/// <param name="configure">A delegate that configures <see cref="ICommandAppBuilder"/>.</param>
/// <returns>The <see cref="IServiceCollection"/>.</returns>
public static IServiceCollection AddCommandApp(
this IServiceCollection services,
Action<HostBuilderContext, ICommandAppBuilder> configure)
{
ArgumentNullException.ThrowIfNull(configure);

return services.AddCommandApp(default, configure);
}

/// <summary>
/// Adds <see cref="ICommandApp"/> to the service collection with a default command.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to configure.</param>
/// <param name="configure">A delegate that configures <see cref="ICommandAppBuilder"/>.</param>
/// <typeparam name="TDefaultCommand">The command implementation type.</typeparam>
/// <returns>The <see cref="IServiceCollection"/>.</returns>
public static IServiceCollection AddCommandApp<TDefaultCommand>(
this IServiceCollection services,
Action<HostBuilderContext, ICommandAppBuilder>? configure = null)
where TDefaultCommand : class, ICommand
{
return services.AddCommandApp(
CommandAppBuilder.WithDefaultCommand<TDefaultCommand>,
configure);
}

/// <summary>
/// Adds <see cref="ICommandApp"/> to the service collection with a default command.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to configure.</param>
/// <param name="executeAsync">The command implementation delegate.</param>
/// <param name="configure">A delegate that configures <see cref="ICommandAppBuilder"/>.</param>
/// <typeparam name="TParameters">A class whose properties describe the command parameters.</typeparam>
/// <returns>The <see cref="IServiceCollection"/>.</returns>
public static IServiceCollection AddCommandApp<TParameters>(
this IServiceCollection services,
Func<ICommandContext, TParameters, CancellationToken, Task<int>> executeAsync,
Action<HostBuilderContext, ICommandAppBuilder>? configure = null)
where TParameters : CommandParameters
{
ArgumentNullException.ThrowIfNull(executeAsync);

return services.AddCommandApp(
() => CommandAppBuilder.WithDefaultCommand(executeAsync),
configure);
}

internal static IServiceCollection AddCommandApp(
this IServiceCollection services,
Func<ICommandAppBuilder>? builderFactory,
Action<HostBuilderContext, ICommandAppBuilder>? configure)
{
var optionsBuilder = services.AddOptions<CommandAppBuilderOptions>();

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;
}
}
29 changes: 0 additions & 29 deletions XO.Console.Cli.Extensions/HostingCommandAppBuilderExtensions.cs

This file was deleted.

2 changes: 1 addition & 1 deletion XO.Console.Cli.Extensions/XO.Console.Cli.Extensions.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Nerdbank.GitVersioning" Version="3.5.104">
<PackageReference Include="Nerdbank.GitVersioning" Version="3.5.109">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
Loading

0 comments on commit 664006d

Please sign in to comment.