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 codehttps://docs.microsoft.com/it-it/aspnet/core/fundamentals/configuration/options?view=aspnetcore-3.1
Table of content
- Prerequisites
- Related articles
- Some bla bla
- Class configuration mapping
- Bind class mapping to appsetting.json value
- Source Code
Prerequisites
- A project targets to netcoreapp3.1 😉
- A way to resolve injected services manually (Resolving instances with ASP.NET Core DI in static classes)
- A configuration file to map
Related articles
- How to build ASP.NET Core application with JWT authentication
- How to build GitHub project with Azure DevOps
- How to add Swagger to your ASP.NET core 3 project
- NLog: an ASP.NET Core logger provider
- Resolving instances with ASP.NET Core DI in static classes
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!&dC95Utjk#xVp8yTxWNUMqd%f*4T$kMuk!jAJMyUVg+R=+3EkL374$Q4vJ=&2$HRV&fu3sZxFZ6S?M+SZK*bM*Nq^KpphsguWczRR*MjcHCbRkQ$V#BDKykQW=e%H=L&5FYf%$3jbK^gGrWvXnYkCM5duj!A6A#EhUjp*aQWnP2NYRBVDs8xNYNwmJ+8jmwucG*qc$fZdk3VGKR@_9x@xK7BAK$#aSq9_zMqFTeP=cE$VqrqMks4DBzgtbCTQVAVEeg%wRYfg9qY=C$ge8v!Dr*ZfRbEsNv?pvrjCnK+jqgZXYpXgnettxpsq3_!-2KFRdGuKWu@6&Szm&2c*2**^R!mzwtXWUg#Ce%DW*=G7^#n?A+3#26BPCgvHSfuk$-#kSGVXh4#!a_VWts&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
- Create a folder called “Configuration” which contains all class that maps your appsetting.json configuration sections.
- 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 { } }
- 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"; } }
- 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; } } }
- Create a method extension that allows you to resolve configuration binding in different way when DI is not available yet; we use it later
- Create a folder called “MethodExtensions”
- 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 typeT
, to be sure that is a class that is projected to bind a configuration section
Bind class mapping to appsetting.json value
- 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); }
- Go back to
ConfigureJWTService
and bind setting to their class binder - 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)) }; }); }
- 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 useServiceActivator
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); } } }