DEV Community

Cover image for Template Your Own Clean, Precise Boilerplate Code: No AI, No Wallet Drain. Part 2 – Exploring Every Hidden Trail
Giorgi Kobaidze
Giorgi Kobaidze

Posted on

Template Your Own Clean, Precise Boilerplate Code: No AI, No Wallet Drain. Part 2 – Exploring Every Hidden Trail

Table of Contents

Introduction

This is the second part of our three-part article series. In the previous installment, we reverse-engineered Microsoft's approach to creating project templates. Be sure to check it out first if you want the full context.

The reverse-engineering part is essential because it gives us the ability to expose every component and source code. But this next step is equally important. Now it's time to go through every detail one by one and understand what each part does.

First, let me tell you what to expect in this article:

We're going to:

  • Understand the project template we reverse-engineered in Part 1.

  • Identify all the necessary folders, files, code, and more.

  • Get familiar with the terminology.

  • Use a CLI tool for everything.

So sit back, relax, grab a cup of coffee ☕, and get your terminal ready if you plan on following along (which I highly recommend).

Alternatively, you can just grab a coffee and read through the article at your own pace.

Understanding the Reverse-Engineered Parts

In the previous part, we left off at extracting the contents of microsoft.dotnet.web.projecttemplates.9.0.9.0.1.nupkg. As an example, let's use the Web API template. The project structure looks like this:

Structure

If you're a .NET engineer, you'll recognize many familiar elements here, but you'll also encounter some things that are quite unusual. You might be asking yourself the following questions:

  • What's the purpose of the .template.config folder?
  • Okay, we have controllers in the solution, but then why is there minimal API stuff as well? It's not that they can't coexist in the same project, but it's generally not considered best practice.
  • Why are there so many Program.cs files?

And these are just a few of the many questions you might have right off the bat. If you open files themselves, I guarantee you'll have even more questions.

But everything is there for a reason

template.json

