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!