Adding Ingress to Your Multi-Node Kind Cluster

In my last post about Kubernetes I went through how to set up a Kind (Kubernetes in Docker) cluster on your Windows desktop. In this post I’ll show you how to add an nginx ingress controller to your cluster and do a quick demo of it working,

To get started there are a couple of pre-requisites you’ll need to have.

  1. kubectl – Kubenetes command line interface
  2. Docker Desktop – We’ll be running our cluster as a group of containers in Docker.
  3. Kind – Kind makes it easy to manage Kubernetes clusters on you desktop.

I went through the steps to get the pre-requisites set up in my previous post, so I’ll just put the chocolatey commands here in case you need them, but if you need more guidance on how to set up the pre-requisites please see the previous post: Running a Multi-Node Kubernetes Cluster on Windows with Kind.

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

In this post I’ll be working with the latest version of Kind (0.11.1). If you installed Kind previously using Chocolatey you can quickly upgrade with the following command.

choco upgrade kind --version=0.11.1

The first thing we’ll need to do is add a bit more to our configuration file (lines 6-18) for our Kind cluster so we can map the ports from our cluster to our local machine. (Note you can get all of the files in this post from GitHub. The yaml files are in the Kind.0.11.1 directory.)

# Four node (three workers) cluster config
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
  kubeadmConfigPatches:
  - |
    kind: InitConfiguration
    nodeRegistration:
      kubeletExtraArgs:
        node-labels: "ingress-ready=true"
  extraPortMappings:
  - containerPort: 80
    hostPort: 8080
    protocol: TCP
  - containerPort: 443
    hostPort: 4443
    protocol: TCP
- role: worker
- role: worker
- role: worker

To get our cluster up and running we simply run the following kind command pointing to our new configuration file.

kind create cluster --config=cluster-config.yaml

Now to deploy the ingress controller. There is a configuration file in the repository for kubernetes/ingress-nginx. You can apply this using the following command:

kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/static/provider/kind/deploy.yaml  

Caution: These files have a habit of changing often as Kubernetes evolves, so what works today with a particular version of Kubernetes may not work tomorrow with the same version of Kubernetes because the file at that URL may have changed and no longer be compatible (or it may just be broken). I have copied the version of the file I am using with my cluster into my repository as ingress-nginx.yaml so that if (when) this happens there will be a working copy available for these instructions.

It will likely only take a few seconds for the command to complete, but it may take a minute or two for everything to actually complete inside the cluster and be ready to use. You can verify that everything is ready by running the following:

kubectl wait --for=condition=Ready pod -l app.kubernetes.io/component=controller -n ingress-nginx --timeout=300s
kubectl wait --for=condition=Complete job -l app.kubernetes.io/component=admission-webhook -n ingress-nginx --timeout=300s

This will wait until everything is in the proper state before returning.

Now that everything is set up let’s test it out by deploying an nginx pod, a service, and an ingress and verifying that we can indeed reach our running pod via the ingress. The following will create a pod from the nginx docker image, create a service to expose it and an ingress to allow us to connect to it from our local machine. Here’s the yaml for that:

apiVersion: v1
kind: Pod
metadata:
  name: ninja-web-pod
  labels:
    role: webserver
spec:
  containers:
    - name: web
      image: nginx
      ports:
        - name: web
          containerPort: 80
          protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
  name: ninja-svc
spec:
  selector:
    role: webserver
  ports:
    - protocol: TCP
      port: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ninja-ingress
  namespace: default
  annotations:
    kubernetes.io/ingress.class: nginx
spec:
  rules:
    - host: ninja.k8s.local
      http:
        paths:
          - backend:
              service:
                name: ninja-svc
                port:
                  number: 80
            path: /
            pathType: Prefix
            

Deploy the application using kubectl:

kubectl apply -f test-deploy.yaml

Note that I am specifying the host name ninja.k8s.local for my ingress. In order to use that host name we’ll also want to add a host file entry mapping it to your local machine.

127.0.0.1  ninja.k8s.local

