Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Why and How I Created Another Console Framework

5.00/5 (11 votes)
2 Jul 2020CPOL5 min read 6.1K  
Sometimes, for learning, we need to find excuses to start a new project
Why another console framework? I created this small framework to overcome the traditional limitation of console parsing tools and testing GitHub actions for deploying on NuGet. The ingredients of this article are good C# code, GitHub actions, and a framework that simplifies parsing of command-line arguments (allowing a YML definition of the commands you want to launch).

Introduction

Console applications are becoming viral tools for developers. After a trend that was going to move each non-visual tool to GUIs for better control, in the last years, we see the success of CLIs. Just remind to Docker and Kubernetes. In the past months, I developed an open-source headless CMS called RawCMS, and I put a CLI application on it. I started this tool by using the library in the .NET market, and the final experience wasn’t so good. I completed the task, but the next time I had to develop a client application, I decided to invest some time trying to do something better. I finished developing a straightforward console framework that automates command execution and the interaction with the user.

In this article, I will explain how I did it — focus on the exciting part I learned. The code is available on GitHub, as usual, and I released a Nuget package for testing (links at the end of the article).

Why Another Console Framework

The honest answer is because I needed to find an excuse for testing Github’s actions. I could have done a dummy project for this, but I do not like to work with fake thing too much. That’s why I take my last poor experience on .NET RawCMS CLI and try to do something useful. Now, I’ll try to find a rational reason.

The framework I used until now, Commandlineparser, works properly, and it is well structured. It has this simple approach:

C#
class Program
{
    public class Options
    {
        [Option('v', "verbose", Required = false, HelpText = "Set output to verbose messages.")]
        public bool Verbose { get; set; }
    }

    static void Main(string[] args)
    {
        Parser.Default.ParseArguments<Options>(args)
       .WithParsed<Options>(o =>
       {
           if (o.Verbose)
           {
               Console.WriteLine($"Verbose output enabled. Current Arguments: -v {o.Verbose}");
               Console.WriteLine("Quick Start Example! App is in Verbose mode!");
           }
           else
           {
               Console.WriteLine($"Current Arguments: -v {o.Verbose}");
               Console.WriteLine("Quick Start Example!");
           }
       });
    }
}

This lead in organizing the code as follows:

  • You create a class per command that map inputs
  • In this class, you will define annotation on each field that will be parsed from the command line input
  • You will define a piece of code that will receive the class input

I found this approach very well structured, with a good definition of model and business logic. Anyway, for a simple project, ask you to produce many classes, keep them synced, and this is a mess of time for very simple programs. Moreover, it doesn’t provide any support for working in interactive mode or automating scripts.

To overcome these limitations, I decided to use a different approach:

  1. The parameter and command definition will be derived automatically from the method annotations.
  2. I will implement an architecture that will allow the user to call a command directly or simply define a script and run it, utilizing the command inside it.

If what I tried to explain is not clear, maybe we need an example.

C#
class Program
{
    static void Main(string[] args)
    {
        ConsoleAuto.Config(args)
            .LoadCommands()
            .Run();
    }

    [ConsoleCommand]
    public void MyMethod(string inputOne, int inputTwo)
    {
        //do stuff here
    }
}

and you can use it like:

> MyProgram.exec CommandName --arg value --arg2 value
> MyProgram.exec Exec MySettings.yaml # (settings from file input!)
> MyProgram.execWelcome to MyProgram!
This is the commands available, digit the number to get more info
0 - info: Display this text
1 - MyMethod: Execute a test method

I hope now it is a little bit clear.

How It Works

This tool is very simple. It can be decomposed in a few subparts, form top to bottom:

  1. the fluent registration class
  2. the program definition (sequence of command to be executed)
  3. the command implementation
  4. the base execution

The Fluent Registration Class

This class is designed to offer a simple way to be integrated into console applications. You will need something like this:

C#
ConsoleAuto.Config(args, servicecollection)// pass servicecollection to use 
                                           // the same container of main application
    .LoadCommands()                        // Load all commands from entry assembly + 
                                           // base commands
    .LoadCommands(assembly)                // Load from a custom command
    .LoadFromType(typeof(MyCommand))       // Load a single command
    .Register<MyService>()                 // add a service di di container used in my commands
    .Register<IMyService2>(new Service2()) // add a service di di container used 
                                           // in my commands, with a custom implementation
    .Default("MyDefaultCommand")           // specify the default action if app starts 
                                           // without any command
    .Configure(config => { 
        //hack the config here
    })
    .Run();

The Program Definition (Sequence of Command to Be Executed)

The console parsing ends in the hydration of a class that represents the program definition. This class contains the list of commands to be executed and the argument that they need to work. All the things that will be executed at runtime pass from these commands. For example, the welcome message is a command that is executed at each time, or there is an “info” command that displays all available options.

This is the program definition:

C#
public class ProgramDefinition
{
    public Dictionary<string, object> State { get; set; }

    public Dictionary<string, CommandDefinition> Commands { get; set; }
}

That is loaded from a YAML script file or by command-line invocation.

F#
Commands:
    welcome_step1:
       Action: welcome
       Desctiption: 
       Args:
            header: my text (first line)
    welcome_step2:
       Action: welcome
       Desctiption: 
       Args:
           header: my text (second line)
    main_step:
       Action: CommandOne
       Description: 
       Args:
         text: I'm the central command output!
State:
   text: myglobal

The Command Implementation

