Universally store application settings through IConfiguration

image






As part of the development of the Docs Security Suit product, we were faced with the task of storing many different types of application settings both in the database and in the configs. And also so that they can be conveniently read and write. Here the IConfiguration interface will help us, especially since it is universal and convenient for use, which will allow you to store all kinds of settings in one place



Defining Tasks



ASP.Net Core applications now have the ability to work with application settings through the IConfiguration interface. A lot of articles have been written on working with him. This article will tell about the experience of using IConfiguration to store the settings of our application, such as settings for connecting to an LDAP server, to an SMTP server, etc. The goal is to configure the existing mechanism for working with application configurations to work with the database. In this article you will not find a description of the standard approach for using the interface.



The application architecture is built on DDD in conjunction with CQRS. In addition, we know that the IConfiguration interface object stores all settings as a key-value pair. Therefore, we first described a certain essence of the settings on the domain in this form:



public class Settings: Entity { public string Key { get; private set; } public string Value { get; private set; } protected Settings() { } public Settings(string key, string value) { Key = key; SetValue(value); } public void SetValue(string value) { Value = value; } }
      
      





The project uses EF Core as an ORM. And the migration is responsible FluentMigrator.

Add a new entity to our context:



 public class MyContext : DbContext { public MyContext(DbContextOptions options) : base(options) { } public DbSet<Settings> Settings { get; set; } … }
      
      





Next, for our new entity, we need to describe the configuration of EF:



 internal class SettingsConfiguration : IEntityTypeConfiguration<Settings> { public void Configure(EntityTypeBuilder<Settings> builder) { builder.ToTable("Settings"); } }
      
      





And write a migration for this entity:



 [Migration(2019020101)] public class AddSettings: AutoReversingMigration { public override void Up() { Create.Table("Settings") .WithColumn(nameof(Settings.Id)).AsInt32().PrimaryKey().Identity() .WithColumn(nameof(Settings.Key)).AsString().Unique() .WithColumn(nameof(Settings.Value)).AsString(); } }
      
      





And where is the mentioned IConfiguration?



We use the IConfigurationRoot interface



Our project has an api application built on ASP.NET Core MVC. And by default, we use IConfiguration for the standard storage of application settings, for example, connecting to the database:



 public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { void OptionsAction(DbContextOptionsBuilder options) => options.UseSqlServer(Configuration.GetConnectionString("MyDatabase")); services.AddDbContext<MyContext>(OptionsAction); ... }
      
      





These settings are stored by default in environment variables:



 public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .ConfigureAppConfiguration((hostingContext, config) => { config.AddEnvironmentVariables(); })
      
      





And according to the idea, we can use this object to store the intended settings, but then they will intersect with the general settings of the application itself (as mentioned above - connecting to the database)



In order to separate the connected objects in the DI, we decided to use the IConfigurationRoot child interface:



 public void ConfigureServices(IServiceCollection services) { services.AddScoped<IConfigurationRoot>(); ... }
      
      





When connecting it to the container of our service, we can safely work with a separately configured settings object, without interfering with the settings of the application itself.



However, our object in the container does not know anything about our essence in the domain and how to work with the database.



image



We describe the new configuration provider



Recall that our task is to store the settings in the database. And for this you need to describe the new configuration provider IConfigurationRoot, inherited from ConfigurationProvider. For the new provider to work correctly, we must describe the method of reading from the database - Load () and the method of writing to the database - Set ():



 public class EFSettingsProvider : ConfigurationProvider { public EFSettingsProvider(MyContext myContext) { _myContext = myContext; } private MyContext _myContext; public override void Load() { Data = _myContext.Settings.ToDictionary(c => c.Key, c => c.Value); } public override void Set(string key, string value) { base.Set(key, value); var configValues = new Dictionary<string, string> { { key, value } }; var val = _myContext.Settings.FirstOrDefault(v => v.Key == key); if (val != null && val.Value.Any()) val.SetValue(value); else _myContext.Settings.AddRange(configValues .Select(kvp => new Settings(kvp.Key, kvp.Value)) .ToArray()); _myContext.SaveChanges(); } }
      
      





Next, you need to describe a new source for our configuration that implements IConfigurationSource:



 public class EFSettingsSource : IConfigurationSource { private DssContext _dssContext; public EFSettingSource(MyContext myContext) { _myContext = myContext; } public IConfigurationProvider Build(IConfigurationBuilder builder) { return new EFSettingsProvider(_myContext); } }
      
      





And for simplicity, add the extension to IConfigurationBuilder:



 public static IConfigurationBuilder AddEFConfiguration( this IConfigurationBuilder builder, MyContext myContext) { return builder.Add(new EFSettingSource(myContext)); }
      
      





