Table of Contents

Wiring up services, configuration, logging, and validation

The home page covers defining a command and running it. This article covers the four integration points that turn a single command into a real application: dependency injection, configuration, logging, and validation.

Dependency injection

AppBuilder builds on Microsoft.Extensions.Hosting, so commands are resolved from the same container as everything else. Register your services with ConfigureServices and take them as constructor parameters:

using var appBuilder = AppBuilder.Create(args)
                                 .ConfigureServices(services =>
                                 {
                                     services.AddSingleton<IUserService, UserService>();
                                     services.AddTransient<CreateProjectUseCase>();
                                 });

var executor = appBuilder.ConfigureCommandApp(config => config.AddCommand<UserAddCommand>("add"));

If you already package registrations as a ServicesBundle from Ploch.Common.DependencyInjection, register the bundle instead:

.AddServicesBundle<MyFeatureBundle>()

Configuration

ConfigureAppConfiguration exposes the standard IConfigurationBuilder, so appsettings.json, environment variables, and user secrets all work as they do in a web host:

.ConfigureAppConfiguration(configuration => configuration.AddJsonFile("appsettings.json", optional: true))

Bind the result wherever you need it — IConfiguration is available from the container, as are options types registered with services.Configure<T>(...).

Logging with Serilog

Add the Ploch.CommandLine.Spectre.Serilog package and register its bundle. It configures Serilog against the application's configuration and routes framework logging through it:

.AddServicesBundle<SerilogConfigurationBundle>()

Commands then take ILogger<T> as a constructor parameter in the usual way.

Note

Keep log output and user-facing output separate. Write anything the user is meant to read through IOutput, and use ILogger<T> for diagnostics. Mixing the two makes a CLI hard to pipe and hard to script against.

Validating settings with FluentValidation

Add the Ploch.CommandLine.Spectre.FluentValidation package and register your validators by assembly scan:

.ConfigureServices(services =>
    services.AddCommandLineSettingsFluentValidation(builder =>
        builder.AddAssembly(typeof(Program).Assembly)))

Then write a validator per settings type:

public class UserAddSettingsValidator : AbstractValidator<UserAddSettings>
{
    public UserAddSettingsValidator()
    {
        RuleFor(settings => settings.Name).NotEmpty();
        RuleFor(settings => settings.Email).NotEmpty().EmailAddress();
    }
}

Validation runs before DoExecute/DoExecuteAsync. A failure short-circuits the command and returns ExitCode.InvalidInput (2) without your code running.

Composing sub-commands

Spectre's configurator supports branches, so a multi-level CLI is a matter of nesting:

.ConfigureCommandApp(config =>
{
    config.SetApplicationName("sample");

    config.AddBranch("user", user =>
    {
        user.SetDescription("Manage user accounts.");
        user.AddCommand<UserAddCommand>("add").WithExample("user", "add", "Alice");
        user.AddCommand<UserListCommand>("list");
    });
});

WithDescription and WithExample feed the generated --help output, so filling them in is what makes the CLI self-documenting.

Next steps