Armazenar universalmente as configurações do aplicativo por meio da configuração ICon

imagem

Como parte do desenvolvimento do produto Docs Security Suit, tivemos a tarefa de armazenar muitos tipos diferentes de configurações de aplicativos, tanto no banco de dados quanto nas configurações. E também para que eles possam ler e escrever convenientemente. Aqui, a interface IConfiguration nos ajudará, especialmente por ser universal e conveniente de usar, o que permitirá armazenar todos os tipos de configurações em um único local.

Definindo tarefas


Os aplicativos ASP.Net Core agora têm a capacidade de trabalhar com as configurações do aplicativo por meio da interface de configuração ICon. Muitos artigos foram escritos sobre como trabalhar com ele. Este artigo irá contar sobre a experiência do uso da configuração ICon para armazenar as configurações de nosso aplicativo, como configurações para conexão com um servidor LDAP, um servidor SMTP, etc. O objetivo é configurar o mecanismo existente para trabalhar com configurações de aplicativos para trabalhar com o banco de dados. Neste artigo, você não encontrará uma descrição da abordagem padrão para usar a interface.

A arquitetura do aplicativo é construída no DDD em conjunto com o CQRS. Além disso, sabemos que o objeto da interface IConfiguration armazena todas as configurações como um par de valores-chave. Portanto, primeiro descrevemos uma certa essência das configurações no domínio desta 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; } } 

O projeto usa o EF Core como ORM. E a migração é responsável pelo FluentMigrator.
Adicione uma nova entidade ao nosso contexto:

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

Em seguida, para nossa nova entidade, precisamos descrever a configuração do EF:

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

E escreva uma migração para esta entidade:

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

E onde está a configuração IC mencionada?

Usamos a interface IConfigurationRoot


Nosso projeto possui um aplicativo API desenvolvido no ASP.NET Core MVC. E, por padrão, usamos IConfiguration para o armazenamento padrão das configurações do aplicativo, por exemplo, conectando-se ao banco de dados:

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

Essas configurações são armazenadas por padrão nas variáveis ​​de ambiente:

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

E, de acordo com a ideia, podemos usar esse objeto para armazenar as configurações pretendidas, mas elas se cruzam com as configurações gerais do próprio aplicativo (como mencionado acima - conectando-se ao banco de dados)

Para separar os objetos conectados no DI, decidimos usar a interface filho IConfigurationRoot:

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

Quando você o conecta ao contêiner do nosso serviço, podemos trabalhar com segurança com um objeto de configurações definidas separadamente, sem interferir nas configurações do próprio aplicativo.

No entanto, nosso objeto no contêiner não sabe nada sobre nossa essência no domínio e como trabalhar com o banco de dados.

imagem

Descrevemos o novo provedor de configuração


Lembre-se de que nossa tarefa é armazenar as configurações no banco de dados. E para isso, você precisa descrever o novo provedor de configuração IConfigurationRoot, herdado do ConfigurationProvider. Para que o novo provedor funcione corretamente, devemos descrever o método de leitura do banco de dados - Load () e o método de gravação no banco de dados - 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(); } } 

Em seguida, você precisa descrever uma nova fonte para nossa configuração que implementa IConfigurationSource:

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

E, para simplificar, adicione a extensão ao IConfigurationBuilder:

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

Agora, podemos especificar o provedor descrito por nós no local em que conectamos o objeto ao 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(); }); ... } 

O que nossas manipulações com o novo provedor nos deram?

IConfigurationRoot Exemplos


Primeiro, vamos definir um determinado modelo Dto que será transmitido para o cliente do nosso aplicativo, por exemplo, para armazenar as configurações de conexão com o ldap:

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

Na "caixa", a IConfiguration pode escrever e ler bem uma instância de um objeto. E para trabalhar com a coleção, são necessárias pequenas melhorias.

Para armazenar vários objetos do mesmo tipo, escrevemos uma extensão 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(); } } } 

Assim, podemos trabalhar com várias instâncias de nossas configurações.

Exemplo de gravação de configurações no banco de dados


Como mencionado acima, nosso projeto usa a abordagem CQRS. Para escrever as configurações, descrevemos um comando simples:

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

E então o manipulador da nossa equipe:

 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 gravar os dados de nossas configurações de LDAP no banco de dados em uma linha, de acordo com a lógica descrita.

No banco de dados, nossas configurações são assim:

imagem

Exemplo de configurações de leitura do banco de dados


Para ler as configurações do ldap, escreveremos uma consulta simples:

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

E então o manipulador do nosso pedido:

 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 no exemplo, usando o método Bind, preenchemos nosso objeto ldapSettings com dados do banco de dados - pelo nome LdapSettingsD, para determinarmos a chave (seção) pela qual precisamos receber dados e o método Load descrito em nosso provedor é chamado.

O que vem depois?


E então planejamos adicionar todos os tipos de configurações no aplicativo ao nosso repositório compartilhado.

Esperamos que nossa solução seja útil para você e que você compartilhe suas perguntas e comentários conosco.

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


All Articles