Let's start by introducing the one file where all the 'MAGIC' happens: the one and only template.json file. This file is essential for turning your project into a template. Essentially, it's a JSON file (thank you, Microsoft, for using JSON, you've made our lives so much easier) that configures your template project. The possibilities with this file are almost endless, you can customize your project however you want:

  • Want to add, remove, or rename files based on user input? You got it!

  • Want to use a placeholder for a specific word in multiple places in your code? Roger that!

  • Want to use regex to transform text and insert it into another placeholder? Be my (well, more like Microsoft's) guest!

  • Want to support multiple languages for your .NET CLI? Certo! (If you're Italian and that's not quite right, don’t judge me, I had to Google it.)

And a whole lot more you can do!

This file must be in the .template.config folder, which itself resides in the root of your solution.

Let's explore the structure of the file. Obviously, this file is massive in this template. What you're seeing in the screenshot is a collapsed view showing only the highest-level properties. Check out the screenshot below:

High-level structure

This file actually contains more than 400 lines. Unsurprisingly.

Let's focus on the properties shown here and go through each of them:

$schema - Points to a JSON schema definition (https://json.schemastore.org/template). This schema tells your editor the structure the file should have, enabling auto-completion, syntax validation, and documentation tooltips.

Without this, the editor won't know whether your JSON is valid for a .NET template.

author - This is quite self-explanatory. This is you, or your company, depending on the scenario and policy.

classifications - This is an array of categories, such as Web, Web API, API, Service. These can come in handy when there are many templates and you want to filter them. For example, to see all templates in the API category, run: dotnet new list API. When assigning classifications to your template, do it thoughtfully, don't just put anything there. This helps other developers find your template more easily.

description - Use this property to provide a detailed description of exactly what your template does.

groupIdentity - A unique ID used to group related templates together. For example, Microsoft.Web.WebApi. If Microsoft creates multiple versions of this template, they all share the same group ID.

precedence - This is a numeric value that determines which template "wins" if multiple templates match the same dotnet new command. The higher the number, the higher the priority. You'll rarely need to use this property in your custom template.

identity - The unique identity of this template. Unlike groupIdentity, this refers to a specific instance, such as C# 9 Web API.

shortName - This is the keyword you give to the CLI to generate a project from a specific template. For example, when you run dotnet new webapi, 'webapi' is the shortName here. When creating your custom template, try your best to come up with a short, intuitive, and memorable shortName. You can also assign multiple short names if needed.

tags - Provides structured metadata. For example:

"tags": {
  "language": "C#", // C#, F#, VB
  "type": "project" // "project", "item"
}
Enter fullscreen mode Exit fullscreen mode

This metadata is used by IDEs (Visual Studio, Rider) to display the template in the correct context, so be mindful when assigning values. If your template is just a single file, use item instead of project. The same applies to languages.

sourceName - This is a placeholder name inside the template source. In this case, our sourceName is Company.WebApplication1. Whatever you specify as the project name in the command will replace this placeholder throughout the template source.

You can do this in 2 ways: by using -o (--output) or -n (--name) options, both will work.

For example: dotnet new webapi -o BlackBear

preferNameDirectory - If this option is set to true, the template engine automatically creates a new directory with the chosen project name and places the files there.

guids - A list of GUIDs in the template that will be regenerated when a new project is created.

Example: Project files (.csproj, .sln) often contain GUIDs. If you didn't replace them, every new project created from this template would have the same GUIDs, which would cause conflicts.

By listing them here, the engine ensures every project gets fresh unique GUIDs.

sources - Defines what files/folders to include in the generated project.

Using this property you can:

  • Exclude files
  • Rename files
  • Apply conditions

symbols - Defines template parameters, which you can pass in as flags. Using symbols you can control how your project will be generated.

Example: dotnet new webapi --use-controllers -o BlackBear
This command generates a new Web API and uses the classic controllers (thanks to the --use-controllers option defined in the template.json file) instead of the minimal API, which is the new standard.

primaryOutputs - Lists the main files generated by the template.

Typically, this includes the .csproj file, since it's the entry point for the project.

This is important for post-actions (e.g., dotnet restore needs to know which project to restore).

defaultName - The name used if the user doesn't provide a value for the -n (or -o) option.

Example: If not specified, a new ASP.NET project is simply named WebApplication.

Good practice is to pick a generic but descriptive name here.

postActions - A list of actions that run after the template is created.

Examples:

Run dotnet restore to install NuGet packages

Show a message like “Go DotNet Go”

Run a script

These help guide developers immediately after scaffold creation, so they don't get stuck.

For more information, visit the following link: .NET Templating

Let's Start Walking from Program.cs and Figure Everything Out Along the Way

As you've probably guessed, this template supports multiple variations and combinations for your generated project. That's why there are so many Program.cs files, for example.

Of course, they're not all used together, you only need one Program.cs file. Depending on the parameters you pass during generation, the template selects the Program.cs that's most relevant to your input. This approach helps avoid piling too many conditional statements into a single file, which, as you know, is a nightmare to implement and a literal hell to maintain. It's better to draw new maps than to clutter one with notes and symbols.

As you can see in the first screenshot, those files are named differently.

  • Program.cs
  • Program.Main.cs
  • Program.MinimalAPIs.OrgOrIndividualB2CAuth.cs
  • Program.MinimalAPIs.WindowsOrNoAuth.cs

Well, duh, They're in the same location, so how could they have the same name, right? Still, when you generate a new project, you only get one Program.cs file, instead of something like Program.Main.cs, even if the latter exists in the template. You're probably wondering how. Like everything else in software engineering, this isn't magic either. Let's see how template.json manages it:

Template JSON

As you can see in the screenshot, if the UseProgramMain condition is met, the Program.Main.cs file is simply renamed to Program.cs, and the other files are excluded from the solution.

Let's go even further and see what happens if this condition isn't met:

Template JSON

As you can see, in this case this file is excluded entirely.

By the way, the difference between Program.cs and Program.Main.cs is that the latter contains the old-style entry point of the application - the Program class with the Main method inside it.

Now let's understand what this UseProgramMain thing is, where it comes from, and how its value is set.

If you type dotnet new webapi --help, it will show the command documentation. One of the options will be --use-program-main. Check out the screenshot:

dotnet new webapi help

This option is used to set a value for this symbol. If you search for this parameter in template.json, you probably won't find it. As you may have noticed, besides this file, the .template.config folder contains several other files and folders, which basically serve as helper items for the main template.json file.

Let's look into the dotnetcli.host.json file and see what's inside:

Here's a screenshot of a small part of this file, which basically contains information about symbols, including the UseProgramMain symbol. Symbols can have both long names and short names, and both can be used in your CLI.

File contents

Now let's get back to the template.json file and take a closer look at the symbol itself:

Template JSON

First of all, this is just a portion of the template.json file. Thanks to VS Code's feature that shows which higher-level property a section belongs to in JSON, we can identify that we're looking at the symbols object (excluding primaryOutputs, defaultName, and postActions, which are outside of it and collapsed).

Now this is where things get really interesting. The first thing to point out is that we actually have several types of symbols:

Parameter - Provided by the user (via CLI flags, IDE wizards, or defaults).

Computed - Boolean logic value based on other symbols (toggles).

Generated - Auto-generated values via built-in generators (guid, now, random, etc.).

Derived - Value transformed from another symbol (e.g., stripping part of a name).

Bind - Value bound from the host environment (e.g., environment variables, host-supplied metadata).

In this screenshot, using color highlighting, with each color marking the same symbols in different places, you can see how these symbols are dependent on each other.

UseMinimalAPIs and UseControllers symbols are of type parameter, meaning their values are specified by the user.

Meanwhile, UsingControllers and UsingMinimalAPIs are computed symbols. As you can see in the screenshot, they depend entirely on the values of the other two symbols. The user can't change their values directly. Think of it like encapsulation in OOP, these symbols are internal details of your template. The only way to influence their values is through the parameter-type symbols, which are part of the public interface.

Surprisingly, there are no derived and bind symbols in this large template JSON file, but we'll use both of them in the 3rd part of this series where we'll create our own template.

But we have plenty of generated symbols. Let's look at an example:

Generated symbol

Let's break it down and see what it does:

"generator": "port" - the built-in generator port produces a free TCP port number. The port is guaranteed to not be in use at generation time.

Useful for ASP.NET apps, Docker configs, and sample projects where you want a random, free port for development.

"parameters": { "low": 5000, "high": 5300 } - These parameters control the range of values the generator can pick.

Here it will choose a free port between 5000 and 5300.

If all ports in that range are taken, generation may fail.

Localization Folders

You might be wondering: what are those weird folders inside the .template.config folder?

Localization folders

Well, those are localization folders for your template. Each one corresponds to a language or culture code:

🌐cs → Czech

🌐de → German

🌐en → English

🌐es → Spanish

🌐fr → French

🌐it → Italian

🌐ja → Japanese

🌐ko → Korean

🌐pl → Polish

🌐pt-BR → Portuguese (Brazil)

🌐tr → Turkish

🌐zh-Hans → Chinese (Simplified)

🌐zh-Hant → Chinese (Traditional)

Each folder contains a strings.json file, which holds translations for things like name, description, and parameters.

Check out the contents of one of these files (Spanish version):

File content

In the localize folder, you can find more texts, such as descriptions for symbols, post-actions, and more. Check out the screenshot of the strings (English version):

File content

If you want to generate a project in a specific language, you first need to set your DOTNET_CLI_UI_LANGUAGE environment variable to the corresponding value.

Let's try this right now:

First, let's check if our environment variable is already set:

PowerShell:

$Env:DOTNET_CLI_UI_LANGUAGE
Enter fullscreen mode Exit fullscreen mode

Bash:

echo $DOTNET_CLI_UI_LANGUAGE
Enter fullscreen mode Exit fullscreen mode

Command result

If this command doesn't print anything, as in my case, that means the variable is not set, and English is used by default.

If you want to set a different language for your dotnet CLI interface, it's easy, just assign the desired value to this variable.

In PowerShell, you can do this by running the following command:

$Env:DOTNET_CLI_UI_LANGUAGE = "de-DE"
Enter fullscreen mode Exit fullscreen mode

In Bash, run the following command:

export DOTNET_CLI_UI_LANGUAGE=de-DE
Enter fullscreen mode Exit fullscreen mode

Once you do this, you can run the previous command again to check if the environment variable has been set.

Command result

Now, let's see what is generated when we fetch the dotnet CLI documentation. Just run the following command: dotnet new --help

Command result

See? Now everything is in German.

Let's generate a new project and check its contents. Run the following command: dotnet new webapi PretzelTime

Command result

Keep in mind, the project files themselves won't be translated. Localization only affects your CLI.

If you want to delete this environment variable entirely, use the following command:

PowerShell:

Remove-Item Env:DOTNET_CLI_UI_LANGUAGE
Enter fullscreen mode Exit fullscreen mode

Bash:

unset DOTNET_CLI_UI_LANGUAGE
Enter fullscreen mode Exit fullscreen mode

And that concludes our awesome exploration.

What's Next?

In the next part of the series, we'll use this information and knowledge to build our own custom template. It will include complex scenarios to truly explore the power of templates. That part will be completely hands-on, so stay tuned and get your editors and coding instincts ready!

Top comments (0)