So, I’m losing faith in my google skills, there didn’t seem to be a one stop shop that I could find to give information on setting up a .net core console application with a IServiceProvider
and utilizing IOptions<T>
.
In the process of needing configuration for the first time in a console app — crazy I know. The project is currently using AutoFac as its IOC container — though having to look into .netcore’s built in IOC container, I may want to switch to it!
The basis of wanting to utilize configuration for the app for the first time is utilizing differing endpoints for external resources, depending on the environment. These configuration values would be loaded at the applications entry point (or thereabouts) and would need to be accessed deep within the internals of the app, very likely not even within the same project.
How can I do this without having to set some static member somewhere in which everything has access? That led me to find IOptions<T>
(Doc) — T being a configuration class. IOptions<T>
allows for the injection of configuration values into a class, this is exactly what’s needed, and avoids the thing I was worried about having to either pass a configuration collection all over the call stack, or using a static member somewhere in the app.
The first thing we need to do in the console app, is to create a configuration file.
appsettings.json :
{
"Environment": "whatever"
}
and load it in the entry point of our console app
Program.cs :
public static class Program
{
static Program()
{
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile($"appsettings.json", optional: false, reloadOnChange: false)
.AddEnvironmentVariables();
var configuration = builder.Build();
}
}
Ok! File loaded, it currently does nothing! Next, we’ll want to load an environment specific json file, but in order to do that, we’ll need a concept of an environment. Seems like the environment is often controlled via an “environment variable” (no relation?). There are various ways to set environment variables, depending on OS. Some of those methods include:
I’m going to set a new environment variable for a seemingly standard ASPNETCORE_ENVIRONMENT
to “local”. Another sample environment we’ll use is “test”.
Now we can go about creating a few new configuration files for the other environments.
appsettings.local.json :
{
"Environment": "local"
}
appsettings.test.json
{
"Environment": "test"
}
Now we have a “base” configuration appsettings.json, and environment specific configurations appsettings.local.json and appsettings.test.json. This coupled with our new environment variable should allow us to start working with some configuration in a meaningful way (pretty soon).
For now, let’s take a look at what loading the different environment configuration files looks like. From our original example of Program.cs
public static class Program
{
static Program()
{
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile($"appsettings.json", optional: false, reloadOnChange: false)
.AddEnvironmentVariables();
var configuration = builder.Build();
}
}
Let’s make a few updates:
public static class Program
{
static Program()
{
string env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
if (string.IsNullOrWhiteSpace(env))
{
// TODO this could fall back to an environment, rather than exceptioning?
throw new Exception("ASPNETCORE_ENVIRONMENT env variable not set.");
}
Console.WriteLine($"Bootstrapping application using environment {env}");
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile($"appsettings.json", optional: false, reloadOnChange: false)
.AddJsonFile($"appsettings.{env}.json", optional: false, reloadOnChange: false)
.AddEnvironmentVariables();
var configuration = builder.Build();
}
}
In this above, you can see we’re loading into env the value stored in the environment variable ASPNETCORE_ENVIRONMENT, when/if this variable isn't available we (currently) throw an exception. We then print the environment variable value we loaded, and finally load the appropriate appsettings.{env}.json. You can see the loaded environment changes depending on the value of the environment variable ASPNETCORE_ENVIRONMENT.
Now that we’re successfully loading configuration files based on an environment variable, let’s get into some IOptions.
First thing we’ll need is a configuration class, we’ll do something nice and simple like environment specific configuration for a API endpoint:
ApiConfig.cs
public class ApiConfig
{
public string RootUrl { get; set; }
public int Port { get; set; }
}
In the above we just have a class defined, and we would create json to represent those classes in each of our environment configuration files.
appsettings.local.json
{
"Environment": "local",
"ApiConfig": {
"RootUrl": "http://localhost",
"Port": 5001
}
}
appsettings.test.json
{
"Environment": "local",
"ApiConfig": {
"RootUrl": "https://some.url.com",
"Port": 51321
}
}
We now have enough “stuff” in place that we can load something into an IOptions<T>
— our ApiConfig, or IOptions<ApiConfig>
.
We’ll make another change to our Program ctor:
public static class Program
{
private static readonly IServiceProvider ServiceProvider;
static Program()
{
string env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
if (string.IsNullOrWhiteSpace(env))
{
// TODO this could fall back to an environment, rather than exceptioning?
throw new Exception("ASPNETCORE_ENVIRONMENT env variable not set.");
}
Console.WriteLine($"Bootstrapping application using environment {env}");
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile($"appsettings.json", optional: false, reloadOnChange: false)
.AddJsonFile($"appsettings.{env}.json", optional: false, reloadOnChange: false)
.AddEnvironmentVariables();
var configuration = builder.Build();
var serviceCollection = new ServiceCollection();
serviceCollection.AddOptions();
serviceCollection.Configure<ApiConfig> (configuration.GetSection(nameof(ApiConfig)));
this.ServiceProvider = serviceCollection.BuildServiceProvider();
}
}
A few new things above:
- serviceCollection — where we register our services
- ServiceProvider — where we “get” our registered services.
We can now “resolve” our registered components as per the normal dotnetcore resolver, and our new IOptions<T>
can be used like so:
SomeClass.cs
public class SomeClass
{
private readonly IOptions<ApiConfig> _apiConfig;
public SomeClass(IOptions<ApiConfig> apiConfig)
{
_apiConfig = apiConfig;
}
public void DoStuff()
{
Console.WriteLine(_apiConfig.Value.RootUrl);
Console.WriteLine(_apiConfig.Value.Port);
}
}
The environment configuration loaded will determine which “instance” of ApiConfig
we inject into SomeClass.
One last hiccup on my end, is I’m actually using AutoFac, and not the .net core IOC container . Due to this all my resolutions are occurring through AutoFac, while my IOptions<T>
are being registered through .net core’s IOC container.
This is another pretty simple change (though it at least took me a while to figure it out). I ended up throwing together a new helper method that takes in my IServiceProvider, as well as my ContainerBuilder (from AutoFac registration). The helper method looks like:
public static void RegisterConfigurationInjectionsAutoFac(IServiceProvider serviceProvider, ContainerBuilder builder)
{
builder.Register(context => serviceProvider.GetService<IOptions<ApiConfig>>());
}
and can be called directly from our normal composition root/entry point that puts together the AutoFac container.
Top comments (2)
Woah, how did I not know about the ConfigurationBuilder class?
Are you using the one that comes with ASP, or is that an AutoFac thing?
yar!
ConfigurationBuilder
is built in .net core thinger ConfigurationBuilder fromMicrosoft.Extensions.Configuration
package (or the metapackage if you're using asp.net core.)