Skip to content

Commit d28d5db

Browse files
committed
ReadMe and other features
1 parent 2a4c556 commit d28d5db

17 files changed

+647
-141
lines changed

.gitignore

-3
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,6 @@ publish/
140140
# Publish Web Output
141141
*.[Pp]ublish.xml
142142
*.azurePubxml
143-
# TODO: Comment the next line if you want to checkin your web deploy settings
144-
# but database connection strings (with potential passwords) will be unencrypted
145-
#*.pubxml
146143
*.publishproj
147144

148145
# Microsoft Azure Web App publish settings. Comment the next line if you want to

ReadMe.md

+157-57
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,8 @@ using ConsoleAppFramework;
168168
ConsoleApp.Run(args, (string name) => Console.WriteLine($"Hello {name}"));
169169
```
170170

171+
> The latest Visual Studio changed the execution timing of Source Generators to either during save or at compile time. If you encounter unexpected behavior, try compiling once or change the option to "Automatic" under TextEditor -> C# -> Advanced -> Source Generators.
172+
171173
You can execute command like `sampletool --name "foo"`.
172174

173175
* The return value can be `void`, `int`, `Task`, or `Task<int>`
@@ -257,7 +259,7 @@ To add aliases to parameters, list the aliases separated by `|` before the comma
257259

258260
Unfortunately, due to current C# specifications, lambda expressions and [local functions do not support document comments](https://github.com/dotnet/csharplang/issues/2110), so a class is required.
259261

260-
In addition to `-h|--help`, there is another special built-in option: `--version`. In default, it displays the `AssemblyInformationalVersion` or `AssemblyVersion`. You can configure version string by `ConsoleApp.Version`, for example `ConsoleApp.Version = "2001.9.3f14-preview2";`.
262+
In addition to `-h|--help`, there is another special built-in option: `--version`. In default, it displays the `AssemblyInformationalVersion` without source revision or `AssemblyVersion`. You can configure version string by `ConsoleApp.Version`, for example `ConsoleApp.Version = "2001.9.3f14-preview2";`.
261263

262264
Command
263265
---
@@ -355,6 +357,43 @@ app.Add<MyCommands>("foo");
355357
app.Run(args);
356358
```
357359

360+
### Register from attribute
361+
362+
Instead of using `Add<T>`, you can automatically add commands by applying the `[RegisterCommands]` attribute to a class.
363+
364+
```csharp
365+
[RegisterCommands]
366+
public class Foo
367+
{
368+
public void Baz(int x)
369+
{
370+
Console.Write(x);
371+
}
372+
}
373+
374+
[RegisterCommands("bar")]
375+
public class Bar
376+
{
377+
public void Baz(int x)
378+
{
379+
Console.Write(x);
380+
}
381+
}
382+
```
383+
384+
These are automatically added when using `ConsoleApp.Create()`.
385+
386+
```csharp
387+
var app = ConsoleApp.Create();
388+
389+
// Commands:
390+
// baz
391+
// bar baz
392+
app.Run(args);
393+
```
394+
395+
You can also combine this with `Add` or `Add<T>` to add more commands.
396+
358397
### Performance of Commands
359398

360399
In `ConsoleAppFramework`, the number and types of registered commands are statically determined at compile time. For example, let's register the following four commands:
@@ -454,6 +493,30 @@ partial void RunCore(string[] args)
454493

455494
The C# compiler performs complex generation for string constant switches, making them extremely fast, and it would be difficult to achieve faster routing than this.
456495

496+
Disable Naming Conversion
497+
---
498+
Command names and option names are automatically converted to kebab-case by default. While this follows standard command-line tool naming conventions, you might find this conversion inconvenient when creating batch files for internal applications. Therefore, it's possible to disable this conversion at the assembly level.
499+
500+
```csharp
501+
using ConsoleAppFramework;
502+
503+
[assembly: ConsoleAppFrameworkGeneratorOptions(DisableNamingConversion = true)]
504+
505+
var app = ConsoleApp.Create();
506+
app.Add<MyProjectCommand>();
507+
app.Run(args);
508+
509+
public class MyProjectCommand
510+
{
511+
public void ExecuteCommand(string fooBarBaz)
512+
{
513+
Console.WriteLine(fooBarBaz);
514+
}
515+
}
516+
```
517+
518+
You can disable automatic conversion by using `[assembly: ConsoleAppFrameworkGeneratorOptions(DisableNamingConversion = true)]`. In this case, the command would be `ExecuteCommand --fooBarBaz`.
519+
457520
Parse and Value Binding
458521
---
459522
The method parameter names and types determine how to parse and bind values from the command-line arguments. When using lambda expressions, optional values and `params` arrays supported from C# 12 are also supported.
@@ -824,66 +887,81 @@ Dependency Injection(Logging, Configuration, etc...)
824887
---
825888
The execution processing of `ConsoleAppFramework` fully supports `DI`. When you want to use a logger, read a configuration, or share processing with an ASP.NET project, using `Microsoft.Extensions.DependencyInjection` or other DI libraries can make processing convenient.
826889

