Creating Project Templates for dotnet – Part 3 – Optional Code/Content

This is part 3 of a 4 part series of posts

In my last two posts I explored how to set up and build a template and then how to add support for optional files. Now, building on what we have already done we can add another parameter to our template to add in authentication conditionally (opt-in). First, let’s start be adding in the authentication code. Once that is in place we can circle back and make it optional in our template by adding and configuring a new parameter.

First we’ll need to add the Microsoft.AspNetCore.Authentication.Cookies & Microsoft.AspNetCore.Authentication.OpenIdConnect NuGet packages into our project file. Normally I would do this using the package manager window or console, but in this case I will just add the relevant lines to the project file directly so we can see the content we will be making optional later on.

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <Folder Include="Models\" />
    <Folder Include="wwwroot\lib\" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="ChaosMonkey.Guards.NetStandard" Version="1.0.23" />
    <PackageReference Include="DotNetNinja.AutoBoundConfiguration" Version="1.1.0" />
    <PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="2.2.0" />
    <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="5.0.2" />
  </ItemGroup>

</Project>

Then we are going to need a class (~/Configuration/AuthenticationSettings.cs) to hold all of the configuration information for our identity provider (Auth0). We’ll use my DotNetNinja.AutoBoundConfigurations library to bind this class to our configuration at start up.

using System.Collections.Generic;
using DotNetNinja.AutoBoundConfiguration;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;

namespace SampleWeb.Configuration
{
    [AutoBind("Authentication")]
    public class AuthenticationSettings
    {
        public string Authority { get; set; }
        public string ClientId { get; set; }
        public string ClientSecret { get; set; }
        public string NameClaimType { get; set; }
        public string RoleClaimType { get; set; }
        public List<string> Scopes { get; set; } = new List<string> {"openid"};

        public string ClaimsIssuer { get; set; }
        public string CallbackPath { get; set; } = "/signin-oidc";
        public string ResponseType { get; set; } = OpenIdConnectResponseType.Code;

        public string DefaultAuthenticationScheme { get; set; } = CookieAuthenticationDefaults.AuthenticationScheme;
        public string DefaultSignInScheme { get; set; } = CookieAuthenticationDefaults.AuthenticationScheme;
        public string DefaultChallengeScheme { get; set; } = CookieAuthenticationDefaults.AuthenticationScheme;

        public bool SaveTokens { get; set; } = true;
        
        public string ScopesValue => string.Join(' ', Scopes ?? new List<string>()).Trim();
    }
}

Next we’ll add a class (~/Configuration/CustomAuthenticationExtensions.cs) for an extension method to configure authentication during application start up.

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;

namespace SampleWeb.Configuration
{
    public static class CustomAuthenticationExtensions
    {
        public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, AuthenticationSettings settings)
        {
            return services
                .AddAuthentication(options => {
                    options.DefaultAuthenticateScheme = settings.DefaultAuthenticationScheme;
                    options.DefaultSignInScheme = settings.DefaultSignInScheme;
                    options.DefaultChallengeScheme = settings.DefaultChallengeScheme;
                })
                .AddCookie()
                .AddOpenIdConnect("Auth0", options => {
                    options.Authority = $"https://{settings.Authority}";
                    options.ClientId = settings.ClientId;
                    options.ClientSecret = settings.ClientSecret;
                    options.ResponseType = settings.ResponseType;
                    options.Scope.Clear();
                    options.Scope.Add(settings.ScopesValue);
                    options.CallbackPath = new PathString(settings.CallbackPath);
                    options.ClaimsIssuer = settings.ClaimsIssuer;
                    options.MapInboundClaims = true;
                    options.SaveTokens = settings.SaveTokens;
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        NameClaimType = settings.NameClaimType,
                        RoleClaimType = settings.RoleClaimType
                    };
                    options.Events = new OpenIdConnectEvents
                    {
                        OnRedirectToIdentityProviderForSignOut = (context) =>
                        {
                            var logoutUri = $"https://{settings.Authority}/v2/logout?client_id={settings.ClientId}";
                            var postLogoutUri = context.Properties.RedirectUri;
                            if (!string.IsNullOrEmpty(postLogoutUri))
                            {
                                if (postLogoutUri.StartsWith("/"))
                                {
                                    var request = context.Request;
                                    postLogoutUri = $"{request.Scheme}://{request.Host}{request.PathBase}{postLogoutUri}";
                                }
                                logoutUri += $"&returnTo={ Uri.EscapeDataString(postLogoutUri)}";
                            }
                            context.Response.Redirect(logoutUri);
                            context.HandleResponse();
                            return Task.CompletedTask;
                        }
                    };
                }).Services;
        }
    }
}

