User Secrets – What Are They And Why Do I Need Them?

Jamie Taylor5 comments
User Secrets Header ImageSource: https://unsplash.com/photos/BcjdbyKWquw Copyright 2017: Kristina Flour

Today’s header image was created by Kristina Flower at Unsplash

Just a quick caveat before we begin.

This article was written before .NET Core 2.0 was RTM’d (it was still in Preview 2 when I wrote this article). Almost everything is the same, but some of the screenshots and code snippets make it obvious that I’d written the article with a 1.x code base.

Again, the process is the same. Just remember to include version 2.0 of theMicrosoft.Extensions.Configuration.UserSecrets package.


We’ve all been there: added some functionality which talks to an external API which requires a secret key, hit commit, then days later wondered why out secret key is being used to authorise applications we didn’t write.

In fact, more developers than we care to think about have done this. How do I know? Take a look at the following searches on GitHub:

in fact, Github have a page devoted to showing developers how to remove sensitive (passwords, api keys, etc.) from their commit history.

which you can read here

It’s a big issue, as you can see. It’s all too easy to accidentally commit sensitive information to a repository. Which is where User Secrets comes in.

If you work on open source stuff (even if you work on closed source stuff), you don’t want to be storing sensitive information in configuration files. Especially if they are stored on disk in plain text.

I’m thinking ini, xml and json config files here. But anything plain text is not safe

If some nefarious person gains access to the server that the source code is stored on, then they have access to all of your sensitive information. Granted, if someone gets access to the server then you have slightly bigger issues than just your application’s sensitive information.

Unless it’s open source, in which case anyone can search for said sensitive information right there in your code base.

Just like the searches I listed above

Hopefully the folks who have had to issue those commits have changed the passwords and api keys that they were using, but that doesn’t always happen.

User Secrets

Anyway, what does user secrets have to do with source control?

User Secrets are specific pieces of sensitive information which need to be kept out of source control: api keys, connection strings, administrator passwords, etc.

although what you’re doing storing passwords in source code, is beyond me

User secrets aren’t new to .NET Core, it was first committed to the ASPNET repository in March 25th 2015 and moved to the Configuration repository in September of 2016.

the glory of open source, eh?

The whole point of User Secrets is that developers can store their sensitive data for the application they’re working on (passwords, api keys, connection strings, etc.) in a file separate from the code tree. This means that they cannot be accidentally committed into source control.

How Does It Work?

Developers use a command line tool (or a tool within Visual Studio) called the Secret Manager to add secrets for a specific application to a directory which is semantically different (and far away) from where the source code is being written.

Secrets are then added to a sub-directory within the developer’s home directory.

I just read that back, and I’m sorry. It’s a difficult sentence. Let me try again

A hidden directory is added to the developer’s home directory called .microsoft and a sub-directory is added to that called usersecrets. Within this directory, sub-directories for all application projects with user secrets are created.

this is done by the secret manager tool; we’ll see how in a moment.

user secrets location
Here is my user secrets directory on my Ubuntu machine

Within each application secrets directory, a file called secrets.json is stored. Any secrets added to a project via the secret manager tool will be added to this file.

Secrets Directory Contents

The contents of the secrets.json file is plain text, in fact Microsoft has this to say about the contents of the file and their security:

The Secret Manager tool does not encrypt the stored secrets and should not be treated as a trusted store. It is for development purposes only. The keys and values are stored in a JSON configuration file in the user profile directory.

Source: https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets#secret-manager

Where You Would Use Secrets

Local development only.

Seriously, local development only. Never in staging or production. Just local development. Again, here are Microsoft’s words on User Secrets in production:

The Secret Manager tool is used only in development. You can safeguard Azure test and production secrets with the Microsoft Azure Key Vault configuration provider.

Source: https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets

For staging and production, you can use the Microsoft Azure Key Vault or appsettings.Staging.json and appsettings.Production.json files. These can be generated or supplied to your build system, and pushed to the server.

securing the server is out of the scope of this post, and entirely dependant on the Operating System on the server

Creating and Using User Secrets

As with almost everything else I’ve written about, we’re going to build an example application to show off the usage of User Secrets.

everything’s easier to learn with an example, right?

