IOptionMonitor: another way to manage .NetCore configuration

There is a way to listen for appsetting.json file changes ?
Yes, absolutely yes.
The solution I would like to explain you uses “IOptionMonitor<T>” to listen for .NetCore configuration changes and make this changes available in your code

https://docs.microsoft.com/it-it/aspnet/core/fundamentals/configuration/options?view=aspnetcore-3.1

Table of content

Prerequisites

Some bla bla

To write this artiche I’ve decided to map JWT configuration of my demo project and make it available via IOptionMonitor. This tutorial assumes that you already have a .netcore application or you have forked/downloaded my release/NLog branch

My appsetting.json is look like this.. Let’s start!

{
  // --------
  // JWT section
  // --------

  "Jwt": {
    "Key": "BGy+7Eb7cyqgZy?N6Sw7Z!&amp;dC95Utjk#xVp8yTxWNUMqd%f*4T$kMuk!jAJMyUVg+R=+3EkL374$Q4vJ=&amp;2$HRV&amp;fu3sZxFZ6S?M+SZK*bM*Nq^KpphsguWczRR*MjcHCbRkQ$V#BDKykQW=e%H=L&amp;5FYf%$3jbK^gGrWvXnYkCM5duj!A6A#EhUjp*aQWnP2NYRBVDs8xNYNwmJ+8jmwucG*qc$fZdk3VGKR@_9x@xK7BAK$#aSq9_zMqFTeP=cE$VqrqMks4DBzgtbCTQVAVEeg%wRYfg9qY=C$ge8v!Dr*ZfRbEsNv?pvrjCnK+jqgZXYpXgnettxpsq3_!-2KFRdGuKWu@6&amp;Szm&amp;2c*2**^R!mzwtXWUg#Ce%DW*=G7^#n?A+3#26BPCgvHSfuk$-#kSGVXh4#!a_VWts&amp;Je$mDBtR+8f@4C*@f=XfMRJBkys^buWNS8w=MbRRKzZAU%R%Qy+LH5$%y6m%-am7vv!D47KmRe@G@UKF=g*_xpv3*Q",
    "Issuer": "ASPNETCore"
  },

  // --------
  // logging section
  // --------
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },

  //
  "AllowedHosts": "*"
}

Class configuration mapping

  1. Create a folder called “Configuration” which contains all class that maps your appsetting.json configuration sections.
  2. Create an empty interface called “IAppSettingConfiguration“, this will be useful later
using ASPNETCore.MethodExtensions;

namespace ASPNETCore.Configuration
{
    /// <summary>
    /// used to restrict <seealso cref="IConfigurationExtensions.Get{T}(Microsoft.Extensions.Configuration.IConfiguration, string)"/>
    /// </summary>
    public interface IAppSettingConfiguration
    {
    }
}

  1. Create a static class that are going to contains all section keys of appsetting.json file. This avoid “magic-strings” scattered around the code:
namespace ASPNETCore.Configuration
{
    /// <summary>
    /// Configuration Keys section
    /// </summary>
    public static class ConfigurationKeys
    {
        /// <summary>
        /// JWt section key
        /// </summary>
        public static readonly string Jwt = "Jwt";
    }
}

  1. Create a class that reflect exactly the structure of Jwt configuration section:
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;

namespace ASPNETCore.Configuration
{
    /// <summary>
    /// Wrapper for Jwt section from configuration file.
    /// Used as <seealso cref="IOptionsMonitor<JwtConfiguration>"/>
    /// </summary>
    public class JwtConfiguration : IAppSettingConfiguration
    {
        /// <summary>
        /// Jwt Key. Used to create <seealso cref="SymmetricSecurityKey"/> 
        /// </summary>
        public string Key { get; set; }

        /// <summary>
        /// Jwt Issuer. Used to create <seealso cref="JwtSecurityToken"/> 
        /// </summary>
        public string Issuer { get; set; }
    }
}

  1. Create a method extension that allows you to resolve configuration binding in different way when DI is not available yet; we use it later
    1. Create a folder called “MethodExtensions”
    2. Create a file called “IConfigurationExtensions“:
using Microsoft.Extensions.Configuration;

namespace ASPNETCore.MethodExtensions
{
    using ASPNETCore.Configuration;

    /// <summary>
    /// IConfiguration method Extensions
    /// </summary>
    public static class IConfigurationExtensions
    {
        /// <summary>
        /// Get a configuration
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="self"></param>
        /// <param name="section"></param>
        /// <returns></returns>
        public static T Get<T>(this IConfiguration self, string section)
            where T : class, IAppSettingConfiguration
        {
            var s = self?
                 .GetSection(section);
            if(s != null)
                return s.Get<T>();
            return null;
        }
    }
}

This use IAppSettingConfiguration to restrict a little bit the type T, to be sure that is a class that is projected to bind a configuration section

