Ken Muse

Custom .NET Item Templates


This is a post in the series Creating dotnet templates. The posts in this series include:

Part of DevOps is supporting the people and processes with the right tools. There is one tool that is particularly powerful for organizations — reusable templates that make it easy to share best practices for new files and projects. Teams can share common approaches, and companies can provide starting points for files, projects, and solutions. Many programming languages support this approach, but it’s surprising how rarely I see teams take advantage of this. The .NET platform is particularly strong in this area, allowing you to distribute project templates, file templates, and recommended analyzers as templates.

It’s good to understand that the guidance on distributing content in .NET has changed slightly over the years (in a good way!). In the past, we used NuGet packages to distribute templates and libraries. Originally, we might put a file template in a package to be configured automatically when the NuGet package installed. This had a few problems. First, the file was part of the package and could be impacted by NuGet updates and restores. This made customizing or configuring the file harder. Second, there wasn’t a clear line between the immutable content from a package and the immutable content. This made dependency management more challenging.

Modern NuGet has improved this by incorporating NuGet functionality into the build process and altering content handling. This means that a package can contribute to the build by incorporating tasks, including scripts, and running analyzers. It no longer requires special scripts or manual updates to project files (which are challenging to “upgrade” or “uninstall”). Content handling also changed to remain immutable. As a result, dotnet new emerged to support templates and other mutable content. This approach allows us to support more complex template needs while ensuring created content is owned by the project. Content is not impacted by updates to the NuGet package unless you choose to run dotnet new again.

Today, we’re going to explore the basics of creating a simple single-file template for use with dotnet new. There is documentation available from Microsoft covering some of the basics of templates, but I find it is often easiest to walk through an example and explain the why behind the design.

Creating a File Template