We’ll leverage the built in MVC template, add a user secret to it and consume it while running in development. We’ll then run the application in staging and production to see how the same code can be used to load our sensitive configuration data while in those environments.

a lot of the hard work has been abstracted away for us by the .NET Core team, as we’ll see

I do everything with VSCode and the terminal, but I’ll include a section for adding user secrets from within Visual Studio 2017, too.

if you want to skip all of this and jump straight to the code, you can clone it from this GitHub repo

The application project

Head over to the terminal and issue the following command:

dotnet new mvc --name userSecrets --framework netcoreapp1.1

This will create an MVC project in a directory called userSecrets. Open that directory with VSCode and you’ll be presented with something like the following:

userSecrets Project Loaded with VSCode

The first thing we need to do is add a user secrets Id to the csproj file. So we need to edit the csproj to include the following in the PropertyGroup section:

<PropertyGroup>
<Description>A .NET Core MVC application used to show the process of adding User Secrets</Description>
<VersionPrefix>0.0.0.1</VersionPrefix>
<Authors>Jamie Taylor</Authors>
<TargetFramework>netcoreapp1.1</TargetFramework>
<UserSecretsId>userSecrets-c23d27a4-eb88</UserSecretsId>
</PropertyGroup>

I’ve given a pseudo-random Id and appended it to the name of the application, but you can use whatever convention you’d like

The next thing we need to do is include the UserSecrets package, this is found at Microsoft.Extensions.Configuration.UserSecrets:

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore" Version="1.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="1.1.2" />
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="1.1.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="1.1.1" />
<PackageReference Include="Microsoft.VisualStudio.Web.BrowserLink" Version="1.1.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" version="1.1.*" />
</ItemGroup>

psst, for .NET Core 2.0 users: use version 2.0.0

And finally, to access the command line tooling, we need to add a reference to that too. The command line tools are available in the Microsoft.Extensions.SecretManager.Tools package:

<ItemGroup>
<DotNetCliToolReference Include="Microsoft.Extensions.SecretManager.Tools" Version="1.0.0" />
</ItemGroup>

psst, for .NET Core 2.0 users: use version 2.0.0

As a side note: I tend to add the command line tools to a separate section in my csproj, but feel free to add them where ever you feel works best for you.

Adding Secrets

Now that we’ve added the correct tooling, we need to restore the packages that we want to consume:

dotnet restore

In order to check that the user secrets tooling has been installed correctly, we can issue the help command and see how to use it:

dotnet user-secrets --help

If we get the following (or similar), then we haven’t installed restored the packages correctly:

No executable found matching command "dotnet-user-secrets"

However, output along the same lines as this shows that the tooling has been installed correctly (and shows how we can use it)

User Secrets Manager 1.0.0-rtm-10308
Usage: dotnet user-secrets [options] [command]
Options:
-?|-h|--help Show help information
--version Show version information
-v|--verbose Show verbose output
-p|--project <PROJECT> Path to project, default is current directory
-c|--configuration <CONFIGURATION> The project configuration to use. Defaults to 'Debug'
--id The user secret id to use.
Commands:
clear Deletes all the application secrets
list Lists all the application secrets
remove Removes the specified user secret
set Sets the user secret to the specified value
Use "dotnet user-secrets [command] --help" for more information about a command.

We’ll add a user secret

in the documentation, this is referred to as setting a value

called SuperStrongPassword by issuing the following command:

dotnet user-secrets set SuperStrongPassword 43WxmKY%HjMV!5OH

Except that wont work (at least on Unix-like machines), and will give the following error

bash: !5: event not found

This is because we have to escape the bang character in order to save it.

exclamation marks in the Unix world are referred to as bangs

This is purely a Unix thing, so you shouldn’t be affected if you’re running Windows. Either way, here is the fixed command for Unix-like operating systems:

dotnet user-secrets set SuperStrongPassword 43WxmKY%HjMV\!5OH

If we need to check the keys and values of all of our User Secrets, we simply issue the following command:

dotnet user-secrets list

Which in this instance should come back with:

SuperStrongPassword = 43WxmKY%HjMV!5OH

or whatever you set the value of SuperStrongPassword to

