Using Asp.NetCore 3 and System.Commandline Together

If we have a problem that needs to keep API and CLI both in the same application, here is the solution to your niche problem. We have .Net Core applications at work that use CLI and API construction at the same time. Meaning that if your application binary can also get executed as Command Line Interface, then you need to branch to CLI handlers before you execute/create API handling. Example:

1
./MyCoolApp db create

The command should not go to WebHost or Host to create the .Net Core pipeline. It should invoke the related db create command and exit with the proper exit code. So, the application I demonstrate has multiple commands like this.

So, finding a unified way of doing it becomes harder than you think. Eventually, we find the interaction between API and Commandline poorly connected because of the abstraction leaks or complexity of the composition. We can also question, “Do they need to sit together?” Well, you can easily say they are separate concerns, and I want to separate them, it is another topic of course. Still, if you are in a situation that you need to melt down these two separate concerns in the same pot, then you need to think of a solution to solve that.

The solution needs to cover all AspNet Core 3.1 functionalities, and also at least it should be able to use ServiceCollection to enable dependency injection. Because you would not want to register or pass your dependencies with the Pure DI approach for the sake of the command-line interface, well, I wouldn’t.

So, let’s collect some requirements along the way, what we said:

  • Harmony with AspNet Core API handling
  • Use dependency injection
  • Don’t use Pure DI in anywhere, and don’t introduce any other DI container (which I encourage)
  • Create also a unit test suite around the implementation
  • (Optional) Create a validation before command gets executed

So, let’s create an application that suits our needs, and I explain my ideas better on a concrete example.

The Implementation

The dependencies we have are:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<Project Sdk="Microsoft.NET.Sdk.Web">

    <PropertyGroup>
        <TargetFramework>netcoreapp3.1</TargetFramework>
    </PropertyGroup>

    <ItemGroup>
      <PackageReference Include="System.CommandLine" Version="2.0.0-beta1.20214.1" />
      <PackageReference Include="System.CommandLine.Hosting" Version="0.3.0-alpha.20214.1" />
    </ItemGroup>

</Project>

As you can see, it is a .Net Core application that has dependencies on System.Commandline and System.Commandline.Hosting.

We have a Program.cs file that contains the application composition. It is straightforward, and basically handles the branching of the application according to the ExitCode as you can see at the lines 19-20.

So, the critical part here is the ApplicationFactory class that helps to test in the future also. Now, it is creating the application’s building blocks such as CommandLineBuilder and WebHostBuilder. The reason why we didn’t use the same ApplicationFactory class in Main is that AspnetCore testing needs the method called CreateHostBuilder in Program.cs to be able to create TestHost. In other words, we’re going to use WebApplicationFactory<T> class in testing, and this testing approach uses the entrance point of the application to create Asp.Net Core API pipeline. So, duplicating is fine.

Let’s come to ApplicationFactory class to understand how it composes the WebHost and CliHost.

At first glance, we see two branches: the first is CreateWebHostBuilder, and the second is CreateCommandLineBuilder. They help to build the parts mentioned above of the application accurately. Creation of WebHost is straightforward, there is nothing, just Startup that gets executed. On the other hand, for the CommandHandling, there are specific extensions that I created to build the pipeline shortly and understandably.

The other things that I can mention, ConfigureServices and ConfigureApplication actions are related to testing again. They help to add test dependencies or fakes to DI container. At line 52 there is UseHost extension that enables NetCore middleware mechanism and changes the ApplicationLifetime to invocation lifetime, which also enables the CTRL+C interruption if you want to cancel your CLI handling with it.

So, let’s come back to the ApplicationFactory class and find the statement of the creation of command handling. It is 50, and with this extension new RootCommand().CreateCommandHandling(), we create our entire command line handling. I explain the details later.

And last but not least, BranchToWebIfNoCommandToExecute() middleware extension to enable the application to branch to Web if there is no applicable method found for the CLI.

Dependency Injection

As you may recall in the ApplicationFactory class, it uses the ConfigureServices method to add dependencies to the ServiceCollection which is the point where we define all dependencies that the command handling needs. Whatever added here is replaceable with action methods that are at the top of the class so that we can replace the implementations according to our test needs.

Extension Points

I share the all extension class named CommandLineExtensions.cs. at the end but, let’s look a bit method by method to understand what they do.

So, how we define the RootCommand that can inject dependencies. Let’s look at a command creation with this approach.

1
2
3
Command version = new Command("version")
    .Configure(c => { c.AddAlias("--version"); })
    .HandledBy<GetVersionCommandLineHandler>();

What it tells us, there is a command called version and has an alias --version and HandledBy a handler. So, that’s it. DI enabled GetVersionCommandLineHandler can inject any dependency on itself and use it, as long as they are registered in ServiceCollection.

If we take a closer look at this extension method:

Here we see that it uses CommandHandler.Create method to create a handler for the specific command. This method belongs to System.Commandline package. Before the execution of the command, two things are happening. If the command is ValidatedCommand, then validate it before execute it; while doing this, also use logger. The second is if there are any parsing errors, check them with if (invocation.HasErrors()) then return if something is wrong. If everything is fine, then Invoke the command by resolving the handler host.Services.GetRequiredService<T>().InvokeAsync(invocation);. Which is the point where CLI handler’s ServiceProvider attempts to resolve the command’s handler.

ValidatedCommand

One of the things that I need while handling the commands is validating it before the execution. When we use DI enabled strategy, validating all the commands in the same place would do the trick, therefore I implemented the ValidatedCommand class to leverage this.

It essentially inherits System.Commandline's Command class and adds some custom validation logic. And after that, HandledBy takes care of all the validation execution before the command gets executed. Let’s demonstrate here:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
Command db = new Command("db")
    .Configure(c => { c.AddAlias("--db"); })
    .AddSubCommand(new ValidatedCommand("init")
        .Configure(c =>
        {
            c.AddAlias("--init");
            c.AddAlias("-i");
            c.AddValidation(sp => { return (true, string.Empty); });
        }).HandledBy<DbInitCommandLineHandler>())
    .AddSubCommand(new ValidatedCommand("migrate")
        .Configure(c =>
        {
            c.AddAlias("--migrate");
            c.AddAlias("-m");
            c.AddValidation(sp => { return (true, string.Empty); });
        }).HandledBy<MigrateToLatestVersionCommandHandler>());

As you may notice, c.AddValication(sp => { return (true, string.Empty); }) statement adds the validation, and good point here is, you’re able to use the ServiceProvider that has been built by the CLI pipeline to facilitate your validation logic with it.

The Handler

Finally, we get to inject the dependencies to the handlers, and everyone is happy. I take a basic handler example here, don’t forget to register the logger:)

It merely demonstrates a database migration handler which gets executed, let’s say before the deployment. You can implement progress bars, status reports, and all other things inside the handlers in this way.

Testing

Testing is significant to validate everything that we have created. To do that, I created a CliTestBase that enables me to test my logic against my CLI application.

This class inherits the ApplicationFactory class to execute necessary steps on the way of accessing a proper CLI pipeline, and also creates a TestConsole to track what is happening in logs. If the output is crucial, then you can even write a test against it.

If we come to test methods which are, in other words: Facts:

Some checks ensure standard out is appropriately written, and ExitCode is OK, so on. But I would also like to mention how we can replace a dependency inside the test. So, take a look at Can_Execute_MigrationStrategy_With_Something_From_Test_Context method. It shows how a fake dependency gets replaced by the test context.

Source Code

You can find the source code here

comments powered by Disqus