Customising templates
So far, we’ve just looked at a very simple Hello World console app, and added some tests. Let’s take a look at something more interesting. Say you want to create a new ASP.NET project. Looking at the list of templates above, you have a few choices. You could create an empty web project, an MVC project, a project with Angular, or one with React.js. But these are fairly rigid templates. Can you customise these at all? The good news — yes, you can.
Templates can take in parameters that change what’s being generated. The — help command will provide details on the parameters this template understands. Let’s start with a simple example:
> dotnet new classlib --help
Class library (C#)
Author: Microsoft
Description: A project for creating a class library that targets .NET Standard or .NET Core
Options:
-f|--framework The target framework for the project.
netcoreapp2.1 - Target netcoreapp2.1
netstandard2.0 - Target netstandard2.0
Default: netstandard2.0
--no-restore If specified, skips the automatic restore of the project on create.
bool - Optional
Default: false / (*) true
* Indicates the value used if the switch is provided without a value.
Here you can see that the classlib template has two parameters:–frameworkto specify what target framework is written to the project file; and –no-restore, to control if NuGet restore is performed when the project is created.
dotnet new classlib --framework netcoreapp2.1 --no-restore
The web templates have similar parameters, but there are many more of them than we have space to list here. Try dotnet new mvc –help to get an idea of what’s available. There are parameters to decide what type of authentication you want, whether to disable HTTPS or not, whether to use LocalDB instead of SQLite, and so on. Each of these parameters changes how the template code is generated, either replacing content in files or including/excluding files as appropriate.
While we’re talking about help, here are two very useful commands: dotnet help new, which opens a web page on the dotnet new command itself; and dotnet new {template} –help, which shows help for the named template and its parameters.
Adding customised templates
The real power of the dotnet new command is the ability to add new, custom templates. Even better, templates can be distributed and shared, simply by packing them into a NuGet package and uploading to nuget.org. This makes it very easy to get started with a framework, or automate the boilerplate of creating new projects or project items.
To add a new custom template, use the dotnet new –install {template} command, passing in either the name of a NuGet package, or a file folder for a local template. But how do you find new templates?
One way is to search for the framework you’re using and see if templates are available, but that’s a bit hit and miss. Fortunately, you can instead visit dotnetnew.azurewebsites.net and search for templates by keywords. There are over 500 templates tracked on the site, which makes it a good discovery resource.
For example, you could install a set of templates for AWS Lambda projects with dotnet new –install Amazon.Lambda.Templates. One very nice feature of installing templates via NuGet packages is that each package can contain more than one template. This AWS Lambda package contains 28 different templates, including a tutorial project.
Of course, if you no longer want to use the template, simply uninstall it with dotnet new –uninstall {package}. The name passed here is the name of the installed template package, and if you’re not sure of the name, simply run dotnet new –uninstall to get a list.
Creating your own templates
You can also create your own custom templates. These don’t have to be for popular frameworks, but might be for internal or personal projects. Essentially, if you often find yourself creating a specific folder structure, set of references, or boilerplate files, consider creating project or item templates. Project templates are simply plain text files, including the .csproj files — there’s no requirement that the generated templates are .NET Core specific, and they can be made to target any framework.
It’s very easy to create a new template and they are surprisingly easy to maintain. Traditionally, templates that can perform text substitution use a special syntax, like $VARIABLE$ markers that will be replaced when the template is evaluated. Unfortunately, this is usually invalid syntax for the file type, which makes it impossible to run the project to test that the template is correct. This leads to bugs and slow iteration times, and basically, a bit of a maintenance headache.
Fortunately, the designers of the template engine have thought about this, and come up with a much nicer way of working: running templates.
The idea is simple — the template is just plain text files. No special formats, no special markers. So, a C# file is always a valid C# file. If a template wishes to substitute some text, such as replacing a C# namespace for one that’s based on the project name, this is handled with simple search and replace. For example, imagine we had a template that looked like this:
namespace RootNamespace
{
public static class Main
{
// ...
}
}
The template’s JSON configuration defines a symbol that will replace the namespace. The symbol value would be based on the project name, possibly with a built-in transform applied to make sure it only contains valid characters. The symbol would also define the text it was replacing — “RootNamespace.” When the template engine processes each file, if it sees “RootNamespace,” it will replace it with the symbol value.
This simple search and replace is usually based on a symbol that is based on a parameter, such as the template name, the output name, or an actual custom parameter. But, it’s also possible to create symbols based on generators, to create GUIDs, random numbers, or the current timestamp, and so on.
But no template is complete without conditional code — something that’s added or removed based on a parameter. How does dotnet new handle this and keep “running templates” as an option? This is actually handled on a per-file-type basis, with some default config built in, and the ability to define your own style for unknown file formats. Essentially, the idea is to use the file-specific pre-processor (such as #if for C# or C++) for those file types that support it, and specially formatted comments for those that don’t, such as JSON.
```cs
public class HomeController : Controller
{
public IActionResult Index() => View();
public IActionResult About()
{
ViewData["Message"] = "Your application description page.";
return View();
}
#if (EnableContactPage)
public IActionResult Contact()
{
ViewData["Message"] = "Your contact page.";
return View();
}
#endif
public IActionResult Error() => View();
}
```
All of the metadata for a template lives in a template.json file. This includes the template’s short name, description, author, tags, and supported language. Because a template can only target a single language, it also includes a “group identity” option, which multiple templates can specify, one for each language. The metadata file can also include optional information about source and target of files to be copied or renamed, conditional file copies, substitution symbols, command-line parameters and post-creation actions such a package restore. But by default, the template engine will copy and process all files in the template’s file structure.
{
"author": "Matt Ellis",
"classifications": [ "Hello world" ],
"name": "Hello world template",
"identity": "Matt.HelloWorldTemplate.CSharp",
"shortName": "helloworld",
"guids": [ "d23e3131-49a0-4930-9870-695e3569f8e6" ],
"sourceName": "MyTemplate"
}
The template.json file must be placed at the root of the template’s folder structure, in a folder called .template.config. The rest of the folder structure is entirely up to you — the template engine will keep the same folder structure when evaluating the template. In other words, if you add a README.md file to the root of your template’s folder structure, then the template engine will create a README.md in the root of the output folder when you call dotnet new. So, if you use –output MyApp, you will get a file called MyApp/README.md.
> tree -a
.
├── .template.config
│ └── template.json
├── MyTemplate.csproj
├── Program.cs
└── Properties
└── AssemblyInfo.cs
2 directories, 4 files
To install and test your template, simply call dotnet new –install {template} as you would to install a custom template, but this time, pass in the path to the root of the template folder structure. To uninstall, use dotnet new –uninstall {template}. Again, if you’re not sure of what to pass, use `dotnet new –uninstall` to get a full list.
Distributing your templates
Once you’re ready to distribute your template, you can pack it into a NuGet package and upload to nuget.org. You’ll need to create a .nuspec file as normal, but with two slight tweaks: add a packageType element and set the name attribute to “Template,” then make sure the template folder structure is copied into a folder called “content”
<package>
<metadata>
<id>MattDemo.HelloWorldTemplate</id>
<version>1.0</version>
<authors>Matt Ellis</authors>
<description>Hello World template</description>
<packageTypes>
<packageType name="Template" />
</packageTypes>
</metadata>
<files>
<file src=".template.config/template.json" target="content/.template.config" />
<file src="MyTemplate.csproj" target="content/" />
<file src="Program.cs" target="content/" />
<file src="Properties/*" target="content/Properties/" />
</files>
</package>
Additionally, it’s possible to include multiple templates in a single package — simply create multiple folders under “content” and add a .tempate.config/template.json
for each template.
There are many more options and capabilities in the template.json file, but covering them all is beyond the scope of this article. But, based on all we’ve covered here, you can see that the template engine is very powerful, flexible, and yet fairly straightforward to use. Please check out the Microsoft docs site as well as the wiki for the dotnet/templating GitHub site.
The Template Engine
One of the most interesting things about dotnet new is that it’s designed to be used from multiple hosts. The dotnet new CLI tool is simply one host — the template engine itself can be used as an API from other applications. This is great for those of us who prefer to work with an IDE instead of the command line, but still want to be able to easily add custom project templates, something that isn’t always easy with an IDE.
We can see this in action in JetBrains Rider. The New Project dialog is powered by the template engine APIs, listing all the available templates, even custom templates. When the user wishes to create a project, the template engine is used to generate the files.
If you look closely, you’ll see that Rider has more templates than the .NET CLI. This is because Rider ships extra templates to support .NET Framework and Xamarin projects. The template engine API allows hosts to install templates to a custom location, and can list them from both, meaning Rider will show custom templates installed by dotnet new –install as well as using the Install button in the new project dialog’s “More templates” page. Once reloaded, the new template is shown in the list, just like all the others.
New, custom projects with ease
The dotnet new command makes it easy to create new projects and project items. The default set of templates will get you started with building .NET Core applications, either command line or ASP.NET based, and will help create test projects and target other .NET languages. Custom templates can be easily installed to create projects with other requirements, such as a different folder structure or framework dependencies. And the format for custom templates makes it easy to create your own, taking advantage of substitution variables and conditional code, but still keeping the template project compilable and maintainable. Together with the dotnet sln command, as well as the other extensible dotnet CLI commands, the new template engine makes it easy to create and manage projects, project items and solutions consistently, cross platform and directly from the command line.