What About Visual Studio?

In Visual Studio, there is a User Secrets Manager which is accessed by right clicking on the project (which represents the csproj) that you want to add secrets for, and selecting Manage User Secrets.

This will create the secrets.json file and open it in Visual Studio:

user secrets in Visual studio
I’ve loaded the secrets.json file by right clicking on the project and choosing Manage User Secrets

Then it’s a case of typing your secrets manually and saving the file.

Location of User Secrets

I touched on this earlier, but I feel like covering it again.

The User Secrets file (secrets.json) for our project will be found at one of the following paths (dependant on the operating system that you are running):

  • Windows: %APPDATA%\microsoft\UserSecrets\<userSecretsId>\secrets.json
  • Unix: ~/.microsoft/usersecrets/<userSecretsId>/secrets.json

So in our example, the user secrets will be found at one of these two paths:

  • Windows: %APPDATA%\microsoft\UserSecrets\userSecrets-c23d27a4-eb88\secrets.json
  • Unix: ~/.microsoft/usersecrets/userSecrets-c23d27a4-eb88/secrets.json

Opening our secrets.json file, we’ll find a very simple structure:

{
"SuperStrongPassword": "43WxmKY%HjMV!5OH"
}
Consuming the User Secrets

Just to re-iterate before we continue:

you should only use User Secrets in your development environment. Never in Staging or Production

In order to load our User Secrets, we need to make a few changes to the Startup.cs file. We need to:

  • Add the UserSecrets namespace
  • Add User Secrets to the application builder
  • Read the value from our User Secrets file

Let’s take the startup.cs file and replace it with the following:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.UserSecrets;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using userSecrets.Models;
namespace userSecrets
{
public class Startup
{
string userSecret = string.Empty;
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
// Add support for user secrets in development mode
if (env.IsDevelopment())
{
builder.AddUserSecrets<Startup>();
}
Configuration = builder.Build();
}
public IConfigurationRoot Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
userSecret = Configuration["SuperStrongPassword"];
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
var result = string.IsNullOrEmpty(userSecret) ? "Not found" : userSecret;
app.Run(async (context) =>
{
await context.Response.WriteAsync($"Secret is {result}");
});
}
}
}

Taking a look at the relevant lines of code:

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.UserSecrets;

Here we’ve added the relevant namespaces.

string userSecret = string.Empty;
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
// Add support for user secrets in development mode
if (env.IsDevelopment())
{
builder.AddUserSecrets<Startup>();
}
Configuration = builder.Build();
}

Here we’ve added a string to hold our user secret and we’ve instructed the application builder to add the user secrets configuration to our application (we’ll consume it in a moment).

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
userSecret = Configuration["SuperStrongPassword"];
}

Here we’re telling our application to go get the value of SuperSecretPassword from the configuration dictionary (our user secrets have already been loaded into memory by this point) and store it in our string from earlier.

I feel like part of that paragraph is a little teaching gandma to suck eggs

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
var result = string.IsNullOrEmpty(userSecret) ? "Not found" : userSecret;
app.Run(async (context) =>
{
await context.Response.WriteAsync($"Secret is {result}");
});
}

Here is where we’re consuming the value of our UserSecrets. We’re creating a response which contains that value of our loaded UserSecret.

Fire the application up with the terminal

it’s important that you do it with the terminal first, as there’s a caveat here that we need to cover

dotnet run

Then head over to whatever port on localhost

you’ll see, in a moment, that I was given port 5000

and you should get a response from the server:

UserSecrets not found
But… I… What?

Looking back at our Configure method, we were using the value from the User Secrets or the string “Secret is Not Found” if it couldn’t be found:

var result = string.IsNullOrEmpty(userSecret) ? "Not found" : userSecret;

Which proves that the user secret couldn’t be found. But why?

Caveat

Remember how I (and Microsoft) said that we should only use User Secrets in development? I wasn’t kidding, and we actually set our application up specifically to use them only in development:

string userSecret = string.Empty;
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
// Add support for user secrets in development mode
if (env.IsDevelopment())
{
builder.AddUserSecrets<Startup>();
}
Configuration = builder.Build();
}

And if you look at the output in the terminal from when we started the application:

