This article is the final installment in a comprehensive series, offering a deep dive into the practical creation of custom .NET project templates. Moving beyond theoretical discussions, this guide provides a hands-on journey, empowering you to build robust, reusable templates that significantly enhance your development workflow. By mastering custom templating, you can eliminate repetitive setup tasks, allowing you to focus immediately on application-specific logic and accelerate project initiation.

Unleashing Development Efficiency with Custom Templates

Imagine a scenario where every new project or module begins with a perfectly structured, pre-configured codebase, tailored to your exact specifications. This article transforms that vision into reality. We will explore how to design and implement custom project and item templates that reflect your team\’s best practices, ensuring consistency and efficiency across all your endeavors. This detailed technical walkthrough will equip you with the knowledge to create highly adaptable templates, suitable for internal use or sharing within the broader development community.

Our journey will cover:

  • Defining a clear project structure and requirements for an effective custom template.
  • Deconstructing the essential components and logic required for template construction.
  • Building a complete custom template from the ground up, starting with an empty directory.

Revisiting Foundations: The Journey So Far

This article builds upon the foundational concepts introduced in the preceding parts of this series. While prior review is beneficial for a complete understanding of the underlying mechanics and design philosophies, it\’s not strictly mandatory. You can still follow this guide step-by-step and successfully create custom templates.

  • Part 1: Microsoft’s Implementation: Explored Microsoft\’s internal strategies for template creation and the mechanics at play beneath the surface.
  • Part 2: Exploring Every Hidden Trail: Delved into identifying and understanding the functionality of each component within a template project.

For those eager to grasp the deeper architectural choices and operational intricacies, revisiting the earlier parts is highly recommended. However, if your primary goal is rapid template construction, this guide provides all the necessary practical steps.

Blueprinting Our Custom Template

To ensure a realistic and comprehensive learning experience, our custom template will be designed to meet a set of specific requirements, mimicking challenges often encountered in real-world projects.

Core Solution Folder Structure:

  • src/: Dedicated to source code.
  • tests/: Contains various test projects.
  • samples/: Houses lightweight manual testing applications.
  • A basic README template, also available as a standalone item template.

src/ Folder Details:

This folder will hold the primary application logic. Our example will feature two projects: Company.App (the main application, e.g., a Web API) and Company.App.Engine (an optional, separate module referenced by the main app). While this structure is simplified, it demonstrates common organizational patterns. The names Company and App serve as placeholders, intended to be replaced with user-defined project names upon template instantiation. The src/ folder will also include static front-end assets (HTML, CSS, JS, images) for a unique landing page experience.

tests/ Folder Details:

This section of the template is designed for flexibility. By default, it will include unit tests, but developers will have the option to exclude the entire tests/ folder if not needed. Furthermore, while integration tests will be available, they will not be included by default, requiring explicit user selection. This allows for tailored test setups based on project requirements.

samples/ Folder Details:

The samples/ folder is optional and typically used for developing libraries or APIs, providing a simple console application to manually test functionality. This can be particularly useful for quickly verifying endpoint behaviors in a Web API project.

From Concepts to Code: Building the Template

Let’s translate these requirements into a functional template. We’ll use command-line tools and a code editor (e.g., PowerShell, Windows Terminal, VS Code) to construct our project.

Initial Setup and Repository Configuration

  1. Create the Root Folder: Begin by creating a new directory for your templates:
    powershell
    mkdir dotnet-templates
    cd .\dotnet-templates\
  2. Define the NuGet Package: Create a DotnetTemplates.csproj file in the root. This XML file specifies how your templates will be packaged into a NuGet package for distribution. Key elements include <PackageType>Template</PackageType>, <PackageId>, <PackageVersion>, and <IncludeContentInPack>true</IncludeContentInPack> to ensure your template files are included.
    Replace “Giorgi” with your own name or company name in the <Authors>, <Title>, and <PackageId> fields.
  3. Initialize Git: Prepare your repository for version control:
    powershell
    dotnet new gitignore
    git init

    This adds a standard .gitignore file and initializes a new Git repository.

Establishing the Template Structure

  1. Create the Main Template Directory: Create a templates folder, and inside it, a web-app-template folder. Within web-app-template, create .template.config and then template.json.
  2. Add Configuration Files: Navigate into web-app-template and add standard configuration files:
    powershell
    dotnet new gitignore
    dotnet new editorconfig
  3. Establish Core Directories: Create the primary project sub-folders:
    powershell
    mkdir src, tests, samples