Bind class mapping to appsetting.json value

  1. Go back to Startup.cs file and add a Singleton in ConfigureServices to be able to to resolve IOptionsMonitor
/// <summary>
/// This method gets called by the runtime. Use this method to add services to the container. 
/// </summary>
/// <param name="services"></param>
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
    services.AddTransient<IPrincipal>(provider => provider.GetService<IHttpContextAccessor>()?.HttpContext?.User);

    //
    services.AddSingleton(typeof(IOptionsMonitor<>), typeof(OptionsMonitor<>));

    //
    services.ConfigureSwaggerService();
    services.ConfigureJWTService(_configuration);
    services.ConfigureAppServices(_configuration);

}
  1. Go back to ConfigureJWTService and bind setting to their class binder
  2. Use this bind setting to build a TokenValidationParameters
private static JwtConfiguration original;
/// <summary>
/// Configure JWT. use in <seealso cref="Startup.ConfigureServices(IServiceCollection)"/>
/// </summary>
/// <param name="self"></param>
/// <param name="configuration"></param>
public static void ConfigureJWTService(this IServiceCollection self, IConfiguration configuration)
{
    self.Configure<JwtConfiguration>(configuration.GetSection(ConfigurationKeys.Jwt));
    original = configuration.Get<JwtConfiguration>(ConfigurationKeys.Jwt);

    System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

    self.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
            {
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = true,
                    ValidateAudience = true,
                    ValidateLifetime = true,
                    ValidateIssuerSigningKey = true,
                    ValidIssuer = original.Issuer ?? string.Empty,
                    ValidAudience = original.Issuer ?? string.Empty,
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(original.Key ?? string.Empty))
                };
            });
}
  1. Look for ConfigureJWT method and add an event listener to configuration changes. This could be useful to track changes or do some particular action. In my case I’m going to advise if JWT configuration change runtime to avoid token validation error due to miss-configuration

    To do that I need to resolve DI, but I can’t inject this setting via constructor, so I’m going to use ServiceActivator class utils
/// <summary>
/// Configure JWT. use in <seealso cref="Startup.Configure(IApplicationBuilder, Microsoft.AspNetCore.Hosting.IWebHostEnvironment)"/>
/// </summary>
/// <param name="self"></param>
public static void ConfigureJWT(this IApplicationBuilder self)
{
    self.UseAuthentication();

    using (var serviceScope = ServiceActivator.GetScope())
    {
        ILoggerFactory loggerFactory = serviceScope.ServiceProvider.GetService<ILoggerFactory>();
        IOptionsMonitor<JwtConfiguration> option = (IOptionsMonitor<JwtConfiguration>)serviceScope.ServiceProvider.GetService(typeof(IOptionsMonitor<JwtConfiguration>));
                
        ILogger logger = loggerFactory.CreateLogger(typeof(RegisterJWT));

        option.OnChange((o, s) =>
        {
            if(original?.Issuer != option?.CurrentValue.Issuer || original?.Key != option?.CurrentValue.Key)
                logger.LogWarning($"JwtConfiguration changed. Restart application to apply changes or discard them");
            else
                logger.LogInformation($"JwtConfiguration reset to original.");
        });
    }
}

Use IOptionMonitor<T> via DI

You simply have to inject your option in your class constructor and use it via “CurrentValue” accessor, for example:

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace ASPNETCore.Services
{
    using ASPNETCore.Configuration;
    using ASPNETCore.Dto;

    /// <summary>
    /// Implements ILoginService
    /// </summary>
    public class LoginService: ILoginService
    {
        private readonly IConfiguration _config;
        private readonly ILogger<LoginService> _logger;
        private readonly IOptionsMonitor<JwtConfiguration> _options;

        /// <summary>
        /// ctor
        /// </summary>
        /// <param name="config"></param>
        /// <param name="logger"></param>
        /// <param name="options"></param>
        public LoginService(IConfiguration config, ILogger<LoginService> logger, IOptionsMonitor<JwtConfiguration> options)
        {
            _config = config;
            _logger = logger;
            _options = options;
        }

        // stuff...

        /// <summary>
        /// generate a basic JWT token
        /// </summary>
        /// <param name="user"></param>
        /// <returns></returns>
        public string GenerateJWT(UserLogin user)
        {
            var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.CurrentValue.Key));
            var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);

            List<Claim> claims = new List<Claim>
            {
                new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
            };

            var token = new JwtSecurityToken(_options.CurrentValue.Issuer,
              _options.CurrentValue.Issuer,
              claims,
              expires: DateTime.Now.AddMinutes(120),
              signingCredentials: credentials);

            return new JwtSecurityTokenHandler().WriteToken(token);
        }
    }
}

Source Code

Leave a Comment