Hosting environment: Production
Content root path: /home/jay/Code/userSecrets
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.

wait, what?

Ah. Since we didn’t explicitly state which environment we’re in, .NET Core started in Production.

and this is precisely why I made you start the application from the command line

And this make sense, if you think about it. We shouldn’t have to supply a separate command line argument to start the application in production, otherwise starting the application on the server would be more complex.

To temporarily start our application in development, we need to run the following command:

ASPNETCORE_ENVIRONMENT=Development dotnet run

This should give us the following output in the terminal:

Hosting environment: Development
Content root path: /home/jay/Code/Blog-Tutorials/userSecrets
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.

Which means that if we refresh our browser, we should get the User Secret in our response:

UserSecret Found
Oh yeah!

As a side note, you can make the change more permanent if you want by using one of the two following commands (depending on the operating system that you’re using):

  • Windows: set ASPNETCORE_ENVIRONMENT=Development
  • Unix: export ASPNETCORE_ENVIRONMENT=Development

This is a permanent, system-wide, change, though. So don’t be surprised if your other applications start behaving differently.

Consuming User Secrets in a Controller

All of that is all well and good, but how do we consume the contents of our User Secrets in a Controller (or some other) method?

It turns out that it’s quite easy to do.

thank you .NET Core team for making this super easy, by the way

What we need to do is:

  • Create a POCO to store the User Secrets in
  • Add the UserSecrets namespace in the Startup.cs
  • Add User Secrets to the application builder
  • Read the value from our User Secrets file to an instance of the POCO in Startup.cs
The POCO

Since our example User Secrets are pretty simple we only need a simple model.

Let’s take another look at the secrets.json file:

{
"SuperStrongPassword": "43WxmKY%HjMV!5OH"
}

So our POCO just needs to match that.

Create a Models directory in the root of the project, add a file called Secrets.cs, and add the following contents to it:

using System;
namespace userSecrets.Models
{
public class Secrets
{
public string SuperStrongPassword { get; set; }
}
}

If we add more things to the User Secrets, we’ll need to add them to this POCO.

which we’ll need to remember

Adding Stuff to the Startup Class

Replace the contents of your startup.cs with the following:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.UserSecrets;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using userSecrets.Models;
namespace userSecrets
{
public class Startup
{
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
// Add support for user secrets in development mode
if (env.IsDevelopment())
{
builder.AddUserSecrets<Startup>();
}
Configuration = builder.Build();
}
public IConfigurationRoot Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<Secrets>(Configuration);
// Add framework services.
services.AddMvc();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}

We’ve covered most of this already, but I’ve highlighted the new stuff. The most important change is:

public void ConfigureServices(IServiceCollection services)
{
services.Configure<Secrets>(Configuration);
// Add framework services.
services.AddMvc();
}

Here we’re telling the IServiceCollection to take the contents of the UserSecrets configuration and deserialise it to an instance of the Secrets class we just reated.

There’s some proper ace magic here, in that .NET Core will only deserialise keys which are in both the User Secrets file and the POCO that we’re deserialising to. If, for instance we added a key to the User Secrets which wasn’t present in the POCO, then .NET Core wont raise an error, it will just ignore it.

and the same for the opposite direction

Consuming User Secrets in a Controller

Since we’ve added an instance of the Secrets class to the IServicesCollection in the startup class, we don’t need to do very much to consume it in a controller (or anywhere else within the application). We’ll leverage .NET Core’s built in dependency injection to ensure that it gets added to our controller, then consume it.

Let’s use the HomeController’s About method as our example, since it already exists and it’ll be faster to wire it up.

not that it takes a long time to wire it up at all

Replace the contents of your HomeController with the following:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using userSecrets.Models;
namespace userSecrets.Controllers
{
public class HomeController : Controller
{
private Secrets _secrets { get; }
public HomeController(IOptions<Secrets> secrets)
{
this._secrets = secrets.Value;
}
public IActionResult Index()
{
return View();
}
public IActionResult About()
{
ViewData["Message"] = "Our super secret password is:";
ViewData["UserSecret"] = string.IsNullOrEmpty(this._secrets.SuperStrongPassword)
? "Are you in production?"
: this._secrets.SuperStrongPassword;
return View();
}
public IActionResult Contact()
{
ViewData["Message"] = "Your contact page.";
return View();
}
public IActionResult Error()
{
return View();
}
}
}

