Application Configuration in ASP.NET 5 MVC 6 (or Where’d my web.config go?)


CaseStudyImage

Introduction

Where’d my web.config go?

The first thing you will notice when you begin to set up application configuration in the new ASP.NET 5 framework is that the web.config file is gone.  XML has gone the way of the dinosaurs and been replaced by JSON.  It may seem a little confusing at first, but once you get it down things get a lot simpler.  No more tedious release vs debug transformations and the new configuration file are much easier to read and understand (for me anyway).  Plus… we get the benefits of strongly typed configuration data that can be separated into models.  In this article I’ll walk you through everything you need to know to get started using .Net Core's new configuration system.

How It Works

The new configuration file is aptly named appsettings.json, although you can name it anything you want.  When you create a new ASP.NET 5 Web Application project using the scaffolding wizard, this file is automatically placed in the root of your project.  We can also create separate versions of this file to override values for each environment such as debug and release, but more on that later.

The default contents of the appsettings.json file.

   1:  {
   2:    "Data": {
   3:      "DefaultConnection": {
   4:        "ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=blah-blah-blah..."
   5:      }
   6:    },
   7:    "Logging": {
   8:      "IncludeScopes": false,
   9:      "LogLevel": {
  10:        "Default": "Verbose",
  11:        "System": "Information",
  12:        "Microsoft": "Information"
  13:      }
  14:    }
  15:  }

As you can see above, the default content gives us some basic application settings in JavaScript Object Notation (JSON).  Here’s where we’ll find the connection string just as we would have in our old web.config, and there’s some Logging configuration values here too.  Notice the hierarchical structure of the key-value-pairs.  Let’s break away from the appsettings.json file for a moment (we’ll get back to it later), and look at Startup.cs.

First a quick note:

You'll also need to make sure you have the correct packages loaded via Nuget's new package system by placing the following lines in project.json. Note: these are included by default in the scaffold generated project.

   1:  // Make sure you have these assemblies added under dependencies in project.json
   2:    "dependencies": {
   3:      ///...
   4:      ///...
   5:      "Microsoft.Extensions.Configuration.FileProviderExtensions" : "1.0.0-rc1-final",
   6:      "Microsoft.Extensions.Configuration.Json": "1.0.0-rc1-final",
   7:      "Microsoft.Extensions.Configuration.UserSecrets": "1.0.0-rc1-final",
   8:      ///...
   9:      ///...
  10:    }

The default contents of the Startup.cs file.

   1:  // These two usings are needed for the configuration we have below.
   2:  using Microsoft.AspNet.Hosting;
   3:  using Microsoft.Extensions.Configuration;
   4:   
   5:  namespace HelloConfigWorld
   6:  {
   7:      public class Startup
   8:      {
   9:          public Startup(IHostingEnvironment env)
  10:          {
  11:              // Set up configuration sources.
  12:              var builder = new ConfigurationBuilder()
  13:                  .AddJsonFile("appsettings.json")
  14:                  .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);
  15:   
  16:              if (env.IsDevelopment())
  17:              {
  18:                  builder.AddUserSecrets();
  19:              }
  20:   
  21:              builder.AddEnvironmentVariables();
  22:              Configuration = builder.Build();
  23:          }
  24:   
  25:          public IConfigurationRoot Configuration { get; set; }
  26:   
  27:          /// The rest of Startup.cs.
  28:          ///...
  29:          ///...
  30:          ///...
  31:   
  32:      }
  33:  }

In Startup.cs we have a ConfigurationBuilder. At its simplest, the "Configuration" is a series of "Providers". After we new up a ConfigurationBuilder() we simply add providers. i.e. We add the JSON file provider via builder.AddJsonFile("appsettings.json"), in the same way we add the environment variables provider via builder.AddEnvironmentVariables(). To illustrate my point look below at the following simple example from Microsoft's documentation. Here we've done something totally different, we've added a MemoryConfigurationProvider(). If you miss XML there's a built-in XML provider, and you can even write your own providers if you need special cases.

   1:  var builder = new ConfigurationBuilder();
   2:  builder.Add(new MemoryConfigurationProvider());
   3:  var config = builder.Build();
   4:  config.Set("somekey", "somevalue");
   5:   
   6:  // do some other work
   7:   
   8:  string setting2 = config["somekey"]; // returns "somevalue"

Overriding Configuration Values

