If you have ever set up a new solution by copying an old project and manually renaming namespaces, you already know how error prone that gets. The dotnet CLI has a templating engine built in for exactly this problem, and once you package your own template, any developer on the team can spin up a fully wired solution with one command. This walkthrough covers how to structure a template, the important properties inside template.json, and how to package and install it so it shows up correctly both from the CLI and inside Visual Studio.
Why bother with a custom template
Most teams standardize on a handful of architectural patterns, a backend for frontend setup with a specific auth flow, a microservice with a fixed logging and health check baseline, or a Blazor solution wired to a particular identity provider. A template turns that standard into something a developer can pull down with dotnet new instead of copy pasting a reference project and hunting down every hardcoded name. It also removes an entire category of mistakes where someone forgets to rename a namespace or leaves a stale GUID behind in a solution file.
Template folder structure
The folder layout matters more than most people expect the first time they build one. A .template.config folder has to sit inside your content folder, and it needs a template.json file plus an icon.png that Visual Studio displays once the template is installed. Get this wrong and the CLI either refuses to recognize the template or installs it without ever showing it in Visual Studio’s New Project dialog.

A real example of this pattern is the Blazor.BFF.OpenIDConnect.Template project, a template for a Blazor ASP.NET Core solution with three projects that implements a backend for frontend security architecture using OpenID Connect. It is a good reference because it is a solution level template rather than a single project, which forces you to deal with multiple csproj files, a shared solution file, and cross project namespace references all at once.
The template.json file
template.json is the configuration file that tells the dotnet templating engine what your template is called, how it should be identified, and which values inside your content need to be replaced when someone runs dotnet new against it. Below is a trimmed version showing the core structure, author, classifications, name, identity, shortName, tags, sourceName, and the symbols used for generated values.
{
"author": "damienbod",
"classifications": [
"AspNetCore",
"WASM",
"OpenIDConnect",
"OAuth2",
"Web",
"Cloud",
"Console",
"Solution",
"Blazor"
],
"name": "ASP.NET Core Blazor BFF hosted WASM OpenID Connect",
"identity": "Blazor.BFF.OpenIDConnect.Template",
"shortName": "blazorbffoidc",
"tags": {
"language": "C#",
"type": "solution"
},
"sourceName": "BlazorBffOpenIDConnect",
"preferNameDirectory": "true",
"guids": [
"CFDA20EC-841D-4A9C-A95C-2C674DA96F23",
"74A2A84B-C3B8-499F-80ED-093854CABDEA",
"BD70F728-398A-4A88-A7C7-A3D9B78B5AE6"
],
"symbols": {
"HttpsPortGenerated": {
"type": "generated",
"generator": "port",
"parameters": {
"low": 44300,
"high": 44399
}
},
"HttpsPortReplacer": {
"type": "generated",
"generator": "coalesce",
"parameters": {
"sourceVariableName": "HttpsPort",
"fallbackVariableName": "HttpsPortGenerated"
},
"replaces": "44348"
}
}
}
Running dotnet new against this template creates a solution named after whatever value you pass with -n, with the shortName blazorbffoidc used to invoke it, and every occurrence of BlazorBffOpenIDConnect in your source files swapped for that new name. The classifications array is just metadata for filtering inside Visual Studio’s template picker, it has no effect on the CLI behaviour.
Getting the tags property right
This is the one property that trips people up the most. The type value inside tags has to be set to solution, project, or item, and it has to be exact.
"tags": {
"language": "C#",
"type": "solution" // project, item
},
If this value is missing or misspelled, the template still installs fine through the CLI and dotnet new still creates the project without complaint. The failure is silent and only shows up inside Visual Studio, where the template simply never appears in the New Project dialog. If you are debugging why a template works from the terminal but is invisible in the IDE, this property is the first thing to check.
Auto-generating HTTP ports
Every new solution needs its own HTTPS port so multiple projects can run side by side on a developer machine without clashing. Rather than asking the user to type in a port number, which fails silently if they leave it blank, you can generate one automatically within a range.
"symbols": {
"HttpsPortGenerated": {
"type": "generated",
"generator": "port",
"parameters": {
"low": 44300,
"high": 44399
}
},
"HttpsPortReplacer": {
"type": "generated",
"generator": "coalesce",
"parameters": {
"sourceVariableName": "HttpsPort",
"fallbackVariableName": "HttpsPortGenerated"
},
"replaces": "44348"
}
}
The generator picks a random port between 44300 and 44399 and replaces every occurrence of 44348 in your launchSettings.json with that generated value. The catch is that the placeholder port, 44348 in this case, has to already exist literally in your template content. The engine only performs a substitution, it does not invent a port for content that does not reference one, so if you change the placeholder port in your source project you must update this value to match.
Replacing solution GUIDs
Visual Studio solution files carry project GUIDs, and if two solutions on the same machine share identical GUIDs you can run into confusing tooling issues, particularly with source control integrations and NuGet package caches. The guids array tells the template engine which GUIDs in your source solution file should be regenerated for every new instance.
"guids": [
"CFDA20EC-841D-4A9C-A95C-2C674DA96F23",
"74A2A84B-C3B8-499F-80ED-093854CABDEA",
"BD70F728-398A-4A88-A7C7-A3D9B78B5AE6"
],
Same rule as the port replacement, these GUIDs must exist verbatim in your .sln file already. List a GUID that is not present anywhere in your content and it simply has nothing to replace, no error, no warning. It is worth opening your solution file directly and copying the exact GUID strings rather than retyping them, a single mismatched character means the substitution silently does nothing.
Namespaces, sourceName, and project names
The sourceName value is the anchor for the whole substitution mechanism. Whatever string you set here gets replaced everywhere in your content, project file names, namespaces, and folder names, with the value passed through the -n parameter on the CLI or the project name field in Visual Studio. This means when you are authoring the template content itself, every namespace and project reference has to consistently use that exact sourceName string, otherwise some references get renamed and others get left behind, leaving you with a solution that will not compile.
Classifications for discoverability
The classifications array only matters inside Visual Studio’s create new project screen, where it powers the filter tags a developer can use to search for your template among all the others installed on their machine. It has no bearing on how dotnet new behaves from the command line, so if you only care about CLI usage you can keep this list short.
Packaging the template as a NuGet package
Distributing a template to a team works best as a NuGet package, since that gives you versioning and a familiar install path. A nuspec file describes the package metadata and can be used to build a nupkg that gets pushed to NuGet or an internal feed.
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd">
<metadata>
<id>Blazor.BFF.OpenIDConnect.Template</id>
<version>1.2.6</version>
<title>Blazor.BFF.OpenIDConnect.Template</title>
<license type="file">LICENSE</license>
<description>Blazor backend for frontend (BFF) template for WASM ASP.NET Core hosted</description>
<projectUrl>https://github.com/damienbod/Blazor.BFF.OpenIDConnect.Template</projectUrl>
<authors>damienbod</authors>
<owners>damienbod</owners>
<icon>./BlazorBffOpenIDConnect/.template.config/icon.png</icon>
<language>en-US</language>
<tags>Blazor BFF WASM ASP.NET Core</tags>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<copyright>2022 damienbod</copyright>
<summary>This template provides a simple Blazor template with BFF server authentication WASM hosted</summary>
<releaseNotes>Improved template with http port generator, update packages</releaseNotes>
<repository type="git" url="https://github.com/damienbod/Blazor.BFF.OpenIDConnect.Template" />
<packageTypes>
<packageType name="Template" />
</packageTypes>
</metadata>
</package>
None of this is specific to templates, it is a standard nuspec structure, but note the packageType entry set to Template. That tag is what tells NuGet and Visual Studio’s package manager to treat this as an installable template rather than a regular library reference. Skip that and the package installs but never registers as a template source.
Installing and running the template
Once packaged, installing it and generating a new project is a two line operation from the CLI.
// install
dotnet new -i Blazor.BFF.OpenIDConnect.Template
// run
dotnet new blazorbffoidc -n YourCompany.Bff
The -n flag is what feeds the sourceName replacement described earlier, so YourCompany.Bff becomes the new solution name and replaces every occurrence of the original sourceName across the generated files. After installing through the CLI, the same template becomes available inside Visual Studio’s New Project dialog, provided the tags and icon were configured correctly.

Practical observations and limitations
The CLI experience is reliable and works the same way everywhere, which is why it is worth treating as the primary path even if your team mostly works inside Visual Studio. The IDE experience has rough edges. Prompting for the HTTPS port as a parameter does not behave well inside Visual Studio, since no default value gets applied if the user leaves the field empty, which is why generating the port automatically instead of asking for it is the safer choice.
Getting a template working as a VSIX extension is a separate and considerably more involved effort, with target type mismatches and XML configuration errors that are not always easy to diagnose. For most internal team tooling, the effort to CLI install plus NuGet package plus Visual Studio discoverability through the tags property already covers the common case well, and chasing the VSIX path is only worth it if you are distributing a template broadly outside your own organization.
When this approach is worth it
If your team spins up more than a couple of new solutions a year that follow the same architectural shape, packaging that shape as a dotnet template pays for itself quickly. It is less useful for one-off prototypes or projects that genuinely differ enough that a template would need heavy customization anyway. The GitHub repository at sayedihashimi/template-sample referenced in the original source is a good place to see a minimal working example before you start authoring your own.
Leave a Reply