Speichern Sie Anwendungseinstellungen universell über IConfiguration

Bild

Im Rahmen der Entwicklung des Docs Security Suit-Produkts standen wir vor der Aufgabe, viele verschiedene Arten von Anwendungseinstellungen sowohl in der Datenbank als auch in den Konfigurationen zu speichern. Und damit sie bequem gelesen und geschrieben werden können. Hier hilft uns die IConfiguration-Oberfläche, zumal sie universell und bequem zu bedienen ist und es Ihnen ermöglicht, alle Arten von Einstellungen an einem Ort zu speichern

Aufgaben definieren


ASP.Net Core-Anwendungen können jetzt über die IConfiguration-Schnittstelle mit Anwendungseinstellungen arbeiten. Es wurden viele Artikel über die Arbeit mit ihm geschrieben. In diesem Artikel wird über die Erfahrungen mit der Verwendung von IConfiguration zum Speichern der Einstellungen unserer Anwendung berichtet, z. B. Einstellungen für die Verbindung mit einem LDAP-Server, einem SMTP-Server usw. Ziel ist es, den vorhandenen Mechanismus für die Arbeit mit Anwendungskonfigurationen für die Arbeit mit der Datenbank zu konfigurieren. In diesem Artikel finden Sie keine Beschreibung des Standardansatzes für die Verwendung der Schnittstelle.

Die Anwendungsarchitektur basiert auf DDD in Verbindung mit CQRS. Außerdem wissen wir, dass das IConfiguration-Schnittstellenobjekt alle Einstellungen in Form eines Schlüssel-Wert-Paares speichert. Daher haben wir zunächst eine bestimmte Essenz der Einstellungen in der Domäne in dieser Form beschrieben:

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

Das Projekt verwendet EF Core als ORM. Und die Migration ist verantwortlich für FluentMigrator.
Fügen Sie unserem Kontext eine neue Entität hinzu:

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

Als nächstes müssen wir für unsere neue Entität die Konfiguration von EF beschreiben:

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

Und schreiben Sie eine Migration für diese Entität:

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

Und wo ist die erwähnte IConfiguration?

Wir verwenden die IConfigurationRoot-Schnittstelle


Unser Projekt verfügt über eine API-Anwendung, die auf ASP.NET Core MVC basiert. Standardmäßig verwenden wir IConfiguration für die Standardspeicherung von Anwendungseinstellungen, z. B. für die Verbindung zur Datenbank:

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

Diese Einstellungen werden standardmäßig in Umgebungsvariablen gespeichert:

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

Und gemäß der Idee können wir dieses Objekt verwenden, um die beabsichtigten Einstellungen zu speichern, aber dann überschneiden sie sich mit den allgemeinen Einstellungen der Anwendung selbst (wie oben erwähnt - Verbindung zur Datenbank herstellen)

Um die verbundenen Objekte im DI zu trennen, haben wir uns für die untergeordnete IConfigurationRoot-Schnittstelle entschieden:

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

Wenn wir es mit dem Container unseres Dienstes verbinden, können wir sicher mit einem separat konfigurierten Einstellungsobjekt arbeiten, ohne die Einstellungen der Anwendung selbst zu beeinträchtigen.

Unser Objekt im Container weiß jedoch nichts über unser Wesen in der Domäne und wie man mit der Datenbank arbeitet.

Bild

Wir beschreiben den neuen Konfigurationsanbieter


Denken Sie daran, dass unsere Aufgabe darin besteht, die Einstellungen in der Datenbank zu speichern. Dazu müssen Sie den neuen Konfigurationsanbieter IConfigurationRoot beschreiben, der von ConfigurationProvider geerbt wurde. Damit der neue Anbieter ordnungsgemäß funktioniert, müssen wir die Methode zum Lesen aus der Datenbank - Load () und die Methode zum Schreiben in die Datenbank - Set () beschreiben:

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

Als Nächstes müssen Sie eine neue Quelle für unsere Konfiguration beschreiben, die IConfigurationSource implementiert:

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

Fügen Sie der Einfachheit halber die Erweiterung zu IConfigurationBuilder hinzu:

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

Jetzt können wir den von uns beschriebenen Anbieter an der Stelle angeben, an der wir das Objekt mit dem DI verbinden:

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

Was haben uns unsere Manipulationen mit dem neuen Anbieter gebracht?

IConfigurationRoot-Beispiele


Definieren wir zunächst ein bestimmtes Dto-Modell, das an den Client unserer Anwendung gesendet wird, um beispielsweise die Einstellungen für die Verbindung zu ldap zu speichern:

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

Aus der "Box" kann IConfiguration eine Instanz eines Objekts gut schreiben und lesen. Und um mit der Sammlung arbeiten zu können, sind kleine Verbesserungen erforderlich.

Um mehrere Objekte desselben Typs zu speichern, haben wir eine Erweiterung für IConfigurationRoot geschrieben:

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

Somit können wir mit mehreren Instanzen unserer Einstellungen arbeiten.

Beispiel für das Schreiben von Einstellungen in die Datenbank


Wie oben erwähnt, verwendet unser Projekt den CQRS-Ansatz. Um die Einstellungen zu schreiben, beschreiben wir einen einfachen Befehl:

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

Und dann der Handler unseres Teams:

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

Infolgedessen können wir die Daten unserer ldap-Einstellungen gemäß der beschriebenen Logik in einer Zeile in die Datenbank schreiben.

In der Datenbank sehen unsere Einstellungen folgendermaßen aus:

Bild

Beispiel für das Lesen von Einstellungen aus der Datenbank


Um die ldap-Einstellungen zu lesen, schreiben wir eine einfache Abfrage:

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

Und dann der Bearbeiter unserer Anfrage:

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

Wie wir aus dem Beispiel sehen, füllen wir unser ldapSettings-Objekt mit der Bind-Methode mit Daten aus der Datenbank - unter dem Namen LdapSettingsDto bestimmen wir den Schlüssel (Abschnitt), über den wir Daten empfangen müssen, und dann wird die in unserem Provider beschriebene Load-Methode aufgerufen.

Was kommt als nächstes?


Und dann planen wir, alle Arten von Einstellungen in der Anwendung zu unserem freigegebenen Repository hinzuzufügen.

Wir hoffen, dass unsere Lösung für Sie nützlich ist und Sie Ihre Fragen und Kommentare mit uns teilen.

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


All Articles