Mike Lindegarde... Online

Things I'm likely to forget.

Configuration using ASP.NET Core 1.0 and StructureMap

Update

Configuration.GetSection(string).Bind(object) has been moved to a new package in .NET Core 1.1:  Microsoft.Extensions.Configuration.Binder.  You will need Microsoft.Extensions.Options.ConfigurationExtensions for the services.Configure<TConfig>(Configuration.GetSection(string)) bits.

First, the bad news

Before .NET Core I used build specific web.config transforms.  When building MVC apps I took advantage XML transforms to have build specific configurations (obvious examples being Debug vs. Release).  If the project type didn't have transforms out of the box I used something like SlowCheetah to handle the XML transform (for example WPF).

While just about every tutorial out there tells you how to setup environment specific appsettings.json files, I haven't found any information about build specific application settings.  Hopefully I'm just missing something.  While this isn't a huge loss, it was convenient to be able to select "Debug - Mock API" as my build configuration and have a transform in place to adjust my web.config as necessary.

A Basic Example

Microsoft's new approach to configuration makes it incredibly easy to use strongly typed configurations via the IOptions<T> interface. Let's start with the following appsettings.json file:

{
  "Logging": {
    "UseElasticsearch":  true, 
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    }
  },
  "IdentityServer": {
    "Authority": "http://localhost:5000",
    "Scope": "Some Scope"
  }
}

In order to take advantage of strongly typed configuration you'll also need a simple POCO (Plain Old CLR Object) that matches the JSON you've added to the appsettings.json file:

namespace Project.Api.Config
{
    public class IdentityServerConfig
    {
        public string Authority {get; set;}
        public string Scope {get; set;}
    }
}

With those two things in place, it's simply a matter of adding the appropriate code to your project's Startup.cs.  The following example code includes several things that are not necessary for this basic example.  My hope is that you might see something that answers a question you may have that I don't explicitly address in this post.

public Startup(IHostingEnvironment env)
{
	var builder = new ConfigurationBuilder()
		.SetBasePath(env.ContentRootPath)
		.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
		.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
		.AddJsonFile("appsettings.local.json", optional: true);

	builder.AddEnvironmentVariables();
	Configuration = builder.Build();
	
	LoggingConfig loggingConfig = new LoggingConfig();
	Configuration.GetSection("Logging").Bind(loggingConfig);

	LoggerConfiguration loggerConfig = new LoggerConfiguration()
		.Enrich.FromLogContext()
		.Enrich.WithProperty("application","Application Name")
		.WriteTo.LiterateConsole();

	if(loggingConfig.UseElasticsearch)
	{
		loggerConfig.WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri("http://localhost:9200"))
		{
			AutoRegisterTemplate = true,
			CustomFormatter = new ExceptionAsObjectJsonFormatter(renderMessage:true),
			IndexFormat="logs-{0:yyyy.MM.dd}"
		});
	}

	Log.Logger = loggerConfig.CreateLogger();
}

public IServiceProvider ConfigureServices(IServiceCollection services)
{
	services.Configure<IdentityServerConfig>(Configuration.GetSection("IdentityServer"));

	services.AddMvc().AddMvcOptions(options => 
	{
		options.Filters.Add(new GlobalExceptionFilter(Log.Logger));
	});
	
	services.AddSwaggerGen();
	services.ConfigureSwaggerGen();
	services.AddMemoryCache();

	return services.AddStructureMap(Configuration);
}

public void Configure(
	IApplicationBuilder app, 
	IHostingEnvironment env, 
	ILoggerFactory loggerFactory,
	IApplicationLifetime appLifetime)
{
	IdentityServerConfig idSrvConfig = new IdentityServerConfig();
	Configuration.GetSection("IdentityServer").Bind(idSrvConfig);

	loggerFactory.AddSerilog();

	app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
	{
		Authority = idSrvConfig.Authority,
		ScopeName = idSrvConfig.Scope,
		RequireHttpsMetadata = false,
		AutomaticAuthenticate = true,
		AutomaticChallenge = true
	});

	app.UseMvc();
	app.UseSwagger();
	app.UseSwaggerUi();

	appLifetime.ApplicationStopped.Register(Log.CloseAndFlush);
}

Let's take a closer look at what that code is doing...

StructureMap

By default you get Microsoft's IoC container.  While it does the job for simple projects, I much prefer the power that StructureMap gives me.  However, I was having trouble getting IOptions<IdentityServerConfig> properly injected into my controllers. 

The solution to my problem ended up being pretty straight forward.  Just make sure that all of your calls to services.configure<T> come before you make you're call to:

// do this:
services.Configure<IdentityServerConfig>(Configuration.GetSection("IdentityServer"));

// before this:
container.Populate(services);

In hind site that's a pretty obvious thing to do.  StructureMap won't know anything about what you've added to the default IoC container after you call container.Populate(servcies).

Using your settings

After the configuration has been loaded and StructureMap has been configured you can get access to the values from your appsettings.json file by injecting IOptions<T> (where T would be IdentityServerConfig in my example) into the controller (or whatever class you need).

That's great, unless you need to access the values in Startup.cs for some reason.  The solution to that problem is to use the following code after the configuration has been loaded (via builder.Build()):

IdentityServerConfig idSrvConfig = new IdentityServerConfig();
Configuration.GetSection("IdentityServer").Bind(idSrvConfig);

While that's pretty simple code, I had some trouble finding that information.

Overriding settings

If you look at the "Logging" section in my appsettings.json you'll notice there is a Boolean value indicating whether or not Elasticsearch should be used.  I have Elasticsearch running locally, but not in the development environment.

{
  "Logging": {
    "UseElasticsearch":  false, 
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    }
  },
  "IdentityServer": {
    "Authority": "http://localhost:5000",
    "Scope": "Some Scope"
  }
}

To get around that problem I added a Boolean value to my configuration that I can override with a settings file that only exists on my computer:

{
  "Logging": {
    "UseElasticsearch": true
  }
}

Notice that this file only needs to have the values you're overriding.  You can then configure the configuration builder to load the local settings file if it exists:

var builder = new ConfigurationBuilder()
	.SetBasePath(env.ContentRootPath)
	.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
	.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
	.AddJsonFile("appsettings.local.json", optional: true);

The order you add the JSON files does matter.  I always set things up so that my local file will override any other change.