{"id":13458166,"url":"https://github.com/Cysharp/ConsoleAppFramework","last_synced_at":"2025-03-24T15:30:51.575Z","repository":{"id":39902926,"uuid":"171842606","full_name":"Cysharp/ConsoleAppFramework","owner":"Cysharp","description":"Micro-framework for console applications to building CLI tools/Daemon/Batch for .NET, C#.","archived":false,"fork":false,"pushed_at":"2024-05-22T10:35:52.000Z","size":4565,"stargazers_count":1212,"open_issues_count":4,"forks_count":83,"subscribers_count":42,"default_branch":"master","last_synced_at":"2024-05-22T16:56:24.169Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Cysharp.png","metadata":{"files":{"readme":"ReadMe.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-02-21T09:35:05.000Z","updated_at":"2024-07-29T11:00:24.075Z","dependencies_parsed_at":"2023-02-09T12:31:38.942Z","dependency_job_id":"9c5ae8e7-85bb-47ed-9d5d-0b9dd55ca63b","html_url":"https://github.com/Cysharp/ConsoleAppFramework","commit_stats":{"total_commits":269,"total_committers":23,"mean_commits":"11.695652173913043","dds":0.4163568773234201,"last_synced_commit":"4ffbd35918cb7495e9089d5647897d622a8ca9c4"},"previous_names":["cysharp/microbatchframework"],"tags_count":49,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Cysharp%2FConsoleAppFramework","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Cysharp%2FConsoleAppFramework/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Cysharp%2FConsoleAppFramework/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Cysharp%2FConsoleAppFramework/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Cysharp","download_url":"https://codeload.github.com/Cysharp/ConsoleAppFramework/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":221981634,"owners_count":16911428,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-07-31T09:00:45.991Z","updated_at":"2025-03-24T15:30:51.559Z","avatar_url":"https://github.com/Cysharp.png","language":"C#","readme":"﻿ConsoleAppFramework\n===\n[![GitHub Actions](https://github.com/Cysharp/ConsoleAppFramework/workflows/Build-Debug/badge.svg)](https://github.com/Cysharp/ConsoleAppFramework/actions) [![Releases](https://img.shields.io/github/release/Cysharp/ConsoleAppFramework.svg)](https://github.com/Cysharp/ConsoleAppFramework/releases)\n\nConsoleAppFramework v5 is Zero Dependency, Zero Overhead, Zero Reflection, Zero Allocation, AOT Safe CLI Framework powered by C# Source Generator; achieves exceptionally high performance, fastest start-up time(with NativeAOT) and minimal binary size. Leveraging the latest features of .NET 8 and C# 13 ([IncrementalGenerator](https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md), [managed function pointer](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-9.0/function-pointers#function-pointers-1), [params arrays and default values lambda expression](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/lambda-expressions#input-parameters-of-a-lambda-expression), [`ISpanParsable\u003cT\u003e`](https://learn.microsoft.com/en-us/dotnet/api/system.ispanparsable-1), [`PosixSignalRegistration`](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.posixsignalregistration), etc.), this library ensures maximum performance while maintaining flexibility and extensibility.\n\n![image](https://github.com/Cysharp/ConsoleAppFramework/assets/46207/db4bf599-9fe0-4ce4-801f-0003f44d5628)\n\u003e Set `RunStrategy=ColdStart WarmupCount=0` to calculate the cold start benchmark, which is suitable for CLI application.\n\nThe magical performance is achieved by statically generating everything and parsing inline. Let's take a look at a minimal example:\n\n```csharp\nusing ConsoleAppFramework;\n\n// args: ./cmd --foo 10 --bar 20\nConsoleApp.Run(args, (int foo, int bar) =\u003e Console.WriteLine($\"Sum: {foo + bar}\"));\n```\n\nUnlike typical Source Generators that use attributes as keys for generation, ConsoleAppFramework analyzes the provided lambda expressions or method references and generates the actual code body of the Run method.\n\n```csharp\ninternal static partial class ConsoleApp\n{\n    // Generate the Run method itself with arguments and body to match the lambda expression\n    public static void Run(string[] args, Action\u003cint, int\u003e command)\n    {\n        // code body\n    }\n}\n```\n\n\u003cdetails\u003e\u003csummary\u003eFull generated source code\u003c/summary\u003e\n\n```csharp\nnamespace ConsoleAppFramework;\n\ninternal static partial class ConsoleApp\n{\n    public static void Run(string[] args, Action\u003cint, int\u003e command)\n    {\n        if (TryShowHelpOrVersion(args, 2, -1)) return;\n\n        var arg0 = default(int);\n        var arg0Parsed = false;\n        var arg1 = default(int);\n        var arg1Parsed = false;\n\n        try\n        {\n            for (int i = 0; i \u003c args.Length; i++)\n            {\n                var name = args[i];\n\n                switch (name)\n                {\n                    case \"--foo\":\n                    {\n                        if (!TryIncrementIndex(ref i, args.Length) || !int.TryParse(args[i], out arg0)) { ThrowArgumentParseFailed(\"foo\", args[i]); }\n                        arg0Parsed = true;\n                        break;\n                    }\n                    case \"--bar\":\n                    {\n                        if (!TryIncrementIndex(ref i, args.Length) || !int.TryParse(args[i], out arg1)) { ThrowArgumentParseFailed(\"bar\", args[i]); }\n                        arg1Parsed = true;\n                        break;\n                    }\n                    default:\n                        if (string.Equals(name, \"--foo\", StringComparison.OrdinalIgnoreCase))\n                        {\n                            if (!TryIncrementIndex(ref i, args.Length) || !int.TryParse(args[i], out arg0)) { ThrowArgumentParseFailed(\"foo\", args[i]); }\n                            arg0Parsed = true;\n                            break;\n                        }\n                        if (string.Equals(name, \"--bar\", StringComparison.OrdinalIgnoreCase))\n                        {\n                            if (!TryIncrementIndex(ref i, args.Length) || !int.TryParse(args[i], out arg1)) { ThrowArgumentParseFailed(\"bar\", args[i]); }\n                            arg1Parsed = true;\n                            break;\n                        }\n                        ThrowArgumentNameNotFound(name);\n                        break;\n                }\n            }\n            if (!arg0Parsed) ThrowRequiredArgumentNotParsed(\"foo\");\n            if (!arg1Parsed) ThrowRequiredArgumentNotParsed(\"bar\");\n\n            command(arg0!, arg1!);\n        }\n        catch (Exception ex)\n        {\n            Environment.ExitCode = 1;\n            if (ex is ValidationException or ArgumentParseFailedException)\n            {\n                LogError(ex.Message);\n            }\n            else\n            {\n                LogError(ex.ToString());\n            }\n        }\n    }\n\n    [MethodImpl(MethodImplOptions.AggressiveInlining)]\n    static bool TryIncrementIndex(ref int index, int length)\n    {\n        if (index \u003c length)\n        {\n            index++;\n            return true;\n        }\n        return false;\n    }\n\n    static partial void ShowHelp(int helpId)\n    {\n        Log(\"\"\"\nUsage: [options...] [-h|--help] [--version]\n\nOptions:\n  --foo \u003cint\u003e     (Required)\n  --bar \u003cint\u003e     (Required)\n\"\"\");\n    }\n}\n```\n\u003c/details\u003e\n\nAs you can see, the code is straightforward and simple, making it easy to imagine the execution cost of the framework portion. That's right, it's zero. This technique was influenced by Rust's macros. Rust has [Attribute-like macros and Function-like macros](https://doc.rust-lang.org/book/ch19-06-macros.html), and ConsoleAppFramework's generation can be considered as Function-like macros.\n\nThe `ConsoleApp` class, along with everything else, is generated entirely by the Source Generator, resulting in no dependencies, including ConsoleAppFramework itself. This characteristic should contribute to the small assembly size and ease of handling, including support for Native AOT.\n\nMoreover, CLI applications typically involve single-shot execution from a cold start. As a result, common optimization techniques such as dynamic code generation (IL Emit, ExpressionTree.Compile) and caching (ArrayPool) do not work effectively. ConsoleAppFramework generates everything statically in advance, achieving performance equivalent to optimized hand-written code without reflection or boxing.\n\nConsoleAppFramework offers a rich set of features as a framework. The Source Generator analyzes which modules are being used and generates the minimal code necessary to implement the desired functionality.\n\n* SIGINT/SIGTERM(Ctrl+C) handling with gracefully shutdown via `CancellationToken`\n* Filter(middleware) pipeline to intercept before/after execution\n* Exit code management\n* Support for async commands\n* Registration of multiple commands\n* Registration of nested commands\n* Setting option aliases and descriptions from code document comment\n* `System.ComponentModel.DataAnnotations` attribute-based Validation\n* Dependency Injection for command registration by type and public methods\n* `Microsoft.Extensions`(Logging, Configuration, etc...) integration\n* High performance value parsing via `ISpanParsable\u003cT\u003e`\n* Parsing of params arrays\n* Parsing of JSON arguments\n* Double-dash escape arguments\n* Help(`-h|--help`) option builder\n* Default show version(`--version`) option\n\nAs you can see from the generated output, the help display is also fast. In typical frameworks, the help string is constructed after the help invocation. However, in ConsoleAppFramework, the help is embedded as string constants, achieving the absolute maximum performance that cannot be surpassed!\n\nGetting Started\n--\nThis library is distributed via NuGet, minimal requirement is .NET 8 and C# 13.\n\n\u003e dotnet add package [ConsoleAppFramework](https://www.nuget.org/packages/ConsoleAppFramework)\n\nConsoleAppFramework is an analyzer (Source Generator) and does not have any dll references. When referenced, the entry point class `ConsoleAppFramework.ConsoleApp` is generated internally.\n\nThe first argument of `Run` or `RunAsync` can be `string[] args`, and the second argument can be any lambda expression, method, or function reference. Based on the content of the second argument, the corresponding function is automatically generated.\n\n```csharp\nusing ConsoleAppFramework;\n\nConsoleApp.Run(args, (string name) =\u003e Console.WriteLine($\"Hello {name}\"));\n```\n\n\u003e When using .NET 8, you need to explicitly set LangVersion to 13 or above.\n\u003e ```xml\n\u003e  \u003cPropertyGroup\u003e\n\u003e      \u003cTargetFramework\u003enet8.0\u003c/TargetFramework\u003e\n\u003e      \u003cLangVersion\u003e13\u003c/LangVersion\u003e\n\u003e  \u003c/PropertyGroup\u003e\n\n\u003e 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 -\u003e C# -\u003e Advanced -\u003e Source Generators.\n\nYou can execute command like `sampletool --name \"foo\"`.\n\n* The return value can be `void`, `int`, `Task`, or `Task\u003cint\u003e`\n    * If an `int` is returned, that value will be set to `Environment.ExitCode`\n* By default, option argument names are converted to `--lower-kebab-case`\n    * For example, `jsonValue` becomes `--json-value`\n    * Option argument names are case-insensitive, but lower-case matches faster\n\nWhen passing a method, you can write it as follows:\n\n```csharp\nConsoleApp.Run(args, Sum);\n\nvoid Sum(int x, int y) =\u003e Console.Write(x + y);\n```\n\nAdditionally, for static functions, you can pass them as function pointers. In that case, the managed function pointer arguments will be generated, resulting in maximum performance.\n\n```csharp\nunsafe\n{\n    ConsoleApp.Run(args, \u0026Sum);\n}\n\nstatic void Sum(int x, int y) =\u003e Console.Write(x + y);\n```\n\n```csharp\npublic static unsafe void Run(string[] args, delegate* managed\u003cint, int, void\u003e command)\n```\n\nUnfortunately, currently [static lambdas cannot be assigned to function pointers](https://github.com/dotnet/csharplang/discussions/6746), so defining a named function is necessary.\n\nWhen defining an asynchronous method using a lambda expression, the `async` keyword is required.\n\n```csharp\n// --foo, --bar\nawait ConsoleApp.RunAsync(args, async (int foo, int bar, CancellationToken cancellationToken) =\u003e\n{\n    await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);\n    Console.WriteLine($\"Sum: {foo + bar}\");\n});\n```\n\nYou can use either the `Run` or `RunAsync` method for invocation. It is optional to use `CancellationToken` as an argument. This becomes a special parameter and is excluded from the command options. Internally, it uses [`PosixSignalRegistration`](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.posixsignalregistration) to handle `SIGINT`, `SIGTERM`, and `SIGKILL`. When these signals are invoked (e.g., Ctrl+C), the CancellationToken is set to CancellationRequested. If `CancellationToken` is not used as an argument, these signals will not be handled, and the program will terminate immediately. For more details, refer to the [CancellationToken and Gracefully Shutdown](#cancellationtokengracefully-shutdown-and-timeout) section.\n\nOption aliases and Help, Version\n---\nBy default, if `-h` or `--help` is provided, or if no arguments are passed, the help display will be invoked.\n\n```csharp\nConsoleApp.Run(args, (string message) =\u003e Console.Write($\"Hello, {message}\"));\n```\n\n```txt\nUsage: [options...] [-h|--help] [--version]\n\nOptions:\n  --message \u003cstring\u003e     (Required)\n```\n\nIn ConsoleAppFramework, instead of using attributes, you can provide descriptions and aliases for functions by writing Document Comments. This avoids the common issue in frameworks where arguments become cluttered with attributes, making the code difficult to read. With this approach, a natural writing style is achieved.\n\n```csharp\nConsoleApp.Run(args, Commands.Hello);\n\nstatic class Commands\n{\n    /// \u003csummary\u003e\n    /// Display Hello.\n    /// \u003c/summary\u003e\n    /// \u003cparam name=\"message\"\u003e-m, Message to show.\u003c/param\u003e\n    public static void Hello(string message) =\u003e Console.Write($\"Hello, {message}\");\n}\n```\n\n```txt\nUsage: [options...] [-h|--help] [--version]\n\nDisplay Hello.\n\nOptions:\n  -m|--message \u003cstring\u003e    Message to show. (Required)\n```\n\nTo add aliases to parameters, list the aliases separated by `|` before the comma in the comment. For example, if you write a comment like `-a|-b|--abcde, Description.`, then `-a`, `-b`, and `--abcde` will be treated as aliases, and `Description.` will be the description.\n\nUnfortunately, 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.\n\nIn 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\";`.\n\nCommand\n---\nIf you want to register multiple commands or perform complex operations (such as adding filters), instead of using `Run/RunAsync`, obtain the `ConsoleAppBuilder` using `ConsoleApp.Create()`. Call `Add`, `Add\u003cT\u003e`, or `UseFilter\u003cT\u003e` multiple times on the `ConsoleAppBuilder` to register commands and filters, and finally execute the application using `Run` or `RunAsync`.\n\n```csharp\nvar app = ConsoleApp.Create();\n\napp.Add(\"\", (string msg) =\u003e Console.WriteLine(msg));\napp.Add(\"echo\", (string msg) =\u003e Console.WriteLine(msg));\napp.Add(\"sum\", (int x, int y) =\u003e Console.WriteLine(x + y));\n\n// --msg\n// echo --msg\n// sum --x --y\napp.Run(args);\n```\n\nThe first argument of `Add` is the command name. If you specify an empty string `\"\"`, it becomes the root command. Unlike parameters, command names are case-sensitive and cannot have multiple names.\n\nWith `Add\u003cT\u003e`, you can add multiple commands at once using a class-based approach, where public methods are treated as commands. If you want to write document comments for multiple commands, this approach allows for cleaner code, so it is recommended. Additionally, as mentioned later, you can also write clean code for Dependency Injection (DI) using constructor injection.\n\n```csharp\nvar app = ConsoleApp.Create();\napp.Add\u003cMyCommands\u003e();\napp.Run(args);\n\npublic class MyCommands\n{\n    /// \u003csummary\u003eRoot command test.\u003c/summary\u003e\n    /// \u003cparam name=\"msg\"\u003e-m, Message to show.\u003c/param\u003e\n    [Command(\"\")]\n    public void Root(string msg) =\u003e Console.WriteLine(msg);\n\n    /// \u003csummary\u003eDisplay message.\u003c/summary\u003e\n    /// \u003cparam name=\"msg\"\u003eMessage to show.\u003c/param\u003e\n    public void Echo(string msg) =\u003e Console.WriteLine(msg);\n\n    /// \u003csummary\u003eSum parameters.\u003c/summary\u003e\n    /// \u003cparam name=\"x\"\u003eleft value.\u003c/param\u003e\n    /// \u003cparam name=\"y\"\u003eright value.\u003c/param\u003e\n    public void Sum(int x, int y) =\u003e Console.WriteLine(x + y);\n}\n```\n\nWhen you check the registered commands with `--help`, it will look like this. Note that you can register multiple `Add\u003cT\u003e` and also add commands using `Add`.\n\n```txt\nUsage: [command] [options...] [-h|--help] [--version]\n\nRoot command test.\n\nOptions:\n  -m|--msg \u003cstring\u003e    Message to show. (Required)\n\nCommands:\n  echo    Display message.\n  sum     Sum parameters.\n```\n\nBy default, the command name is derived from the method name converted to `lower-kebab-case`. However, you can change the name to any desired value using the `[Command(string commandName)]` attribute.\n\nIf the class implements `IDisposable` or `IAsyncDisposable`, the Dispose or DisposeAsync method will be called after the command execution.\n\n### Nested command\n\nYou can create a deep command hierarchy by adding commands with paths separated by space(` `) when registering them. This allows you to add commands at nested levels.\n\n```csharp\nvar app = ConsoleApp.Create();\n\napp.Add(\"foo\", () =\u003e { });\napp.Add(\"foo bar\", () =\u003e { });\napp.Add(\"foo bar barbaz\", () =\u003e { });\napp.Add(\"foo baz\", () =\u003e { });\n\n// Commands:\n//   foo\n//   foo bar\n//   foo bar barbaz\n//   foo baz\napp.Run(args);\n```\n\n`Add\u003cT\u003e` can also add commands to a hierarchy by passing a `string commandPath` argument.\n\n```csharp\nvar app = ConsoleApp.Create();\napp.Add\u003cMyCommands\u003e(\"foo\");\n\n// Commands:\n//  foo         Root command test.\n//  foo echo    Display message.\n//  foo sum     Sum parameters.\napp.Run(args);\n```\n\n### Register from attribute\n\nInstead of using `Add\u003cT\u003e`, you can automatically add commands by applying the `[RegisterCommands]` attribute to a class.\n\n```csharp\n[RegisterCommands]\npublic class Foo\n{\n    public void Baz(int x)\n    {\n        Console.Write(x);\n    }\n}\n\n[RegisterCommands(\"bar\")]\npublic class Bar\n{\n    public void Baz(int x)\n    {\n        Console.Write(x);\n    }\n}\n```\n\nThese are automatically added when using `ConsoleApp.Create()`.\n\n```csharp\nvar app = ConsoleApp.Create();\n\n// Commands:\n//   baz\n//   bar baz\napp.Run(args);\n```\n\nYou can also combine this with `Add` or `Add\u003cT\u003e` to add more commands.\n\n### Performance of Commands\n\nIn `ConsoleAppFramework`, the number and types of registered commands are statically determined at compile time. For example, let's register the following four commands:\n\n```csharp\napp.Add(\"foo\", () =\u003e { });\napp.Add(\"foo bar\", (int x, int y) =\u003e { });\napp.Add(\"foo bar barbaz\", (DateTime dateTime) =\u003e { });\napp.Add(\"foo baz\", async (string foo = \"test\", CancellationToken cancellationToken = default) =\u003e { });\n```\n\nThe Source Generator generates four fields and holds them with specific types.\n\n```csharp\npartial class ConsoleAppBuilder\n{\n    Action command0 = default!;\n    Action\u003cint, int\u003e command1 = default!;\n    Action\u003cglobal::System.DateTime\u003e command2 = default!;\n    Func\u003cstring, global::System.Threading.CancellationToken, Task\u003e command3 = default!;\n\n    partial void AddCore(string commandName, Delegate command)\n    {\n        switch (commandName)\n        {\n            case \"foo\":\n                this.command0 = Unsafe.As\u003cAction\u003e(command);\n                break;\n            case \"foo bar\":\n                this.command1 = Unsafe.As\u003cAction\u003cint, int\u003e\u003e(command);\n                break;\n            case \"foo bar barbaz\":\n                this.command2 = Unsafe.As\u003cAction\u003cglobal::System.DateTime\u003e\u003e(command);\n                break;\n            case \"foo baz\":\n                this.command3 = Unsafe.As\u003cFunc\u003cstring, global::System.Threading.CancellationToken, Task\u003e\u003e(command);\n                break;\n            default:\n                break;\n        }\n    }\n}\n```\n\nThis ensures the fastest execution speed without any additional unnecessary allocations such as arrays and without any boxing since it holds static delegate types.\n\nCommand routing also generates a switch of nested string constants.\n\n```csharp\npartial void RunCore(string[] args)\n{\n    if (args.Length == 0)\n    {\n        ShowHelp(-1);\n        return;\n    }\n    switch (args[0])\n    {\n        case \"foo\":\n            if (args.Length == 1)\n            {\n                RunCommand0(args, args.AsSpan(1), command0);\n                return;\n            }\n            switch (args[1])\n            {\n                case \"bar\":\n                    if (args.Length == 2)\n                    {\n                        RunCommand1(args, args.AsSpan(2), command1);\n                        return;\n                    }\n                    switch (args[2])\n                    {\n                        case \"barbaz\":\n                            RunCommand2(args, args.AsSpan(3), command2);\n                            break;\n                        default:\n                            RunCommand1(args, args.AsSpan(2), command1);\n                            break;\n                    }\n                    break;\n                case \"baz\":\n                    RunCommand3(args, args.AsSpan(2), command3);\n                    break;\n                default:\n                    RunCommand0(args, args.AsSpan(1), command0);\n                    break;\n            }\n            break;\n        default:\n            ShowHelp(-1);\n            break;\n    }\n}\n```\n\nThe C# compiler performs complex generation for string constant switches, making them extremely fast, and it would be difficult to achieve faster routing than this.\n\nDisable Naming Conversion\n---\nCommand 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.\n\n```csharp\nusing ConsoleAppFramework;\n\n[assembly: ConsoleAppFrameworkGeneratorOptions(DisableNamingConversion = true)]\n\nvar app = ConsoleApp.Create();\napp.Add\u003cMyProjectCommand\u003e();\napp.Run(args);\n\npublic class MyProjectCommand\n{\n    public void ExecuteCommand(string fooBarBaz)\n    {\n        Console.WriteLine(fooBarBaz);\n    }\n}\n```\n\nYou can disable automatic conversion by using `[assembly: ConsoleAppFrameworkGeneratorOptions(DisableNamingConversion = true)]`. In this case, the command would be `ExecuteCommand --fooBarBaz`.\n\nParse and Value Binding\n---\nThe 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.\n\n```csharp\nConsoleApp.Run(args, (\n    [Argument]DateTime dateTime,  // Argument\n    [Argument]Guid guidvalue,     // \n    int intVar,                   // required\n    bool boolFlag,                // flag\n    MyEnum enumValue,             // enum\n    int[] array,                  // array\n    MyClass obj,                  // object\n    string optional = \"abcde\",    // optional\n    double? nullableValue = null, // nullable\n    params string[] paramsArray   // params\n    ) =\u003e { });\n```    \n\nWhen using `ConsoleApp.Run`, you can check the syntax of the command line in the tooltip to see how it is generated.\n\n![image](https://github.com/Cysharp/ConsoleAppFramework/assets/46207/af480566-adac-4767-bd5e-af89ab6d71f1)\n\nFor the rules on converting parameter names to option names, aliases, and how to set documentation, refer to the [Option aliases](#option-aliases-and-help-version) section.\n\nParameters marked with the `[Argument]` attribute receive values in order without parameter names. This attribute can only be set on sequential parameters from the beginning.\n\nTo convert from string arguments to various types, basic primitive types (`string`, `char`, `sbyte`, `byte`, `short`, `int`, `long`, `uint`, `ushort`, `ulong`, `decimal`, `float`, `double`) use `TryParse`. For types that implement `ISpanParsable\u003cT\u003e` (`DateTime`, `DateTimeOffset`, `Guid`, `BigInteger`, `Complex`, `Half`, `Int128`, etc.), [IParsable\u003cTSelf\u003e.TryParse](https://learn.microsoft.com/en-us/dotnet/api/system.iparsable-1.tryparse?view=net-8.0#system-ispanparsable-1-tryparse(system-readonlyspan((system-char))-system-iformatprovider-0@)) or [ISpanParsable\u003cTSelf\u003e.TryParse](https://learn.microsoft.com/en-us/dotnet/api/system.ispanparsable-1.tryparse?view=net-8.0#system-ispanparsable-1-tryparse(system-readonlyspan((system-char))-system-iformatprovider-0@)) is used.\n\nFor `enum`, it is parsed using `Enum.TryParse(ignoreCase: true)`.\n\n`bool` is treated as a flag and is always optional. It becomes `true` when the parameter name is passed.\n\n### Array\n\nArray parsing has three special patterns.\n\nFor a regular `T[]`, if the value starts with `[`, it is parsed using `JsonSerializer.Deserialize`. Otherwise, it is parsed as comma-separated values. For example, `[1,2,3]` or `1,2,3` are allowed as values. To set an empty array, pass `[]`.\n\nFor `params T[]`, all subsequent arguments become the values of the array. For example, if there is an input like `--paramsArray foo bar baz`, it will be bound to a value like `[\"foo\", \"bar\", \"baz\"]`.\n\n### Object\n\nIf none of the above cases apply, `JsonSerializer.Deserialize\u003cT\u003e` is used to perform binding as JSON. However, `CancellationToken` and `ConsoleAppContext` are treated as special types and excluded from binding. Also, parameters with the `[FromServices]` attribute are not subject to binding.\n\nIf you want to change the deserialization options, you can set `JsonSerializerOptions` to `ConsoleApp.JsonSerializerOptions`.\n\n\u003e NOTE: If they are not set when NativeAOT is used, a runtime exception may occur. If they are included in the parsing process, be sure to set source generated options.\n\n### Custom Value Converter\n\nTo perform custom binding to existing types that do not support `ISpanParsable\u003cT\u003e`, you can create and set up a custom parser. For example, if you want to pass `System.Numerics.Vector3` as a comma-separated string like `1.3,4.12,5.947` and parse it, you can create an `Attribute` with `AttributeTargets.Parameter` that implements `IArgumentParser\u003cT\u003e`'s `static bool TryParse(ReadOnlySpan\u003cchar\u003e s, out Vector3 result)` as follows:\n\n```csharp\n[AttributeUsage(AttributeTargets.Parameter)]\npublic class Vector3ParserAttribute : Attribute, IArgumentParser\u003cVector3\u003e\n{\n    public static bool TryParse(ReadOnlySpan\u003cchar\u003e s, out Vector3 result)\n    {\n        Span\u003cRange\u003e ranges = stackalloc Range[3];\n        var splitCount = s.Split(ranges, ',');\n        if (splitCount != 3)\n        {\n            result = default;\n            return false;\n        }\n\n        float x;\n        float y;\n        float z;\n        if (float.TryParse(s[ranges[0]], out x) \u0026\u0026 float.TryParse(s[ranges[1]], out y) \u0026\u0026 float.TryParse(s[ranges[2]], out z))\n        {\n            result = new Vector3(x, y, z);\n            return true;\n        }\n\n        result = default;\n        return false;\n    }\n}\n```\n\nBy setting this attribute on a parameter, the custom parser will be called when parsing the args.\n\n```csharp\nConsoleApp.Run(args, ([Vector3Parser] Vector3 position) =\u003e Console.WriteLine(position));\n```\n\n### Double-dash escaping\n\nArguments after double-dash (`--`) can be received as escaped arguments without being parsed. This is useful when creating commands like `dotnet run`.\n```csharp\n// dotnet run --project foo.csproj -- --foo 100 --bar bazbaz\nvar app = ConsoleApp.Create();\napp.Add(\"run\", (string project, ConsoleAppContext context) =\u003e\n{\n    // run --project foo.csproj -- --foo 100 --bar bazbaz\n    Console.WriteLine(string.Join(\" \", context.Arguments));\n    // --project foo.csproj\n    Console.WriteLine(string.Join(\" \", context.CommandArguments!));\n    // --foo 100 --bar bazbaz\n    Console.WriteLine(string.Join(\" \", context.EscapedArguments!));\n});\napp.Run(args);\n```\nYou can get the escaped arguments using `ConsoleAppContext.EscapedArguments`. From `ConsoleAppContext`, you can also get `Arguments` which contains all arguments passed to `Run/RunAsync`, and `CommandArguments` which contains the arguments used for command execution.\n\n### Syntax Parsing Policy and Performance\n\nWhile there are some standards for command-line arguments, such as UNIX tools and POSIX, there is no absolute specification. The [Command-line syntax overview for System.CommandLine](https://learn.microsoft.com/en-us/dotnet/standard/commandline/syntax) provides an explanation of the specifications adopted by System.CommandLine. However, ConsoleAppFramework, while referring to these specifications to some extent, does not necessarily aim to fully comply with them.\n\nFor example, specifications that change behavior based on `-x` and `-X` or allow bundling `-f -d -x` as `-fdx` are not easy to understand and also take time to parse. The poor performance of System.CommandLine may be influenced by its adherence to complex grammar. Therefore, ConsoleAppFramework prioritizes performance and clear rules. It uses lower-kebab-case as the basis while allowing case-insensitive matching. It does not support ambiguous grammar that cannot be processed in a single pass or takes time to parse.\n\n[System.CommandLine seems to be aiming for a new direction in .NET 9 and .NET 10](https://github.com/dotnet/command-line-api/issues/2338), but from a performance perspective, it will never surpass ConsoleAppFramework.\n\nCancellationToken(Gracefully Shutdown) and Timeout\n---\nIn ConsoleAppFramework, when you pass a `CancellationToken` as an argument, it can be used to check for interruption commands (SIGINT/SIGTERM/SIGKILL - Ctrl+C) rather than being treated as a parameter. For handling this, ConsoleAppFramework performs special code generation when a `CancellationToken` is included in the parameters.\n\n```csharp\nusing var posixSignalHandler = PosixSignalHandler.Register(ConsoleApp.Timeout);\nvar arg0 = posixSignalHandler.Token;\n\nawait Task.Run(() =\u003e command(arg0!)).WaitAsync(posixSignalHandler.TimeoutToken);\n```\n\nIf a CancellationToken is not passed, the application is immediately forced to terminate when an interruption command (Ctrl+C) is received. However, if a CancellationToken is present, it internally uses [`PosixSignalRegistration`](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.posixsignalregistration) to hook SIGINT/SIGTERM/SIGKILL and sets the CancellationToken to a canceled state. Additionally, it prevents forced termination to allow for a graceful shutdown.\n\nIf the CancellationToken is handled correctly, the application can perform proper termination processing based on the application's handling. However, if the CancellationToken is mishandled, the application may not terminate even when an interruption command is received. To avoid this, a timeout timer starts after the interruption command, and the application is forcibly terminated again after the specified time.\n\nThe default timeout is 5 seconds, but it can be changed using `ConsoleApp.Timeout`. For example, setting it to `ConsoleApp.Timeout = Timeout.InfiniteTimeSpan;` disables the forced termination caused by the timeout.\n\nThe hooking behavior using `PosixSignalRegistration` is determined by the presence of a `CancellationToken` (or always takes effect if a filter is set). Therefore, even for synchronous methods, it is possible to change the behavior by including a `CancellationToken` as an argument.\n\nExit Code\n---\nIf the method returns `int` or `Task\u003cint\u003e`, `ConsoleAppFramework` will set the return value to the exit code. Due to the nature of code generation, when writing lambda expressions, you need to explicitly specify either `int` or `Task\u003cint\u003e`.\n\n```csharp\n// return Random ExitCode...\nConsoleApp.Run(args, int () =\u003e Random.Shared.Next());\n```\n\n```csharp\n// return StatusCode\nawait ConsoleApp.RunAsync(args, async Task\u003cint\u003e (string url, CancellationToken cancellationToken) =\u003e\n{\n    using var client = new HttpClient();\n    var response = await client.GetAsync(url, cancellationToken);\n    return (int)response.StatusCode;\n});\n```\n\nIf the method throws an unhandled exception, ConsoleAppFramework always set `1` to the exit code. Also, in that case, output `Exception.ToString` to `ConsoleApp.LogError` (the default is `Console.WriteLine`). If you want to modify this code, please create a custom filter. For more details, refer to the [Filter](#filtermiddleware-pipline--consoleappcontext) section. \n\nAttribute based parameters validation\n---\n`ConsoleAppFramework` performs validation when the parameters are marked with attributes for validation from `System.ComponentModel.DataAnnotations` (more precisely, attributes that implement `ValidationAttribute`). The validation occurs after parameter binding and before command execution. If the validation fails, it throws a `ValidationException`.\n\n```csharp\nConsoleApp.Run(args, ([EmailAddress] string firstArg, [Range(0, 2)] int secondArg) =\u003e { });\n```\n\nFor example, if you pass arguments like `args = \"--first-arg invalid.email --second-arg 10\".Split(' ');`, you will see validation failure messages such as:\n\n```txt\nThe firstArg field is not a valid e-mail address.\nThe field secondArg must be between 0 and 2.\n```\n\nBy default, the ExitCode is set to 1 in this case.\n\nFilter(Middleware) Pipeline / ConsoleAppContext\n---\nFilters are provided as a mechanism to hook into the execution before and after. To use filters, define an `internal class` that implements `ConsoleAppFilter`.\n\n```csharp\ninternal class NopFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) // ctor needs `ConsoleAppFilter next` and call base(next)\n{\n    // implement InvokeAsync as filter body\n    public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken)\n    {\n        try\n        {\n            /* on before */\n            await Next.InvokeAsync(context, cancellationToken); // invoke next filter or command body\n            /* on after */\n        }\n        catch\n        {\n            /* on error */\n            throw;\n        }\n        finally\n        {\n            /* on finally */\n        }\n    }\n}\n```\n\nFilters can be attached multiple times to \"global\", \"class\", or \"method\" using `UseFilter\u003cT\u003e` or `[ConsoleAppFilter\u003cT\u003e]`. The order of filters is global → class → method, and the execution order is determined by the definition order from top to bottom.\n\n```csharp\nvar app = ConsoleApp.Create();\n\n// global filters\napp.UseFilter\u003cNopFilter\u003e(); //order 1\napp.UseFilter\u003cNopFilter\u003e(); //order 2\n\napp.Add\u003cMyCommand\u003e();\napp.Run(args);\n\n// per class filters\n[ConsoleAppFilter\u003cNopFilter\u003e] // order 3\n[ConsoleAppFilter\u003cNopFilter\u003e] // order 4\npublic class MyCommand\n{\n    // per method filters\n    [ConsoleAppFilter\u003cNopFilter\u003e] // order 5\n    [ConsoleAppFilter\u003cNopFilter\u003e] // order 6\n    public void Echo(string msg) =\u003e Console.WriteLine(msg);\n}\n```\n\nFilters allow various processes to be shared. For example, the process of measuring execution time can be written as follows:\n\n```csharp\ninternal class LogRunningTimeFilter(ConsoleAppFilter next) : ConsoleAppFilter(next)\n{\n    public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken)\n    {\n        var startTime = Stopwatch.GetTimestamp();\n        ConsoleApp.Log($\"Execute command at {DateTime.UtcNow.ToLocalTime()}\"); // LocalTime for human readable time\n        try\n        {\n            await Next.InvokeAsync(context, cancellationToken);\n            ConsoleApp.Log($\"Command execute successfully at {DateTime.UtcNow.ToLocalTime()}, Elapsed: \" + (Stopwatch.GetElapsedTime(startTime)));\n        }\n        catch\n        {\n            ConsoleApp.Log($\"Command execute failed at {DateTime.UtcNow.ToLocalTime()}, Elapsed: \" + (Stopwatch.GetElapsedTime(startTime)));\n            throw;\n        }\n    }\n}\n```\n\nIn case of an exception, the `ExitCode` is usually `1`, and the stack trace is also displayed. However, by applying an exception handling filter, the behavior can be changed.\n\n```csharp\ninternal class ChangeExitCodeFilter(ConsoleAppFilter next) : ConsoleAppFilter(next)\n{\n    public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken)\n    {\n        try\n        {\n            await Next.InvokeAsync(context, cancellationToken);\n        }\n        catch (Exception ex)\n        {\n            if (ex is OperationCanceledException) return;\n\n            Environment.ExitCode = 9999; // change custom exit code\n            ConsoleApp.LogError(ex.Message); // .ToString() shows stacktrace, .Message can avoid showing stacktrace to user.\n        }\n    }\n}\n```\n\nFilters are executed after the command name routing is completed. If you want to prohibit multiple executions for each command name, you can use `ConsoleAppContext.CommandName` as the key.\n\n```csharp\ninternal class PreventMultipleSameCommandInvokeFilter(ConsoleAppFilter next) : ConsoleAppFilter(next)\n{\n    public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken)\n    {\n        var basePath = Assembly.GetEntryAssembly()?.Location.Replace(Path.DirectorySeparatorChar, '_');\n        var mutexKey = $\"{basePath}$$${context.CommandName}\"; // lock per command-name\n\n        using var mutex = new Mutex(true, mutexKey, out var createdNew);\n        if (!createdNew)\n        {\n            throw new Exception($\"already running command:{context.CommandName} in another process.\");\n        }\n\n        await Next.InvokeAsync(context, cancellationToken);\n    }\n}\n```\n\nIf you want to pass values between filters or to commands, you can use `ConsoleAppContext.State`. For example, if you want to perform authentication processing and pass around the ID, you can write code like the following. Since `ConsoleAppContext` is an immutable record, you need to pass the rewritten context to Next using the `with` syntax.\n\n```csharp\ninternal class AuthenticationFilter(ConsoleAppFilter next) : ConsoleAppFilter(next)\n{\n    public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken)\n    {\n        var requestId = Guid.NewGuid();\n        var userId = await GetUserIdAsync();\n\n        // setup new state to context\n        var authedContext = context with { State = new ApplicationContext(requestId, userId) };\n        await Next.InvokeAsync(authedContext, cancellationToken);\n    }\n\n    // get user-id from DB/auth saas/others\n    async Task\u003cint\u003e GetUserIdAsync()\n    {\n        await Task.Delay(TimeSpan.FromSeconds(1));\n        return 1999;\n    }\n}\n\nrecord class ApplicationContext(Guid RequiestId, int UserId);\n```\n\nCommands can accept `ConsoleAppContext` as an argument. This allows using the values processed by filters.\n\n```csharp\nvar app = ConsoleApp.Create();\n\napp.UseFilter\u003cAuthenticationFilter\u003e();\n\napp.Add(\"\", (int x, int y, ConsoleAppContext context) =\u003e\n{\n    var appContext = (ApplicationContext)context.State!;\n    var requestId = appContext.RequiestId;\n    var userId = appContext.UserId;\n\n    Console.WriteLine($\"Request:{requestId} User:{userId} Sum:{x + y}\");\n});\n\napp.Run(args);\n```\n\n`ConsoleAppContext` also has a `ConsoleAppContext.Arguments` property that allows you to obtain the (`string[] args`) passed to Run/RunAsync.\n\n### Sharing Filters Between Projects\n\n`ConsoleAppFilter` is defined as `internal` for each project by the Source Generator. Therefore, an additional library is provided for referencing common filter definitions across projects.\n\n\u003e PM\u003e Install-Package [ConsoleAppFramework.Abstractions](https://www.nuget.org/packages/ConsoleAppFramework.Abstractions)\n\nThis library includes the following classes:\n\n* `IArgumentParser\u003cT\u003e`\n* `ConsoleAppContext`\n* `ConsoleAppFilter`\n* `ConsoleAppFilterAttribute\u003cT\u003e`\n\nInternally, when referencing `ConsoleAppFramework.Abstractions`, the `USE_EXTERNAL_CONSOLEAPP_ABSTRACTIONS` compilation symbol is added. This disables the above classes generated by the Source Generator, and prioritizes using the classes within the library.\n\n### Performance of filter\n\nIn general frameworks, filters are dynamically added at runtime, resulting in a variable number of filters. Therefore, they need to be allocated using a dynamic array. In ConsoleAppFramework, the number of filters is statically determined at compile time, eliminating the need for any additional allocations such as arrays or lambda expression captures. The allocation amount is equal to the number of filter classes being used plus 1 (for wrapping the command method), resulting in the shortest execution path.\n\n```csharp\napp.UseFilter\u003cNopFilter\u003e();\napp.UseFilter\u003cNopFilter\u003e();\napp.UseFilter\u003cNopFilter\u003e();\napp.UseFilter\u003cNopFilter\u003e();\napp.UseFilter\u003cNopFilter\u003e();\n\n// The above code will generate the following code:\n\nsealed class Command0Invoker(string[] args, Action command) : ConsoleAppFilter(null!)\n{\n    public ConsoleAppFilter BuildFilter()\n    {\n        var filter0 = new NopFilter(this);\n        var filter1 = new NopFilter(filter0);\n        var filter2 = new NopFilter(filter1);\n        var filter3 = new NopFilter(filter2);\n        var filter4 = new NopFilter(filter3);\n        return filter4;\n    }\n\n    public override Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken)\n    {\n        return RunCommand0Async(context.Arguments, args, command, context, cancellationToken);\n    }\n}\n```\n\nWhen an `async Task` completes synchronously, it returns the equivalent of `Task.CompletedTask`, so `ValueTask` is not necessary.\n\nDependency Injection(Logging, Configuration, etc...)\n---\nThe 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.\n\nIf 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).\n\n```csharp\nvar app = ConsoleApp.Create()\n    .ConfigureServices(service =\u003e\n    {\n        service.AddTransient\u003cMyService\u003e();\n    });\n\napp.Add(\"\", ([FromServices] MyService service, int x, int y) =\u003e Console.WriteLine(x + y));\n\napp.Run(args);\n```\n\nWhen 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.\n\nLet'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`.\n\n```csharp\n// Package Import: ZLogger\nvar app = ConsoleApp.Create()\n    .ConfigureLogging(x =\u003e\n    {\n        x.ClearProviders();\n        x.SetMinimumLevel(LogLevel.Trace);\n        x.AddZLoggerConsole();\n        x.AddZLoggerFile(\"log.txt\");\n    });\n\napp.Add\u003cMyCommand\u003e();\napp.Run(args);\n\n// inject logger to constructor\npublic class MyCommand(ILogger\u003cMyCommand\u003e logger)\n{\n    public void Echo(string msg)\n    {\n        logger.ZLogInformation($\"Message is {msg}\");\n    }\n}\n```\n\nFor 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`.\n\n```csharp\n// Microsoft.Extensions.DependencyInjection\nvar services = new ServiceCollection();\nservices.AddTransient\u003cMyService\u003e();\n\nusing var serviceProvider = services.BuildServiceProvider();\n\n// Any DI library can be used as long as it can create an IServiceProvider\nConsoleApp.ServiceProvider = serviceProvider;\n\n// When passing to a lambda expression/method, using [FromServices] indicates that it is passed via DI, not as a parameter\nConsoleApp.Run(args, ([FromServices]MyService service, int x, int y) =\u003e Console.WriteLine(x + y));\n```\n\n`ConsoleApp` has replaceable default logging methods `ConsoleApp.Log` and `ConsoleApp.LogError` used for Help display and exception handling. If using `ILogger\u003cT\u003e`, it's better to replace these as well.\n\n```csharp\napp.UseFilter\u003cReplaceLogFilter\u003e();\n\n// inject logger to filter\ninternal sealed class ReplaceLogFilter(ConsoleAppFilter next, ILogger\u003cProgram\u003e logger)\n    : ConsoleAppFilter(next)\n{\n    public override Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken)\n    {\n        ConsoleApp.Log = msg =\u003e logger.LogInformation(msg);\n        ConsoleApp.LogError = msg =\u003e logger.LogError(msg);\n\n        return Next.InvokeAsync(context, cancellationToken);\n    }\n}\n```\n\n\u003e I don't recommend using `ConsoleApp.Log` and `ConsoleApp.LogError` directly as an application logging method, as they are intended to be used as output destinations for internal framework output.\n\u003e For error handling, it would be better to define your own custom filters for error handling, which would allow you to record more details when handling errors.\n\nDI can also be effectively used when reading application configuration from `appsettings.json`. For example, suppose you have the following JSON file.\n\n```json\n{\n  \"Position\": {\n    \"Title\": \"Editor\",\n    \"Name\": \"Joe Smith\"\n  },\n  \"MyKey\": \"My appsettings.json Value\",\n  \"AllowedHosts\": \"*\"\n}\n```\n\n```xml\n\u003cItemGroup\u003e\n    \u003cNone Update=\"appsettings.json\"\u003e\n        \u003cCopyToOutputDirectory\u003ePreserveNewest\u003c/CopyToOutputDirectory\u003e\n    \u003c/None\u003e\n\u003c/ItemGroup\u003e\n```\n\nUsing `Microsoft.Extensions.Configuration.Json`, reading, binding, and registering with DI can be done as follows.\n\n```csharp\n// Package Import: Microsoft.Extensions.Configuration.Json\nvar app = ConsoleApp.Create()\n    .ConfigureDefaultConfiguration()\n    .ConfigureServices((configuration, services) =\u003e\n    {\n        // Package Import: Microsoft.Extensions.Options.ConfigurationExtensions\n        services.Configure\u003cPositionOptions\u003e(configuration.GetSection(\"Position\"));\n    });\n\napp.Add\u003cMyCommand\u003e();\napp.Run(args);\n\n// inject options\npublic class MyCommand(IOptions\u003cPositionOptions\u003e options)\n{\n    public void Echo(string msg)\n    {\n        ConsoleApp.Log($\"Binded Option: {options.Value.Title} {options.Value.Name}\");\n    }\n}\n\npublic class PositionOptions\n{\n    public string Title { get; set; } = \"\";\n    public string Name { get; set; } = \"\";\n}\n```\n\nWhen `Microsoft.Extensions.Configuration` 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\u003cIConfigurationBuilder\u003e configure`.\n\nFurthermore, overloads of `Action\u003cIConfiguration, IServiceCollection\u003e configure` and `Action\u003cIConfiguration, ILoggingBuilder\u003e configure` are added to `ConfigureServices` and `ConfigureLogging`, allowing you to retrieve the Configuration when executing the delegate.\n\nwithout Hosting dependency, I've preferred these import packages.\n\n```xml\n\u003cItemGroup\u003e\n\t\u003cPackageReference Include=\"Microsoft.Extensions.Configuration.Json\" Version=\"9.0.0\" /\u003e\n\t\u003cPackageReference Include=\"Microsoft.Extensions.Options.ConfigurationExtensions\" Version=\"9.0.0\" /\u003e\n\t\u003cPackageReference Include=\"ZLogger\" Version=\"2.5.9\" /\u003e\n\u003c/ItemGroup\u003e\n```\n\nAs 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`.\n\n```csharp\napp.UseFilter\u003cServiceProviderScopeFilter\u003e();\n\ninternal class ServiceProviderScopeFilter(IServiceProvider serviceProvider, ConsoleAppFilter next) : ConsoleAppFilter(next)\n{\n    public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken)\n    {\n        // create Microsoft.Extensions.DependencyInjection scope\n        await using var scope = serviceProvider.CreateAsyncScope();\n\n        var originalServiceProvider = ConsoleApp.ServiceProvider;\n        ConsoleApp.ServiceProvider = scope.ServiceProvider;\n        try\n        {\n            await Next.InvokeAsync(context, cancellationToken);\n        }\n        finally\n        {\n            ConsoleApp.ServiceProvider = originalServiceProvider;\n        }\n    }\n}\n```\n\nHowever, since the construction of the filters is performed before execution, automatic injection using scopes is only effective for the command body itself.\n\nIf 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`.\n\n```csharp\n// Package Import: Microsoft.Extensions.Hosting\nvar app = Host.CreateApplicationBuilder()\n    .ToConsoleAppBuilder();\n```\n\nIn this case, it builds the HostBuilder, creates a Scope for the ServiceProvider, and disposes of all of them after execution.\n\nConsoleAppFramework 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.\n\nColorize\n---\nThe framework doesn't support colorization directly; however, utilities like [Cysharp/Kokuban](https://github.com/Cysharp/Kokuban) make console colorization easy.\n\nPublish to executable file\n---\nThere are multiple ways to run a CLI application in .NET:\n\n* [dotnet run](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-run)\n* [dotnet build](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-build)\n* [dotnet publish](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-publish)\n\n`run` is convenient when you want to execute the `csproj` directly, such as for starting command tools in CI. `build` and `publish` are quite similar, so it's possible to discuss them in general terms, but it's a bit difficult to talk about the precise differences. For more details, it's a good idea to check out [`build` vs `publish` -- can they be friends? · Issue #26247 · dotnet/sdk](https://github.com/dotnet/sdk/issues/26247).\n\nAlso, to run with Native AOT, please refer to the [Native AOT deployment overview](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/). In any case, ConsoleAppFramework thoroughly implements a dependency-free and reflection-free approach, so it shouldn't be an obstacle to execution.\n\nv4 -\u003e v5 Migration Guide\n---\nv4 was running on top of `Microsoft.Extensions.Hosting`, so build a Host in the same way and set up a ServiceProvider.\n\n```csharp\nusing var host = Host.CreateDefaultBuilder().Build(); // use using for host lifetime\nusing var scope = host.Services.CreateScope(); // create execution scope\nConsoleApp.ServiceProvider = scope.ServiceProvider;\n```\n\n* `var app = ConsoleApp.Create(args); app.Run();` -\u003e `var app = ConsoleApp.Create(); app.Run(args);`\n* `app.AddCommand/AddSubCommand` -\u003e `app.Add(string commandName)`\n* `app.AddRootCommand` -\u003e `app.Add(\"\")`\n* `app.AddCommands\u003cT\u003e` -\u003e `app.Add\u003cT\u003e`\n* `app.AddSubCommands\u003cT\u003e` -\u003e `app.Add\u003cT\u003e(string commandPath)`\n* `app.AddAllCommandType` -\u003e `NotSupported`(use `Add\u003cT\u003e` manually)\n* `[Option(int index)]` -\u003e `[Argument]`\n* `[Option(string shortName, string description)]` -\u003e `Xml Document Comment`\n* `ConsoleAppFilter.Order` -\u003e `NotSupported`(global -\u003e class -\u003e method declarative order)\n* `ConsoleAppOptions.GlobalFilters` -\u003e `app.UseFilter\u003cT\u003e`\n* `ConsoleAppBase` -\u003e inject `ConsoleAppContext`, `CancellationToken` to method\n\nLicense\n---\nThis library is under the MIT License.\n","funding_links":[],"categories":["C\\#","C#","Source Generators"],"sub_categories":["Console / CLI"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FCysharp%2FConsoleAppFramework","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FCysharp%2FConsoleAppFramework","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FCysharp%2FConsoleAppFramework/lists"}