ConsoleAppFramework
ConsoleAppFramework is an infrastructure of creating CLI(Command-line interface) tools, daemon, and multi batch application. You can create full feature of command line tool on only one-line.
This simplicity is by C# 10.0 and .NET 6 new features, similar as ASP.NET Core 6.0 Minimal APIs.
Most minimal API is one-line(with top-level-statements, global-usings).
ConsoleApp.Run(args, (string name) => Console.WriteLine($"Hello {name}"));
Of course, ConsoleAppFramework has extensibility.
// Register two commands(use short-name, argument)
// hello -m
// sum [x] [y]
var app = ConsoleApp.Create(args);
app.AddCommand("hello", ([Option("m", "Message to display.")] string message) => Console.WriteLine($"Hello {message}"));
app.AddCommand("sum", ([Option(0)] int x, [Option(1)] int y) => Console.WriteLine(x + y));
app.Run();
You can register public method as command. This provides a simple way to registering multiple commands.
// AddCommands register as command.
// echo --msg --repeat(default = 3)
// sum [x] [y]
var app = ConsoleApp.Create(args);
app.AddCommands<Foo>();
app.Run();
public class Foo : ConsoleAppBase
{
public void Echo(string msg, int repeat = 3)
{
for (var i = 0; i < repeat; i++)
{
Console.WriteLine(msg);
}
}
public void Sum([Option(0)]int x, [Option(1)]int y)
{
Console.WriteLine((x + y).ToString());
}
}
If you have many commands, you can define class separetely and use AddAllCommandType
to register all commands one-line.
// Register `Foo` and `Bar` as SubCommands(You can also use AddSubCommands<T> to register manually).
// foo echo --msg
// foo sum [x] [y]
// bar hello2
var app = ConsoleApp.Create(args);
app.AddAllCommandType();
app.Run();
public class Foo : ConsoleAppBase
{
public void Echo(string msg)
{
Console.WriteLine(msg);
}
public void Sum([Option(0)]int x, [Option(1)]int y)
{
Console.WriteLine((x + y).ToString());
}
}
public class Bar : ConsoleAppBase
{
public void Hello2()
{
Console.WriteLine("H E L L O");
}
}
ConsoleAppFramework is built on .NET Generic Host, you can use configuration, logging, DI, lifetime management by Microsoft.Extensions packages. ConsoleAppFramework do parameter binding from string args, routing many commands, dotnet style help builder, etc.
Here is the full-sample of power of ConsoleAppFramework.
// You can use full feature of Generic Host(same as ASP.NET Core).
var builder = ConsoleApp.CreateBuilder(args);
builder.ConfigureServices((ctx,services) =>
{
// Register EntityFramework database context
services.AddDbContext<MyDbContext>();
// Register appconfig.json to IOption<MyConfig>
services.Configure<MyConfig>(ctx.Configuration);
// Using Cysharp/ZLogger for logging to file
services.AddLogging(logging =>
{
logging.AddZLoggerFile("log.txt");
});
});
var app = builder.Build();
// setup many command, async, short-name/description option, subcommand, DI
app.AddCommand("calc-sum", (int x, int y) => Console.WriteLine(x + y));
app.AddCommand("sleep", async ([Option("t", "seconds of sleep time.")] int time) =>
{
await Task.Delay(TimeSpan.FromSeconds(time));
});
app.AddSubCommand("verb", "childverb", () => Console.WriteLine("called via 'verb childverb'"));
// You can insert all public methods as sub command => db select / db insert
// or AddCommand<T>() all public methods as command => select / insert
app.AddSubCommands<DatabaseApp>();
// some argument from DI.
app.AddRootCommand((ConsoleAppContext ctx, IOptions<MyConfig> config, string name) => { });
app.Run();
// ----
[Command("db")]
public class DatabaseApp : ConsoleAppBase, IAsyncDisposable
{
readonly ILogger<DatabaseApp> logger;
readonly MyDbContext dbContext;
readonly IOptions<MyConfig> config;
// you can get DI parameters.
public DatabaseApp(ILogger<DatabaseApp> logger,IOptions<MyConfig> config, MyDbContext dbContext)
{
this.logger = logger;
this.dbContext = dbContext;
this.config = config;
}
[Command("select")]
public async Task QueryAsync(int id)
{
// select * from...
}
// also allow defaultValue.
[Command("insert")]
public async Task InsertAsync(string value, int id = 0)
{
// insert into...
}
// support cleanup(IDisposable/IAsyncDisposable)
public async ValueTask DisposeAsync()
{
await dbContext.DisposeAsync();
}
}
public class MyConfig
{
public string FooValue { get; set; } = default!;
public string BarValue { get; set; } = default!;
}
ConsoleAppFramework can create easily to many command application. Also enable to use GenericHost configuration is best way to share configuration/workflow when creating batch application for other .NET web app. If tool is for CI, git pull and run by dotnet run -- [Command] [Option]
is very helpful.
dotnet's standard CommandLine api - System.CommandLine is low level, require many boilerplate codes. ConsoleAppFramework is like ASP.NET Core in CLI Applications, no needs boilerplate. However, with the power of Generic Host, it is simple and easy, but much more powerful.
Table of Contents
- Getting Started
- ConsoleApp / ConsoleAppBuilder
- Delegate convention
- AddCommand
- Complex Argument
- Exit Code
- Implicit Using
- CommandAttribute
- OptionAttribute
- Command parameters validation
- Daemon
- Abort Timeout
- Filter
- Logging
- Configuration
- DI
- Cleanup
- ConsoleAppContext
- ConsoleAppOptions
- Terminate handling in Console.Read
- Publish to executable file
- v3 Legacy Compatibility
- License
Getting Started
NuGet: ConsoleAppFramework
Install-Package ConsoleAppFramework
If you are using .NET 6, automatically enabled implicit global using ConsoleAppFramework;
. So you can write one line code.
ConsoleApp.Run(args, (string name) => Console.WriteLine($"Hello {name}"));
You can execute command like sampletool --name "foo"
.
The Option parser is no longer needed. You can also use the OptionAttribute
to describe the parameter and set short-name.
ConsoleApp.Run(args, ([Option("n", "name of send user.")] string name) => Console.WriteLine($"Hello {name}"));
Usage: sampletool [options...]
Options:
-n, --name <String> name of user. (Required)
Commands:
help Display help.
version Display version.
Method parameter will be required parameter, optional parameter will be oprional parameter with default value. Also support boolean flag, if parameter is bool, in default it will be optional parameter and with --foo
set true to parameter.
// lambda expression does not support default value so require to use local function
static void Hello([Option("m")]string message, [Option("e")] bool end, [Option("r")] int repeat = 3)
{
for (int i = 0; i < repeat; i++)
{
Console.WriteLine(message);
}
if (end)
{
Console.WriteLine("END");
}
}
ConsoleApp.Run(args, Hello);
Options:
-m, --message <String> (Required)
-e, --end (Optional)
-r, --repeat <Int32> (Default: 3)
help
command (or no argument to pass) and version
command is enabled in default(You can disable this in options or can override by add same name of command). Also enables command --help
option. This help format is similar as dotnet
command, version
command shows AssemblyInformationalVersion
or AssemblylVersion
.
> sampletool help
Usage: sampletool [options...]
Options:
-n, --name <String> name of user. (Required)
-r, --repeat <Int32> repeat count. (Default: 3)
Commands:
help Display help.
version Display version.
> sampletool version
1.0.0
You can use Run<T>
or AddCommands<T>
to add multi commands easily.
ConsoleApp.Run<MyCommands>(args);
// require to inherit ConsoleAppBase
public class MyCommands : ConsoleAppBase
{
// You can receive DI services in constructor.
// All public methods is registred.
// Using [RootCommand] attribute will be root-command
[RootCommand]
public void Hello(
[Option("n", "name of send user.")] string name,
[Option("r", "repeat count.")] int repeat = 3)
{
for (int i = 0; i < repeat; i++)
{
Console.WriteLine($"Hello My ConsoleApp from {name}");
}
}
// [Option(int)] describes that parameter is passed by index
[Command("escape")]
public void UrlEscape([Option(0)] string input)
{
Console.WriteLine(Uri.EscapeDataString(input));
}
// define async method returns Task
[Command("timer")]
public async Task Timer([Option(0)] uint waitSeconds)
{
Console.WriteLine(waitSeconds + " seconds");
while (waitSeconds != 0)
{
// ConsoleAppFramework does not stop immediately on terminate command(Ctrl+C)
// for allows gracefully shutdown(keeping safe cleanup)
// so you should pass Context.CancellationToken to async method.
// If not, abort timeout by HostOptions.ShutdownTimeout(default is 00:00:05).
await Task.Delay(TimeSpan.FromSeconds(1), Context.CancellationToken);
waitSeconds--;
Console.WriteLine(waitSeconds + " seconds");
}
}
}
You can call like
sampletool -n "foo" -r 3
sampletool escape http://foo.bar/
sampletool timer 10
This is recommended way to register multi commands.
If you omit [Command]
attribute, command and option name is used by there name and convert to kebab-case
in default.
// Command is url-escape
// Option is --input-file
public void UrlEscape(string inputFile)
{
}
This converting behaviour can configure by ConsoleAppOptions.NameConverter
.
ConsoleApp / ConsoleAppBuilder
ConsoleApp
is an entrypoint of creating ConsoleAppFramework app. It has three APIs, Create
, CreateBuilder
, CreateFromHostBuilder
and Run
.
// Create is shorthand of CraeteBuilder(args).Build();
var app = ConsoleApp.Create(args);
// Builder returns IHost so you can configure application hosting option.
var app = ConsoleApp.CreateBuilder(args)
.ConfigureServices(services =>
{
})
.Build();
// Run is shorthand of Create(args).AddRootCommand(rootCommand).Run();
// If you want to create simple app, this API is faster.
ConsoleApp.Run(args, /* lambda expression */);
// Run<T> is shorthand of Create(args).AddCommands<T>().Run();
// AddCommands<T> is recommend option to register many commands.
ConsoleApp.Run<MyCommands>(args);
When calling Create/CreateBuilder/CreateFromHostBuilder
, also configure ConsoleAppOptions
. Full option details, see ConsoleAppOptions section.
var app = ConsoleApp.Create(args, options =>
{
options.ShowDefaultCommand = false;
options.NameConverter = x => x.ToLower();
});
Advanced API of ConsoleApp
, CreateFromHostBuilder
creates ConsoleApp from IHostBuilder.
// Setup services outside of ConsoleAppFramework.
var hostBuilder = Host.CreateDefaultBuilder()
.ConfigureServices();
var app = ConsoleApp.CreateFromHostBuilder(hostBuilder);
ConsoleAppBuilder
itself is IHostBuilder
so you can use any configuration methods like ConfigureServices
, ConfigureLogging
, etc. If method chain is not returns ConsoleAppBuilder
(for example, using external lib's extension methods), can not get ConsoleApp
directly. In that case, use BuildAsConsoleApp()
instead of Build()
.
ConsoleApp
exposes some utility properties.
IHost
HostILogger<ConsoleApp>
LoggerIServiceProvider
ServicesIConfiguration
ConfigurationIHostEnvironment
EnvironmentIHostApplicationLifetime
Lifetime
Run()
and RunAsync(CancellationToken)
to finally invoke application. Run is shorthand of RunAsync().GetAwaiter().GetResult()
so receives same result of await RunAsync()
. On Entrypoint, there is not much need to do await RunAsync()
. Therefore, it is usually a good to choose Run()
.
Delegate convention
AddCommand
accepts Delegate
in argument. In C# 10.0 allows naturaly syntax of lambda expressions.
app.AddCommand("no-argument", () => { });
app.AddCommand("any-arguments", (int x, string y, TimeSpan z) => { });
app.AddCommand("instance", new MyClass().Cmd);
app.AddCommand("async", async () => { });
app.AddCommand("attribute", ([Option("msg")]string message) => { });
static void Hello1() { }
app.AddCommand("local-static", Hello1);
void Hello2() { }
app.AddCommand("local-method", Hello2);
async Task Async() { }
app.AddCommand("async-method", Async);
void OptionalParameter(int x = 10, int y = 20) { }
app.AddCommand("optional", OptionalParameter);
public class MyClass
{
public void Cmd()
{
Console.WriteLine("OK");
}
}
lambda expressions can not use optional parameter so if you want to need it, using local/static functions.
Delegate(both lambda and method) allows to receive ConsoleAppContext
or any your DI types. DI types is ignored as parameter.
// option is --param1, --param2
app.AddCommand("di", (ConsoleAppContext ctx, ILogger logger, int param1, int param2) => { });
AddCommand
AddRootCommand
RootCommand
means default(no command name) command of application. ConsoleApp.Run(Delegate)
uses root command.
AddCommand
/ AddCommands<T>
AddCommand
requires first argument as command-name. AddCommands<T>
allows to register many command via ConsoleAppBase
ConsoleAppBase
has Context
, it has executing information and CancellationToken
.
// Commands:
// hello
// world
app.AddCommands<MyCommands>();
app.Run();
// Inherit ConsoleAPpBase
public class MyCommands : ConsoleAppBase, IDisposable
{
readonly ILogger<MyCommands> logger;
// You can receive DI services in constructor.
public MyCommands(ILogger<MyCommands> logger)
{
this.logger = logger;
}
// All public methods is registered.
// Using [RootCommand] attribute will be root-command
[RootCommand]
public void Hello()
{
// Context has any useful information.
Console.WriteLine(this.Context.Timestamp);
}
public async Task World()
{
await Task.Delay(1000, this.Context.CancellationToken);
}
// If implements IDisposable, called for cleanup
public void Dispose()
{
}
}
AddSubCommand
/ AddSubCommands<T>
AddSubCommand(string parentCommandName, string commandName, Delegate command)
registers nested command.
// Commands:
// foo bar1
// foo bar2
// foo bar3
app.AddSubCommand("foo", "bar1", () => { });
app.AddSubCommand("foo", "bar2", () => { });
app.AddSubCommand("foo", "bar3", () => { });
AddSubCommands<T>
is similar as AddCommands<T>
but used type-name(or [Command]
name) as parentCommandName.
// Commands:
// my-commands hello
// my-commands world
app.AddSubCommands<MyCommands>();
AddAllCommandType
AddAllCommandType
searches all ConsoleAppBase
type in assembly and register by AddSubCommands<T>
.
// Commands:
// foo echo
// foo sum
// bar hello2
app.AddAllCommandType();
// Batches.
public class Foo : ConsoleAppBase
{
public void Echo(string msg)
{
Console.WriteLine(msg);
}
public void Sum(int x, int y)
{
Console.WriteLine((x + y).ToString());
}
}
public class Bar : ConsoleAppBase
{
public void Hello2()
{
Console.WriteLine("H E L L O");
}
}
This is most easy to create many commands so useful for application batch that requires many many command.
Commands are searched from loaded assemblies(in default AppDomain.CurrentDomain.GetAssemblies()
), when does not touch other assemblies type, it will be trimmed and can not load it. In that case, use AddAllCommandType(params Assembly[] searchAssemblies)
overload to pass target assembly, for example AddAllCommandType(typeof(Foo).Assembly)
preserve types.
Complex Argument
If the argument is not primitive, you can pass JSON string.
public class ComplexArgTest : ConsoleAppBase
{
public void Foo(int[] array, Person person)
{
Console.WriteLine(string.Join(", ", array));
Console.WriteLine(person.Age + ":" + person.Name);
}
}
public class Person
{
public int Age { get; set; }
public string Name { get; set; }
}
You can call like here.
> sampletool -array [10,20,30] -person {"Age":10,"Name":"foo"}
# including space, use escaping
> SampleApp.exe -array [10,20,30] -person "{\"Age\":10,\"Name\":\"foo bar\"}"
be careful with JSON string double quotation.
For the array handling, it can be a treat without correct JSON.
e.g. one-length argument can handle without []
.
Foo(int[] array)
> SampleApp.exe -array 9999
multiple-argument can handle by split with
or ,
.
Foo(int[] array)
> SampleApp.exe -array "11 22 33"
> SampleApp.exe -array "11,22,33"
> SampleApp.exe -array "[11,22,33]"
string argument can handle without "
.
Foo(string[] array)
> SampleApp.exe -array hello
> SampleApp.exe -array "foo bar baz"
> SampleApp.exe -array foo,bar,baz
> SampleApp.exe -array "["foo","bar","baz"]"
Exit Code
If the method returns int
or Task<int>
or `ValueTask value, ConsoleAppFramework will set the return value to the exit code.
public class ExampleApp : ConsoleAppBase
{
[Command("exit")]
public int ExitCode()
{
return 12345;
}
[Command("exit-with-task")]
public async Task<int> ExitCodeWithTask()
{
return 54321;
}
}
NOTE: If the method throws an unhandled exception, ConsoleAppFramework always set
1
to the exit code.
Implicit Using
In .NET 6, global using ConsoleAppFramework
is enabled in default. If you remove global using, setup this element to target .csproj
.
<ItemGroup>
<Using Remove="ConsoleAppFramework" />
</ItemGroup>
CommandAttribute
CommandAttribute
enables subscommand on RunConsoleAppFramework<T>()
(for single type CLI app), changes command name on RunConsoleAppFramework()
(for muilti type command routing), also describes the description.
RunConsoleAppFramework<App>();
public class App : ConsoleAppBase
{
// as Root Command(no command argument)
public void Run()
{
}
[Command("sec", "sub comman of this app")]
public void Second()
{
}
}
RunConsoleAppFramework();
public class App2 : ConsoleAppBase
{
// routing command: `app2 exec`
[Command("exec", "exec app.")]
public void Exec1()
{
}
}
// command attribute also can use to class.
[Command("mycmd")]
public class App3 : ConsoleAppBase
{
// routing command: `mycmd e2`
[Command("e2", "exec app 2.")]
public void ExecExec()
{
}
}
OptionAttribute
OptionAttribute configure parameter, it can set shortName or order index, and help description.
If you want to add only description, set "" or null to shortName parameter.
public void Hello(
[Option("n", "name of send user.")]string name,
[Option("r", "repeat count.")]int repeat = 3)
{
}
[Command("escape")]
public void UrlEscape([Option(0, "input of this command")]string input)
{
}
[Command("unescape")]
public void UrlUnescape([Option(null, "input of this command")]string input)
{
}
Command parameters validation
Values of command parameters can be validated via validation attributes from System.ComponentModel.DataAnnotations
namespace and custom ones inheriting ValidationAttribute
type.
using System.ComponentModel.DataAnnotations;
// ...
internal class TestConsoleApp : ConsoleAppBase
{
[Command("some-command")]
public void SomeCommand(
[EmailAddress] string firstArg,
[Range(0, 2)] int secondArg) => Console.WriteLine($"hello from {nameof(TestConsoleApp)}");
}
Output (command invoked with params [--first-arg "invalid-email-address" --second-arg" 10])
Some parameters have invalid values:
first-arg (invalid-email-address): The String field is not a valid e-mail address.
second-arg (10): The field Int32 must be between 0 and 2.
Daemon
If use infinite-loop, it becomes daemon program. ConsoleAppContext.CancellationToken
is lifecycle token of application. You can check CancellationToken.IsCancellationRequested
and shutdown gracefully.
public class Daemon : ConsoleAppBase
{
[RootCommand]
public async Task Run()
{
// you can write infinite-loop while stop request(Ctrl+C or docker terminate).
try
{
while (!this.Context.CancellationToken.IsCancellationRequested)
{
try
{
Console.WriteLine("Wait One Minutes");
}
catch (Exception ex)
{
// error occured but continue to run(or terminate).
Console.WriteLine(ex, "Found error");
}
// wait for next time
await Task.Delay(TimeSpan.FromMinutes(1), this.Context.CancellationToken);
}
}
catch (Exception ex) when (!(ex is OperationCanceledException))
{
// you can write finally exception handling(without cancellation)
}
finally
{
// you can write cleanup code here.
}
}
}
Abort Timeout
ConsoleAppFramework's execution lifetime is managed via generic host. If you do cancel(Ctrl+C), host starts cancellation process with timeout. If you don't pass CancellationToken
in async method, does not cancel immediately.
public async Task Wait1()
{
// Not good.
await Task.Delay(TimeSpan.FromMinutes(60));
}
public async Task Wait2()
{
// Good.
await Task.Delay(TimeSpan.FromMinutes(60), this.Context.CancellationToken);
}
Default timeout time is 00:00:05
, you can change via ConfigureHostOptions
.
var app = ConsoleApp.CreateBuilder(args)
.ConfigureHostOptions(options =>
{
// change timeout.
options.ShutdownTimeout = TimeSpan.FromMinutes(30);
})
.Build();
Filter
Filter can hook before/after batch running event. You can implement ConsoleAppFilter
for it and attach to global/class/method.
public class MyFilter : ConsoleAppFilter
{
// Filter is instantiated by DI so you can get parameter by constructor injection.
public async override ValueTask Invoke(ConsoleAppContext context, Func<ConsoleAppContext, ValueTask> next)
{
try
{
/* on before */
await next(context); // next
}
catch
{
/* on after */
throw;
}
finally
{
/* on finally */
}
}
}
ConsoleAppContext.Timestamp
has start time so if subtraction from now, get elapsed time.
public class LogRunningTimeFilter : ConsoleAppFilter
{
public override async ValueTask Invoke(ConsoleAppContext context, Func<ConsoleAppContext, ValueTask> next)
{
context.Logger.LogInformation("Call method at " + context.Timestamp.ToLocalTime()); // LocalTime for human readable time
try
{
await next(context);
context.Logger.LogInformation("Call method Completed successfully, Elapsed:" + (DateTimeOffset.UtcNow - context.Timestamp));
}
catch
{
context.Logger.LogInformation("Call method Completed Failed, Elapsed:" + (DateTimeOffset.UtcNow - context.Timestamp));
throw;
}
}
}
In default, ConsoleAppFramework does not prevent double startup but if create filter, can do.
public class MutexFilter : ConsoleAppFilter
{
public override async ValueTask Invoke(ConsoleAppContext context, Func<ConsoleAppContext, ValueTask> next)
{
var name = context.MethodInfo.DeclaringType.Name + "." + context.MethodInfo.Name;
using (var mutex = new Mutex(true, name, out var createdNew))
{
if (!createdNew)
{
throw new Exception($"already running {name} in another process.");
}
await next(context);
}
}
}
There filters can pass to ConsoleAppOptions.GlobalFilters
on startup or attach by attribute on class, method.
var app = ConsoleApp.Create(args, options =>
{
options.GlobalFilters = new ConsoleAppFilter[]
{
new MutextFilter() { Order = -9999 } ,
new LogRunningTimeFilter() { Oder = -9998 },
}
});
[ConsoleAppFilter(typeof(MyFilter3))]
public class MyBatch : ConsoleAppBase
{
[ConsoleAppFilter(typeof(MyFilter4), Order = -9999)]
[ConsoleAppFilter(typeof(MyFilter5), Order = 9999)]
public void Do()
{
}
}
Execution order can control by int Order
property.
Logging
In default, Context.Logger
has ILogger<ConsoleApp>
and ILogger<T>
can inject to constructor. Default ConsoleLogger
format in Host.CreateDefaultBuilder
is supernumerary and not suitable for console application. ConsoleAppFramework provides SimpleConsoleLogger
to replace default ConsoleLogger in default. If you want to keep default ConsoleLogger
, use ConsoleAppOptions.ReplaceToUseSimpleConsoleLogger
to false
.
If you want to use high performance logger/output to file, also use Cysharp/ZLogger that easy to integrate ConsoleAppFramework.
using ZLogger;
var app = ConsoleApp.CreateDefaultBuilder(args)
.ConfigureLogging(x =>
{
x.ClearProviders(); // clear all providers
x.SetMinimumLevel(LogLevel.Trace); // change log level if you want
x.AddZLoggerConsole(); // add ZLogger Console
x.AddZLoggerFile("fileName.log"); // add ZLogger file output
})
.Build();
Configuration
ConsoleAppFramework is just an infrastructure. You can add appsettings.json
or other configs as .NET Core offers via Microsoft.Extensions.Options
.
You can add appsettings.json
and appsettings.{environment}.json
and typesafe load via map config to Class w/IOption.
Here's single contained batch with Config loading sample.
// appconfig.json(Content, Copy to Output Directory)
{
"Foo": 42,
"Bar": true
}
using Microsoft.Extensions.DependencyInjection;
var app = ConsoleApp.CreateBuilder(args)
.ConfigureServices((hostContext, services) =>
{
// mapping config json to IOption<MyConfig>
// requires "Microsoft.Extensions.Options.ConfigurationExtensions" package
// if you want to map subscetion in json, use Configure<T>(hostContext.Configuration.GetSection("foo"))
services.Configure<MyConfig>(hostContext.Configuration);
})
.Build();
public class ConfigAppSample : ConsoleAppBase
{
MyConfig config;
// get configuration from DI.
public ConfigAppSample(IOptions<MyConfig> config)
{
this.config = config.Value;
}
public void ShowOption()
{
Console.WriteLine(config.Bar);
Console.WriteLine(config.Foo);
}
}
for the details, please see .NET Core Generic Host documentation.
DI
You can use DI(constructor injection) by GenericHost.
IOptions<MyConfig> config;
ILogger<MyApp> logger;
public MyApp(IOptions<MyConfig> config, ILogger<MyApp> logger)
{
this.config = config;
this.logger = logger;
}
DI also allows delegate registration.
app.AddCommand("di", (ConsoleAppContext ctx, ILogger logger, int param1, int param2) => { });
DI also inject to filter.
Cleanup
You can implement IDisposable.Dispose
or IAsyncDisposable.DisposeAsync
explicitly, that is called after command finished.
public class MyApp : ConsoleAppBase, IDisposable
{
public void Hello()
{
Console.WriteLine("Hello");
}
// Dispose/DisposeAsync method is not registered as Command.
public void Dispose()
{
Console.WriteLine("DISPOSED");
}
}
If implements both IDisposable
and IAsyncDisposable
, called only IAsyncDisposable
.
public class MyApp : ConsoleAppBase, IDisposable, IAsyncDisposable
{
public void Hello()
{
Console.WriteLine("Hello");
}
public void Dispose()
{
Console.WriteLine("Not called.");
}
public async ValueTask DisposeAsync()
{
Console.WriteLine("called.");
}
}
ConsoleAppContext
ConsoleAppContext is injected to property on method executing.
public class ConsoleAppContext
{
public string?[] Arguments { get; }
public DateTime Timestamp { get; }
public CancellationToken CancellationToken { get; }
public ILogger<ConsoleAppEngine> Logger { get; }
public MethodInfo MethodInfo { get; }
public IServiceProvider ServiceProvider { get; }
public IDictionary<string, object> Items { get; }
public void Cancel();
public void Terminate();
}
Cancel()
set CancellationToken
to canceled. Also Terminate()
set token to cancled and terminate process(internal throws OperationCanceledException
immediately).
ConsoleAppOptions
You can configure framework behaviour by ConsoleAppOptions.
var app = ConsoleApp.Create(args, options =>
{
options.StrictOption = false, // default is true.
options.ShowDefaultCommand = false, // default is true
});
public class ConsoleAppOptions
{
/// <summary>Argument parser uses strict(-short, --long) option. Default is true.</summary>
public bool StrictOption { get; set; } = true;
/// <summary>Show default command(help/version) to help. Default is true.</summary>
public bool ShowDefaultCommand { get; set; } = true;
public bool ReplaceToUseSimpleConsoleLogger { get; set; } = true;
public JsonSerializerOptions? JsonSerializerOptions { get; set; }
public ConsoleAppFilter[]? GlobalFilters { get; set; }
public bool NoAttributeCommandAsImplicitlyDefault { get; set; }
public Func<string, string> NameConverter { get; set; } = KebabCaseConvert;
public string? ApplicationName { get; set; } = null;
}
If StrictOption = false, does not distinguish between the number of -
. For example, this method
public void Hello([Option("m", "Message to display.")]string message)
can pass argument by -m
, --message
and -message
. This is styled like a go lang command. But if you want to strictly distinguish argument of -
, set StrcitOption = true
(default), that allows -m
and --message
.
Also, by default, the help
and version
commands appear as help, which can be hidden by setting ShowDefaultCommand = false
.
NameConverter is used type-name, method-name, parameter-name converting as command. Default is convert to lower kebab-case.
// my-command query-data --organization-id --user-id
public class MyCommand
{
public void QueryData(string organizationId, string userId);
}
You can set func to change this behaviour like NameConverter = x => x.ToLower();
.
ApplicationName
configure help usages Usage: ***
, default(null) shows filename without extension.
Terminate handling in Console.Read
ConsoleAppFramework handle terminate signal(Ctrl+C) gracefully with ConsoleAppContext.CancellationToken
. If your application waiting with Console.Read/ReadLine/ReadKey, requires additional handling.
// case of Console.Read/ReadLine, pressed Ctrl+C, Read returns null.
ConsoleApp.Run(args, (ConsoleAppContext ctx) =>
{
var read = Console.ReadLine();
if (read == null) ctx.Terminate();
});
// case of Console.ReadKey, can not cancel itself so use with Task.Run and WaitAsync.
ConsoleApp.Run(args, async (ConsoleAppContext ctx) =>
{
var key = await Task.Run(() => Console.ReadKey()).WaitAsync(ctx.CancellationToken);
});
Publish to executable file
dotnet run is useful for local development or execute in CI tool. For example in CI, git pull and execute by dotnet run -- --options
is easy to manage and execute utilities.
dotnet publish to create executable file. .NET Core 3.0 offers Single Executable File via PublishSingleFile
.
CLI tool can use .NET Core Local/Global Tools. If you want to create it, check the Tutorial: Create a .NET tool using the .NET CLI and Use a global tool or Use a local tool.
v3 Legacy Compatibility
v1-v3 does not exist minimal api style(ConsoleApp.Create/CreateBuilder
).
await Host.CreateDefaultBuilder()
.RunConsoleAppFrameworkAsync<Program>(args);
RunConsoleAppFrameworkAsync
is still exists but does not recommend to use. Also, since v4, there is a change in the default behavior. When RunConsoleAppFrameworkAsync
is used, the option settings of v3 and earlier will be used.
options.NoAttributeCommandAsImplicitlyDefault = true;
options.StrictOption = false;
options.NameConverter = x => x.ToLower();
options.ReplaceToUseSimpleConsoleLogger = false;
You can also get this option setting in ConsoleAppOptions.CreateLegacyCompatible()
.
License
This library is under the MIT License.