Generating Core Projects

We\’ll leverage existing .NET templates and then modify them.

  1. Solution File: Create the solution file, using a unique placeholder for the project name:
    powershell
    dotnet new sln -n CUSTOM_TEMPLATE_PLACEHOLDER

    Using a distinct placeholder like CUSTOM_TEMPLATE_PLACEHOLDER prevents accidental replacement of common words in your code.
  2. Main Web API Project: Generate the primary application:
    powershell
    dotnet new webapi --use-controllers -o src/CUSTOM_TEMPLATE_PLACEHOLDER

    After generation, remove OpenAPI-related lines from Program.cs and CUSTOM_TEMPLATE_PLACEHOLDER.csproj to simplify the template.
  3. Optional Engine Module: Create a class library for an additional module:
    powershell
    dotnet new classlib -o src/CUSTOM_TEMPLATE_PLACEHOLDER.Engine

Constructing Test Projects

  1. Create Test Sub-folders:
    powershell
    mkdir tests/UnitTests, tests/IntegrationTests
  2. Generate Unit Tests (xUnit):
    powershell
    dotnet new xunit -o tests/UnitTests/CUSTOM_TEMPLATE_PLACEHOLDER.UnitTests
    dotnet new xunit -o tests/UnitTests/CUSTOM_TEMPLATE_PLACEHOLDER.Engine.UnitTests
  3. Generate Integration Tests (xUnit):
    powershell
    dotnet new xunit -o tests/IntegrationTests/CUSTOM_TEMPLATE_PLACEHOLDER.IntegrationTests
    dotnet new xunit -o tests/IntegrationTests/CUSTOM_TEMPLATE_PLACEHOLDER.Engine.IntegrationTests

Adding the Sample Project

  1. Generate Console Application:
    powershell
    dotnet new console -o samples/CUSTOM_TEMPLATE_PLACEHOLDER.Sample

Integrating Projects into the Solution

All generated projects must be added to the .sln file. From the web-app-template directory, run a series of dotnet sln add commands for each project:

dotnet sln add src/CUSTOM_TEMPLATE_PLACEHOLDER/CUSTOM_TEMPLATE_PLACEHOLDER.csproj
dotnet sln add .\src\CUSTOM_TEMPLATE_PLACEHOLDER.Engine\CUSTOM_TEMPLATE_PLACEHOLDER.Engine.csproj
dotnet sln add .\tests\UnitTests\CUSTOM_TEMPLATE_PLACEHOLDER.UnitTests\CUSTOM_TEMPLATE_PLACEHOLDER.UnitTests.csproj
dotnet sln add .\tests\UnitTests\CUSTOM_TEMPLATE_PLACEHOLDER.Engine.UnitTests\CUSTOM_TEMPLATE_PLACEHOLDER.Engine.UnitTests.csproj
dotnet sln add .\tests\IntegrationTests\CUSTOM_TEMPLATE_PLACEHOLDER.IntegrationTests\CUSTOM_TEMPLATE_PLACEHOLDER.IntegrationTests.csproj
dotnet sln add .\tests\IntegrationTests\CUSTOM_TEMPLATE_PLACEHOLDER.Engine.IntegrationTests\CUSTOM_TEMPLATE_PLACEHOLDER.Engine.IntegrationTests.csproj
dotnet sln add .\samples\CUSTOM_TEMPLATE_PLACEHOLDER.Sample\CUSTOM_TEMPLATE_PLACEHOLDER.Sample.csproj

Implementing the Front-End Landing Page

Create a wwwroot folder within src/CUSTOM_TEMPLATE_PLACEHOLDER/. Inside wwwroot, add index.html, css/style.css, js/script.js, and assets/icon.png. These files provide a static landing page, which in our example, includes a fun F1 pitstop game. You can find these assets in the provided GitHub repository.

Configuring Template Behavior with template.json

The template.json file is central to defining how your template functions, including its options and file transformations.

Start with the basic metadata:

{
    "$schema": "http://json.schemastore.org/template",
    "author": "Your Name",
    "classifications": [ "webapi" ],
    "identity": "YourName.Dotnet.Template.Webapi",
    "name": "Your Custom Web API template",
    "description": "Template for generating custom Web API projects",
    "shortName": "yourtemplateapi",
    "tags": {
        "language": "C#",
        "type": "project"
    },
    "preferNameDirectory": true,
    "sourceName": "CUSTOM_TEMPLATE_PLACEHOLDER"
}