Now you're thinking to yourself, he said earlier I didn't have to worry about those pesky transformations that came from using web.config. Well, let me show you how to handle overriding configuration based on you're current environment. The providers are added in order, and each new provider will override the values set by the previous provider. By adding them in order we can have base values, overridden by say development or release values, overridden by user secrets (which I'll cover next week in an upcoming post), and finally overridden by environment variables. The order is configurable but this is a basic best practice.

Look back at the default Startup.cs, notice the lines,

   1:  var builder = new ConfigurationBuilder()
   2:      .AddJsonFile("appsettings.json")
   3:      .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);

Now supposing we're in our development environment, we would first read appsettings.json, then we would read appsettings.development.json overriding all matching values.  Let's add a development file to our project. The values in this file will override any values that previously existed in appsettings.json. To add the new file right click on your project, and choose Add->New Item. Next choose ASP.NET Configuration File from the menu and name it appsettings.development.json.

Do this again to create an appsettings.release.json, and you should see this at the bottom of your Solution Explorer.

The code below helps illustrate values being first defined in a base file, then if those values exist in a file added further down the chain, how the values are replaced. Values that do not exist in subsequently loaded providers are maintained in their original state.

   1:  // appsettins.json
   2:  {
   3:    "myKey": "baseValue",
   4:    "anotherKey": "unchangedValue"
   5:  }
   6:   
   7:  // appsettins.development.json
   8:  {
   9:    "myKey": "developmentValue"
  10:  }
  11:   
  12:  // appsettins.release.json
  13:  {
  14:    "myKey": "releaseValue"
  15:  } 
  16:   
  17:  // Set up configuration sources.
  18:  var builder = new ConfigurationBuilder()
  19:     .AddJsonFile("appsettings.json")
  20:     .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);
  21:   
  22:  Configuration = builder.Build();
  23:   
  24:  // In development.
  25:  Configuration.Get("myKey");         //<-- developmentValue (overidden)
  26:  Configuration.Get("anotherKey");    //<-- unchangedValue
  27:   
  28:  // In release.
  29:  Configuration.Get("myKey");         //<-- releaseValue (overidden)
  30:  Configuration.Get("anotherKey");    //<-- unchangedValue

It's as easy as that. You can also override values based on other environments, such as staging. Your file doesn't have to be called appsettings.json, you could just as easily have foo.json and override with bar.json, etc. Much more flexible than the old way, wouldn't you say?

Using the configuration in your application.

Okay, so now we have our application values configured, how do we get them so we can use them in say a controller? First, It's worth saying something about strongly typed values. Before, in the web.config days, all we stored were strings, and then when we consumed them in our application, we parsed them into whatever we needed. Now, in the new system we store values in the JSON as whatever type we need. Integers as numbers, booleans as booleans, etc. No more coercing our values to use them. We do this by creating POCO classes and "loading" them with our config values on startup. Next we're going to create some configuration options for an SMTP client and use them in our application.

Let's add an SMTP configuration to our application via appsettings.json. We could override them in other files depending on environment. Perhaps we use a differnt server in production with different credentials, but we've already know how to do that so I'll keep it simple.

   1:  {
   2:    "Data": {
   3:      "DefaultConnection": {
   4:        "ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=blah-blah-blah..."
   5:      }
   6:    },
   7:    "Logging": {
   8:      "IncludeScopes": false,
   9:      "LogLevel": {
  10:        "Default": "Verbose",
  11:        "System": "Information",
  12:        "Microsoft": "Information"
  13:      }
  14:    },
  15:    // Defined later and overridden in appsettings.development.json or appsettings.release.json.
  16:    "SmtpOptions": {
  17:      "UserName": "Troy",
  18:      "Password": "Locke",
  19:      "FromName": "Troy Locke",
  20:      "FromAddress": "Troy@slnzero.com",
  21:      "DeliveryMethod": "Network",
  22:      "Host": "smtp.slnzero.com",
  23:      "Port":25,
  24:      "DefaultCredentials": false,
  25:      "EnableSsl": false
  26:    }
  27:  }

Notice that we have strings, an integer to specify the Port, and boolean values for DefaultCredentials and EnableSsl. Nice!

