Templating Projects On .NET Core

.NET Core 2.0 is the newest modular and high-performance implementation of the .NET framework for creating web applications and services that run on Windows, Linux, and Mac. During my research of .NET Core 2.0 features, I noticed that new project generation subsystem has undergone significant changes. Let’s look into it.

What’s New In .NET Core 2.0 Templates

There are new types of projects available out of the box. Users can easily create new templates that can now contain not only applications but also parts of the independent files, like configs.

New project creation is now being run by a separate open-source tool with its source code available on GitHub.

Folks at Microsoft did a hell of a job enabling us to create new templates almost effortlessly, as now templates are nothing more than your projects containing one additional file describing the parameters and instructions on how to handle them during the creation.

In other words, to turn your usual project into a template, all you need to do is add one file, configure it, and then use that project as a template countless numbers of times.

For free. Gratuitously. 😉

The Effectiveness of Templating

In our team, we use BenchmarkDotNet to test the disputable code parts. If during the development or code review someone is suspicious about the productivity rates of a certain implementation, it’s our common practice to give proof in form of a project carrying tests.

This way, on a regular basis most of the team members create projects having the same packages connecting to them and the same code added time and time again. This doesn’t bug us that much, but the repeated action automatization is a must good thing to have. Also, we have a great opportunity knocking – to try out the new template creation mechanism.

So, let’s cut to the chase and create a template.

New Template Creation

As I said, a template is just a project with an extra file in it. The file is called template.json and it has to sit in the .template.config folder. So we grab a finished project, put the file in the folder, and then install the project as a template.

This is what the underlying implementation of the file looks like:

{
  "$schema": "http://json.schemastore.org/template",
  "author": "Roman Patutin",
  "classifications": [ "Custom Templates" ],
  "name": "BenchmarkDotNet Template",
  "identity": "Shakuro.BenchmarkDotNet.CSharp",
  "shortName": "benchmark",
  "guids": [ "dc46e9be-12b0-43c5-ac94-5c7019d59196" ],
  "sourceName": "BenchmarkDotNetTemplate" 
}

The main thing about this description is the shortName key value, as it will be further used to create a project from templates.

Just in case, I’ll put a step-by-step instruction on how I created the project that I later used as a template.

mkdir ProjectName
cd ProjectName
dotnet new console
dotnet add package BenchmarkDotNet 
mkdir .\.template.config
"{}" | Out-File ".\.template.config\template.json"

After that, I copied the file description template implementation into the template.json file. Template Engine saves the information about the installed templates to the hard drive. So before using the new template, install it on your computer by using the dotnet new: -i command switcher

dotnet new -i [drive]:\[Path]\[ ProjectName]

When the installation is done, dotnet displays the list of installed templates to make sure it knows what has been installed. If you forgot the alias of your template, you can always see the entire list by using the dotnet new –l command.

Make World Code A Better Place

In my case, testing is usually done like this: a disputed piece of code with alternative implementations is copied into a test class, runs tests while the project itself is saved into a separate repository.

This way, we usually need a class where we can play around with implementations required for testing. Since our entire project is a template with a template.json file in its folder, all we need to do is add another file, re-install the template and boom! Upon the next new project creation, the class will also be added from a template. To clarify which class that is, let’s call it ClassUnderTheTest.

After this, the structure of the project looks like this:

.template.config

     template.json

BenchmarkDotNetTemplate.csproj
 ClassUnderTheTest.cs
 Program.cs

The class under the test:

namespace BenchmarkDotNetTemplate
{
   public class ClassUnderTheTest
   {
       public ClassUnderTheTest()
       {
       }
   }
}

Program.cs:

using System;
using BenchmarkDotNet.Running;

namespace BenchmarkDotNetTemplate
{
   class Program
   {
       static void Main(string[] args)
       {
          var summary = BenchmarkRunner.Run<ClassUnderTheTest>();
          Console.ReadLine();
       }
   }
}

At this point, we can create a one-line project which is an insignificant, yet helpful way to deal with this. In the current implementation, we have a problem of projects always having the same name of BenchmarkDotNetTemplate and the test class with the ClassUnderTheTest name. So sooner or later you might lose the track of things eventually.

This is where we can get some assistance from the ability to add custom parameters and use their values during the project creation. The parameters are declared in the symbols section of the template.json file.

To rename a project, we can use the framework’s built-in functionality. It’s enough to specify the sourceName and primaryOutputs keys to open the opportunity to use the standard –n(–name) parameter to specify the name of the project in creation.

However, to rename a file located inside the project, we are adding a parameter to use its value to specify the new name.

"symbols":{
        "className": {
             "type": "parameter",
             "dataType":"string",
             "defaultValue": "ClassUnderTheTest",
             "replaces": "ClassUnderTheTest",
             "fileRename": "ClassUnderTheTest"
        }
  }

I’d like to pay special attention to the fact that a certain symbol contains the replaces and fileName keys. Replaces contains a string literal that will be swapped for the value from the command line in all the template files. The fileName key will change the file names with no extension to the same value.

The keys we added to the template are automatically parsed during the template installation which enables you to always get a message about the available switches by using the -h key together with the name of the template in dotnet new.

For example, after the following command execution,

dotnet new benchmark -h

we’ll see this message:

Options:

  -c|--className

            string - Optional

            Default: ClassUnderTheTest

Since we are down for automating everything we stumble upon, why not do that for the nuget packages restore that have links to them after the template creation. There is a special actions mechanism in templates for this and it is performed upon the project creation.

These actions have to be in a special section of postActions in the template.json configurations file.

 "postActions": [{
         "description": "Restore NuGet packages required by this project.",
         "manualInstructions": [
         { "text": "Run 'dotnet restore'" }
         ],
         "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025",
         "continueOnError": true
    }]

The complete list of available actions can be found in the documentation on GitHub. It’s worth saying that for all the actions performed after the project creation, we use the predefined GUIDs and skip the step when using a different value.

Sharing With Team

Another important part that can benefit from templating is the distribution of the similar-version libraries in a test project. We can simply upload it into some repository and after the update, install it on workstations.

But we are not looking for the road less traveled, right? More so, templates can be wrapped in the nuget packages. To pack a template into a nuget package, move the entire project including the template.json file into the content folder and the .nuspec file to the same level as the folder. As a value for the packageTypes/packageType section, specify template. After that, you can create nuget by using the following command:

nuget pack   

Now you can share the template with your team on your nuget server. Why nuget pack? Because dotnet pack is not yet supported.

Future Evolution

The Template Engine project is in its early stages and yet has a lot of prominent functional features shining through. The main things that have already been announced in the roadmap are the yeoman integration and interactive mode.

As for the further potential implementations unfolding – let’s wait to see and get our hands dirty 😈