ASP .NET Core JSON Configuration and Dependency Injection
This article was originally written in July 2016, and has been updated in November 2019 to the latest .NET Core 3 version, and the updated sample can be found in this GitHub repository.
In the previous versions of ASP .NET, any configuration setting or parameter you needed was added in web.config
(complete description of the old web.config
file), or added in a separate XML file and referenced in web.config
(for scenarios like database connection strings, or storing APIs access tokens).
The new configuration system provides support for JSON, XML, INI and for in-memory configuration, while also allowing you to create your custom configuration provider.
Let’s assume that in our application we want the response messages not to be hard-coded in Startup
anymore, but stored in a configuration file so we don’t have to stop, modify or recompile our application every time the messages or the routes change.
More clearly, we want to map the keys from the JSON file below as routes and the values as the responses we want our application to give, so when a user browses in our application to /some-route
, if some-route
is present in the JSON configuration file, then the response will be the value from the file, if else to display a default message.
{
"hi": "Hi!",
"hello": "Hello!",
"bye": "Goodbye!",
"default": "This is default!"
}
Also, if we modify the configuration file while the application is running, our application should be able to use the latest configuration. (As we will see, we will not have much to do here since this is built into ASP .NET).
There can be any number of defined paths in our configuration file and they can change with any frequency (so that hard-coding them in our application is not an option).
Using the ASP .NET Core JSON Configuration Provider
As we said earlier, the new ASP .NET implements a JSON configuration provider that allows us to read and use configurations from JSON files (and not only), and we can have strong typing (where we define classes for our configuration settings) or we can access them using directly their key.
For a complete article on creating Strongly Typed Configuration Settings in ASP .NET Core, see this article by Rick Strahl.
To use configuration settings in ASP .NET Core, simply instantiate a Configuration
object (using a ConfigurationBuilder
) and indicate the source of the JSON file. Then, we can add multiple sources and types of sources that populate our application’s configuration.
At its simplest,
Configuration
is just a collection of sources, which provide the ability to read and write name/value pairs. If a name/value pair is written toConfiguration
, it is not persisted. This means that the written value will be lost when the sources are read again.
Developers are not limited to using a single configuration source. In fact several may be set up together such that a default configuration is overridden by settings from another source if they are present.
Because at the time when we write the application we can’t know the exact paths, we will not create strongly-typed configurations but we will take the path from our application an check to see wether that path exists in our configuration file.
Building the configurable Greeting service
First of all, we create a new web application using the dotnet
CLI, then, we create a new file, greetings.json
, in the same directory as our Program.cs
, Startup.cs
files, where we add our custom routes and messages we want our application to respond with:
{
"hi": "Hi!",
"hello": "Hello!",
"bye": "Goodbye!",
"default": "This is default!"
}
In Startup
we create a property of type IConfiguration
where we will keep the data from the configuration file, the add a constructor for the Startup
class that will instantiate a ConfigurationBuilder
, which will populate the configuration object:
public IConfiguration Configuration { get; set; }
public Startup()
{
var cb = new ConfigurationBuilder(IWebHostEnvironment env)
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("greetings.json", optional: false, reloadOnChange: true);
Configuration = cb.Build();
}
The constructor has an IWebHostEnvironment
parameter that can be used to establish the directory of the JSON configuration file. Since we placed it in the same folder as the other files, we can simply get the current directory: Directory.GetCurrentDirectory()
.
After we instantiate the ConfigurationBuilder
we chain two method calls - one for establishing the directory of the configuration file, the other for determine the actual name of the file. Then, we call the .AddJsonFile()
method takes the following arguments:
- the name of the file - in our case
greetings.json
- a
bool
that determines wether this configuration file is optional or not, used to determine the order in which the system searches the files (if there are multiple files) if the same configuration name exists in multiple files. - a
bool
that specifies what happens if the configuration file is modified while the application is running -reloadOnChange
Next, we add routing to our application:
public void ConfigureServices(IServiceCollection services)
{
services.AddRouting();
}
Now it’s time to put everything together in the Configure
method:
public void Configure(IApplicationBuilder app)
{
var rb = new RouteBuilder(app);
rb.MapGet("{route}", ctx =>
{
var routeMessage = Configuration.AsEnumerable()
.FirstOrDefault(r => r.Key == ctx.GetRouteValue("route")
.ToString())
.Value;
var defaultMessage = Configuration.AsEnumerable()
.FirstOrDefault(r => r.Key == "default")
.Value;
return ctx.Response.WriteAsync(routeMessage ?? defaultMessage);
});
app.UseRouter(rb.Build());
}
We create a new RouteBuilder
and map a new GET
request, then read the route parameter and try to retrieve it from our configuration file, through Configuration
, which contains all configuration data:
var routeMessage = Configuration.AsEnumerable()
.FirstOrDefault(r => r.Key == context.GetRouteValue("route")
.ToString())
.Value;
We know that our configuration is now accessible through the Configuration
property that we populated in the Startup
constructor, and the configuration settings are an IEnumerable<KeyValuePair<string, string>>
, that is a collection of key-value pairs of strings, so we can use Linq to search for the key-value pair in our file that has the same key as our path and take the value from that pair.
For some examples of using Linq with lambdas check this article from Code Magazine.
We also search for the default message in our JSON so that if the path does not exist in the file, we have a standard response.
var defaultMessage = Configuration.AsEnumerable()
.FirstOrDefault(r => r.Key == "default")
.Value;
Then, depending on wether the route actually exists in our configuration file or not, we return either the message of that specific route, or the default message.
If we run the application and open a browser, we can check if our routing works:
http://localhost:5000/hello - This should display Hello!
http://localhost:5000/hi - This should display Hi!
http://localhost:5000/bye - This should display Goodbye!
http://localhost:5000/default or http://localhost:5000/anything-else - This should display This is default!
Without closing the application, go to greeting.json
and either add a new key-value pair, or modify an existing one’s value, save the file and navigate to that path. You will see that the application was able to load the configuration file without restarting or recompiling.
Making use of ASP .NET Core Dependency Injection
For a more detailed view of dependency injection in ASP .NET Core applications, read the article from the Official ASP .NET Core Documentation.
Our application works, but there is a lot of logic code in Startup
, a place for configuration.
We will try to extract the part of Startup
that deals with actually getting the response from the JSON
file in a separate class and learn how to inject that service in various places (like other services, controllers or even in Startup
).
First of all, let’s think at what public methods and properties should a greeting service should have. Basically, it should only have a single method that receives the path a user navigated to and should return a string, the response taken from the JSON file.
In order to make dependency injection work, we will make use of interfaces. Meaning we will build an interface for the greeting service, IGreetingService
.
To see an example with interfaces, check this tutorial.
Let’s create a new file, IGreetingService.cs
:
public interface IGreetingService
{
string Greet(string route);
}
Now, in another file, GreetingService.cs
we will add the actual implementation of the service:
using System.Linq;
using Microsoft.Extensions.Configuration;
public class GreetingService : IGreetingService
{
private IConfiguration Configuration {get;set;}
public GreetingService(IConfiguration configuration)
{
Configuration = configuration;
}
public string Greet(string route)
{
var routeMessage = Configuration.AsEnumerable()
.FirstOrDefault(r => r.Key == route)
.Value;
var defaultMessage = Configuration.AsEnumerable()
.FirstOrDefault(r => r.Key == "default")
.Value;
return (routeMessage != null) ? routeMessage : defaultMessage;
}
}
Basically, this class has the Greet
method which contains most of the logic we had in Startup
for retrieving the response from the JSON file. It also has an IConfiguration
property, this time injected in the controller (we will see a bit later where and how this is done).
Now let’s take a look at the Startup
class, this time at the Configure
method:
public void Configure(IApplicationBuilder app, IGreetingService greetingService)
{
var routeBuilder = new RouteBuilder(app);
routeBuilder.MapGet("{route}", context =>
{
var route = context.GetRouteValue("route").ToString();
return context.Response.WriteAsync(greetingService.Greet(route));
});
app.UseRouter(routeBuilder.Build());
}
The new thing here is that we have an IGreetingService
parameter in the method signature that we use when returning the message.
Both the IGreetingService
and IConfiguration
parameters are configured in the ConfigureServices
method, also from Startup
:
public void ConfigureServices(IServiceCollection services)
{
services.AddRouting();
services.Add(new ServiceDescriptor(typeof(IConfiguration),
provider => new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("greetings.json",
optional: false,
reloadOnChange: true)
.Build(),
ServiceLifetime.Singleton));
services.AddTransient<IGreetingService, GreetingService>();
}
First of all we add the routing service (just like in the previous examples).
The last line of this method states that every time some class requests a parameter of type IGreetingService
, the DI (dependency injection) engine will provide it with a new (every time a new) implementation of GreetingService
.
The second method call is the most difficult of all: it says that every time someone requests an IConfiguration
parameter, the engine should provide the same instance (singleton) generated by this chain of method calls:
new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("greetings.json",
optional: false,
reloadOnChange: true)
.Build(),
which is the same as earlier.
Conclusion
We created a web application for which we configured the paths and the associated responses in an external JSON file. We extracted this functionality in a service, GreetingService
that was provided using dependency injection.