Creating a Solution Template for the .NET Core CLI

Published on Friday, June 5, 2020

Consistency is important in software development. From folder names to variable names, the more consistent a codebase is, the more easy it is to work in. At csg we aim to have a consistent approach to different custom software applications we build for our clients. One of the early ways we practice this is by starting each new git repo with common set of files and folders. Until recently, the workflow to accomplish this has been to clone what we call the "Skeleton" repo for a particular project type (API, website, class library, etc.), and then do a bunch of renames of namespaces, folders, etc. It turned out to be really easy to take our existing "skeleton projects" and turn them into custom templates for dotnet-new.

While there are many freely available templates for dotnet-new, we find it useful to have a lot of the boilerplate we would build out directly in a template.

A typical skeleton structure we might use for a website use looks something like this:

  • build
    • repo.props
  • src
    • Skeleton.Classlib1
    • Skeleton.Classlib2
    • Skeleton.Website
  • tests
    • Skeleton.TestProject1
    • Skeleton.TestProject2
  • .gitignore
  • azure-pipelines.yml
  • build.ps1
  • Directory.Build.props
  • SkeletonWebsite.sln
  • version.json

This structure is similar to (and was mostly ripped off from) some of the ASP.NET repo structures circa 2017. I won't go into what all these files are in this post, but most of the meat is in the src/ folder, where various class libraries and other projects go. Some solutions have lots of projects, some have only a few. The contents of the various files in the project might look something like this:

using System;
namespace Skeleton.Api.Controllers
{
     [Authorize]
     [ProducesResponseType(401)]
     [Route("api/[controller]")]
     [ApiController]
     public class WidgetController : ControllerBase
     {
        private readonly ISkeletonRepository _repo;

        public WidgetController(ISkeletonRepository repo)
        {
       	    _repo = repo;
        }
     }     
}

Note that the code contains classes, interfaces and other identifiers containing the word Skeleton. To turn this structure into a template, first I added a folder named .template.config, and inside place a file named template.json which looks like this:

{
  "$schema": "http://json.schemastore.org/template",
  "author": "Justin R. Buchanan",
  "classifications": [ "ASP.NET", "Website", "Solution" ],
  "identity": "Csg.Templates.AspNetCore.Website",
  "name": "CSG ASP.NET Core Website Solution",
  "sourceName": "Skeleton",
  "preferNameDirectory": true,
  "shortName": "csgaspnetweb",
  "symbols": {
    "title": {
      "type": "parameter",
      "datatype": "string",
      "replaces": "PROJECT-TITLE",
      "isRequired": true,
      "description": "The display name used in assembly info and as the default website title"
    }
  },
  "sources": [
    {
      "modifiers": [
        {
          "exclude": [
            "\*\*/.git/\*\*",
            "\*\*/.vs/\*\*"
          ]
        }
      ]
    }
  ]
}

The important bits above are sourceName, which is used to search and replace file contents, and shortName, which is used when creating an instance of this template from the CLI. After publishing this template, developers will be able to create instances using the dotnet cli as follows:

dotnet new csgaspnetweb -title "My Test Website" -output Test

The above command would produce a folder structure similar to above, but would have renamed the word "Skeleton" in all file contents, file names, and folder names with "Test", and renamed all instances of PROJECT-TITLE with "My Test Website". In addition, it would ignore .git and .vs folders. Once a template is created the way we want it, I publish the template to our internal NuGet feed using Azure DevOps CI/CD pipelines. Developers can then install or update their templates using dotnet new -i [template-package-name].