Rasmus Olsson

Creating a .NET template with template.json

Tags: dotnet,
June 15, 2023

In this article, we're going to craft a .NET template together with template.json. Initially, we will develop a simple template for a WebAPI. After that We will introduce an optional parameter to incorporate a PostgreSQL database connection, and see how template.json can be customized to accommodate this functionality.

Let's start by creating our template solution and an example Web API project:

dotnet new sln -n templates dotnet new webapi -n WebApiTemplate dotnet sln add WebApiTemplate

Opening and inspecting the solution, we should see the following structure (if on .NET 6)

root └───WebApiTemplate │ appsettings.Development.json │ appsettings.json │ Program.cs │ WeatherForecast.cs │ WebApiTemplate.csproj │ ├───Controllers │ WeatherForecastController.cs │ └───Properties launchSettings.json

And now lets create a .template.json file and insert some basic template.json content. (standing at root directory)

New-Item -Path '.\WebApiTemplate\.template.config\template.json' -ItemType File -Force Set-Content -Path '.\WebApiTemplate\.template.config\template.json' -Value @' { "$schema": "http://json.schemastore.org/template", "author": "rasmusolsson.dev", "classifications": ["Example", "Template", "Healthcheck"], "identity": "Example.HealthCheckTemplate.0001", "name": "My webapi with conventions", "shortName": "my-webapi", "tags": { "language": "C#", "Type": "Project" } } '@

Let's install and list the template to see how it looks:

dotnet new -i . dotnet new list --columns-all --author rasmusolsson.dev

Output:

Template Name Short Name Language Type Author Tags -------------------------- ---------- -------- ------- ---------------- ---------------------------- My webapi with conventions my-webapi [C#] project rasmusolsson.dev Example/Template/Healthcheck

Let's see how these map against the template.

Template Name => "name": "My webapi with conventions" Short name => "shortName": "my-webapi" Tags => "classifications": ["Example", "Template", "Healthcheck"] Language => "tags": { "language": "C#" } Type => "tags": { "Type": "Project" }

Now let's use the template we created:

dotnet new my-webapi -o temp

The code for the template minus the .template.config folder should now be available:

├───temp │ │ appsettings.Development.json │ │ appsettings.json │ │ Program.cs │ │ WeatherForecast.cs │ │ WebApiTemplate.csproj │ │ │ ├───Controllers │ │ WeatherForecastController.cs │ │ │ └───Properties │ launchSettings.json

Now lets make this abit more complicated.

Lets say we want to have options to create a webapi project that uses a postgres database and that its optional. We of course have multiple options here, we can just create another template called for example my-webapi-with-postgres or we can use the same template, my-webapi, and parameterize the template.json so that it gives us what we need.

Lets go with the second option to explore the template.json further.

We will start by adding a repository inside the template that will be the access-point to the database.

New-Item -Path '.\WebApiTemplate\Repository\Repository.cs' -ItemType File -Force New-Item -Path '.\WebApiTemplate\Repository\IRepository.cs' -ItemType File -Force Set-Content -Path '.\WebApiTemplate\Repository\Repository.cs' -Value @' using System; using Npgsql; namespace WebApiTemplate.Repository { public class Repository : IRepository { private readonly string _connectionString; public Repository(string connectionString) { _connectionString = connectionString; } public async Task AddPerson(string name, int age) { using var con = new NpgsqlConnection(_connectionString); con.Open(); const string sql = "INSERT INTO people (Name, Age) VALUES (@Name, @Age)"; using var cmd = new NpgsqlCommand(sql, con); cmd.Parameters.AddWithValue("@Name", name); cmd.Parameters.AddWithValue("@Age", age); await cmd.ExecuteNonQueryAsync(); Console.WriteLine("Record inserted successfully"); } } } '@ Set-Content -Path '.\WebApiTemplate\Repository\IRepository.cs' -Value @' namespace WebApiTemplate.Repository; public interface IRepository { Task AddPerson(string name, int age); } '@

We now have the following:

└───WebApiTemplate │ appsettings.Development.json │ appsettings.json │ Program.cs │ WeatherForecast.cs │ WebApiTemplate.csproj │ ├───.template.config │ template.json │ ├───Controllers │ WeatherForecastController.cs │ ├───Properties │ launchSettings.json │ └───Repository IRepository.cs Repository.cs

We also need to add it to dependency injection for the connectionString

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); builder.Services.AddTransient<NpgsqlConnection>(_ => new NpgsqlConnection(connectionString)); builder.Services.AddTransient<IRepository, Repository>();