For this sample, we’ll distribute the default .editorconfig used for [styling the code in ASP.NET Core(https://github.com/dotnet/aspnetcore/blob/main/.editorconfig). If you’re not familiar with .editorconfig, it’s a way to maintain a consistent format in code across multiple IDEs. It specifies coding conventions, such as whether to use commas vs tabs, the level of indentation, and the positioning of braces for methods and control statements. The format is documented here. It is exceptionally powerful with .NET, which allows you to configure build analyzers and enforce the conventions at build time. You can learn more about that here.

Its worth noting .NET 6 that the template for dotnet new editorconfig is now built-into the tools. Why would we customize this? If you’re using an older version of .NET Core or if you want to define the coding standard to match existing company practices, then this could be valuable. It’s also a very simple example of something we could implement. 😄

First, we start by setting up our project. I recommend setting your project up with the ability to expand. That is, we’re starting with creating an .editorconfig, but in the future we may want to have additional templates available.

A basic structure for a template project would generally look like this:

  • 📄 templates.csproj
  • 📂 templates
    • 📂 myeditorconfig
      • 📄 .editorconfig
      • 📂 .template.config
        • 📄 dotnetcli.host.json
        • 📄 ide.host.json
        • 📄 template.json

Not all of these files are needed, so let’s take a moment to understand what each file does and what it expects.

Understanding the project structure

Let’s go just a bit deeper and understand what these files do, which ones we need, and why we organize this way.

  • 📄 templates.csproj
    Contains the definition for building the NuGet package used to distribute our template. It is not strictly required. Technically, the only requirement is that you have a properly structured NuGet file at the end of the process. This can be manually created, use a NuSpec file, or use an MSBuild project file. My personal opinion — the project file is the most intuitive approach for most developers.
  • 📂 templates
    This folder contains one or more folders which represent templates we want to distribute. By having a parent folder, it’s easier to configure the .csproj for all of the contained templates. While there’s technically nothing to prevent you from removing this folder, I generally would recommend keeping it to enable you to include multiple templates in one package.
    • 📂 myeditorconfig
      Contains the template definition and supporting files for our ‘myeditorconfig’ template. It’s a best practice to name this folder to match the template name or its shortname.
      • 📄 .editorconfig
        The actual file template we’re going to distribute. In this case, the file will be deployed as-is without any text substitution. It’s worth noting that a file template does not limit you to just one file. All of the content in the folder outside of .template.config is part of the template, so it is possible to package multiple related files.
      • 📂 .template.config
        Special configuration folder which contains the details about this template for IDEs and for the dotnet command line. This folder must exist at the same level as the file template that is being distributed.
        • 📄 template.json
          Defines the template itself, including the name, any configuration details, and the definitions for any required replacement parameters. This one is required.
        • 📄 dotnetcli.host.json
          Optional file that provides additional details about parameters, including accepted short names. For more details, read this post. The schema is defined here: http://json.schemastore.org/dotnetcli.host. For this sample, we can ignore this file.
        • 📄 ide.host.json
          Optional file that controls the icon and details for the template which are displayed in Visual Studio. Optionally, it can also be used to hide the template from Visual Studio. The schema is defined here: http://json.schemastore.org/vs-2017.3.host. For this sample, we can ignore this file.

With this understanding, we can simplify our template slightly:

  • 📄 templates.csproj
  • 📂 templates
    • 📂 myeditorconfig
      • 📄 .editorconfig
      • 📂 .template.config
        • 📄 template.json

The project file

This file defines the NuGet package that will be built. It’s a traditional .NET project file, adjusted to ignore the build process. The details about the MSBuild targets that support this can be found here: https://docs.microsoft.com/en-us/nuget/reference/msbuild-targets

 1<Project Sdk="Microsoft.NET.Sdk">
 2
 3  <PropertyGroup>
 4    <!— Defines this as a template —>
 5    <PackageType>Template</PackageType>
 6
 7    <!— Package details —>
 8    <!-- By using VersionPrefix, I can pass the VersionSuffix during CI operations -->
 9    <!-- I prefer this over the more immutable PackageVersion -->
10    <VersionPrefix>1.0.0</VersionPrefix>
11    <PackageId>KenMuse.Templates</PackageId>
12    <Title>Our Sample Templates</Title>
13    <Authors>Ken Muse</Authors>
14    <Description>Templates to standardize .NET application development.</Description>
15    <PackageTags>dotnet-new;templates</PackageTags>
16    <TargetFramework>netstandard2.0</TargetFramework>
17    
18    <!— Don't need to compile anything —>
19    <NoBuild>true</NoBuild>
20    
21    <!— Don't need any assemblies that would be built —>
22    <IncludeBuildOutput>false</IncludeBuildOutput>
23    
24    <!— We need this to create a NuGet Package —>
25    <IsPackable>true</IsPackable>
26
27    <!— Needed to prevent NU5128 warning —>
28    <SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
29
30    <!— Enable NuGet to handle files with names that start with "." —>
31    <NoDefaultExcludes>true</NoDefaultExcludes> <!— NU5119 —>
32
33    <!— Should we include Content items? —>
34    <IncludeContentInPack>true</IncludeContentInPack>
35    <ContentTargetFolders>content</ContentTargetFolders>
36  </PropertyGroup>
37
38  <ItemGroup>
39    <Content Include="templates\**\*" Exclude="templates\**\bin\**;templates\**\obj\**" />
40    <Compile Remove="**\*" />
41  </ItemGroup>
42
43</Project>

The Template config

Aside from [the file we’re distributing](( https://github.com/dotnet/aspnetcore/blob/main/.editorconfig), the only other file is the template configuration. This file exists in the folder .template.config, and it is always named template.json. It provides a long name and a short name for the file template, as well as a globally unique identifier. It also has a classification (for searching) and a tag which indicates that that we’re packaging a file “item”. The file looks like this:

 1{
 2  "$schema": "http://json.schemastore.org/template",
 3  "author": "Ken Muse",
 4  "classifications": [ "Config" ], 
 5  "name": "Custom EditorConfig",
 6  "identity": "KenMuse.Template.EditorConfig",
 7  "shortName": "myeditorconfig",
 8  "tags": {
 9    "type":"item"
10  }
11}

If I wanted to associate the file to a specific language, I would include “language” in the tags. For example:

1  "tags": {
2    "language": "C#",
3    "type":"item"
4  }

Distribution

To distribute the template, you simply need to add it to a NuGet Package Manager. This can be any compliant system, including a private NuGet server, a shared folder, Azure Artifacts, or Github Packages. From there, it’s a simple as using dotnet new -i YouPackageName to install the package and make the file template locally available. Because the short name we chose is myeditorconfig, I can then type dotnet new myeditorconfig to create and add the file to any .NET project.

That’s it for today. Happy DevOp’ing!