عالميا تخزين إعدادات التطبيق من خلال IConfiguration

صورة

كجزء من تطوير منتج Docs Security Suit ، واجهنا مهمة تخزين العديد من أنواع مختلفة من إعدادات التطبيق في قاعدة البيانات والتكوينات. وكذلك بحيث يمكن قراءتها بسهولة وكتابتها. هنا ستساعدنا واجهة IConfiguration ، خاصةً أنها عالمية ومريحة للاستخدام ، مما سيتيح لك تخزين جميع أنواع الإعدادات في مكان واحد

تحديد المهام


تمتلك تطبيقات ASP.Net Core الآن القدرة على العمل مع إعدادات التطبيق من خلال واجهة IConfiguration. لقد كتب الكثير من المقالات حول العمل معه. تشرح هذه المقالة تجربة استخدام IConfiguration لتخزين إعدادات تطبيقنا ، مثل إعدادات الاتصال بخادم LDAP أو خادم SMTP ، إلخ. الهدف هو تكوين الآلية الحالية للعمل مع تكوينات التطبيق للعمل مع قاعدة البيانات. في هذه المقالة ، لن تجد وصفًا للنهج القياسي لاستخدام الواجهة.

تم بناء بنية التطبيق على DDD بالتزامن مع CQRS. بالإضافة إلى ذلك ، نحن نعلم أن كائن واجهة IConfiguration يخزن كل الإعدادات كزوج قيمة أساسية. لذلك ، قمنا أولاً بوصف جوهر معين للإعدادات على المجال في هذا النموذج:

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

يستخدم المشروع EF Core كـ ORM. والهجرة مسؤولة FluentMigrator.
إضافة كيان جديد إلى سياقنا:

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

بعد ذلك ، بالنسبة لكياننا الجديد ، نحتاج إلى وصف تكوين EF:

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

واكتب عملية ترحيل لهذا الكيان:

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

وأين هو IConfiguration المذكورة؟

نحن نستخدم واجهة IConfigurationRoot


يحتوي مشروعنا على تطبيق api مبني على ASP.NET Core MVC. بشكل افتراضي ، نستخدم IConfiguration للتخزين القياسي لإعدادات التطبيق ، على سبيل المثال ، الاتصال بقاعدة البيانات:

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

يتم تخزين هذه الإعدادات بشكل افتراضي في متغيرات البيئة:

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

وفقًا للفكرة ، يمكننا استخدام هذا الكائن لتخزين الإعدادات المقصودة ، ولكن بعد ذلك سيتقاطعون مع الإعدادات العامة للتطبيق نفسه (كما هو مذكور أعلاه - الاتصال بقاعدة البيانات)

لفصل الكائنات المتصلة في DI ، قررنا استخدام الواجهة الفرعية IConfigurationRoot:

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

عندما تقوم بتوصيله إلى حاوية خدمتنا ، يمكننا العمل بأمان مع كائن إعدادات مكونة بشكل منفصل ، دون التدخل في إعدادات التطبيق نفسه.

ومع ذلك ، فإن كائننا في الحاوية لا يعرف شيئًا عن جوهرنا في المجال وكيفية العمل مع قاعدة البيانات.

صورة

نحن تصف مزود التكوين الجديد


أذكر أن مهمتنا هي تخزين الإعدادات في قاعدة البيانات. ولهذا تحتاج إلى وصف موفر التكوين الجديد IConfigurationRoot ، الموروثة من ConfigurationProvider. لكي يعمل الموفر الجديد بشكل صحيح ، يجب أن نصف طريقة القراءة من قاعدة البيانات - Load () وطريقة الكتابة إلى قاعدة البيانات - 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(); } } 

بعد ذلك ، تحتاج إلى وصف مصدر جديد للتكوين الخاص بنا الذي يقوم بتنفيذ IConfigurationSource:

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

وللبساطة ، أضف الامتداد إلى IConfigurationBuilder:

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

الآن ، يمكننا تحديد الموفر الذي وصفه لنا في المكان الذي نقوم فيه بتوصيل الكائن بـ 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(); }); ... } 

ماذا تعطينا التلاعب لدينا مع مزود جديد؟

IConfigurationRoot أمثلة


أولاً ، دعنا نحدد نموذج Dto معين سيتم بثه إلى عميل تطبيقنا ، على سبيل المثال ، لتخزين إعدادات الاتصال بـ ldap:

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

من "المربع" ، يمكن لـ IConfiguration أن يكتب ويقرأ مثيلًا لأحد الكائنات جيدًا. وللتعامل مع المجموعة تحتاج إلى تحسينات طفيفة.

لتخزين العديد من الكائنات من نفس النوع ، كتبنا امتدادًا لـ 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(); } } } 

وبالتالي ، يمكننا العمل مع العديد من مثيلات إعداداتنا.

مثال على إعدادات الكتابة في قاعدة البيانات


كما ذكر أعلاه ، يستخدم مشروعنا نهج CQRS. لكتابة الإعدادات ، نحن نصف أمرًا بسيطًا:

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

ثم معالج فريقنا:

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

نتيجة لذلك ، يمكننا كتابة بيانات إعدادات ldap الخاصة بنا في قاعدة البيانات في سطر واحد وفقًا للمنطق الموصوف.

في قاعدة البيانات ، تبدو إعداداتنا كما يلي:

صورة

مثال على قراءة الإعدادات من قاعدة البيانات


لقراءة إعدادات ldap ، سنكتب استعلامًا بسيطًا:

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

ثم معالج طلبنا:

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

كما نرى من المثال ، باستخدام طريقة الربط ، نحن نملأ كائن ldapSettings الخاص بنا بالبيانات من قاعدة البيانات - بالاسم LdapSettingsDto نحدد المفتاح (القسم) الذي نحتاج إليه لاستلام البيانات ثم تسمى طريقة التحميل الموصوفة في مزودنا.

ماذا بعد؟


ثم نخطط لإضافة جميع أنواع الإعدادات في التطبيق إلى مستودعنا المشترك.

نأمل أن يكون حلنا مفيدًا لك وأن تشارك أسئلتك وتعليقاتك معنا.

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


All Articles