SkillAgentSearch skills...

ConsoleAppFramework

Zero Dependency, Zero Overhead, Zero Reflection, Zero Allocation, AOT Safe CLI Framework powered by C# Source Generator.

Install / Use

/learn @Cysharp/ConsoleAppFramework
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

ConsoleAppFramework

GitHub Actions Releases Version Downloads

ConsoleAppFramework 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, managed function pointer, params arrays and default values lambda expression, ISpanParsable<T>, PosixSignalRegistration, etc.), this library ensures maximum performance while maintaining flexibility and extensibility.

image

.NET 10.0 and Set RunStrategy=ColdStart WarmupCount=0 to calculate the cold start benchmark, which is suitable for CLI application.

The magical performance is achieved by statically generating everything and parsing inline. Let's take a look at a minimal example:

using ConsoleAppFramework;

// args: ./cmd --foo 10 --bar 20
ConsoleApp.Run(args, (int foo, int bar) => Console.WriteLine($"Sum: {foo + bar}"));

Unlike 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.

internal static partial class ConsoleApp
{
    // Generate the Run method itself with arguments and body to match the lambda expression
    public static void Run(string[] args, Action<int, int> command)
    {
        // code body
    }
}
<details><summary>Full generated source code</summary>
namespace ConsoleAppFramework;

internal static partial class ConsoleApp
{
    public static void Run(string[] args, Action<int, int> command)
    {
        if (TryShowHelpOrVersion(args, 2, -1)) return;

        var arg0 = default(int);
        var arg0Parsed = false;
        var arg1 = default(int);
        var arg1Parsed = false;

        try
        {
            for (int i = 0; i < args.Length; i++)
            {
                var name = args[i];

                switch (name)
                {
                    case "--foo":
                    {
                        if (!TryIncrementIndex(ref i, args.Length) || !int.TryParse(args[i], out arg0)) { ThrowArgumentParseFailed("foo", args[i]); }
                        arg0Parsed = true;
                        break;
                    }
                    case "--bar":
                    {
                        if (!TryIncrementIndex(ref i, args.Length) || !int.TryParse(args[i], out arg1)) { ThrowArgumentParseFailed("bar", args[i]); }
                        arg1Parsed = true;
                        break;
                    }
                    default:
                        if (string.Equals(name, "--foo", StringComparison.OrdinalIgnoreCase))
                        {
                            if (!TryIncrementIndex(ref i, args.Length) || !int.TryParse(args[i], out arg0)) { ThrowArgumentParseFailed("foo", args[i]); }
                            arg0Parsed = true;
                            break;
                        }
                        if (string.Equals(name, "--bar", StringComparison.OrdinalIgnoreCase))
                        {
                            if (!TryIncrementIndex(ref i, args.Length) || !int.TryParse(args[i], out arg1)) { ThrowArgumentParseFailed("bar", args[i]); }
                            arg1Parsed = true;
                            break;
                        }
                        ThrowArgumentNameNotFound(name);
                        break;
                }
            }
            if (!arg0Parsed) ThrowRequiredArgumentNotParsed("foo");
            if (!arg1Parsed) ThrowRequiredArgumentNotParsed("bar");

            command(arg0!, arg1!);
        }
        catch (Exception ex)
        {
            Environment.ExitCode = 1;
            if (ex is ValidationException or ArgumentParseFailedException)
            {
                LogError(ex.Message);
            }
            else
            {
                LogError(ex.ToString());
            }
        }
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    static bool TryIncrementIndex(ref int index, int length)
    {
        if (index < length)
        {
            index++;
            return true;
        }
        return false;
    }

    static partial void ShowHelp(int helpId)
    {
        Log("""
Usage: [options...] [-h|--help] [--version]

Options:
  --foo <int>     [Required]
  --bar <int>     [Required]
""");
    }
}
</details>

As 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, and ConsoleAppFramework's generation can be considered as Function-like macros.

The 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.

Moreover, 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.

ConsoleAppFramework 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.

  • SIGINT/SIGTERM(Ctrl+C) handling with gracefully shutdown via CancellationToken
  • Filter(middleware) pipeline to intercept before/after execution
  • Exit code management
  • Support for async commands
  • Registration of multiple commands
  • Registration of nested commands
  • Setting option aliases and descriptions from code document comment
  • System.ComponentModel.DataAnnotations attribute-based Validation
  • Dependency Injection for command registration by type and public methods
  • Microsoft.Extensions(Logging, Configuration, etc...) integration
  • High performance value parsing via ISpanParsable<T>
  • Parsing of params arrays
  • Parsing of JSON arguments
  • Double-dash escape arguments
  • Help(-h|--help) option builder
  • Default show version(--version) option

As 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!

Getting Started

This library is distributed via NuGet, minimal requirement is .NET 8 and C# 13.

dotnet add package ConsoleAppFramework

ConsoleAppFramework is an analyzer (Source Generator) and does not have any dll references. When referenced, the entry point class ConsoleAppFramework.ConsoleApp is generated internally.

The 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.

using ConsoleAppFramework;

ConsoleApp.Run(args, (string name) => Console.WriteLine($"Hello {name}"));

When using .NET 8, you need to explicitly set LangVersion to 13 or above.

 <PropertyGroup>
     <TargetFramework>net8.0</TargetFramework>
     <LangVersion>13</LangVersion>
 </PropertyGroup>

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.

You can execute command like sampletool --name "foo".

  • The return value can be void, int, Task, or Task<int>
    • If an int is returned, that value will be set to Environment.ExitCode
  • By default, option argument names are converted to --lower-kebab-case
    • For example, jsonValue becomes --json-value
    • Option argument names are case-insensitive, but lower-case matches faster

When passing a method, you can write it as follows:

ConsoleApp.Run(args, Sum);

void Sum(int x, int y) => Console.Write(x + y);

Additionally, for static functions, you can pass them as function pointers. In that case, the managed function pointer

View on GitHub
GitHub Stars2.2k
CategoryDevelopment
Updated6h ago
Forks122

Languages

C#

Security Score

95/100

Audited on Mar 27, 2026

No findings