Finally, we’ll also need to add an AccountController class (~/Controllers/AccountController.cs) so we can handle sign in & sign out requests.

using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;

namespace SampleWeb.Controllers
{
    public class AccountController: Controller
    {
        [HttpGet]
        public virtual async Task SignIn(string returnUrl = "/")
        {
            await HttpContext.ChallengeAsync("Auth0", new AuthenticationProperties() { RedirectUri = returnUrl });
        }

        [HttpGet]
        [Authorize]
        public virtual async Task SignOut()
        {
            await HttpContext.SignOutAsync("Auth0", new AuthenticationProperties
            {
                RedirectUri = Url.Content("~/")
            });
            await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        }
    }
}

Now that those files are in place, let’s add our parameter to the template.json. We’ll need to define the parameter setting the default value to false so that a user of our template has to explicitly opt into adding in this feature:

...
    "symbols": {
        ...
        "authentication": {
            "type": "parameter",
            "datatype":"bool",
            "defaultValue": "false"            
        }
    }
...

Then we will want to add in a new condition to exclude the files above when authentication is not requested:

...
    "sources": [
        {
            "modifiers": [
                ...
                {
                    "condition": "(!authentication)",
                    "exclude": [
                        "src/DotNetNinja.Templates.Mvc/Configuration/AuthenticationSettings.cs",
                        "src/DotNetNinja.Templates.Mvc/Configuration/CustomAuthenticationExtensions.cs",
                        "src/DotNetNinja.Templates.Mvc/Controllers/AccountController.cs"
                    ]
                }
            ]
        }
...

When authentication is enabled we add in lines to get our configuration settings and invoke our extension method. If not, we add in the original line of code to initialize AutoBoundConfiguration without getting the settings

         public void ConfigureServices(IServiceCollection services)
        {
#if(authentication)
            var oidc = services.AddAutoBoundConfigurations(Configuration)
                .FromAssembly(typeof(Program).Assembly).Provider.Get<AuthenticationSettings>();
            services.AddCustomAuthentication(oidc);
#endif
#if(!authentication)
            services.AddAutoBoundConfigurations(Configuration).FromAssembly(typeof(Program).Assembly);
#endif
            services.AddControllersWithViews();
        }

We’ll also need to add a using statement to our class when authentication is enabled and we’ll need to invoke the UseAuthentication method in the Configure method.

using System.Diagnostics.CodeAnalysis;
using DotNetNinja.AutoBoundConfiguration;
#if(authentication)
using DotNetNinja.Templates.Mvc.Configuration;
#endif
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }
            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

#if(authentication)
            app.UseAuthentication();
#endif            
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");
            });
        }

At this point we have all the code in place, but there is no way for a user to sign in. To enable that we’ll conditionally add some new menu options to our _Layout.cshtml template page.

@*#if(authentication)
@{
    var isAuthenticated = User.Identity.IsAuthenticated;
    var userName = (isAuthenticated) ? User.Identity.Name : "Anonymous";
}
#endif*@
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData[ViewKey.Title] | DotNetNinja.Templates.Mvc</title>
    <link rel="stylesheet" href="~/lib/bootstrap/css/bootstrap.min.css" />
    <link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-dark bg-dark fixed-top mb-3">
            <div class="container">
                <a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">DotNetNinja.Templates.Mvc</a>
                <button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
                        </li>
                    </ul>
