Strongly typed configuration sections with options pattern in ASP.NET Core
Image from Pexels

Strongly typed configuration sections with options pattern in ASP.NET Core

Loading configuration values directly to classes and use them in the runtime

Configuration is probably the most essential part of every application, yet we do not give much attention to it during application design and development. .NET Core has quite nice way to store configurations (usually JSON files) and already built-in extensions to access configuration values and directly cast them to a specific type. There is already build-in binder extension that binds specific config section to a class instance and it comes as a NuGet package authored by Microsoft (Microsoft.Extensions.Configuration.Binder).

I guess after reading this, you thing, "What, but I have all it needs to work the configuration values". This is partially true. For the small and mid sized application, this is just enough to access and cast your configuration values, but when it comes to large scaled applications and microservices it is easy to get lost with all the "magic" strings that point to your configuration sections. Another thing is that it is easy to get lost with calling binding method and you easily end up calling same type binding from more than one place in your code.

The solution for above mentioned problems comes as another NuGet package that allows you to dependency inject bindings in the startup and than just consume the strongly type, pre-populated models in controllers. We'll talk about Microsoft.Extensions.Options.ConfigurationExtensions and how to use it to utilize dependency injection for using your configuration and over come code cluttering and repetition. Let's start with building configuration model and injecting it from the startup.

For this purpose I used simple WebApi project which reruns collection of string which is built depending on the configuration in the JSON file. First thing first, we'll add the configuration to a JSON file.

{
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ValuesConfig": {
    "ValuesCount": 3,
    "ValuesPreset": "value_"
  }
}
    

Important thing to note is that configuration editing does not have same effect as in classic .NET Framework. Once configuration file in classic ASP.NET application is changed, it restarts the application on IIS in order to load new configuration. In case of ASP.NET Core this is not the case. Once configuration loads on the application startup it stays immutable throughout the whole lifespan of the application instance although config file might change while application is running, it does not cause application to restart automatically. 

Now let's build corresponding model that matches structure of our ValuesConfig section. We'll simply create a class that has the same structure as our configuration section.

    public class ValuesConfiguration
    {
        public int ValuesCount { get; set; }
        public String ValuesPreset { get; set; }
    }
    
Note

Make sure that model class matches the structure of the section you want to map from or you will have the property values set to a default value of it's declaring type.

All it's left to setup options pattern in dependency injection is to configure it with services collection in the startup.

    public class Startup
    {
        public IConfiguration Configuration { get; }

        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public void ConfigureServices(IServiceCollection services)
        {
            //Configure section binding
            services.Configure<ValuesConfiguration>(Configuration.GetSection("ValuesConfig"));

            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseMvc();
        }
    }
    

This will add IOptions<ValuesConfiguration> to dependency injection container and we'll be able to access it from the controller constructor. Configure method basically registers a singleton lifetime IOptions<T> implementation which is available throughout the whole life-cycle of the application instance. The parameter of th method is the name or path to a section in the configuration file.