I’ve highlighted the important lines, so let’s take a look at them:

private Secrets _secrets { get; }
public HomeController(IOptions<Secrets> secrets)
{
this._secrets = secrets.Value;
}

Here we’re using .NET Core’s built in dependency injection to make sure that we get our Secrets class, which was populated in the startup class.

ViewData["Message"] = "Our super secret password is:";
ViewData["UserSecret"] = string.IsNullOrEmpty(this._secrets.SuperStrongPassword)
? "Are you in production?"
: this._secrets.SuperStrongPassword;
return View();

Here we’re consuming our Secrets class.

That’s all we need to do, so let’s spin up out application and give it a whirl:

we’ll explicitly test in development first

ASPNETCORE_ENVIRONMENT=Development dotnet run

Then head to Home/About and you should get something along the lines of the following:

Consuming UserSecrets in About
Oh yeah!

What about in Production? Let’s try that now:

ASPNETCORE_ENVIRONMENT=Production dotnet run

Refreshing the About page should give us something like:

Consuming UserSecrets in About - not found
Ah

We actually preempted this in the About method. Let’s take a look:

public IActionResult About()
{
ViewData["Message"] = "Our super secret password is:";
ViewData["UserSecret"] = string.IsNullOrEmpty(this._secrets.SuperStrongPassword)
? "Are you in production?"
: this._secrets.SuperStrongPassword;
return View();
}

it’s almost as if I knew

What About Production?

Cast your mind back to when I said:

For staging and production, you can use the Microsoft Azure Key Vault or appsettings.Staging.json and appsettings.Production.json files

quoting oneself is quite pretentious, you know

That’s what we’ll do, we’ll add an appsettings.Production.json file to the root of the project and use that. We’ve already got the code which will read it for us, which is found in the startup class:

public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
// Add support for user secrets in development mode
if (env.IsDevelopment())
{
builder.AddUserSecrets<Startup>();
}
Configuration = builder.Build();
}

We’re already reading an appsettings.Production.json file (if present) and attempting to deserialise a relevant part of it to an instance of the Secrets class.

seriously, .NET Core does so much heavy lifting for us

Create a file called appsettings.Production.json file in the root of the project and paste the following into it:

{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
},
"SuperStrongPassword" : "Pr0Duct1on"
}

The contents of this file should match the appsettings.Development.json file, but with one important difference:

{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
},
"SuperStrongPassword" : "Pr0Duct1on"
}

Restart the application (in production):

ASPNETCORE_ENVIRONMENT=Production dotnet run

And head back to Home/About and this should happen:

Consuming AppSettings in About
Oh yeah!

And Staging?

Do exactly the same as above, but create an appsettings.Staging.json file.

and chose a different value for SuperStrongPassword, obviously

Again, you’ll want to either have these files already on your production server, or generated by the build action. You are using a build server, right? It’ super easy to set something like AppVeyor up specifically for this.

in fact, I’ve already written an article about using AppVeyor for building and deploying .NET Core applications


Conclusion

We’ve seen the problem that User Secrets were designed to solve, how to create them, and how to consume them in your application. We’ve also seen how appsettings should be used in place of UserSecrets when outside of development (i.e. in Staging and Production).

We’ve also seen that .NET Core does all of the hard work of figuring out which files to deserialise, in which situations.

Have you used User Secrets in any of your .NET Core applications yet? If so, what did you think of them? Did they seem as amazing and magical to you as they did to me? If you haven’t used them would you be more willing to use them now that you’ve seen how to do it? Let me know in the comments and let’s keep the conversation going.

If this post has helped you out, please consider   Buy me a coffeeBuying me a coffee
Jamie Taylor
A .NET developer specialising in ASP.NET MVC websites and services, with a background in WinForms and Games Development. When not programming using .NET, he is either learning about .NET Core (and usually building something cross platform with it), speaking Japanese to anyone who'll listen, learning about languages, writing for his non-dev blog, or writing for a blog about video games (which he runs with his brother)