@*#if(authentication)
                    <ul class="navbar-nav ml-auto">
                        @if (!isAuthenticated)
                        {
                            <li class="nav-item">
                                <a class="nav-link" asp-controller="Account" asp-action="SignIn">Sign In</a>
                            </li> 
                        }
                        else
                        {
                            <li class="nav-item">
                                <a class="nav-link" asp-controller="Account" asp-action="SignOut">Sign Out | @userName</a>
                            </li>
                        }
                    </ul>
#endif*@
                </div>
            </div>
        </nav>
    </header>
    <main role="main" class="pb-3">
        <div class="container">
            @RenderBody()
        </div>
    </main>
    <footer class="border-top footer p-2 text-muted">
        <div class="container">
            <div class="row">
                <div class="col-sm-9">
                    &copy; @DateTime.Now.Year DotNetNinja.Templates.Mvc
                </div>
            </div>
        </div>
    </footer>
    <script src="~/lib/jquery/jquery.min.js"></script>
    <script src="~/lib/bootstrap/js/bootstrap.bundle.min.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>
    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>

And lastly let’s remove the references to the NuGet packages we added for authentication when authentication is not enabled.

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
    <UserSecretsId>73fd3ede-b11d-40ed-b023-aaef44aad0b8</UserSecretsId>
  </PropertyGroup>

  <ItemGroup>
    <Folder Include="Models\" />
    <Folder Include="wwwroot\lib\" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="ChaosMonkey.Guards.NetStandard" Version="1.0.23" />
    <PackageReference Include="DotNetNinja.AutoBoundConfiguration" Version="1.1.0" />
<!--#if(authentication)-->
    <PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="2.2.0" />
    <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="5.0.2" />
<!--#endif-->
  </ItemGroup>

</Project>

Everything should be in place now. Let’s rebuild the template package and try it out.

.\Test-Template.ps1
# don't forget to do this n a different "test" folder!
dotnet new ninjamvc -n SampleWeb -au

This should generate a new application named SampleWeb and if you dig into the generated code you should see that all of our optional files and code are in place. If you run the command without the -au argument, you should get the same output as before our changes.

In the final article in this series we’ll take a look at how to enable support for your new template in Visual Studio.

Resources

Creating Project Templates for dotnet – Part 2 – Optional Files

This is part 2 of a 4 part series of posts

In my previous post “Creating Project Templates for dotnet – Part 1 – Getting Started” we created a simple project template that creates an ASP.NET MVC Web application and an XUnit test project. This project contains some of my favorite NuGet packages and has been updated to use libman to manage the static dependencies..

Next let’s take a look at how to support optional files in your project. This might be groups of class files to support a particular type of functionality you and your team use in most projects or something as simple as a read me or license file. For our example I’m going to add in a ReadMe.md, a License.txt, an .editorconfig, and a .gitignore file.

For the read me file I’ll just create a simple markdown file with a heading for my project.

# DotNetNinja.Templates.Mvc

The DotNetNinja.Templates.Mvc in the file will be replaced by the template engine when the file is created leaving me with a ReadMe.md file that has a heading of my solution name.

Next I’ll add an MIT License file as I am planning on using this template mostly for sample projects on GitHub. For that I’m just going to copy the license file from my repository into the content folder.

To get an appropriately configured .gitignore file for .Net projects I’ll use the gitignore template that ships with .Net 5.0.

dotnet new gitignore

And finally to get a basic .editorconfig file I’ll just export my settings from Visual Studio by going to Tools > Options > Text Editor > C# > Code Style > General and clicking the big “Generate .editorconfig file from settings” button.

