Building a Modern Development Platform: Creating Custom dotnet new Templates ποΈ
Why Custom Templates? π‘
Every organization has standards:
- β Consistent project structure
- β Standard dependencies and NuGet packages
- β Configuration patterns (logging, health checks, observability)
- β Service defaults and resilience handlers
- β README templates and documentation
Without templates:
Developer A creates new project
βββ Picks own folder structure
βββ Uses different dependency versions
βββ Sets up logging differently
βββ Everyone copies-paste templates anyway
Developer B creates new project
βββ Different folder structure
βββ Different dependency versions
βββ Different logging setup
βββ Inconsistent codebase
With custom templates:
dotnet new mycompanytemplate -n MyProject
βββ Instantly generates standardized, production-ready project
βββ β
Correct folder structure
βββ β
Standard dependencies
βββ β
Preconfigured logging and observability
βββ β
Service defaults for resilience
βββ β
Ready to run
How Templates Work π§
The dotnet new system consists of two key parts:
1. Template Definition π
mytemplate/
βββ .template.config/
β βββ template.json β Configuration
βββ src/
β βββ MyApp.csproj β Template files
βββ README.md
βββ global.json
The .template.config/template.json file tells the CLI:
- What this template is called
- What parameters it accepts
- How to handle conditional file inclusion/exclusion
- What placeholder names to replace
2. Installation π¦
Templates can be installed from:
- NuGet package (published to nuget.org or private feed)
- Local directory (for development)
- Nupkg file (for testing before publishing)
Once installed, use it like any built-in template:
dotnet new mytemplate -n MyProject
The Minimum Template Configuration π
Hereβs the absolute minimum you need:
mytemplate/
βββ .template.config/
β βββ template.json
βββ yourfiles/
βββ file.cs
βββ README.md
The template.json file:
{
"$schema": "http://json.schemastore.org/template",
"author": "Your Company",
"name": "My Template",
"shortName": "mytemplate",
"identity": "Company.MyTemplate.CSharp",
"classifications": ["Common", "Library"],
"sourceName": "MyTemplate"
}
Key Fields:
| Field | Purpose | Example |
|---|---|---|
$schema |
JSON schema URL for editor IntelliSense | http://json.schemastore.org/template |
author |
Who created this | βYour Companyβ |
name |
Display name | βMy Awesome Templateβ |
shortName |
Used in dotnet new command |
βmytemplateβ |
identity |
Unique identifier (reverse domain format) | βCompany.MyTemplate.CSharpβ |
classifications |
Tags for searching/filtering | [βCommonβ, βConsoleβ] |
sourceName |
Text to replace with userβs project name | βMyTemplateβ |
When a user runs:
dotnet new mytemplate -n MyAwesomeApp
The template engine:
- β
Finds all occurrences of
MyTemplatein filenames and content - β
Replaces them with
MyAwesomeApp - β Generates the project
A Real-World Example: .NET Aspire Template π
Let me show you a complete, practical template for creating cloud-native solutions with .NET Aspire:
Template Structure
template/
βββ Template.csproj # Packaging project
βββ NetSolution/ # Template content
βββ .template.config/
β βββ template.json # Configuration
βββ NetSolution.slnx # Solution file
βββ apphost.cs # Single-file Aspire AppHost
βββ global.json
βββ .gitignore
βββ README.md
βββ src/
βββ NetSolution.Api/ # Minimal API project
β βββ Program.cs
β βββ Health.cs
β βββ NetSolution.Api.csproj
βββ NetSolution.AppHost/ # Aspire host
β βββ apphost.cs
β βββ NetSolution.AppHost.csproj
βββ NetSolution.ServiceDefaults/
β βββ Extensions.cs
β βββ NetSolution.ServiceDefaults.csproj
βββ NetSolution.Data/ # Conditional: only if --IncludeCosmos
βββ CosmosContext.cs
βββ Models/
βββ NetSolution.Data.csproj
The template.json Configuration
{
"$schema": "http://json.schemastore.org/template",
"author": "Your Company",
"name": ".NET Solution Template",
"description": "Create a complete .NET Aspire solution with optional Cosmos DB",
"shortName": "netsolution",
"identity": "Company.NetSolution.Template",
"classifications": ["Common", "Cloud", "Aspire"],
"sourceName": "NetSolution",
"symbols": {
"IncludeCosmos": {
"type": "parameter",
"datatype": "bool",
"defaultValue": "false",
"displayName": "Include Cosmos DB",
"description": "Add Entity Framework Core with Cosmos DB support"
}
},
"sources": [
{
"modifiers": [
{
"condition": "(!IncludeCosmos)",
"exclude": [
"src/NetSolution.Data/**/*"
]
}
]
}
]
}
What this does:
- β
Defines one parameter:
IncludeCosmos(boolean, defaults to false) - β
Excludes the Data project folder if
IncludeCosmosis false - β Allows conditional project generation
Using Conditional Compilation in Code
In your C# files, use preprocessor directives:
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
builder.Services.AddMinimalApi();
#if (IncludeCosmos)
builder.Services.AddCosmosDbContext();
#endif
var app = builder.Build();
// ...
For XML files (csproj, slnx), use comment syntax:
<!--#if (IncludeCosmos)-->
<ItemGroup>
<ProjectReference Include="..\NetSolution.Data\NetSolution.Data.csproj" />
</ItemGroup>
<!--#endif-->
Packaging Your Template π¦
Step 1: Create a Packaging Project
Create Template.csproj at the template root:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<PackageType>Template</PackageType>
<PackageVersion>1.0.0</PackageVersion>
<PackageId>Company.NetSolution.Template</PackageId>
<Title>Company .NET Solution Template</Title>
<Authors>Your Company</Authors>
<Description>Complete .NET Aspire solution with optional Cosmos DB</Description>
<PackageTags>dotnet-new;templates;aspire;cloud-native</PackageTags>
<TargetFramework>netstandard2.0</TargetFramework>
<IncludeContentInPack>true</IncludeContentInPack>
<IncludeBuildOutput>false</IncludeBuildOutput>
<ContentTargetFolders>content</ContentTargetFolders>
</PropertyGroup>
<ItemGroup>
<Content Include="NetSolution\**\*"
Exclude="NetSolution\**\bin\**;NetSolution\**\obj\**" />
<Compile Remove="**\*" />
</ItemGroup>
</Project>
Key Settings:
<PackageType>Template</PackageType>- Identifies this as a template package<IncludeContentInPack>true</IncludeContentInPack>- Include all template files<IncludeBuildOutput>false</IncludeBuildOutput>- Donβt include binaries<ContentTargetFolders>content</ContentTargetFolders>- Put templates in content folder<Compile Remove="**\*" />- Donβt compile template code
Step 2: Pack the Template
cd template
dotnet pack
This creates bin/Release/Company.NetSolution.Template.1.0.0.nupkg
Step 3: Install and Test
# Install from local nupkg
dotnet new install ./bin/Release/Company.NetSolution.Template.1.0.0.nupkg
# Verify installation
dotnet new list
# Create a test project
mkdir test-output
cd test-output
dotnet new netsolution -n TestApp --IncludeCosmos
cd TestApp/src/TestApp.AppHost
dotnet run
Installing Templates π
From Local Directory (Development)
# Install from folder
dotnet new install ./path/to/template/NetSolution
# Later, uninstall
dotnet new uninstall ./path/to/template/NetSolution
Use case: Developing/testing templates locally
From NuGet Package
# Install specific version
dotnet new install Company.NetSolution.Template
# Install latest version
dotnet new install Company.NetSolution.Template::latest
# List installed templates
dotnet new list
# Get template details
dotnet new netsolution --help
From Custom NuGet Feed
dotnet new install Company.NetSolution.Template \
--nuget-source https://your-nuget-feed.com/v3/index.json
Using the Template π
Basic Usage
# Create project without optional features
dotnet new netsolution -n MyAwesomeApp
cd MyAwesomeApp
With Parameters
# Enable Cosmos DB support
dotnet new netsolution -n MyAwesomeApp --IncludeCosmos
# Alternative syntax
dotnet new netsolution -n MyAwesomeApp --IncludeCosmos true
View Available Parameters
dotnet new netsolution --help
Output:
Options:
-n, --name <name> The name for the output being created. If no name
is specified, the name of the current directory is
used.
--IncludeCosmos Include Cosmos DB support with Data project
(default: false)
Advanced Configuration π―
Adding More Parameters
Edit template.json and add to the symbols section:
{
"symbols": {
"IncludeCosmos": {
"type": "parameter",
"datatype": "bool",
"defaultValue": "false",
"displayName": "Include Cosmos DB",
"description": "Add Entity Framework Core with Cosmos DB support"
},
"IncludeAuth": {
"type": "parameter",
"datatype": "bool",
"defaultValue": "false",
"displayName": "Include Authentication",
"description": "Add OpenID Connect/OAuth2 authentication"
},
"ApiVersion": {
"type": "parameter",
"datatype": "text",
"defaultValue": "v1",
"displayName": "API Version",
"description": "API version for generated endpoints",
"replaces": "API_VERSION"
}
}
}
Parameter Types
| Type | Usage | Example |
|---|---|---|
bool |
Yes/no options | --IncludeCosmos |
text |
String replacement | --ApiVersion v2 |
choice |
Select from options | --Framework net8.0 |
Conditional File Exclusion
{
"sources": [
{
"modifiers": [
{
"condition": "(!IncludeCosmos)",
"exclude": [
"src/NetSolution.Data/**/*"
]
},
{
"condition": "(!IncludeAuth)",
"exclude": [
"src/NetSolution.Api/Auth/**/*"
]
}
]
}
]
}
Replace Patterns
Use replaces to substitute text throughout the template:
{
"symbols": {
"ApiVersion": {
"type": "parameter",
"datatype": "text",
"defaultValue": "v1",
"replaces": "API_VERSION"
}
}
}
Then in your code:
const string API_VERSION = "API_VERSION";
public class Api
{
public string GetVersion() => API_VERSION;
}
When generated with --ApiVersion v2, the constant becomes:
const string API_VERSION = "v2";
Publishing to NuGet π€
Step 1: Create NuGet.org Account
- Go to nuget.org
- Sign up or sign in
- Create an API key in your account settings
Step 2: Pack and Push
# Pack the template
dotnet pack
# Push to nuget.org
dotnet nuget push ./bin/Release/Company.NetSolution.Template.1.0.0.nupkg \
--api-key YOUR_API_KEY \
--source https://api.nuget.org/v3/index.json
Step 3: Install from NuGet
Once published, anyone can install:
dotnet new install Company.NetSolution.Template
dotnet new netsolution -n MyProject
Best Practices β
1. Keep Templates Up-to-Date π
Your template should reflect current best practices:
- β Use latest .NET LTS or current version
- β Include modern packages (Aspire, minimal APIs, etc.)
- β Add new observability/health check patterns
- β Remove deprecated packages
Bump the version when updating:
<PackageVersion>1.1.0</PackageVersion>
2. Include Comprehensive Documentation π
In your templateβs README:
# MyTemplate
## What Gets Generated
- Aspire orchestration
- Minimal API
- Health checks
- Service defaults
## Installation
```bash
dotnet new install Company.MyTemplate
Usage
dotnet new mytemplate -n MyProject --Feature1
Parameters
--Feature1: Enable feature 1 ```
3. Test With Real Projects π§ͺ
Before publishing:
# Install locally
dotnet new install ./NetSolution
# Create multiple test projects
dotnet new netsolution -n TestApp1
dotnet new netsolution -n TestApp2 --IncludeCosmos
# Verify each builds and runs
cd TestApp1 && dotnet build
cd TestApp2 && dotnet run
4. Use Semantic Versioning π
1.0.0 Initial release
1.1.0 New feature added (backward compatible)
1.1.1 Bug fix
2.0.0 Breaking changes (new major version)
5. Include Helpful Examples π‘
In generated files, include comments showing how to use generated code:
// Example: Add a new endpoint
// app.MapGet("/items/{id}", GetItemById);
//
// private static async Task<Item> GetItemById(string id, CosmosContext db)
// {
// return await db.Items.FindAsync(id);
// }
app.MapHealthChecks("/health");
6. Validate Parameter Names β οΈ
Parameter names in C# preprocessing are case-sensitive:
#if (IncludeCosmos) // β
Correct - matches symbol name
#endif
#if (includecosmos) // β Wrong - won't work
#endif
7. Keep Templates Focused π―
One template = one clear purpose:
- β
netsolution- Complete Aspire solutions - β
netlibrary- Reusable class libraries - β
networker- Background job services
Donβt try to handle every scenario in one template.
Troubleshooting π§
Template Not Found After Installation
# Try the full path
dotnet new install /full/path/to/template/NetSolution
# Verify it's installed
dotnet new list | grep netsolution
Conditional Content Not Working
β
C# files: Use #if (ParameterName) without quotes
#if (IncludeCosmos)
// ...
#endif
β XML files: Use comment syntax
<!--#if (IncludeCosmos)-->
<ItemGroup>
<!-- ... -->
</ItemGroup>
<!--#endif-->
β Case sensitivity matters - IncludeCosmos β includecosmos
Files Not Being Excluded
Verify the condition syntax in template.json:
{
"condition": "(!IncludeCosmos)", // Parentheses and ! are required
"exclude": [
"src/NetSolution.Data/**/*"
]
}
NuGet Install Issues
Clear the cache and retry:
dotnet nuget locals all --clear
dotnet new install Company.MyTemplate
Resources π
- Official Docs: Custom templates for dotnet new
- Examples: dotnet/templating samples
- Wiki: dotnet/templating GitHub Wiki
- Blog: How to create your own templates for dotnet new
- Schema: template.json JSON Schema
Conclusion π
Custom dotnet new templates standardize your organizationβs development:
Benefits:
- β Consistency across all new projects
- β Reduced onboarding time for new developers
- β Best practices baked into every project
- β Easy to update and versioning control
- β Community can benefit from your templates
Next Steps:
- π― Identify common patterns in your organization
- ποΈ Create a template capturing those patterns
- π¦ Package and publish to NuGet
- π’ Share with your team
- π Maintain and improve over time
Your developers will thank you for making project creation instant and consistent!
Resources: π