And with that we should be able to access our pod @ http://ninja.k8s.local:8080/ and see the default nginx page served up.

Creating Project Templates for dotnet – Part 4 – Visual Studio Support

This is part 4 of a 4 part series of posts

In the previous posts in this series we have explored how to set up a project as a template and the basics of the new templating system, how to optionally include/exclude files, and finally how to handle optional content within various files in the project. In this final post we’ll take a look at how to add support for your template in Visual Studio so that users of your template can also use the template from within the IDE itself.

The first thing we need to do is enable support within Visual Studio’s options for third party templates. To do that we need to open up the options using the menus – Tools | Options and then expand Environment to find Preview Features and enable Show all .Net Core templates in the New project dialog (requires restart).

Next we’ll add a couple of items to the template.json file. Immediately after shortname we’ll add defaultName (This drives the default name Visual Studio will generate for a new project) and description (Which will show up under your project type name in the dialog) and the Framework section under symbols (Which will drive the frameworks selection drop down in the additional properties dialog).

{    
...    
    "shortName": "ninjamvc",     
    "defaultName": "DotNetNinjaMVC",   
    "description": ".Net 5.0 MVC Web Application - Batteries Included",         
    ...
    "symbols": {
        "Framework":{
            "type": "parameter",
            "description": "The target framework for the project.",
            "datatype": "choice",
            "choices": [
                {
                    "choice": "net5.0",
                    "description": ".Net 5.0"
                }
            ],
            "replaces": "net5.0",
            "defaultValue": "net5.0"
        },
        "ReadMe": {
            "type": "parameter",
            "datatype":"bool",
            "defaultValue": "true",
            "description": "Include a Read Me file (ReadMe.md) in the solution."
        },
       ...
}

With that done we need to add a new file named ide.host.json to our .template.config folder which should be located @ ~\src\Content\.template.config. This file will allow us to set up all of our command line options to show up as check-boxes in the new project dialogs within Visual Studio.

{
    "$schema":"http://json.schemastore.org/vs-2017.3.host",
    "symbolInfo": [
        {
            "id": "ReadMe",
            "name": {
                "text": "Include a Read Me file (ReadMe.md) in the solution."
            },
            "isVisible": "true"
        },        
        {
            "id": "License",
            "name": {
                "text": "Include an MIT License file (License.txt) in the solution."
            },
            "isVisible": "true"
        },
        {
            "id": "GitIgnore",
            "name": {
                "text": "Include a Git Ignore file (.gitignore) in the solution."
            },
            "isVisible": "true"
        },
        {
            "id": "EditorConfig",
            "name": {
                "text": "Include an Editor Config file (.editorconfig) in the solution."
            },
            "isVisible": "true"
        },
        {
            "id": "Authentication",
            "name": {
                "text": "Include code integrating Auth0 authentication in the solution."
            },
            "isVisible": "true"
        }
    ]
}

The first item in the file sets the schema of the json and enables intellisense in Visual Studio & VS Code when editing the file which is very handy. The rest of the file is an array of elements that map to the elements for our parameters in the template.json file and provide information for Visual Studio to be able to display these options in the dialogs.

  • id: maps to the name of the argument in the template.json file.
  • name: maps to the text that will be displayed withing Visual Studio along side the check box for the option.
  • isVisible: makes the option visible in the IDE.

I’ve also updated my Test-Template.ps1 file to add in the following snippet which will clear the template cache used by Visual Studio as well so that changes to the template appear in Visual Studio when the template is reinstalled during testings.

Remove-Item ~/.templateengine -Recurse -Force

Also note that I have updated the names of the options from my initial posts (changed the casing).

With all those changes we should now be able to test our template. Run the test-template.ps1 file to clear the cache, build and reinstall the template and then launch Visual Studio. You should now see that the template is available and when you use it you should be presented with dialogs that allow you to enable/disable all of the feature.

That ends this series of posts on creating templates, but we have really only scratched the surface of what can be done with the template engine. Check out the resources below for more information. You can also checkout the completed source code for my template on GitHub.

Resources

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