Once the files are created we need to configure our template to allow a user to include/exclude them when running our template. To do that we need to open up our template.json file in the .template.config folder and add in some more settings. First we’ll add in a block of settings to specify the parameter names for the command line arguments for our template.

   {    
    "$schema": "http://json.schemastore.org/template",
...
    "symbols": {
        "readme": {
            "type": "parameter",
            "datatype":"bool",
            "defaultValue": "true"
        },
        "license": {
            "type": "parameter",
            "datatype":"bool",
            "defaultValue": "true"
        },
        "gitignore": {
            "type": "parameter",
            "datatype":"bool",
            "defaultValue": "true"
        },
        "editorconfig": {
            "type": "parameter",
            "datatype":"bool",
            "defaultValue": "true"
        }
    }
...
}

Note that each item has a data type of boolean and that I have chosen to default them to true so that users of our template do not have to specify these parameters and by default they will get all of the files. If they want to skip one pf them (for example the License.txt file) they can add on a parameter –license false.

Now that we have declare these parameters for our template we need to set up our configuration to exclude the files if the parameters are set to false.

{
    "$schema": "http://json.schemastore.org/template",
...
        "sources": [
        {
            "modifiers": [
                {
                    "condition": "(!readme)",
                    "exclude": [
                        "ReadMe.md"
                    ]
                },
                {
                    "condition": "(!license)",
                    "exclude": [
                        "License.txt"
                    ]
                },
                {
                    "condition": "(!gitignore)",
                    "exclude": [
                        ".GitIgnore"
                    ]
                },
                {
                    "condition": "(!editorconfig)",
                    "exclude": [
                        ".editorconfig"
                    ]
                }
            ]
        }
    ]
}

Notice that “exclude” is an array, so if you need to you can exclude multiple files with one condition. The entire completed file should now look something like this:

{    
    "$schema": "http://json.schemastore.org/template",
    "author": "Larry House",
    "classifications": [ "Web" ],
    "name": "DotNetNinja MVC Template",
    "identity": "DotNetNinja.Templates.Mvc",        
    "shortName": "ninjamvc",                 
    "tags": {
        "language": "C#",    
        "type": "project"                 
    },
    "sourceName": "DotNetNinja.Templates.Mvc",
    "preferNameDirectory" : true,
    "symbols": {
        "readme": {
            "type": "parameter",
            "datatype":"bool",
            "defaultValue": "true"
        },
        "license": {
            "type": "parameter",
            "datatype":"bool",
            "defaultValue": "true"
        },
        "gitignore": {
            "type": "parameter",
            "datatype":"bool",
            "defaultValue": "true"
        },
        "editorconfig": {
            "type": "parameter",
            "datatype":"bool",
            "defaultValue": "true"
        }
    },
    "sources": [
        {
            "modifiers": [
                {
                    "condition": "(!readme)",
                    "exclude": [
                        "ReadMe.md"
                    ]
                },
                {
                    "condition": "(!license)",
                    "exclude": [
                        "License.txt"
                    ]
                },
                {
                    "condition": "(!gitignore)",
                    "exclude": [
                        ".GitIgnore"
                    ]
                },
                {
                    "condition": "(!editorconfig)",
                    "exclude": [
                        ".editorconfig"
                    ]
                }
            ]
        }
    ]
}

Now if we reinstall our template and test it out we can see that our files are included and that if we set one of the parameters to false that the corresponding file is not output into our new solution. I’ve added a PowerShell script to the root of the repository to make this easier.

$shortName = "ninjamvc"
$id = "DotNetNinja.Templates.Mvc"

# Check if the template is already installed
$templates = dotnet new -l
$template = $templates | Select-String $shortName
# If installed then uninstall it
if($null -ne $template){
    Write-Host "Removing $id"
    dotnet new -u $id
}
# Clear Template Cache
Write-Host "Clearing Template Cache"
Remove-Item ~/.templateengine -Recurse -Force

#Build new Package
Write-Host "Building Template Package $id"
.\Build-Template.ps1