Making samples/ Optional

Add a symbols object to template.json to define a parameter. This parameter includeSamples (boolean, default false) will control the inclusion of the samples folder.

"symbols": {
    "includeSamples": {
        "type": "parameter",
        "datatype": "bool",
        "description": "Include a sample project for manual testing of the main application?",
        "defaultValue": "false"
    }
}

Next, add a sources object to configure file exclusion. The exclude pattern will physically remove the samples folder if includeSamples is false.

"sources": [
    {
        "source": "./",
        "target": "./",
        "modifiers": [
            {
                "condition": "(!includeSamples)",
                "exclude": [ "samples/**" ]
            }
        ]
    }
]

Finally, wrap the samples project reference in your .sln file with an #if(includeSamples)...#endif conditional block so it\’s only included when the symbol is true.

Dynamic Test Inclusion

Implement two boolean parameters for tests: includeIntegrationTests (default false) and includeUnitTests (default true).

"includeIntegrationTests": {
    "type": "parameter",
    "datatype": "bool",
    "description": "Include an integration tests project?",
    "defaultValue": "false"
},
"includeUnitTests": {
    "type": "parameter",
    "datatype": "bool",
    "description": "Exclude the unit tests project?",
    "defaultValue": "true"
}

Add corresponding source modifiers to exclude tests/IntegrationTests/** if includeIntegrationTests is false, and tests/UnitTests/** if includeUnitTests is false. Crucially, add a modifier to exclude the entire tests/** folder if (!includeUnitTests) && (!includeIntegrationTests).

Update your .sln file with #if blocks for each test project and the entire tests folder to reflect these conditions.

Optional Submodule Inclusion

Add an includeSubmodule boolean parameter (default false).

"includeSubmodule": {
    "type": "parameter",
    "datatype": "bool",
    "description": "Include a sub-module?",
    "defaultValue": "false"
}

Add a source modifier to exclude src/CUSTOM_TEMPLATE_PLACEHOLDER.Engine/** and its associated unit/integration test projects (tests/CUSTOM_TEMPLATE_PLACEHOLDER.Engine.UnitTests/**, tests/CUSTOM_TEMPLATE_PLACEHOLDER.Engine.IntegrationTests/**) if includeSubmodule is false.
Remember to wrap the submodule and its related test projects in #if(includeSubmodule)...#endif blocks within your .sln file.

Configuring the Web Project (Game Mode)

Modify Program.cs in src/CUSTOM_TEMPLATE_PLACEHOLDER to include app.UseStaticFiles() and app.UseFileServer(new FileServerOptions { EnableDefaultFiles = true }); for static file support. Wrap these lines in a conditional #if(useGameMode)...#endif block.

Add a useGameMode boolean parameter (default true).

"useGameMode": {
    "type": "parameter",
    "datatype": "bool",
    "description": "The game mode will allow you to play an awesome F1 pitstop game",
    "defaultValue": "true"
}

Add a source modifier to exclude src/CUSTOM_TEMPLATE_PLACEHOLDER/wwwroot/** if useGameMode is false.

Configurable Framework Version

Introduce a framework parameter of type choice with net8.0 and net9.0 as options, defaulting to net9.0. Use the replaces property to target a placeholder.

"framework": {
    "type": "parameter",
    "description": "The target framework of the project",
    "datatype": "choice",
    "choices": [
        { "choice": "net8.0" },
        { "choice": "net9.0" }
    ],
    "defaultValue": "net9.0",
    "replaces": "DOTNET_TARGET_FRAMEWORK"
}

In every .csproj file, replace <TargetFramework>net9.0</TargetFramework> with <TargetFramework>DOTNET_TARGET_FRAMEWORK</TargetFramework>.

Testing Your Configured Template

  1. Install Locally: Navigate to your web-app-template folder and install:
    powershell
    dotnet new install .
  2. List Templates: Verify your template is listed:
    powershell
    dotnet new list
  3. View Help: Examine the available options for your template:
    powershell
    dotnet new yourtemplateapi --help
  4. Generate a Project with Options: Test the flexibility by generating a project with specific configurations:
    powershell
    dotnet new yourtemplateapi --includeSubmodule --includeIntegrationTests --framework net8.0 -o MyCoolApp

    Ensure you run this command outside your template project directory. Inspect the generated project structure and .csproj files to confirm all options are applied correctly.

Enhancing Documentation with a README Item Template

Rather than manually creating README.md files, we\’ll create a reusable item template.

  1. Create README Item Template: Inside your templates folder, create readme-template, with a .template.config folder and template.json.
    json
    {
    "$schema": "http://json.schemastore.org/template",
    "author": "Your Name",
    "classifications": [ "README", "Documentation" ],
    "identity": "YourName.Template.Readme",
    "name": "README file",
    "shortName": "fullsendreadme",
    "tags": {
    "type": "item"
    }
    }
  2. Create README.md Content: In readme-template, create README.md with placeholders.
    # PLAIN_TEXT_NAME
    
    A brief description
    
    ## Technologies
    **Framework:** DOTNET_TEMPLATE_VERSION_NUMBER
    
    ## Repository
    **URL:** https://github.com/[user_name]/REPO_NAME_KEBAB_CASE
    
    ## Documentation
    
    ## Features
    
    ## Installation
    
    ## Dependencies
    
    ## Other
    
  3. Generate Symbols for Readability: In your main web app template\’s template.json, add generated symbols to transform the project name and framework version into more readable formats.
    • plainTextName: Uses regex to convert CadillacF1.BrakeTemperatureMonitor to Cadillac F1 - Brake Temperature Monitor.
    • dotNetVersionNumber: Converts net9.0 to .NET 9.0.
    • Add these within the symbols object, using replaces to target the README.md placeholders.
    • Derived Symbol for Kebab-Case: Define a forms object for transformations and a kebabCasedName derived symbol to convert the project name to kebab-case for the GitHub URL.

    Example of generated and derived symbols structure (added to your existing symbols object in web-app-template/template.json):

    "plainTextName": {
        "type": "generated",
        "generator": "regex",
        "datatype": "string",
        "replaces": "PLAIN_TEXT_NAME",
        "parameters": {
            "source": "name",
            "steps": [
                {
                    "regex": "(?<=[a-z0-9])(?=[A-Z])",
                    "replacement": " "
                },
                {
                    "regex": "\\.",
                    "replacement": " - "
                }
            ]
        }
    },
    "dotNetVersionNumber": {
        "type": "generated",
        "generator": "regex",
        "datatype": "string",
        "replaces": "DOTNET_TEMPLATE_VERSION_NUMBER",
        "parameters": {
            "source": "framework",
            "steps": [
                {
                    "regex": "^net(\\d+)\\.(\\d+)$",
                    "replacement": ".NET $1.$2"
                }
            ]
        }
    },
    "forms": {
        "kebabCase": {
            "identifier": "kebabCase"
        }
    },
    "kebabCasedName": {
        "type": "derived",
        "valueSource": "name",
        "replaces": "REPO_NAME_KEBAB_CASE",
        "valueTransform": "kebabCase"
    }
    
  4. Install README Template: Navigate to readme-template and install it:
    dotnet new install .
    
  5. Generate a README: Navigate into a generated project and create a README:
    powershell
    dotnet new fullsendreadme

    This will generate a README.md populated with the project name, framework, and kebab-cased repository name automatically.

Your custom template is now fully functional and highly configurable!

Packaging Your Templates for NuGet Distribution

The final step is to make your templates accessible to others by packaging them as a NuGet package.

  1. Register on NuGet.org: Ensure you have an account on `https://www.nuget.org/` and it is verified.
  2. Pack the Template: Navigate to the root folder of your template project (where DotnetTemplates.csproj is located) and run:
    powershell
    dotnet pack -c Release

    This command will create a .nupkg file (e.g., YourName.Dotnet.Templates.1.0.0.nupkg) in the bin/Release/ directory.
  3. Push to NuGet: Publish your package:
    powershell
    dotnet nuget push bin/Release/YourName.Dotnet.Templates.1.0.0.nupkg --api-key YOUR_API_KEY --source https://api.nuget.org/v3/index.json

    Replace YOUR_API_KEY with your actual NuGet API key. Once pushed, others can install your templates using:
    powershell
    dotnet new install YourName.Dotnet.Templates::1.0.0

Conclusion

This comprehensive guide has equipped you with the skills to design, build, and deploy highly customizable .NET project and item templates. By implementing dynamic options, conditional file generation, and intelligent text transformations, you can drastically streamline your development workflow, enforce consistent project structures, and boost team productivity. The possibilities for customization are vast, and this framework provides a solid foundation upon which you can continue to expand and refine your templating solutions. We encourage you to explore further, contribute to the open-source repository, and share your innovations with the community. Stay tuned for future announcements!

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.
You need to agree with the terms to proceed