Almacene universalmente la configuración de las aplicaciones a través de IConfiguration

imagen

Como parte del desarrollo del producto Docs Security Suit, nos enfrentamos a la tarea de almacenar muchos tipos diferentes de configuraciones de aplicaciones tanto en la base de datos como en las configuraciones. Y también para que puedan leer y escribir convenientemente. Aquí la interfaz IConfiguration nos ayudará, especialmente porque es universal y conveniente para usar, lo que le permitirá almacenar todo tipo de configuraciones en un solo lugar

Definición de tareas


Las aplicaciones ASP.Net Core ahora tienen la capacidad de trabajar con la configuración de la aplicación a través de la interfaz IConfiguration. Se han escrito muchos artículos sobre trabajar con él. Este artículo contará sobre la experiencia de usar IConfiguration para almacenar la configuración de nuestra aplicación, como la configuración para conectarse a un servidor LDAP, a un servidor SMTP, etc. El objetivo es configurar el mecanismo existente para trabajar con configuraciones de aplicaciones para trabajar con la base de datos. En este artículo no encontrará una descripción del enfoque estándar para usar la interfaz.

La arquitectura de la aplicación se basa en DDD junto con CQRS. Además, sabemos que el objeto de interfaz IConfiguration almacena todas las configuraciones como un par clave-valor. Por lo tanto, primero describimos una cierta esencia de la configuración en el dominio de esta forma:

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; } } 

El proyecto usa EF Core como ORM. Y la migración es responsable FluentMigrator.
Agregue una nueva entidad a nuestro contexto:

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

A continuación, para nuestra nueva entidad, necesitamos describir la configuración de EF:

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

Y escriba una migración para esta entidad:

 [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(); } } 

¿Y dónde está la IConfiguración mencionada?

Usamos la interfaz IConfigurationRoot


Nuestro proyecto tiene una aplicación api construida en ASP.NET Core MVC. Y de forma predeterminada, utilizamos IConfiguration para el almacenamiento estándar de la configuración de la aplicación, por ejemplo, para conectarse a la base de datos:

 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); ... } 

Estas configuraciones se almacenan por defecto en las variables de entorno:

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

Y de acuerdo con la idea, podemos usar este objeto para almacenar la configuración deseada, pero luego se cruzarán con la configuración general de la aplicación en sí (como se mencionó anteriormente, conectando a la base de datos)

Para separar los objetos conectados en el DI, decidimos usar la interfaz secundaria IConfigurationRoot:

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

Cuando lo conecta al contenedor de nuestro servicio, podemos trabajar de forma segura con un objeto de configuración configurado por separado, sin interferir con la configuración de la propia aplicación.

Sin embargo, nuestro objeto en el contenedor no sabe nada sobre nuestra esencia en el dominio y cómo trabajar con la base de datos.

imagen

Describimos el nuevo proveedor de configuración


Recuerde que nuestra tarea es almacenar la configuración en la base de datos. Y para esto debe describir el nuevo proveedor de configuración IConfigurationRoot, heredado de ConfigurationProvider. Para que el nuevo proveedor funcione correctamente, debemos describir el método de lectura de la base de datos - Load () y el método de escritura en la base de datos - 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(); } } 

A continuación, debe describir una nueva fuente para nuestra configuración que implemente IConfigurationSource:

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

Y para simplificar, agregue la extensión a IConfigurationBuilder:

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

Ahora, podemos especificar el proveedor que describimos en el lugar donde conectamos el objeto al 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(); }); ... } 

¿Qué nos dieron nuestras manipulaciones con el nuevo proveedor?

IConfigurationRoot Ejemplos


Primero, definamos un cierto modelo de Dto que se transmitirá al cliente de nuestra aplicación, por ejemplo, para almacenar la configuración para conectarse a ldap:

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

Desde el "cuadro" IConfiguration puede escribir y leer bien una instancia de un objeto. Y para trabajar con la colección, se necesitan pequeñas mejoras.

Para almacenar varios objetos del mismo tipo, escribimos una extensión para 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(); } } } 

Por lo tanto, podemos trabajar con varias instancias de nuestra configuración.

Ejemplo de configuración de escritura en la base de datos


Como se mencionó anteriormente, nuestro proyecto utiliza el enfoque CQRS. Para escribir la configuración, describimos un comando simple:

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

Y luego el manejador de nuestro equipo:

 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); } } 

Como resultado, podemos escribir los datos de nuestra configuración ldap en la base de datos en una línea de acuerdo con la lógica descrita.

En la base de datos, nuestra configuración se ve así:

imagen

Ejemplo de configuración de lectura de la base de datos


Para leer la configuración de ldap, escribiremos una consulta simple:

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

Y luego el manejador de nuestra solicitud:

 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); } } 

Como vemos en el ejemplo, usando el método Bind, llenamos nuestro objeto ldapSettings con datos de la base de datos; con el nombre LdapSettingsDto determinamos la clave (sección) por la que necesitamos recibir datos y luego se llama al método Load descrito en nuestro proveedor.

Que sigue


Y luego planeamos agregar todo tipo de configuraciones en la aplicación a nuestro repositorio compartido.

Esperamos que nuestra solución le sea útil y que comparta sus preguntas y comentarios con nosotros.

Source: https://habr.com/ru/post/473938/


All Articles