# Install Template
Write-Host "Installing new Template $id"
dotnet new -i $id

This script will uninstall the template if it is already installed, clear out the template cache (this will be especially important later when we start working with Visual Studio, build our template, and install it.) With that in place all we need to do is run the script.

.\Test-Template.ps1

Let’s start testing our updated template by generating solution with all files:

dotnet new ninjamvc -n SampleWeb

Output:

Now we can try generating a solution without read me file:

dotnet new ninjamvc --Readme false -n TestWeb

Output:

In my next post we will see how to add optional blocks of content in our files and tie that together with optional files to implement an optional feature in our template.

Resources

Creating Project Templates for dotnet – Part 1 – Getting Started

This is part 1 of a 4 part series of posts


Custom project templates can be a great time saver for you and your team and the can promote a level of consistency across all of your projects. In my experience within any given organization a great deal of effort is wasted every time a new project is started creating the new project, writing plumbing code to handle routine, cross cutting concerns like logging, health checks, authentication/authorization as well as just generally configuring the project layout to conform with your teams practices, importing common libraries & corporate style sheets, setting up continuous integration pipelines etc. By doing all this once up front and baking it into a project template you can save that time each time a new project is started and the team can get down to providing actual business value almost immediately.

For this post I’m going to walk through creating a new template for an ASP.NET 5.0 MVC Web Application. We’ll start off with the default template and modify from there over the next few posts.

Getting Started / Setting Up the Project Structure

To get started we’ll create a new repository in source control (GitHub in my case) and clone it.

Initial Repository Listing

Next we let’s add a src folder and in that folder we’ll add a content folder that will contain the contents of our template. Ultimately this content folder will become the content folder within a NuGet package for our template.

md src
cd src
md content
cd content

When my template is run, I want it to create a src folder with my web application in it and a tests folder with my unit tests. To accomplish that we’ll create a new src folder and then use the existing, out of the box MVC template and then create a tests folder and use the existing, out of the box Xunit template.

md src
cd src
dotnet new mvc -n DotNetNinja.Templates.Mvc
cd ..
md tests
cd tests
dotnet new xunit -n DotNetNinja.Templates.Mvc.Tests
cd ..

Now let’s create a solution file, add the projects to it and create the project reference from the unit tests to the web project.

dotnet new sln -n DotNetNinja.Templates.Mvc
dotnet sln add .\src\DotNetNinja.Templates.Mvc
dotnet sln add .\tests\DotNetNinja.Templates.Mvc.Tests
dotnet add .\tests\DotNetNinja.Templates.Mvc.Tests reference .\src\DotNetNinja.Templates.Mvc

At this point we should have a fully working solution created. One of the great things about the new template system is that your templates are actually functioning projects that can be built and run. That also means that all of the tools that you use for day to day development like Intellisense, syntax highlighting, and tools like Resharper can all be used during your template development process making it much easier and more intuitive to create quality templates.

Now that we have scaffolded out the entire solution we can go ahead and open it up in Visual Studio and see what we have to start with.

Making Our Solution a Template

Having laid out the basic structure we are looking for we have a good foundation. Let’s go ahead and convert it to a template. In our content folder we’ll need to add a .template.config folder and place a template.json file in that folder.

md .template.config
cd .template.config
New-Item template.json

Next let’s open the template.json file up, add in our basic template definition and take a look at what we have.