827-
Lambda expressions passed to Run, class constructors, methods, and filter constructors can inject services obtained from `IServiceProvider`. Let's look at a minimal example. Setting any `System.IServiceProvider` to `ConsoleApp.ServiceProvider` enables DI throughout the system.
890+
If you are referencing `Microsoft.Extensions.DependencyInjection`, you can call the `ConfigureServices` method from `ConsoleApp.ConsoleAppBuilder` (ConsoleAppFramework adds methods based on your project's reference status).
828891

829892
```csharp
830-
// Microsoft.Extensions.DependencyInjection
831-
var services = new ServiceCollection();
832-
services.AddTransient<MyService>();
893+
var app = ConsoleApp.Create()
894+
.ConfigureServices(service =>
895+
{
896+
service.AddTransient<MyService>();
897+
});
833898

834-
using var serviceProvider = services.BuildServiceProvider();
899+
app.Add("", ([FromServices] MyService service, int x, int y) => Console.WriteLine(x + y));
835900

836-
// Any DI library can be used as long as it can create an IServiceProvider
837-
ConsoleApp.ServiceProvider = serviceProvider;
838-
839-
// When passing to a lambda expression/method, using [FromServices] indicates that it is passed via DI, not as a parameter
840-
ConsoleApp.Run(args, ([FromServices]MyService service, int x, int y) => Console.WriteLine(x + y));
901+
app.Run(args);
841902
```
842903

843904
When passing to a lambda expression or method, the `[FromServices]` attribute is used to distinguish it from command parameters. When passing a class, Constructor Injection can be used, resulting in a simpler appearance.
844905

845-
Let's try injecting a logger and enabling output to a file. The libraries used are Microsoft.Extensions.Logging and [Cysharp/ZLogger](https://github.com/Cysharp/ZLogger/) (a high-performance logger built on top of MS.E.Logging).
846-
906+
Let's try injecting a logger and enabling output to a file. The libraries used are Microsoft.Extensions.Logging and [Cysharp/ZLogger](https://github.com/Cysharp/ZLogger/) (a high-performance logger built on top of MS.E.Logging). If you are referencing `Microsoft.Extensions.Logging`, you can call `ConfigureLogging` from `ConsoleAppBuilder`.
847907

848908
```csharp
849909
// Package Import: ZLogger
850-
var services = new ServiceCollection();
851-
services.AddLogging(x =>
852-
{
853-
x.ClearProviders();
854-
x.SetMinimumLevel(LogLevel.Trace);
855-
x.AddZLoggerConsole();
856-
x.AddZLoggerFile("log.txt");
857-
});
858-
859-
using var serviceProvider = services.BuildServiceProvider(); // using for logger flush(important!)
860-
ConsoleApp.ServiceProvider = serviceProvider;
910+
var app = ConsoleApp.Create()
911+
.ConfigureLogging(x =>
912+
{
913+
x.ClearProviders();
914+
x.SetMinimumLevel(LogLevel.Trace);
915+
x.AddZLoggerConsole();
916+
x.AddZLoggerFile("log.txt");
917+
});
861918

862-
var app = ConsoleApp.Create();
863919
app.Add<MyCommand>();
864920
app.Run(args);
865921

866922
// inject logger to constructor
867923
public class MyCommand(ILogger<MyCommand> logger)
868924
{
869-
[Command("")]
870925
public void Echo(string msg)
871926
{
872927
logger.ZLogInformation($"Message is {msg}");
873928
}
874929
}
875930
```
876931

877-
`ConsoleApp` has replaceable default logging methods `ConsoleApp.Log` and `ConsoleApp.LogError` used for Help display and exception handling. If using `ILogger<T>`, it's better to replace these as well.
932+
For building an `IServiceProvider`, `ConfigureServices/ConfigureLogging` uses `Microsoft.Extensions.DependencyInjection.ServiceCollection`. If you want to set a custom ServiceProvider or a ServiceProvider built from Host, or if you want to execute DI with `ConsoleApp.Run`, set it to `ConsoleApp.ServiceProvider`.
878933

879934
```csharp
880-
using var serviceProvider = services.BuildServiceProvider(); // using for cleanup(important)
935+
// Microsoft.Extensions.DependencyInjection
936+
var services = new ServiceCollection();
937+
services.AddTransient<MyService>();
938+
939+
using var serviceProvider = services.BuildServiceProvider();
940+
941+
// Any DI library can be used as long as it can create an IServiceProvider
881942
ConsoleApp.ServiceProvider = serviceProvider;
882943

883-
// setup ConsoleApp system logger
884-
var logger = serviceProvider.GetRequiredService<ILogger<Program>>();
885-
ConsoleApp.Log = msg => logger.LogInformation(msg);
886-
ConsoleApp.LogError = msg => logger.LogError(msg);
944+
// When passing to a lambda expression/method, using [FromServices] indicates that it is passed via DI, not as a parameter
945+
ConsoleApp.Run(args, ([FromServices]MyService service, int x, int y) => Console.WriteLine(x + y));
946+
```
947+
948+
`ConsoleApp` has replaceable default logging methods `ConsoleApp.Log` and `ConsoleApp.LogError` used for Help display and exception handling. If using `ILogger<T>`, it's better to replace these as well.
949+
950+
```csharp
951+
app.UseFilter<ReplaceLogFilter>();
952+
953+
// inject logger to filter
954+
internal sealed class ReplaceLogFilter(ConsoleAppFilter next, ILogger<Program> logger)
955+
: ConsoleAppFilter(next)
956+
{
957+
public override Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken)
958+
{
959+
ConsoleApp.Log = msg => logger.LogInformation(msg);
960+
ConsoleApp.LogError = msg => logger.LogError(msg);
961+
962+
return Next.InvokeAsync(context, cancellationToken);
963+
}
964+
}
887965
```
888966

889967
DI can also be effectively used when reading application configuration from `appsettings.json`. For example, suppose you have the following JSON file.
@@ -899,30 +977,32 @@ DI can also be effectively used when reading application configuration from `app
899977
}
900978
```
901979

980+
```xml
981+
<ItemGroup>
982+
<None Update="appsettings.json">
983+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
984+
</None>
985+
</ItemGroup>
986+
```
987+
902988
Using `Microsoft.Extensions.Configuration.Json`, reading, binding, and registering with DI can be done as follows.
903989

904990
```csharp
905991
// Package Import: Microsoft.Extensions.Configuration.Json
906-
var configuration = new ConfigurationBuilder()
907-
.SetBasePath(Directory.GetCurrentDirectory())
908-
.AddJsonFile("appsettings.json")
909-
.Build();
910-
911-
// Bind to services( Package Import: Microsoft.Extensions.Options.ConfigurationExtensions )
912-
var services = new ServiceCollection();
913-
services.Configure<PositionOptions>(configuration.GetSection("Position"));
914-
915-
using var serviceProvider = services.BuildServiceProvider();
916-
ConsoleApp.ServiceProvider = serviceProvider;
992+
var app = ConsoleApp.Create()
993+
.ConfigureDefaultConfiguration()
994+
.ConfigureServices((configuration, services) =>
995+
{
996+
// Package Import: Microsoft.Extensions.Options.ConfigurationExtensions
997+
services.Configure<PositionOptions>(configuration.GetSection("Position"));
998+
});
917999

918-
var app = ConsoleApp.Create();
9191000
app.Add<MyCommand>();
9201001
app.Run(args);
9211002

9221003
// inject options
9231004
public class MyCommand(IOptions<PositionOptions> options)
9241005
{
925-
[Command("")]
9261006
public void Echo(string msg)
9271007
{
9281008
ConsoleApp.Log($"Binded Option: {options.Value.Title} {options.Value.Name}");
@@ -936,25 +1016,23 @@ public class PositionOptions
9361016
}
9371017
```
9381018

939-
If you have other applications such as ASP.NET in the entire project and want to use common DI and configuration set up using `Microsoft.Extensions.Hosting`, you can share them by setting the `IServiceProvider` of `IHost` after building.
1019+
When `Microsoft.Extensions.Configuration.Abstractions` is imported, `ConfigureEmptyConfiguration` becomes available to call. Additionally, when `Microsoft.Extensions.Configuration.Json` is imported, `ConfigureDefaultConfiguration` becomes available to call. In DefaultConfiguration, `SetBasePath(System.IO.Directory.GetCurrentDirectory())` and `AddJsonFile("appsettings.json", optional: true)` are executed before calling `Action<IConfigurationBuilder> configure`.
9401020

941-
```csharp
942-
// Package Import: Microsoft.Extensions.Hosting
943-
var builder = Host.CreateApplicationBuilder(); // don't pass args.
1021+
Furthermore, overloads of `Action<IConfiguration, IServiceCollection> configure` and `Action<IConfiguration, ILoggingBuilder> configure` are added to `ConfigureServices` and `ConfigureLogging`, allowing you to retrieve the Configuration when executing the delegate.
9441022

945-
using var host = builder.Build(); // use using for host lifetime
946-
using var scope = host.Services.CreateScope(); // create execution scope
947-
ConsoleApp.ServiceProvider = scope.ServiceProvider; // use host scoped ServiceProvider
1023+
without Hosting dependency, I've prefere these import packages.
9481024

949-
ConsoleApp.Run(args, ([FromServices] ILogger<Program> logger) => logger.LogInformation("Hello World!"));
1025+
```xml
1026+
<ItemGroup>
1027+
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />
1028+
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" />
1029+
<PackageReference Include="ZLogger" Version="2.5.9" />
1030+
</ItemGroup>
9501031
```
9511032

952-
ConsoleAppFramework has its own lifetime management (see the [CancellationToken(Gracefully Shutdown) and Timeout](#cancellationtokengracefully-shutdown-and-timeout) section), so Host's Start/Stop is not necessary. However, be sure to use the Host itself.
953-
9541033
As it is, the DI scope is not set, but by using a global filter, you can add a scope for each command execution. `ConsoleAppFilter` can also inject services via constructor injection, so let's get the `IServiceProvider`.
9551034

9561035
```csharp
957-
var app = ConsoleApp.Create();
9581036
app.UseFilter<ServiceProviderScopeFilter>();
9591037

9601038
internal class ServiceProviderScopeFilter(IServiceProvider serviceProvider, ConsoleAppFilter next) : ConsoleAppFilter(next)
@@ -963,13 +1041,35 @@ internal class ServiceProviderScopeFilter(IServiceProvider serviceProvider, Cons
9631041
{
9641042
// create Microsoft.Extensions.DependencyInjection scope
9651043
await using var scope = serviceProvider.CreateAsyncScope();
966-
await Next.InvokeAsync(context, cancellationToken);
1044+
1045+
var originalServiceProvider = ConsoleApp.ServiceProvider;
1046+
ConsoleApp.ServiceProvider = scope.ServiceProvider;
1047+
try
1048+
{
1049+
await Next.InvokeAsync(context, cancellationToken);
1050+
}
1051+
finally
1052+
{
1053+
ConsoleApp.ServiceProvider = originalServiceProvider;
1054+
}
9671055
}
9681056
}
9691057
```
9701058

9711059
However, since the construction of the filters is performed before execution, automatic injection using scopes is only effective for the command body itself.
9721060

1061+
If you have other applications such as ASP.NET in the entire project and want to use common DI and configuration set up using `Microsoft.Extensions.Hosting`, you can call `ToConsoleAppBuilder` from `IHostBuilder` or `HostApplicationBuilder`.
1062+
1063+
```csharp
1064+
// Package Import: Microsoft.Extensions.Hosting
1065+
var app = Host.CreateApplicationBuilder()
1066+
.ToConsoleAppBuilder();
1067+
```
1068+
1069+
In this case, it builds the HostBuilder, creates a Scope for the ServiceProvider, and disposes of all of them after execution.
1070+
1071+
ConsoleAppFramework has its own lifetime management (see the [CancellationToken(Gracefully Shutdown) and Timeout](#cancellationtokengracefully-shutdown-and-timeout) section), so Host's Start/Stop is not necessary.
1072+
9731073
Colorize
9741074
---
9751075
The framework doesn't support colorization directly; however, utilities like [Cysharp/Kokuban](https://github.com/Cysharp/Kokuban) make console colorization easy.

sandbox/CliFrameworkBenchmark/CliFrameworkBenchmark.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
<PackageReference Include="CommandLineParser" Version="2.9.1" />
2424
<!--<PackageReference Include="ConsoleAppFramework" Version="4.2.4" />-->
2525
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="4.1.1" />
26+
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
2627
<PackageReference Include="PowerArgs" Version="4.0.3" />
2728
<PackageReference Include="Spectre.Console.Cli" Version="0.49.1" />
2829
<PackageReference Include="System.CommandLine" Version="2.0.0-beta1.20071.2" />

sandbox/GeneratorSandbox/Filters.cs

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11

22
using ConsoleAppFramework;
3+
using System.ComponentModel.DataAnnotations;
4+
35
// using Microsoft.Extensions.DependencyInjection;
46
using System.Diagnostics;
57
using System.Reflection;
@@ -30,6 +32,8 @@ public override async Task InvokeAsync(ConsoleAppContext context, CancellationTo
3032
}
3133
}
3234

35+
36+
3337
internal class AuthenticationFilter(ConsoleAppFilter next) : ConsoleAppFilter(next)
3438
{
3539
public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken)

sandbox/GeneratorSandbox/GeneratorSandbox.csproj

+3-4
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,9 @@
1515
</PropertyGroup>
1616

1717
<ItemGroup>
18-
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
19-
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
20-
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
21-
<PackageReference Include="ZLogger" Version="2.4.1" />
18+
<!--<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />
19+
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" />
20+
<PackageReference Include="ZLogger" Version="2.5.9" />-->
2221
</ItemGroup>
2322

2423
<ItemGroup>

0 commit comments

Comments
 (0)