Menyimpan pengaturan aplikasi secara universal melalui IConfiguration

gambar

Sebagai bagian dari pengembangan produk Setelan Keamanan Docs, kami dihadapkan dengan tugas menyimpan berbagai jenis pengaturan aplikasi baik di basis data maupun di konfigurasi. Dan juga agar mereka dapat dengan mudah dibaca dan ditulis. Di sini antarmuka IConfiguration akan membantu kami, terutama karena itu universal dan nyaman untuk digunakan, yang akan memungkinkan Anda untuk menyimpan semua jenis pengaturan di satu tempat

Mendefinisikan Tugas


Aplikasi Core ASP.Net sekarang memiliki kemampuan untuk bekerja dengan pengaturan aplikasi melalui antarmuka konfigurasi IC. Banyak artikel telah ditulis tentang bekerja dengannya. Artikel ini akan menceritakan tentang pengalaman menggunakan IConfiguration untuk menyimpan pengaturan aplikasi kita, seperti pengaturan untuk menghubungkan ke server LDAP, ke server SMTP, dll. Tujuannya adalah untuk mengkonfigurasi mekanisme yang ada untuk bekerja dengan konfigurasi aplikasi agar berfungsi dengan database. Pada artikel ini Anda tidak akan menemukan deskripsi pendekatan standar untuk menggunakan antarmuka.

Arsitektur aplikasi dibangun di atas DDD bersamaan dengan CQRS. Selain itu, kita tahu bahwa objek antarmuka konfigurasi IC menyimpan semua pengaturan dalam bentuk pasangan nilai kunci. Oleh karena itu, kami pertama kali menjelaskan esensi tertentu dari pengaturan pada domain dalam formulir ini:

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

Proyek ini menggunakan EF Core sebagai ORM. Dan migrasi itu adalah FluentMigrator yang bertanggung jawab.
Tambahkan entitas baru ke konteks kami:

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

Selanjutnya, untuk entitas baru kami, kami perlu menjelaskan konfigurasi EF:

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

Dan tulis migrasi untuk entitas ini:

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

Dan di mana konfigurasi IC yang disebutkan?

Kami menggunakan antarmuka IConfigurationRoot


Proyek kami memiliki aplikasi api yang dibangun di atas ASP.NET Core MVC. Dan secara default, kami menggunakan IConfiguration untuk penyimpanan standar pengaturan aplikasi, misalnya, menghubungkan ke 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); ... } 

Pengaturan ini disimpan secara default dalam variabel lingkungan:

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

Dan sesuai dengan ide, kita dapat menggunakan objek ini untuk menyimpan pengaturan yang dimaksud, tetapi kemudian mereka akan berpotongan dengan pengaturan umum aplikasi itu sendiri (seperti yang disebutkan di atas - menghubungkan ke database)

Untuk memisahkan objek yang terhubung di DI, kami memutuskan untuk menggunakan antarmuka anak IConfigurationRoot:

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

Saat Anda menghubungkannya ke wadah layanan kami, kami dapat bekerja dengan aman dengan objek pengaturan yang dikonfigurasi secara terpisah, tanpa mengganggu pengaturan aplikasi itu sendiri.

Namun, objek kami di wadah tidak tahu apa-apa tentang esensi kami di domain dan cara bekerja dengan database.

gambar

Kami menggambarkan penyedia konfigurasi baru


Ingatlah bahwa tugas kita adalah menyimpan pengaturan dalam basis data. Dan untuk ini, Anda perlu menggambarkan penyedia konfigurasi baru IConfigurationRoot, yang diwarisi dari ConfigurationProvider. Agar penyedia baru berfungsi dengan benar, kita harus menjelaskan metode membaca dari database - Load () dan metode penulisan ke 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(); } } 

Selanjutnya, Anda perlu menjelaskan sumber baru untuk konfigurasi kami yang mengimplementasikan IConfigurationSource:

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

Dan untuk kesederhanaan, tambahkan ekstensi ke IConfigurationBuilder:

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

Sekarang, kita dapat menentukan penyedia yang dijelaskan oleh kami di tempat kami menghubungkan objek ke 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(); }); ... } 

Apa yang manipulasi kami dengan penyedia baru berikan kepada kami?

Contoh IConfigurationRoot


Pertama, mari kita tentukan model Dto tertentu yang akan disiarkan ke klien aplikasi kita, misalnya, untuk menyimpan pengaturan untuk menghubungkan ke ldap:

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

Dari "box" IConfiguration dapat menulis dan membaca satu instance dari objek dengan baik. Dan untuk bekerja dengan koleksinya, perbaikan kecil diperlukan.

Untuk menyimpan beberapa objek dengan tipe yang sama, kami menulis ekstensi untuk 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(); } } } 

Dengan demikian, kita dapat bekerja dengan beberapa contoh pengaturan kita.

Contoh pengaturan penulisan ke database


Seperti disebutkan di atas, proyek kami menggunakan pendekatan CQRS. Untuk menulis pengaturan, kami menjelaskan perintah sederhana:

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

Dan kemudian pawang tim kami:

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

Sebagai hasilnya, kita dapat menulis data pengaturan ldap kita di database dalam satu baris sesuai dengan logika yang dijelaskan.

Dalam database, pengaturan kami terlihat seperti ini:

gambar

Contoh pengaturan membaca dari database


Untuk membaca pengaturan ldap, kami akan menulis kueri sederhana:

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

Dan kemudian pawang dari permintaan kami:

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

Seperti yang kita lihat dari contoh, menggunakan metode Bind, kita mengisi objek ldapSettings kita dengan data dari database - dengan nama LdapSettingsDUntuk kita menentukan kunci (bagian) di mana kita perlu menerima data dan kemudian metode Load yang dijelaskan dalam penyedia kita dipanggil.

Apa selanjutnya


Dan kemudian kami berencana untuk menambahkan segala macam pengaturan dalam aplikasi ke repositori bersama kami.

Kami berharap bahwa solusi kami akan bermanfaat bagi Anda dan Anda akan membagikan pertanyaan dan komentar Anda kepada kami.

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


All Articles