{    
    "$schema": "http://json.schemastore.org/template",
    "author": "Larry House",
    "classifications": [ "Web" ],
    "name": "DotNetNinja MVC Template",
    "identity": "DotNetNinja.Templates.Mvc",        
    "shortName": "ninjamvc",                 
    "tags": {
        "language": "C#",    
        "type": "project"                 
    },
    "sourceName": "DotNetNinja.Templates.Mvc",
    "preferNameDirectory" : true
}
  • $schema: Sets the json schema and gives us intellisense for our json file.
  • author: Sets the author of the template.
  • classifications: An array of project types. This can be used for filtering in the Visual Studio new project dialog.
  • name: Is the friendly/display name for the template.
  • identity: Is a unique identifier for the template, I typically use the NuGet package id here.
  • shortName: is the short name for the template that is used to invoke it with dotnet new (ex. dotnet new ninjamvc)
  • tags: Tags for the template. These show up in the dotnet new list and can be used to filter in Visual Studio as well.
  • sourceName: This one is important as it specifies the name/value in the source to be automatically replaced with the name of the generated project. Typically that includes the project name, project file name, default namespace etc.
  • preferNameDirectory: This should generally be set to true for most templates. It determines if the directory name is used as the project name when a name is not specified.

That covers what is required in the template.json file to make a template and it this point we technically have a working template. You can install a template from a directory just the way we have it laid out, however the normal means of delivering a template is to package it up as a NuGet package.

Packaging the Template

To package up our template as a NuGet package for consumption we’ll need to add a nuspec file to the solution and then use the nuget pack command to package it up. To perform these steps you’ll need to have the NuGet cli installed and available on your path. (downloads | documentation)

You can generate a nuspec file and then modify it as needed by using the nuget spec command. (Or you can just copy the nuspec below and modify to fit your needs)

nuget spec DotNetNinja.Templates.Mvc

Here is my modified nuspec ready to go. (Note the template version is in this file. You will want to update that version with each release of your template!)

<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd">
  <metadata>
    <id>DotNetNinja.Templates.Mvc</id>
    <version>1.0.0</version>
    <description>
      Template for creating an ASP.NET Core MVC Solution
    </description>
    <authors>Larry House</authors>
    <license type="expression">MIT</license>
    <packageTypes>
      <packageType name="Template" />
    </packageTypes>
  </metadata>
  <files>
    <file src="Content\**\*.*" exclude="Content\**\bin\**\*.*;Content\**\obj\**\*.*" target="Content" />
  </files>  
</package>

We will place this file in our outer src folder (at the same level as our content folder) so it is outside our template.

To package the template navigate to the root of your repository and run a nuget pack command pointed to your nuspec file.

nuget pack .\src\DotNetNinja.Templates.Mvc.nuspec -OutputDirectory C:\Packages\NuGet

Note: I have a local NuGet source set up on my development machine in C:\Packages\NuGet. While this is not strictly required (you can install the template by specifying that path to the package) it does make the workflow easier (and it is convenient for testing other nuget packages). To set this up create the folder you want to use (ex. C:\Packages\NuGet) and run the following command.

nuget source add -Name LocalPackages -Source C:\Packages\NuGet

Being the lazy type ( 🙂 ), I’ve added a PowerShell script named Build-Template.ps1 in the root of my solution and put the nuget pack command in there so I don’t have to type out the full command all the time.

Our package should look something like this:

At this point it is probably a good time to install and do a quick test run of our template and ensure that everything is in order.

Assuming you have published your package to a folder that is a nuget source (as mentioned above) simply run the the following to install the template.

dotnet new -i DotNetNinja.Templates.Mvc

Notice that the new template has the name, short name, language, and tags we specified in our config file earlier. Now let’s run the template and create a new project from it. (You’ll probably want to create a test directory somewhere, don’t run it inside your template. Doing that will make a mess. Trust me.)

dotnet new ninjamvc -n SampleWeb

Here is a look at the Solution Explorer of my generated solution in Visual Studio.

If you look through the project you’ll find that all instances of DotNetNinja.Template.Mvc have been replaced with the name we specified above (SampleWeb). That includes the project names, namespaces, folder names etc.

In the next post we’ll take a look extending the template with optional files.

Resources

Running a Multi-Node Kubernetes Cluster on Windows with Kind

