
Dynamic service installation with CommandLine Parser
The CommandLine parser project is a really nice way to map different command line options into your cli. It can be combined with Microsoft dependency injection to modify service installation.
In some situations you want to install different services depending on the command line option. In this case you can combine the options mapping with the DI installation to provide dynamic installation during service startup.
For example, imagine a command line tool which can perform operations using data from a CSV file, or operations from data in a database. The command line options for such a tool may look like this:
The CommandLine parser option models
[Verb("local", HelpText = "Process local file")]
public class LocalFileOptions
{
[Option('l', "localfile", HelpText = "Local file services", Required = true)]
public string CsvFile { get; set; }
}
[Verb("db", HelpText = "Process database")]
public class DbOptions
{
[Option('d', "database", HelpText = "Database services", Required = true)]
public string DatabaseConnection { get; set; }
}
If the local -l file.csv is passed on the command line then a LocalFileOptions is constructed. Likewise, if db -d {connectionstring} is passed then a DbOptions is constructed.
Dynamically installing services for an option
This approach can be extended to only require particular application configuration depending on what the cli option was. Typically when installing IOptions you would validate the properties and throw an exception if a mandatory option was missing or invalid e.g. a database connection string.
However if the user is using the cli to perform an operation that doesn’t use the database, then we don’t need to install the database options and we won’t install the database services.
This has the advantage of ensuring that IOptions<T> options being installed are valid e.g. not permissively allowing null properties, and at the same time we don’t need to share secrets that are not required.
In the example below when the LocalFileOptions are specified, then the LocalFileService is installed and run. Alternatively when the DbOptions are specified then the DatabaseService is installed and run.
static async Task<int> Main(string[] args)
{
var parser = new Parser(c =>
{
c.CaseSensitive = false;
c.CaseInsensitiveEnumValues = false;
c.HelpWriter = System.Console.Error;
});
return await parser
.ParseArguments<
LocalFileOptions,
DbOptions
>(args)
.MapResult(
async (LocalFileOptions opts) =>
await RunHost<LocalFileService, LocalFileOptions>(opts, AddLocalServices),
async (DbOptions opts) =>
await RunHost<DatabaseService, DbOptions>(opts, AddDatabaseServices),
_ => Task.FromResult(1));
}
private static void AddLocalServices(IServiceCollection services)
{
// Install local file services
services
.AddSingleton<IFileProcessor, FileProcessor>();
}
private static void AddDatabaseServices(IServiceCollection serviceCollection)
{
// Install database services
services
.AddSingleton<IDatabaseProcessor, DatabaseProcessor>();
}
private static async Task<int> RunHost<THostedService, TOptions>(
TOptions options, Action<IServiceCollection> configureServices)
where THostedService : class, IHostedService
where TOptions : class, new()
{
await Host
.CreateDefaultBuilder()
.ConfigureHostConfiguration(configBuilder => configBuilder.WithApplicationConfiguration())
.ConfigureServices((context, services) =>
{
services
.AddCliSettings(context.Configuration)
.AddSingleton(Options.Create(options))
.AddHostedService<THostedService>();
configureServices(services); // Install specific services
})
.RunConsoleAsync();
return 0;
}
public class LocalFileService : BackgroundService
{
private readonly IFileProcessor _fileProcessor;
public LocalFileservice(IFileProcessor fileProcessor)
{
_fileProcessor = fileProcessor;
}
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
// Perform local file operations
_fileProcessor.DoStuff();
}
}
public class DatabaseService : BackgroundService
{
private readonly IOptions<DatabaseSettings> _databaseSettings; // Secret settings
private readonly IDatabaseProcessor _databaseProcessor;
public DatabaseService(
IOptions<DatabaseSettings> databaseSettings,
IDatabaseProcessor databaseProcessor)
{
_databaseSettings = databaseSettings;
}
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
// Perform database operations
_databaseProcessor.DoStuff();
}
}