Since we have our options for the specific model and section configured in the dependency injection, we simply access it from the controller constructor.

    [Route("api/[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        private readonly IOptions<ValuesConfiguration> _options;

        public ValuesController(IOptions<ValuesConfiguration> options)
        {
            _options = options;
        }

        [HttpGet]
        public ActionResult<IEnumerable<string>> Get()
        {
            var valuesConfig = _options.Value;
            var result = new List<String>();

            for (int i = 0; i < valuesConfig.ValuesCount; i  )
            {
                result.Add($"{valuesConfig.ValuesPreset}{i}");
            }
            return Ok(result);
        }
    }

    

Simple controller logic relies on the config parameters and demonstrates the how configuration section is translated to class instance.

Polishing up

Although we have only one place where we use configuration section name, we can still make mess and in general, these "magic" strings point to a bad code smell, so we'll make a small change in the model by adding section name as a public constant value.

    public class ValuesConfiguration
    {
        public const string SECTION = "ValuesConfig";
        public int ValuesCount { get; set; }
        public String ValuesPreset { get; set; }
    }
    

This way we are getting rid of using configuration section name anywhere else in the code except in the model which becomes the only carrier of the data structure and source.

        public void ConfigureServices(IServiceCollection services)
        {
            //Configure section binding
            services.Configure<ValuesConfiguration>(Configuration.GetSection(ValuesConfiguration.SECTION));

            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        }
    

Configuration validation

Since configuration model is basically simple POCO class, we can apply same validation strategy via attributes as we do with DTOs in ASP.NET core. Apart from adding attributes to the properties of the configuration model class, we also need to modify the dependency injection registration in the startup, but let's first decorate the model.

    public class ValuesConfiguration
    {
        public const string SECTION = "ValuesConfig";

        [Range(1,10)]
        public int ValuesCount { get; set; }

        [Required]
        public String ValuesPreset { get; set; }
    }
    

Now when we have our model nicely decorated we can update the dependency injection for the options and tell it to validate the attributes. I will just comment the old line so you can see the difference.

        public void ConfigureServices(IServiceCollection services)
        {
            //Configure section binding
            //services.Configure<ValuesConfiguration>(Configuration.GetSection(ValuesConfiguration.SECTION));

            services.AddOptions<ValuesConfiguration>()
                .Bind(Configuration.GetSection(ValuesConfiguration.SECTION))
                .ValidateDataAnnotations();

            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        }
    

Let's check if our validation is actually working by updating the configuration with the invalid value. Since ValuesCount validation attribute is set to have maximum value of 10, well update JSON config file with an invalid value, for example 11.

{
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ValuesConfig": {
    "ValuesCount": 11,
    "ValuesPreset": "value_"
  }
}

    

And now when we run it we will get Microsoft.Extensions.Options.OptionsValidationException exception thrown

Config Validation Exception 

Seems like it works perfect, but there is a small issues with options validation in ASP.NET Core. Apparently validation will be only hit if we try to access it. This is fine for most of the cases, but in some scenarios you want to your application to fail fast in case there is an issue, meaning you do not want to wait for someone to try to access configuration option to get the exception thrown and you would rather have to have this happening on the startup.

To text this, we need another simple endpoint that will prove the above statement.

    [Route("api/[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        private readonly IOptions<ValuesConfiguration> _options;

        public ValuesController(IOptions<ValuesConfiguration> options)
        {
            _options = options;
        }

        [HttpGet]
        public ActionResult<IEnumerable<string>> Get()
        {
            var valuesConfig = _options.Value;
            var result = new List<String>();

            for (int i = 0; i < valuesConfig.ValuesCount; i++)
            {
                result.Add($"{valuesConfig.ValuesPreset}{i}");
            }
            return Ok(result);
        }

        [HttpGet("ping")]
        public ActionResult<String> Ping()
        {
            return Ok("pong");
        }

    }
    

If we run our project and navigate to /api/values/ping we will get OK response with "pong" text message, while /api/values throws exception. As I mentioned, you may not want to have your application running in misconfigured state and be accessible partially, but unfortunately this is not currently possible out of the box.

However, there is a workaround for this problem and it is a simple middleware which needs to be added to the pipeline as a first middleware in the pipeline. Order is important as you would want this middleware to execute before anything else most likely.

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            var valuesConfig = app.ApplicationServices.GetService<IOptions<ValuesConfiguration>>().Value;

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseMvc();
        }
    

Now your application will throw exception on any request sent to it.

All code snippets are part of the solution which is available publically from the GitHub at https://github.com/dejanstojanovic/ConfigurationOptions

References

Disclaimer

Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.


About the author

DEJAN STOJANOVIC

Dejan is a passionate Software Architect/Developer. He is highly experienced in .NET programming platform including ASP.NET MVC and WebApi. He likes working on new technologies and exciting challenging projects

CONNECT WITH DEJAN  Loginlinkedin Logintwitter Logingoogleplus Logingoogleplus

JavaScript

read more

SQL/T-SQL

read more

Umbraco CMS

read more

PowerShell

read more

Comments for this article