There are lots of ways to run Kubernetes on your Windows development machine, for example minikube. Even Docker Desktop for Windows now ships with the ability to run a single node Kubernetes cluster, but the simplest and most flexible way I’ve found to run a multi-node Kubernetes cluster locally on Windows is using Kind or Kubernetes in Docker.

Note

All commands should be run from an elevated (administrator) powershell or command prompt
Also, commands listed are for powershell. If using a command prompt you may need to make the appropriate modifications to the paths etc.

Prerequisites

In order to run Kind locally you’ll need to have or install Docker Desktop for Windows (you’re going to be running the nodes of your cluster in docker containers), kubectl (the Kubernetes command line utility), and of course kind. The easiest way to install them is using the Chocolatey package manager. (Strictly speaking you don’t have to have kubectl to set up a kind cluster, but it will make the cluster far more useful once you have it up and running! 🙂 )

choco install docker-desktop
choco install kubernetes-cli
choco install kind

If you would prefer not to use chocolatey, please see the installation documentation for each component

Creating Your First Cluster

Once everything is installed you’re ready to create your first cluster.

kind create cluster 

Note this will download a decent sized docker image to run your node(s) and may take a couple of minutes if you have a slower connection, but you should only have to do it once for each version of Kubernetes that you run as the image will be cached by Docker.

This gives us a single node cluster running the latest version of Kubernetes (at the time of this writing that’s 1.20). It also gives our cluster the default name of kind. You can control the version of Kubernetes by specifying the image to use for your nodes using the –image switch on your create cluster command.

kind create cluster --image=kindest/node:v1.19.7@sha256:a70639454e97a4b733f9d9b67e12c01f6b0297449d5b9cbbef87473458e26dca

You can get a list of the images available for a particular kind release on the Kind GitHub Releases page. For example the current release of Kind (0.10.0) supports the following images:

1.20: 
kindest/node:v1.20.2@sha256:8f7ea6e7642c0da54f04a7ee10431549c0257315b3a634f6ef2fecaaedb19bab
1.19: 
kindest/node:v1.19.7@sha256:a70639454e97a4b733f9d9b67e12c01f6b0297449d5b9cbbef87473458e26dca
1.18: 
kindest/node:v1.18.15@sha256:5c1b980c4d0e0e8e7eb9f36f7df525d079a96169c8a8f20d8bd108c0d0889cc4
1.17: 
kindest/node:v1.17.17@sha256:7b6369d27eee99c7a85c48ffd60e11412dc3f373658bc59b7f4d530b7056823e
1.16: 
kindest/node:v1.16.15@sha256:c10a63a5bda231c0a379bf91aebf8ad3c79146daca59db816fb963f731852a99
1.15: 
kindest/node:v1.15.12@sha256:67181f94f0b3072fb56509107b380e38c55e23bf60e6f052fbd8052d26052fb5
1.14: 
kindest/node:v1.14.10@sha256:3fbed72bcac108055e46e7b4091eb6858ad628ec51bf693c21f5ec34578f6180

You can also set the name of your cluster using the –name switch

kind create cluster --name my-cluster

Removing your Cluster

Eventually you will want to delete/remove/destroy your cluster and reclaim the resources it is using. You can easily accomplish this using the kind delete command.

kind delete --name my-cluster

Going to Multiple-Nodes

So initially I said that we could set up a multi-node cluster using kind, but so far all of our clusters have been a single node. In order to get a multi-node cluster we’ll need to create a configuration file. Here’s a simple example that creates a 3 node cluster (1 control plane node and 2 worker nodes)

# three node (two workers) cluster config
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
- role: worker

We can apply our config file by using the –config switch with our create cluster command as follows

kind create cluster --name my-cluster --config .\my-cluster-config.yaml

And there we have it, a multi-node Kubernetes cluster running on your Windows desktop. Kind makes it pretty quick and simple so you can spin one up, play with it while doing some training or deploy test workloads locally to test your entire system locally. Once your done, or you mess things up, delete the cluster and spin up a new one when you need it.

Resources