Now, we can specify the provider described by us in the place where we connect the object to the DI:



 public void ConfigureServices(IServiceCollection services) { services.AddScoped<IConfigurationRoot>(provider => { var myContext = provider.GetService<MyContext>(); var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddEFConfiguration(myContext); return configurationBuilder.Build(); }); ... }
      
      





What did our manipulations with the new provider give us?



IConfigurationRoot Examples



First, let's define a certain Dto model that will be broadcast to the client of our application, for example, to store the settings for connecting to ldap:



 public class LdapSettingsDto { public int Id { get; set; } public string UserName { get; set; } public string Password { get; set; } public string Address { get; set; } }
      
      





From the “box” IConfiguration can write and read one instance of an object well. And to work with the collection, small improvements are needed.



To store several objects of the same type, we wrote an extension for IConfigurationRoot:



 public static void SetDataFromObjectProperties(this IConfigurationRoot config, object obj, string indexProperty = "Id") { //   var type = obj.GetType(); int id; try { //   id = int.Parse(type.GetProperty(indexProperty).GetValue(obj).ToString()); } catch (Exception ex) { throw new Exception($"   {indexProperty}  {type.Name}", ex.InnerException); } //   0,            indexProperty if (id == 0) { var maxId = config.GetSection(type.Name) .GetChildren().SelectMany(x => x.GetChildren()); var mm = maxId .Where(c => c.Key == indexProperty) .Select(v => int.Parse(v.Value)) .DefaultIfEmpty() .Max(); id = mm + 1; try { type.GetProperty(indexProperty).SetValue(obj, id); } catch (Exception ex) { throw new Exception($"   {indexProperty}  {type.Name}", ex.InnerException); } } //         foreach (var field in type.GetProperties()) { var key = $"{type.Name}:{id.ToString()}:{field.Name}"; if (!string.IsNullOrEmpty(field.GetValue(obj)?.ToString())) { config[key] = field.GetValue(obj).ToString(); } } }
      
      





Thus, we can work with several instances of our settings.



Example of writing settings to the database



As mentioned above, our project uses the CQRS approach. To write the settings, we describe a simple command:



 public class AddLdapSettingsCommand : IRequest<ICommandResult> { public LdapSettingsDto LdapSettings { get; } public AddLdapSettingsCommand(LdapSettingsDto ldapSettings) { LdapSettings = ldapSettings; } }
      
      





And then the handler of our team:



 public class AddLdapSettingsCommandHandler : IRequestHandler<AddLdapSettingsCommand, ICommandResult> { private readonly IConfigurationRoot _settings; public AddLdapSettingsCommandHandler(IConfigurationRoot settings) { _settings = settings; } public async Task<ICommandResult> Handle(AddLdapSettingsCommand request, CancellationToken cancellationToken) { try { _settings.SetDataFromObjectProperties(request.LdapSettings); } catch (Exception ex) { return CommandResult.Exception(ex.Message, ex); } return await Task.Run(() => CommandResult.Success, cancellationToken); } }
      
      





As a result, we can write the data of our ldap settings in the database in one line in accordance with the described logic.



In the database, our settings look like this:



image



Example of reading settings from the database



To read the ldap settings we will write a simple query:



 public class GetLdapSettingsByIdQuery : IRequest<LdapSettingsDto> { public int Id { get; } public GetLdapSettingsByIdQuery(int id) { Id = id; } }
      
      





And then the handler of our request:



 public class GetLdapSettingsByIdQueryHandler : IRequestHandler<GetLdapSettingsByIdQuery, LdapSettingsDto> { private readonly IConfigurationRoot _settings; public GetLdapSettingsByIdQueryHandler(IConfigurationRoot settings) { _settings = settings; } public async Task<LdapSettingsDto> Handle(GetLdapSettingsByIdQuery request, CancellationToken cancellationToken) { var ldapSettings = new List<LdapSettingsDto>(); _settings.Bind(nameof(LdapSettingsDto), ldapSettings); var ldapSettingsDto = ldapSettings.FirstOrDefault(ls => ls.Id == request.Id); return await Task.Run(() => ldapSettingsDto, cancellationToken); } }
      
      





As we see from the example, using the Bind method, we fill our ldapSettings object with data from the database - by the name LdapSettingsDto we determine the key (section) by which we need to obtain data and then the Load method described in our provider is called.



What next?



And then we plan to add all sorts of settings in the application to our shared repository.



We hope that our solution will be useful to you and you will share your questions and comments with us.



All Articles