and also add connectionString to appsettings.json:

"ConnectionStrings": { "DefaultConnection": "Host=localhost;Username=postgres;Password=your_password;Database=SampleDB" }

and don't forget the .csproj:

<PackageReference Include="Npgsql" Version="8.0.0-preview.4" />

In total there are four different places where we refer to the database in some way:

  • repository folder which includes Repository.cs and interface
  • .csproj
  • connectionString in appsettings
  • dependency injection in program.cs

Because the template should be able to choose if it wants a database we need a parameter for this, lets have a look how we can do this by manipulating the template.json.

the template.json includes something called symbols, in here we will add a symbol for UseDatabase:

"symbols": { "UseDatabase": { "type": "parameter", "datatype": "bool", "defaultValue": "false", "description": "Specifies whether to include database support." } }

This will enable us to use a parameter across our template to include exclude code. Lets see how we can use it.

We can use the preprocessor directives: #if (UseDatabase) and #endif to add this code based on if the variable is true or false.

dependency injection in program.cs:

#if (UseDatabase) using Npgsql; using WebApiTemplate.Repository; #endif . . . #if (UseDatabase) var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); builder.Services.AddTransient<NpgsqlConnection>(_ => new NpgsqlConnection(connectionString)); builder.Services.AddTransient<IRepository, Repository>(); #endif

.csproj: The preprocessor directive is not directly applicable inside the .csproj so we need to use a condition attribute and place it inside for it to be possible. Resulting in:

<PackageReference Condition="$(UseDatabase) == true" Include="Npgsql" Version="8.0.2" />`

appsettings.json: The preprocessor directive is not directly applicable here. We will use //(comments) to include the preprocessor directive. Resulting in:

//#if UseDatabase "ConnectionStrings": { "DefaultConnection": "Host=localhost;Username=postgres;Password=your_password;Database=SampleDB" }, //#endif

repository folder which includes Repository.cs and interface: Here we could initially think that it should be the same appoach for the program.cs in dependency injection, just add the preprocessor directives, but the folder and files will still be left over. So we will have a look at another solution.

To be able to exclude content we can use the sources, modifier and add a exclude condition on the UseDatabase. Here is an example:

"sources": [ { "modifiers": [ { "condition": "(!UseDatabase)", "exclude": [ "Repository/**" ] } ] } ]

Now let's try out our template:

dotnet new my-webapi -o MyWebApiWithDatabase --UseDatabase true

Output:

root │ appsettings.Development.json │ appsettings.json │ Program.cs │ WeatherForecast.cs │ WebApiTemplate.csproj │ ├───Controllers │ WeatherForecastController.cs │ ├───Properties │ launchSettings.json │ └───Repository IRepository.cs Repository.cs dotnet new my-webapi -o MyWebApiWithoutDatabase --UseDatabase false

Note: We can also omit the --UseDatabase flag completely because it defaults to false

Output:

│ appsettings.Development.json │ appsettings.json │ Program.cs │ WeatherForecast.cs │ WebApiTemplate.csproj │ ├───Controllers │ WeatherForecastController.cs │ └───Properties launchSettings.json

Thats pretty much it for this post!

We've taken a gentle stroll through the capabilities of template.json, touching just the tip of the iceberg of what's possible. Below are some references to guide you further on your journey into .NET templating, at your own pace.

https://github.com/dotnet/templating

https://learn.microsoft.com/en-us/dotnet/core/tools/custom-templates

https://github.com/dotnet/templating/tree/main/dotnet-template-samples

Happy coding!

please share