u/devlead

▲ 10 r/dotnet

Follow-up (5 years later): my .NET console stack evolved from template-only to template + source package

In 2021 I posted here about my opinionated .NET console stack: Spectre.Console for CLI parsing, Microsoft.Extensions for DI/logging, and a custom template (Devlead.Console.Template) as an alternative to dotnet new console.

I've kept using that approach and built several CLI tools on it since. The original write-up is still here if useful for context:

https://www.devlead.se/posts/2021/2021-01-15-my-preferred-console-stack

What held up

The core recipe still feels right to me:

  • Spectre.Console.Cli for parsing, help, validation
  • Microsoft.Extensions for DI, logging, configuration
  • Commands/settings separated from bootstrap wiring
  • Same patterns as ASP.NET Core / workers / Azure Functions

That part aged well.

What did not scale

The template-only approach had a flaw I did not appreciate enough in 2021.

Templates are great for scaffolding. They are poor for maintaining shared bootstrap code across many repos.

Once I had multiple console tools, every project had its own copy of the same Program glue: service collection setup, logging, config, Spectre wiring, etc. When bootstrap code changed (logging defaults, Spectre API updates, config additions), I was back to merging or copy/pasting fixes across repositories. That is the drift problem templates do not solve on their own.

What I changed

I split responsibilities:

  1. Template = minimal scaffold (project file, sample command/settings, partial Program stubs)
  2. Source NuGet package (Devlead.Console) = versioned shared bootstrap compiled into the app

Source packages ship .cs under contentFiles/cs/{tfm}/... with BuildAction=Compile. The glue code compiles into your assembly (debuggable, extendable) but updates like a normal package dependency.

Extension still happens in your project via partial methods:

public partial class Program
{
    static partial void AddServices(IServiceCollection services)
    {
        services.AddSingleton<MyService>();
    }

    static partial void ConfigureApp(AppServiceConfig appServiceConfig)
    {
        appServiceConfig
            .AddCommand<MyCommand>("run")
            .WithDescription("Does the thing.")
            .WithExample(["run"]);

        appServiceConfig.SetApplicationName("mytool");
    }
}

Scaffold is unchanged in spirit:

dotnet new install Devlead.Console.Template
dotnet new devleadconsole -n MyTool

Shared bootstrap updates via normal NuGet versioning:

dotnet add package Devlead.Console

I wrote a longer follow-up with the reasoning, packaging notes (Devlead.SourcePack), and real-world usage in tools like ARI/Blobify/DPI:

https://www.devlead.se/posts/2026/2026-06-27-template-plus-source-package-my-dotnet-console-stack-in-2026

Full disclosure: I maintain the template and package (OSS/MIT). The main point of the post is the template vs source package split.

If you use a custom console template (your own or a third-party one), how do you handle bootstrap drift across multiple repos?

  • Re-scaffold periodically?
  • Extract a shared class library?
  • Source packages?
  • Just live with copy/paste?

Curious what has worked for you, especially if you maintain a bunch of internal CLI tools.

u/devlead — 2 months ago