Whenever you create a new .NET project, whether you use Visual Studio’s or Rider’s UI or the CLI, you can choose from a few predefined templates for various use cases, like console, web or desktop apps or test projects. Those should be fine for most developers - even in microservice architectures if you size your microservice according to the two pizzas rule, you shouldn’t create them often, and even if you do, you should tailor the project to the particular microservice instead of using a cookiecutter approach. Some projects, however, would greatly benefit from time savings and higher consistency introduced by a custom project template.

Why do I need a project template?

You might wonder how might a project template help you - after all, deleting the unnecessary stuff from a web app template doesn’t take much time, and if you want to control the minute details you can start with an empty project and customize it however you like. This templates stuff is probably for developers writing their own libraries or something, right?

Actually, you might find a use for them in your team, too. There are some commonalities between projects within an application or organization. You may be using the same project structure, like hexagonal architecture, referencing particular libraries - I happen to install MediatR in almost all the projects I create or reusing some code for logging, testing or OpenAPI docs. There may be Dockerfiles with only minor changes between them, perhaps some YAML too. You could have all those things in a project template and have them there each time you create a new project, saving you time and adding consistency to your projects.

What are project templates, exactly?

Project templates are the same templates we’ve referrenced in the beginning, like web, console or desktop projects you can choose when creating a new project. Those are provided to you, courtesy of the .NET team. However, You can also create custom project templates. It’s as simple as creating a directory for your template, adding a directory .template.config and a file template.json to it. The file contents can be as simple as the following example from the dotnet documentation:

{
  "$schema": "http://json.schemastore.org/template",
  "author": "Travis Chau",
  "classifications": [ "Common", "Console" ],
  "identity": "AdatumCorporation.ConsoleTemplate.CSharp",
  "name": "Adatum Corporation Console Application",
  "shortName": "adatumconsole"
}

That’s basically it, you can now open your console in the template’s directory and install the template with dotnet new install ..

An empty directory is not that useful, though. I will show you a template for a hexagonal architecture project. We’re going to have proper references between projects, a few key libraries like MediatR, Docker support and a few other goodies. If you’d like to see the entire codebase, you can find it here. In this post, I’ll only be using it to showcase features of .NET custom templates.

First of all, let’s update the template.json file:

{
    "$schema": "http://json.schemastore.org/template",
    "author": "redzimski.dev",
    "classifications": ["Web", "API", "Clean Architecture"],
    "identity": "RedzimskiDev.Template.CSharp",
    "name": "RedzimskiDev API Starter",
    "shortName": "redzimskidev",
    "sourceName": "RedzimskiDev.Template",
    "preferNameDirectory": true,
    "tags": {
      "language": "C#",
      "type": "project"
    }
  }

Other than updating some metadata, we’ve added a property called sourceName. This is how we’re referencing the template in the files - in short, when the user provides the name for his project, all the instances of sourceName in the project’s files will be replaced with the provided name. For instance, we have a project called RedzimskiDev.Template.Api - if the user provides a name Company.Project, it will rename the project and all the namespacesto Company.Project.Api. We’ve also added tags - those are there to make the project discoverable by Visual Studio.

Let us expand on this idea. We probably have more text that we would like to replace - let’s replace the name of the OpenAPI specification title. Secondly, let’s make healthchecks configurable - our template will accept a boolean parameter that will dictate whether healthcecks are enabled.

{
  "$schema": "http://json.schemastore.org/template",
  "author": "redzimski.dev",
  "classifications": [
    "Web",
    "API",
    "Clean Architecture"
  ],
  "identity": "RedzimskiDev.Template.CSharp",
  "name": "RedzimskiDev API Starter",
  "shortName": "redzimskidev",
  "sourceName": "RedzimskiDev.Template",
  "preferNameDirectory": true,
  "tags": {
    "language": "C#",
    "type": "project"
  },
  "symbols": {
    "title": {
      "type": "parameter",
      "defaultValue": "Your",
      "replaces": "RedzimskiDev Template"
    },
    "EnableHealthchecks": {
      "type": "parameter",
      "datatype": "bool",
      "defaultValue": "true",
      "description": "Include healthchecks"
    }
  }
}

To accomplish that, we need to add a symbols object with two properties - title, which has a type of parameter, a default value and text it will replace. To make use of it, we’ll need to use the text in code:

services.AddSwaggerGen(options =>
{
    options.SwaggerDoc("v1",
        new OpenApiInfo
        {
            Title = "RedzimskiDev Template API"
        });
});

Adding healthchecks conditionally requires using, well, conditionals.

#if (EnableHealthchecks)
    app.UseHealthChecks("/health");
#endif

First, we’re modifying the Program.cs file to conditionally add healltchchecks to our app.

#if (EnableHealthchecks)
        services.AddHealthChecks();
#endif

Secondly, we conditionally add them to the service collection.

One last addition to our template will be a conditionally enabled Dockerfile. I will not cover adding the Dockerfile here, but all the modifications to the paths are already taken care of by the sourceName property. What we need to add, is another parameter, and a sources array with an appropriate modifier.

{
  "$schema": "http://json.schemastore.org/template",
  "author": "redzimski.dev",
  "classifications": [
    "Web",
    "API",
    "Clean Architecture"
  ],
  "identity": "RedzimskiDev.Template.CSharp",
  "name": "RedzimskiDev API Starter",
  "shortName": "redzimskidev",
  "sourceName": "RedzimskiDev.Template",
  "preferNameDirectory": true,
  "tags": {
    "language": "C#",
    "type": "project"
  },
  "symbols": {
    "title": {
      "type": "parameter",
      "defaultValue": "Your",
      "replaces": "RedzimskiDev Template"
    },
    "EnableHealthchecks": {
      "type": "parameter",
      "datatype": "bool",
      "defaultValue": "true",
      "description": "Include healthchecks"
    },
    "EnableDocker": {
      "type": "parameter",
      "datatype": "bool",
      "defaultValue": "true",
      "description": "Include Docker support"
    }
  },
  "sources": [
    {
      "modifiers": [
        {
          "condition": "(!EnableDocker)",
          "exclude": [
            "**/Dockerfile"
          ]
        }
      ]
    }
  ]
}

We’ve added a parameter called EnableDocker and a source modifier that will exclude the Dockerfile from the project if the parameter is false.

We’ve already covered installing the template. How do we use it, then?

dotnet new redzimskidev -n Company.Project --title 'Company Project' --EnableHealthchecks false --EnableDocker false

After installing the template, this command will create a project called Company.Project with the projects and namespaces replaced accordingly, with the OpenAPI title set to Company Project, and removes healthchecks and Dockerfile.

After you’re happy with the template, you can easily share it as a NuGet package. Enjoy!

If you’d like to see the entire codebase, you can find it here.