Using Oakton for Development-Time Commands in .Net Core Applications

All of the sample code in this blog post is on GitHub in the OaktonDevelopmentCommands project.

Last year I released the Oakton.AspNetCore library that provides an expanded command line experience for ASP.Net Core applications (and environment tests too!) that adds additional command line flags for the basic dotnet run behavior. Oakton.AspNetCore also gives you the ability to embed completely different named command directly into your application — either from your own application or Oakton.AspNetCore can pull in commands from Nuget libraries.

To make this concrete, I started a new sample project on GitHub with the dotnet new webapi template. To add the new command line experience, I added a Nuget reference to Oakton.AspNetCore and modified the Program.Main() method to this:

// I changed the return type to Task
public static Task Main(string[] args)
{
    return CreateHostBuilder(args)
        // This extension method makes Oakton the active
        // command line parser and executor
        .RunOaktonCommands(args);
}

To show the ability to add commands from an external library, I also swapped out the built in DI container with Lamar, and added a reference to the Lamar.Diagnostics Nuget to expose Lamar’s diagnostics reports via the command line.

Just to show the Lamar integration, I added just the one line of code to the host builder configuration you can see below:

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .UseLamar() // Overriding the IoC container to use Lamar
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup();
        });

Now, to switch the actual command line to see the results of all of that, you can see Oakton’s built in command line help by typing the command dotnet run --?, which gives this output:

  ------------------------------------------------------------------------------
    Available commands:
  ------------------------------------------------------------------------------
         check-env -> Execute all environment checks against the application
          describe -> Writes out a description of your running application to either the console or a file
    lamar-scanning -> Runs Lamar's type scanning diagnostics
    lamar-services -> List all the registered Lamar services
    lamar-validate -> Runs all the Lamar container validations
               run -> Runs the configured AspNetCore application
          sayhello -> I'm a simple command that just starts up the app and says hello
  ------------------------------------------------------------------------------

A couple notes about the output above:

  • With the dotnet cli mechanics, the basic usage is dotnet [command] [optional flags to dotnet itself] -- [arguments to your application]. The double dashes marks the boundaries between arguments and flags meant for dotnet itself and arguments or flags for your application. So as an example, to run your application compiled as “Release” and using the “Testing” environment name for your .Net Core application, the command would be dotnet run --configuration Release -- --environment Testing.
  • The check-env, describe, and run commands come in from the base Oakton.AspNetCore library. The run command is the default, so dotnet run actually delegates to the run command.
  • All the lamar-******* commands come from Lamar.Diagnostics because Oakton.AspNetCore can happily find and apply commands from other assemblies in the project.
  • We’ll build out sayhello later in this post

Assuming that you are a Lamar (or a StructureMap) user, the first step to unravel most IoC configuration problems is the old WhatDoIHave() method to see what the service registrations are. To get at that data faster, you can use the dotnet run -- lamar-services command just to dump out the WhatDoIHave() output to the console or a text file.

However, adding Lamar.Diagnostics to your application adds some dependencies you may not want to be deployed. With a little help from the .Net Core SDK project system, we can just tell .Net to only use Lamar.Diagnostics at development time by using the <PrivateAssets> tag in the csproj file like this:

<PackageReference Include="Lamar.Diagnostics" Version="1.1.5">
    <PrivateAssets>all</PrivateAssets>
</PackageReference>

 

I don’t know how to do that without breaking into the csproj file, but once you do, the Lamar.Diagnostics assembly will not be deployed if you’re using dotnet publish to bundle up your application for deployment.

The “AspNetCore” naming is unfortunately a misnomer, because as of .Net Core 3.* the Oakton extension works for any project configured and bootstrapped with the new generic HostBuilder (purely console applications, worker applications, or any kind of web application).

Now, to add a custom command to the application directly into the application without polluting the deployed application, we’re just using some conditional compilation as shown in this super simple example:

// This is necessary to tell Oakton
// to search this assembly for Oakton commands
[assembly:Oakton.OaktonCommandAssembly]

namespace OaktonDevelopmentCommands
{
    // The conditional compilation here just keeps this command from
    // being present in the Release build of the application
#if DEBUG
    // This is also an OaktonAsyncCommand if you need to 
    // call async APIs
    [Description("I'm a simple command that just starts up the app and says hello")]
    public class SayHelloCommand : OaktonCommand<NetCoreInput>
    {
        public override bool Execute(NetCoreInput input)
        {
            // Super cheesy, just starting up the application
            // and shutting it right down
            using (var host = input.BuildHost())
            {
                // You do have access to the host's underlying
                // IoC provider, and hence to any application service
                // including the compiled IConfiguration as well
                
                Console.WriteLine("Hey, I can start up the application!");
            }

            // Gotta return true to let Oakton know that the command succeeded
            // This is important if you're using commands that need to report
            // success or failure to the command line.
            return true;
        }
    }
#endif
}

It’s hoke-y, but the command up above will only be compiled into your application if you are compiling as “Debug” — as you generally do when you’re working locally or running tests. When you deploy as a “Release” build, this command won’t be part of the compiled executable. As silly as it looks, this is being very useful in one of my client projects right now.

Leave a comment