Next we have to create a class for our application to hold the values so we can use them. We'll create a matching POCO called SmtpOptions like so.

   1:  // Our SmtpOptions POCO class will hold our settings at runtime.
   2:  namespace HelloConfigWorld
   3:  {
   4:      public class SmtpOptions
   5:      {
   6:          public string UserName { get; set; }
   7:   
   8:          public string Password { get; set; }
   9:   
  10:          public string FromName { get; set; }
  11:   
  12:          public string FromAddress { get; set; }
  13:   
  14:          public string DeliveryMethod { get; set; }
  15:   
  16:          public string Host { get; set; }
  17:   
  18:          public int Port { get; set; }
  19:   
  20:          public bool DefaultCredentials { get; set; }
  21:   
  22:          public bool EnableSsl { get; set; }
  23:      }
  24:  }

Again, notice we have strongtly typed values for our configuration options, these map straight out of the JSON configuration file appsettings.json into our SmtpOptions class by adding a line in Startup.cs, lets look at how to do that now.

Look back at your Startup.cs again, you'll notice, just below the area we focused on earlier, a method called ConfigureServices() Add the line at the bottom as shown below.

   1:  // This method gets called by the runtime. Use this method to add services to the container.
   2:  public void ConfigureServices(IServiceCollection services)
   3:  {
   4:      // Add framework services.
   5:      services.AddEntityFramework()
   6:          .AddSqlServer()
   7:          .AddDbContext<ApplicationDbContext>(options =>
   8:              options.UseSqlServer(Configuration["Data:DefaultConnection:ConnectionString"]));
   9:   
  10:      ///...
  11:      ///...
  12:      ///...
  13:      
  14:      // Here we add our new SmtpOption values and map them to our POCO we created.
  15:      services.Configure<SmtpOptions>(Configuration.GetSection("SmtpOptions"));
  16:  }

Next we need to be able to reference these values in our Application. .Net Core is big on DI (Dependency Injection) and we're going to use it to "inject" our SmtpOptions class into our app where we need it via the IOptions interface. In the constructor, we simply pass in an IOptions<SmtpOptions>, and assign it to a class variable or property. We then have it available with all it's values for use in the class. Below is a snippet from the default MessageService.cs that implements our options class.

   1:  public class AuthMessageSender : IEmailSender, ISmsSender
   2:  {
   3:      private readonly IOptions&lt;SmtpOptions&gt; _smtpOptions;
   4:      public AuthMessageSender(IOptions&lt;SmtpOptions&gt; smtpOptions)
   5:      {
   6:          _smtpOptions = smtpOptions;
   7:      }
   8:   
   9:      public Task SendEmailAsync(string email, string subject, string message)
  10:      {
  11:          // Use SMTP here to send an email.
  12:          var host = _smtpOptions.Value.Host;
  13:          var port = _smtpOptions.Value.Port;
  14:          var userName = _smtpOptions.Value.UserName;
  15:          var password = _smtpOptions.Value.Password;
  16:          var fromName = _smtpOptions.Value.FromName;
  17:          var fromAddress = _smtpOptions.Value.FromAddress;
  18:          var deliverMethod = _smtpOptions.Value.DeliveryMethod;
  19:          var defaultCredentials = _smtpOptions.Value.DefaultCredentials;
  20:          var enableSsl = _smtpOptions.Value.EnableSsl;
  21:   
  22:          var msg = new MimeMessage();
  23:          msg.From.Add(new MailboxAddress(fromName, fromAddress));
  24:          msg.To.Add(new MailboxAddress(email, email));
  25:          msg.Subject = subject;
  26:   
  27:          ///...
  28:          ///...
  29:          ///...
  30:   
  31:       }
  32:  }

Acknowledgment

You may have noticed AddUserSecrets() and AddEnvironmentVariables() in the Startup.cs examples. I have left these out to keep this post from becoming too long. In short these are methods to “safely” store configuration settings out and away from your applications code. Passwords and such are never in code and thus never stored in version control or passed between developers sharing a code base. By pulling our configuration values from environment variables in production we greatly reduce the likelihood of them being compromised. I fully intend to cover this topic in an upcoming article. Also, I've stated that there is no web.config, this is only partially true, there does exist a web.config file under the /wwwroot directory of the project that is used to configure certain server values.

Summary

As you can see, the new .Net Core is vastly improved over the old web.config. It’s much more versatile in that we can take complete control over our application’s configuration. It’s simpler to read, configure, and setup than the old system. And the use of strongly typed values make for a better practice overall. I hope this article helps you better understand and implement configuration in the new ASP.NET 5 .Net Core framework.


Back