Building a Modern Development Platform: Creating Custom dotnet new Templates πŸ—οΈ

Series Posts

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:

  1. βœ… Finds all occurrences of MyTemplate in filenames and content
  2. βœ… Replaces them with MyAwesomeApp
  3. βœ… 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 IncludeCosmos is 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

  1. Go to nuget.org
  2. Sign up or sign in
  3. 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 πŸ“š

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:

  1. 🎯 Identify common patterns in your organization
  2. πŸ—οΈ Create a template capturing those patterns
  3. πŸ“¦ Package and publish to NuGet
  4. πŸ“’ Share with your team
  5. πŸ”„ Maintain and improve over time

Your developers will thank you for making project creation instant and consistent!


Resources: πŸ“š