The command implementation represents a command that can be invoked. I created two annotations, one for elevating a regular method as a command, and one for describing the parameter.

The implementation is something like:

C#
public class CommandImplementation
{
  public string Name { get; set; }

  public MethodInfo Method { get; set; }

  public Dictionary<string, object> DefaultArgs { get; set; } 

  public ExecutionMode Mode { get; set; } = ExecutionMode.OnDemand;

  public int Order { get; set; }

  public bool IsPublic { get; set; } = true;

  public string Info { get; set; }

  public List<ParamImplementation> Params { get; set; } 
}

The ConsoleAuto scans all assembly to find all commands and adds it to its internal set of available commands.

The Base Execution

When the user asks for command execution, the class is activated by .NET Core DI and the method executed using reflection.

C#
public void Run()
{
  LoadCommands(this.GetType().Assembly);//load all system commands
  
  //let DI resolve the type for you
  this.serviceBuilder.AddSingleton<ConsoleAutoConfig>(this.config);

  foreach (var command in this.config.programDefinition.Commands)
  {
      var commandDef = command.Value;
      var commandImpl = this.config
                    .AvailableCommands
                    .FirstOrDefault(x => x.Name == commandDef.Action);
     
      InvokeCommand(commandImpl, commandDef);
  }
}

private void InvokeCommand
(CommandImplementation commandImpl, CommandDefinition commandDefinition)
{     
  //boring code that get arguments value from command definition 
  commandImpl.Method.Invoke(instance, args.ToArray());
}

The DevOps Setup

After spending a couple of hours in this funny library, I finally arrived at the point where I can start testing the GitHub actions.

What I wanted to set up was a simple flow that:

  1. builds the code
  2. tests the code
  3. packages the code into a Nuget package
  4. pushes the package to NuGet

The GitHub setup was easy, and you just need to create a YAML file into the .github/workflows folder.

The file I used is this one.

name: .NET Core

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2
    - name: Print version  
      run:  sed -i "s/1.0.0/1.0.$GITHUB_RUN_NUMBER.0/g" ConsoleAuto/ConsoleAuto.csproj
          && cat ConsoleAuto/ConsoleAuto.csproj
    - name: Setup .NET Core
      uses: actions/setup-dotnet@v1
      with:
        dotnet-version: 3.1.101
    - name: Install dependencies
      run: dotnet restore
    - name: Build
      run: dotnet build --configuration Release --no-restore
    - name: Test
      run: dotnet test --no-restore --verbosity normal
    - name: publish on version change
      id: publish_nuget
      uses: rohith/publish-nuget@v2
      with:
        # Filepath of the project to be packaged, relative to root of repository
        PROJECT_FILE_PATH: ConsoleAuto/ConsoleAuto.csproj

        # NuGet package id, used for version detection & defaults to project name
        PACKAGE_NAME: ConsoleAuto

        # Filepath with version info, relative to root of repository 
        # & defaults to PROJECT_FILE_PATH
        VERSION_FILE_PATH: ConsoleAuto/ConsoleAuto.csproj

        # Regex pattern to extract version info in a capturing group
        VERSION_REGEX: <Version>(.*)<\/Version>

        # Useful with external providers like Nerdbank.GitVersioning, 
        # ignores VERSION_FILE_PATH & VERSION_REGEX
        # VERSION_STATIC: ${{steps.version.outputs.Version }}

        # Flag to toggle git tagging, enabled by default
        TAG_COMMIT: true

        # Format of the git tag, [*] gets replaced with actual version
        # TAG_FORMAT: v*

        # API key to authenticate with NuGet server
        NUGET_KEY: ${{secrets.NUGET_API_KEY}}

        # NuGet server uri hosting the packages, defaults to https://api.nuget.org
        # NUGET_SOURCE: https://api.nuget.org

        # Flag to toggle pushing symbols along with nuget package to the server, 
        # disabled by default
        # INCLUDE_SYMBOLS: false

Please note the automatic versioning based on the progressive build number.

The steps are:

  1. patching the versions
  2. build
  3. test
  4. publish to NuGet

The paths are easily done replacing the version in .csproj file. It is done as the first step so that all following compilation is made with the right build number.

BAT
sed -i "s/1.0.0/1.0.$GITHUB_RUN_NUMBER.0/g" ConsoleAuto/ConsoleAuto.csproj

The script assumes that in csproj, there is always version tag 1.0.0. You can use regexp to improve that solution.

The build\test commands are quite standard and are equivalent to:

BAT
dotnet restore
dotnet build --configuration Release --no-restore
dotnet test --no-restore --verbosity normal

The final publishing step is a third party addon that runs internally the pack command and then the push one. If you look to the GitHub history, you will find something similar to this:

BAT
dotnet pack  --no-build -c Release ConsoleAuto/ConsoleAuto.csproj 
dotnet nuget push *.nupkg 

What I Learned From this Experience

In our field, all we need is already discovered or done. The fast way to complete a task is by doing it using what you find. Never reinvent the wheel. Anyway, when you need to learn something new, you may be encouraged by trying to create something useful. This can make your learning more practical and let you clash with real problems.

After a Saturday spent on this library, I learned:

  • There was already a library that maps automatically form methods to command line arguments. This library is very well done and also has a web interface for running commands visually using swagger UI.
  • The GitHub actions are very cool, but the problem is on third party plugins that are not well documented or fully working. Just give them some time to mature. But for this topic, I will need another article. 😃

References

History

  • 3rd July, 2020: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)