Stockez universellement les paramètres d'application via IConfiguration

image

Dans le cadre du développement du produit Docs Security Suit, nous avons été confrontés à la tâche de stocker de nombreux types de paramètres d'application différents à la fois dans la base de données et dans les configurations. Et aussi pour qu'ils puissent être lus et écrits facilement. Ici, l'interface IConfiguration nous aidera, d'autant plus qu'elle est universelle et pratique à utiliser, ce qui vous permettra de stocker toutes sortes de paramètres en un seul endroit

Définition des tâches


Les applications ASP.Net Core ont désormais la possibilité de travailler avec les paramètres d'application via l'interface IConfiguration. De nombreux articles ont été écrits pour travailler avec lui. Cet article présentera l'expérience de l'utilisation d'IConfiguration pour stocker les paramètres de notre application, tels que les paramètres de connexion à un serveur LDAP, à un serveur SMTP, etc. L'objectif est de configurer le mécanisme existant pour travailler avec les configurations d'application afin de travailler avec la base de données. Dans cet article, vous ne trouverez pas de description de l'approche standard pour l'utilisation de l'interface.

L'architecture d'application est construite sur DDD en collaboration avec CQRS. De plus, nous savons que l'objet d'interface IConfiguration stocke tous les paramètres sous forme de paire clé-valeur. Par conséquent, nous avons d'abord décrit une certaine essence des paramètres du domaine sous cette forme:

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

Le projet utilise EF Core comme ORM. Et la migration est responsable de FluentMigrator.
Ajoutez une nouvelle entité à notre contexte:

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

Ensuite, pour notre nouvelle entité, nous devons décrire la configuration d'EF:

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

Et écrivez une migration pour cette entité:

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

Et où est la IConfiguration mentionnée?

Nous utilisons l'interface IConfigurationRoot


Notre projet a une application api basée sur ASP.NET Core MVC. Et par défaut, nous utilisons IConfiguration pour le stockage standard des paramètres d'application, par exemple pour la connexion à la base de données:

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

Ces paramètres sont stockés par défaut dans des variables d'environnement:

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

Et selon l'idée, nous pouvons utiliser cet objet pour stocker les paramètres prévus, mais ils se recouperont ensuite avec les paramètres généraux de l'application elle-même (comme mentionné ci-dessus - connexion à la base de données)

Afin de séparer les objets connectés dans la DI, nous avons décidé d'utiliser l'interface enfant IConfigurationRoot:

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

Lorsque vous le connectez au conteneur de notre service, nous pouvons travailler en toute sécurité avec un objet de paramètres configuré séparément, sans interférer avec les paramètres de l'application elle-même.

Cependant, notre objet dans le conteneur ne sait rien de notre essence dans le domaine et comment travailler avec la base de données.

image

Nous décrivons le nouveau fournisseur de configuration


Rappelons que notre tâche consiste à stocker les paramètres dans la base de données. Et pour cela, vous devez décrire le nouveau fournisseur de configuration IConfigurationRoot, hérité de ConfigurationProvider. Pour que le nouveau fournisseur fonctionne correctement, nous devons décrire la méthode de lecture à partir de la base de données - Load () et la méthode d'écriture dans la base de données - 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(); } } 

Ensuite, vous devez décrire une nouvelle source pour notre configuration qui implémente IConfigurationSource:

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

Et pour plus de simplicité, ajoutez l'extension à IConfigurationBuilder:

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

Maintenant, nous pouvons spécifier le fournisseur décrit par nous à l'endroit où nous connectons l'objet à la 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(); }); ... } 

Que nous ont apporté nos manipulations avec le nouveau fournisseur?

IConfigurationRoot Exemples


Tout d'abord, définissons un certain modèle Dto qui sera diffusé au client de notre application, par exemple, pour stocker les paramètres de connexion à ldap:

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

Depuis la «boîte», IConfiguration peut bien écrire et lire une instance d'un objet. Et pour travailler avec la collection, de petites améliorations sont nécessaires.

Pour stocker plusieurs objets du même type, nous avons écrit une extension pour 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(); } } } 

Ainsi, nous pouvons travailler avec plusieurs instances de nos paramètres.

Exemple d'écriture des paramètres dans la base de données


Comme mentionné ci-dessus, notre projet utilise l'approche CQRS. Pour écrire les paramètres, nous décrivons une commande simple:

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

Et puis le gestionnaire de notre équipe:

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

Par conséquent, nous pouvons écrire les données de nos paramètres LDAP dans la base de données sur une seule ligne conformément à la logique décrite.

Dans la base de données, nos paramètres ressemblent à ceci:

image

Exemple de lecture des paramètres de la base de données


Pour lire les paramètres LDAP, nous écrirons une requête simple:

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

Et puis le gestionnaire de notre demande:

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

Comme nous le voyons dans l'exemple, en utilisant la méthode Bind, nous remplissons notre objet ldapSettings avec des données de la base de données - par le nom LdapSettingsD, nous déterminons la clé (section) par laquelle nous devons recevoir les données, puis la méthode Load décrite dans notre fournisseur est appelée.

Et ensuite?


Et puis nous prévoyons d'ajouter toutes sortes de paramètres dans l'application à notre référentiel partagé.

Nous espérons que notre solution vous sera utile et vous partagerez vos questions et commentaires avec nous.

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


All Articles