Bagaimana kami menerjemahkan konfigurasi layanan kami dari XML ke YAML

Latar belakang


Perusahaan kami, antara lain, telah mengembangkan beberapa layanan (lebih tepatnya - 12) yang berfungsi sebagai pendukung sistem kami. Setiap layanan adalah layanan Windows dan melakukan tugas spesifiknya.

Saya ingin mentransfer semua layanan ini ke * nix-OS. Untuk melakukan ini, tinggalkan pembungkus dalam bentuk layanan Windows dan beralih dari .NET Framework ke .NET Standard.

Persyaratan terakhir mengarah pada kebutuhan untuk menyingkirkan beberapa kode-Legacy, yang tidak didukung dalam .NET Standard, termasuk dari dukungan untuk mengonfigurasi server kami melalui XML, diimplementasikan menggunakan kelas dari System.Configuration. Pada saat yang sama, ini menyelesaikan masalah lama terkait dengan fakta bahwa dalam konfigurasi-XML kami membuat kesalahan dari waktu ke waktu ketika mengubah pengaturan (misalnya, kadang-kadang kami menempatkan tag penutup di tempat yang salah atau lupa sama sekali), tetapi pembaca yang luar biasa dari System.Xml XML-configs. XmlDocument secara diam-diam menelan konfigurasi tersebut, menghasilkan hasil yang benar-benar tidak dapat diprediksi.

Diputuskan untuk beralih ke konfigurasi melalui YAML yang trendi. Masalah apa yang kita hadapi dan bagaimana kita mengatasinya? Dalam artikel ini.

Apa yang kita miliki


Bagaimana kita membaca konfigurasi dari XML


Kami membaca XML dengan cara standar untuk sebagian besar proyek lainnya.

Setiap layanan memiliki file pengaturan untuk proyek .NET, yang disebut AppSettings.cs, yang berisi semua pengaturan yang diperlukan oleh layanan. Sesuatu seperti ini:

[System.Configuration.SettingsProvider(typeof(PortableSettingsProvider))] internal sealed partial class AppSettings : IServerManagerConfigStorage, IWebSettingsStorage, IServerSettingsStorage, IGraphiteAddressStorage, IDatabaseConfigStorage, IBlackListStorage, IKeyCloackConfigFilePathProvider, IPrometheusSettingsStorage, IMetricsConfig { } 


Teknik serupa untuk memisahkan pengaturan ke antarmuka membuatnya nyaman untuk menggunakannya nanti melalui wadah DI.

Semua keajaiban utama pengaturan penyimpanan sebenarnya tersembunyi di PortableSettingsProvider (lihat atribut kelas), serta dalam file desainer AppSettings.Designer.cs:

 [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "14.0.0.0")] internal sealed partial class AppSettings : global::System.Configuration.ApplicationSettingsBase { private static AppSettings defaultInstance = ((AppSettings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new AppSettings()))); public static AppSettings Default { get { return defaultInstance; } } [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Configuration.DefaultSettingValueAttribute("35016")] public int ListenPort { get { return ((int)(this["ListenPort"])); } set { this["ListenPort"] = value; } } ... 

Seperti yang Anda lihat, "di belakang layar" disembunyikan semua properti yang kami tambahkan ke konfigurasi server ketika kami mengeditnya melalui desainer pengaturan di Visual Studio.

Kelas PortableSettingsProvider kami, yang disebutkan di atas, langsung membaca file XML, dan hasil baca sudah digunakan di SettingsProvider untuk menulis pengaturan ke properti AppSettings.

Contoh konfigurasi XML yang sedang kita baca (sebagian besar pengaturan disembunyikan karena alasan keamanan):

 <?xml version="1.0" encoding="utf-8"?> <configuration> <configSections> <sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup"> <section name="MetricServer.Properties.Settings" type="System.Configuration.ClientSettingsSection" /> </sectionGroup> </configSections> <userSettings> <MetricServer.Properties.Settings> <setting name="MCXSettings" serializeAs="String"> <value>Inactive, ChartLen: 1000, PrintLen: 50, UseProxy: False</value> </setting> <setting name="KickUnknownAfter" serializeAs="String"> <value>00:00:10</value> </setting> ... </MetricServer.Properties.Settings> </userSettings> </configuration> 

File YAML apa yang ingin saya baca


Sesuatu seperti ini:

 VirtualFeed: MaxChartHistoryLength: 10 Port: 35016 UseThrottling: True ThrottlingIntervalMs: 50000 UseHistoryBroadcast: True CalendarName: "EmptyCalendar" UsMarketFeed: UseImbalances: True 

Masalah transisi


Pertama, konfigurasi dalam XML "flat", tetapi di YAML tidak (bagian dan subbagian didukung). Ini terlihat jelas dalam contoh di atas. Dengan menggunakan XML, kami memecahkan masalah pengaturan datar dengan memperkenalkan parser kami sendiri yang dapat mengubah string tipe tertentu ke dalam kelas kami yang lebih kompleks. Contoh dari string yang sedemikian kompleks:

 <setting name="MCXSettings" serializeAs="String"> <value>Inactive, ChartLen: 1000, PrintLen: 50, UseProxy: False</value> </setting> 

Saya tidak merasa ingin melakukan transformasi seperti itu ketika bekerja dengan YAML. Tetapi pada saat yang sama, kita dibatasi oleh struktur "datar" yang ada dari kelas AppSettings: semua properti pengaturan di dalamnya ditumpuk dalam satu tumpukan.

Kedua, konfigurasi server kami bukan monolit statis, kami mengubahnya dari waktu ke waktu tepat saat pekerjaan server, mis. perubahan ini harus mampu menangkap dengan cepat, dalam runtime. Untuk melakukan ini, dalam implementasi XML, kami mewarisi AppSettings dari INotifyPropertyChanged (pada kenyataannya, setiap antarmuka yang mengimplementasikan AppSettings diwarisi darinya) dan berlangganan pembaruan pengaturan properti peristiwa. Pendekatan ini bekerja karena System.Configuration.ApplicationSettingsBase keluar dari kotak mengimplementasikan INotifyPropertyChanged. Perilaku serupa harus dipertahankan setelah transisi ke YAML.

Ketiga, kita sebenarnya tidak memiliki satu file konfigurasi untuk setiap server, tetapi dua file konfigurasi: satu dengan pengaturan default, yang lainnya dengan yang ditimpa. Ini diperlukan agar dalam setiap beberapa contoh server dari jenis yang sama, mendengarkan port yang berbeda dan memiliki pengaturan yang sedikit berbeda, Anda tidak harus menyalin seluruh set pengaturan.

Dan satu masalah lagi - akses ke pengaturan tidak hanya melalui antarmuka, tetapi juga dengan akses langsung ke AppSettings.Default. Biarkan saya mengingatkan Anda bagaimana ini dinyatakan di belakang panggung AppSettings.Designer.cs:

 private static AppSettings defaultInstance = ((AppSettings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new AppSettings()))); public static AppSettings Default { get { return defaultInstance; } } 

Berdasarkan hal tersebut di atas, perlu untuk datang dengan pendekatan baru untuk menyimpan pengaturan di AppSettings.

Solusi


Toolkit


Untuk membaca langsung, YAML memutuskan untuk menggunakan perpustakaan siap pakai yang tersedia melalui NuGet:

  • YamlDotNet - github.com/aaubry/YamlDotNet . Dari deskripsi perpustakaan (terjemahan):
    YamlDotNet adalah perpustakaan .NET untuk YAML. YamlDotNet menyediakan parser tingkat rendah dan generator YAML, serta model objek tingkat tinggi yang mirip dengan XmlDocument. Juga termasuk pustaka serialisasi yang memungkinkan Anda membaca dan menulis objek dari / ke stream YAML.

  • NetEscapades.Configuration - github.com/andrewlock/NetEscapades.Configuration . Ini adalah penyedia konfigurasi itu sendiri (dalam arti Microsoft.Extensions.Configuration.IConfigurationSource, yang secara aktif digunakan dalam aplikasi ASP.NET Core), yang membaca file-file YAML hanya menggunakan yang disebutkan di atas YamlDotNet.

Baca lebih lanjut tentang cara menggunakan perpustakaan ini di sini .

Transisi ke YAML


Transisi itu sendiri dilakukan dalam dua tahap: pada awalnya kami hanya beralih dari XML ke YAML, tetapi mempertahankan hierarki datar file konfigurasi, dan kemudian kami memasukkan bagian dalam file YAML. Tahap-tahap ini pada prinsipnya dapat digabungkan menjadi satu, dan untuk penyederhanaan presentasi saya akan melakukan hal itu. Semua tindakan yang dijelaskan di bawah ini diterapkan secara berurutan untuk setiap layanan.

Mempersiapkan file YML


Pertama, Anda perlu menyiapkan file YAML itu sendiri. Kami menyebutnya nama proyek (berguna untuk tes integrasi di masa depan, yang seharusnya dapat bekerja dengan server yang berbeda dan membedakan konfigurasi mereka di antara mereka sendiri), menempatkan file secara langsung di root proyek, di sebelah AppSettings:



Dalam file YML, sebagai permulaan, mari kita simpan struktur "flat":

 VirtualFeed: "MaxChartHistoryLength: 10, UseThrottling: True, ThrottlingIntervalMs: 50000, UseHistoryBroadcast: True, CalendarName: EmptyCalendar" VirtualFeedPort: 35016 UsMarketFeedUseImbalances: True 

Mengisi Pengaturan Aplikasi dengan Properti Pengaturan


Kami mentransfer semua properti dari AppSettings.Designer.cs ke AppSettings.cs, secara bersamaan menyingkirkan atribut berlebihan dari desainer dan kode itu sendiri di get / set-parts.

Itu:

 [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Configuration.DefaultSettingValueAttribute("35016")] public int VirtualFeedPort{ get { return ((int)(this["VirtualFeedPort"])); } set { this["VirtualFeedPort"] = value; } } 

Itu menjadi:

 public int VirtualFeedPort { get; set; } 

Kami sepenuhnya menghapus AppSettings .Designer .cs sebagai tidak perlu. Sekarang, omong-omong, Anda dapat sepenuhnya menghapus bagian userSettings di file app.config, jika ada dalam proyek - pengaturan default yang sama disimpan di sana, yang kami tentukan melalui desainer pengaturan.
Silakan.

Kontrol pengaturan dengan cepat


Karena kita harus dapat menangkap pembaruan dari pengaturan kita dalam runtime, kita perlu mengimplementasikan INotifyPropertyChanged di AppSettings kami. System.Configuration.ApplicationSettingsBase dasar tidak lagi ada, masing-masing, Anda tidak dapat mengandalkan sihir apa pun.

Anda dapat mengimplementasikannya "di dahi": dengan menambahkan implementasi metode yang melempar acara yang diinginkan dan memanggilnya di setter masing-masing properti. Tapi ini adalah baris kode tambahan, yang juga perlu disalin di semua layanan.

Mari kita lakukan dengan lebih baik - perkenalkan AutoNotifier kelas dasar tambahan, yang sebenarnya melakukan hal yang sama, tetapi di belakang layar, sama seperti System.Configuration.ApplicationSettingsBase lakukan sebelumnya:

 /// <summary> /// Implements <see cref="INotifyPropertyChanged"/> for classes with a lot of public properties (ie AppSettings). /// This implementation is: /// - fairly slow, so don't use it for classes where getting/setting of properties is often operation; /// - not for properties described in inherited classes of 2nd level (bad idea: Inherit2 -> Inherit1 -> AutoNotifier; good idea: sealed Inherit -> AutoNotifier) /// </summary> public abstract class AutoNotifier : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; private readonly ConcurrentDictionary<string, object> _wrappedValues = new ConcurrentDictionary<string, object>(); //just to avoid manual writing a lot of fields protected T Get<T>([CallerMemberName] string propertyName = null) { return (T)_wrappedValues.GetValueOrDefault(propertyName, () => default(T)); } protected void Set<T>(T value, [CallerMemberName] string propertyName = null) { // ReSharper disable once AssignNullToNotNullAttribute _wrappedValues.AddOrUpdate(propertyName, value, (s, o) => value); OnPropertyChanged(propertyName); } public object this[string propertyName] { get { return Get<object>(propertyName); } set { Set(value, propertyName); } } protected void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } 

Di sini, atribut [CallerMemberName] memungkinkan Anda untuk secara otomatis mendapatkan nama properti objek panggilan, yaitu Pengaturan Aplikasi

Sekarang kita bisa mewarisi AppSettings dari AutoNotifier kelas dasar ini, dan kemudian setiap properti sedikit dimodifikasi:

 public int VirtualFeedPort { get { return Get<int>(); } set { Set(value); } } 

Dengan pendekatan ini, kelas AppSettings kami, bahkan mengandung cukup banyak pengaturan, terlihat kompak, dan pada saat yang sama sepenuhnya mengimplementasikan INotifyPropertyChanged.

Ya, saya tahu bahwa itu mungkin untuk memperkenalkan sedikit lebih banyak sihir, menggunakan, misalnya, Castle.DynamicProxy.Iterceptor, mencegat perubahan pada properti yang diperlukan dan meningkatkan acara di sana. Tetapi keputusan seperti itu bagi saya kelihatannya terlalu penuh.

Membaca pengaturan dari file YAML


Langkah selanjutnya adalah menambahkan pembaca konfigurasi YAML itu sendiri. Ini terjadi di suatu tempat lebih dekat ke awal layanan. Menyembunyikan detail yang tidak perlu yang tidak terkait dengan topik yang sedang dibahas, kami mendapatkan sesuatu yang serupa:

 public static IServerConfigurationProvider LoadServerConfiguration(IReadOnlyDictionary<Type, string> allSections) { IConfigurationBuilder builder = new ConfigurationBuilder().SetBasePath(ConfigFiles.BasePath); foreach (string configFile in configFiles) { string directory = Path.GetDirectoryName(configFile); if (!string.IsNullOrEmpty(directory)) //can be empty if relative path is used { Directory.CreateDirectory(directory); } builder = builder.AddYamlFile(configFile, optional: true, reloadOnChange: true); } IConfigurationRoot config = builder.Build(); // load prepared files and merge them return new ServerConfigurationProvider<TAppSettings>(config, allSections); } 

Dalam kode yang disajikan, ConfigurationBuilder mungkin tidak menarik minat - semua bekerja dengannya mirip dengan bekerja dengan konfigurasi di ASP.NET Core. Tetapi poin-poin berikut ini menarik. Pertama, "out of the box" kami juga mendapat kesempatan untuk menggabungkan pengaturan dari beberapa file. Ini memberikan persyaratan untuk memiliki setidaknya dua file konfigurasi per server, seperti yang saya sebutkan di atas. Kedua, kami meneruskan semua konfigurasi baca ke ServerConfigurationProvider tertentu. Mengapa

Bagian dalam file YAML


Kami akan menjawab pertanyaan ini nanti, dan sekarang kembali ke persyaratan untuk menyimpan pengaturan terstruktur secara hierarkis dalam file YML.

Pada prinsipnya, mengimplementasikan ini cukup sederhana. Pertama, dalam file YML, kami memperkenalkan struktur yang kami butuhkan:

 VirtualFeed: MaxChartHistoryLength: 10 Port: 35016 UseThrottling: True ThrottlingIntervalMs: 50000 UseHistoryBroadcast: True CalendarName: "EmptyCalendar" UsMarketFeed: UseImbalances: True 

Sekarang mari kita pergi ke AppSettings dan mengajarinya cara membagi properti kita menjadi beberapa bagian. Sesuatu seperti ini:

 public sealed class AppSettings : AutoNotifier, IWebSettingsStorage, IServerSettingsStorage, IServerManagerAddressStorage, IGlobalCredentialsStorage, IGraphiteAddressStorage, IDatabaseConfigStorage, IBlackListStorage, IKeyCloackConfigFilePathProvider, IPrometheusSettingsStorage, IHeartBeatConfig, IConcurrentAcceptorProperties, IMetricsConfig { public static IReadOnlyDictionary<Type, string> Sections { get; } = new Dictionary<Type, string> { {typeof(IDatabaseConfigStorage), "Database"}, {typeof(IWebSettingsStorage), "Web"}, {typeof(IServerSettingsStorage), "Server"}, {typeof(IConcurrentAcceptorProperties), "ConcurrentAcceptor"}, {typeof(IGraphiteAddressStorage), "Graphite"}, {typeof(IKeyCloackConfigFilePathProvider), "Keycloak"}, {typeof(IPrometheusSettingsStorage), "Prometheus"}, {typeof(IHeartBeatConfig), "Heartbeat"}, {typeof(IServerManagerAddressStorage), "ServerManager"}, {typeof(IGlobalCredentialsStorage), "GlobalCredentials"}, {typeof(IBlackListStorage), "Blacklist"}, {typeof(IMetricsConfig), "Metrics"} }; ... 

Seperti yang Anda lihat, kami menambahkan kamus langsung ke AppSettings, di mana kunci adalah jenis antarmuka yang mengimplementasikan kelas AppSettings, dan nilainya adalah header dari bagian yang sesuai. Sekarang kita dapat membandingkan hierarki dalam file YML dengan hierarki properti di AppSettings (walaupun tidak lebih dalam dari satu level penumpukan, tetapi dalam kasus kami ini sudah cukup).

Mengapa kita melakukan ini di sini - di AppSettings? Karena dengan cara ini kami tidak menyebarkan informasi tentang pengaturan untuk entitas yang berbeda, dan di samping itu, ini adalah tempat yang paling alami, karena di setiap layanan dan, karenanya, di setiap AppSettings, bagian pengaturannya sendiri.

Jika Anda tidak memerlukan hierarki di pengaturan?


Pada prinsipnya, ini adalah kasus yang aneh, tetapi kami memilikinya tepat pada tahap pertama, ketika kami hanya beralih dari XML ke YAML, tanpa menggunakan keunggulan YAML.

Dalam hal ini, seluruh daftar bagian ini tidak dapat disimpan, dan ServerConfigurationProvider akan jauh lebih sederhana (dibahas nanti).

Tetapi poin penting adalah bahwa jika kita memutuskan untuk meninggalkan hierarki datar, maka kita bisa memenuhi persyaratan mempertahankan kemampuan untuk mengakses pengaturan melalui AppSettings.Default. Untuk melakukan ini, tambahkan konstruktor publik sederhana di sini di AppSettings:

 public static AppSettings Default { get; } public AppSettings() { Default = this; } 

Sekarang kita dapat terus mengakses kelas pengaturan di mana saja melalui AppSettings.Default (asalkan pengaturan tersebut sudah dibaca melalui IConfigurationRoot di ServerConfigurationProvider dan, karenanya, AppSettings telah dipakai).

Jika hierarki datar tidak dapat diterima, maka, bagaimanapun, Anda harus menyingkirkan AppSettings.Default di mana-mana dengan kode dan bekerja dengan pengaturan hanya melalui antarmuka (yang pada prinsipnya bagus). Kenapa begitu - itu akan menjadi lebih jelas.

ServerConfigurationProvider


Kelas ServerConfigurationProvider khusus yang disebutkan sebelumnya berkaitan dengan sihir yang memungkinkan Anda untuk sepenuhnya bekerja dengan konfigurasi YAML hirarkis baru dengan hanya AppSettings datar.

Jika Anda tidak bisa menunggu, ini dia.

Kode Penuh ServerConfigurationProvider
 /// <summary> /// Provides different configurations for current server /// </summary> public class ServerConfigurationProvider<TAppSettings> : IServerConfigurationProvider where TAppSettings : new() { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private readonly IConfigurationRoot _configuration; private readonly IReadOnlyDictionary<Type, string> _sectionsByInterface; private readonly IReadOnlyDictionary<string, Type> _interfacesBySections; /// <summary> /// Section name -> config /// </summary> private readonly ConcurrentDictionary<string, TAppSettings> _cachedSections; public ServerConfigurationProvider(IConfigurationRoot configuration, IReadOnlyDictionary<Type, string> allSections) { _configuration = configuration; _cachedSections = new ConcurrentDictionary<string, TAppSettings>(); _sectionsByInterface = allSections; var interfacesBySections = new Dictionary<string, Type>(); foreach (KeyValuePair<Type, string> interfaceAndSection in _sectionsByInterface) { //section names must be unique interfacesBySections.Add(interfaceAndSection.Value, interfaceAndSection.Key); } _interfacesBySections = interfacesBySections; _configuration.GetReloadToken()?.RegisterChangeCallback(OnConfigurationFileChanged, null); } private void OnConfigurationFileChanged(object _) { UpdateCache(); } private void UpdateCache() { foreach (string sectionName in _cachedSections.Keys) { Type sectionInterface = _interfacesBySections[sectionName]; TAppSettings newSection = ReadSection(sectionName, sectionInterface); TAppSettings oldSection; if (_cachedSections.TryGetValue(sectionName, out oldSection)) { UpdateSection(oldSection, newSection); } } } private void UpdateSection(TAppSettings oldConfig, TAppSettings newConfig) { foreach (PropertyInfo propertyInfo in typeof(TAppSettings).GetProperties().Where(p => p.GetMethod != null && p.SetMethod != null)) { propertyInfo.SetValue(newConfig, propertyInfo.GetValue(oldConfig)); } } public IEnumerable<Type> AllSections => _sectionsByInterface.Keys; public TSettingsSectionInterface FindSection<TSettingsSectionInterface>() where TSettingsSectionInterface : class { return (TSettingsSectionInterface)FindSection(typeof(TSettingsSectionInterface)); } [CanBeNull] public object FindSection(Type sectionInterface) { string sectionName = FindSectionName(sectionInterface); if (sectionName == null) { return null; } //we must return same instance of settings for same requested section (otherwise changing of settings will lead to inconsistent state) return _cachedSections.GetOrAdd(sectionName, typeName => ReadSection(sectionName, sectionInterface)); } private string FindSectionName(Type sectionInterface) { string sectionName; if (!_sectionsByInterface.TryGetValue(sectionInterface, out sectionName)) { Logger.Debug("This server doesn't contain settings for {0}", sectionInterface.FullName); return null; } return sectionName; } private TAppSettings ReadSection(string sectionName, Type sectionInterface) { TAppSettings parsed; try { IConfigurationSection section = _configuration.GetSection(sectionName); CheckSection(section, sectionName, sectionInterface); parsed = section.Get<TAppSettings>(); if (parsed == null) { //means that this section is empty or all its properties are empty return new TAppSettings(); } ReadArrays(parsed, section); } catch (Exception ex) { Logger.Fatal(ex, "Something wrong during reading section {0} in config", sectionName.SafeSurround()); throw; } return parsed; } /// <summary> /// Manual reading of array properties in config /// </summary> private void ReadArrays(TAppSettings settings, IConfigurationSection section) { foreach (PropertyInfo propertyInfo in GetPublicProperties(typeof(TAppSettings), needSetters: true).Where(p => typeof(IEnumerable<string>).IsAssignableFrom(p.PropertyType))) { ClearDefaultArrayIfOverridenExists(section.Key, propertyInfo.Name); IConfigurationSection enumerableProperty = section.GetSection(propertyInfo.Name); propertyInfo.SetValue(settings, enumerableProperty.Get<IEnumerable<string>>()); } } /// <summary> /// Clears array property from default config to use overriden one. /// Standard implementation merges default and overriden array by indexes - this is not what we need /// </summary> private void ClearDefaultArrayIfOverridenExists(string sectionName, string propertyName) { List<IConfigurationProvider> providers = _configuration.Providers.ToList(); if (providers.Count == 0) { return; } string propertyTemplate = $"{sectionName}:{propertyName}:"; if (!providers[providers.Count - 1].TryGet($"{propertyTemplate}{0}", out _)) { //we should use array from default config, because overriden config has no overriden array return; } foreach (IConfigurationProvider provider in providers.Take(providers.Count - 1)) { for (int i = 0; ; i++) { string propertyInnerName = $"{propertyTemplate}{i}"; if (!provider.TryGet(propertyInnerName, out _)) { break; } provider.Set(propertyInnerName, null); } } } private void CheckSection(IConfigurationSection section, string sectionName, Type sectionInterface) { ICollection<PropertyInfo> properties = GetPublicProperties(sectionInterface, needSetters: false); var configProperties = new HashSet<string>(section.GetChildren().Select(c => c.Key)); foreach (PropertyInfo propertyInfo in properties) { if (!configProperties.Remove(propertyInfo.Name)) { if (propertyInfo.PropertyType != typeof(string) && typeof(IEnumerable).IsAssignableFrom(propertyInfo.PropertyType)) { //no way to distinguish absent array and empty array :( Logger.Debug("Property {0} has no valuable items in configs section {1}", propertyInfo.Name, sectionName.SafeSurround()); } else { Logger.Fatal("Property {0} not found in configs section {1}", propertyInfo.Name, sectionName.SafeSurround()); } } } if (configProperties.Any()) { Logger.Fatal("Unexpected config properties {0} in configs section {1}", configProperties.SafeSurroundAndJoin(), sectionName.SafeSurround()); } } private static ICollection<PropertyInfo> GetPublicProperties(Type type, bool needSetters) { if (!type.IsInterface) { return type.GetProperties().Where(x => x.GetMethod != null && (!needSetters || x.SetMethod != null)).ToArray(); } var propertyInfos = new List<PropertyInfo>(); var considered = new List<Type>(); var queue = new Queue<Type>(); considered.Add(type); queue.Enqueue(type); while (queue.Count > 0) { Type subType = queue.Dequeue(); foreach (Type subInterface in subType.GetInterfaces()) { if (considered.Contains(subInterface)) { continue; } considered.Add(subInterface); queue.Enqueue(subInterface); } PropertyInfo[] typeProperties = subType.GetProperties(BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Instance); IEnumerable<PropertyInfo> newPropertyInfos = typeProperties.Where(x => x.GetMethod != null && (!needSetters || x.SetMethod != null) && !propertyInfos.Contains(x)); propertyInfos.InsertRange(0, newPropertyInfos); } return propertyInfos; } } 


ServerConfigurationProvider diparameterisasi oleh kelas pengaturan AppSettings:
 public class ServerConfigurationProvider<TAppSettings> : IServerConfigurationProvider where TAppSettings : new() 

Ini, seperti yang Anda duga, memungkinkan Anda untuk segera menggunakannya di semua layanan.

Baca konfigurasi sendiri (IConfigurationRoot), serta kamus bagian yang disebutkan di atas (AppSettings.Sections) diteruskan ke konstruktor. Ada berlangganan pembaruan file (apakah kami ingin segera menarik perubahan ini kepada kami jika terjadi perubahan pada file YML?):

 _configuration.GetReloadToken()?.RegisterChangeCallback(OnConfigurationFileChanged, null); ... private void OnConfigurationFileChanged(object _) { foreach (string sectionName in _cachedSections.Keys) { Type sectionInterface = _interfacesBySections[sectionName]; TAppSettings newSection = ReadSection(sectionName, sectionInterface); TAppSettings oldSection; if (_cachedSections.TryGetValue(sectionName, out oldSection)) { UpdateSection(oldSection, newSection); } } } 

Seperti yang Anda lihat, di sini, dalam hal memperbarui file YML, kami membahas semua bagian yang kami ketahui dan baca masing-masing. Kemudian, jika bagian telah dibaca sebelumnya dalam cache (mis., Sudah diminta di suatu tempat dalam kode oleh beberapa kelas), maka kami menulis ulang nilai lama dalam cache dengan yang baru.

Tampaknya - mengapa membaca setiap bagian, mengapa tidak hanya membaca yang ada di cache (mis. Dituntut)? Karena dalam membaca bagian ini kami telah menerapkan pemeriksaan untuk konfigurasi yang benar. Dan dalam hal pengaturan yang salah, peringatan yang sesuai dibuang, masalah dicatat. Lebih baik belajar tentang masalah dalam perubahan konfigurasi sesegera mungkin, dari mana kita membaca semua bagian segera.

Memperbarui nilai lama dalam cache dengan nilai baru cukup sepele:

 private void UpdateSection(TAppSettings oldConfig, TAppSettings newConfig) { foreach (PropertyInfo propertyInfo in typeof(TAppSettings).GetProperties().Where(p => p.GetMethod != null && p.SetMethod != null)) { propertyInfo.SetValue(newConfig, propertyInfo.GetValue(oldConfig)); } } 

Tetapi membaca bagian tidak begitu sederhana:

 private TAppSettings ReadSection(string sectionName, Type sectionInterface) { TAppSettings parsed; try { IConfigurationSection section = _configuration.GetSection(sectionName); CheckSection(section, sectionName, sectionInterface); parsed = section.Get<TAppSettings>(); if (parsed == null) { //means that this section is empty or all its properties are empty return new TAppSettings(); } ReadArrays(parsed, section); } catch (Exception ex) { Logger.Fatal(ex, "Something wrong during reading section {0} in config", sectionName.SafeSurround()); throw; } return parsed; } 

Di sini kita pertama-tama membaca bagian itu sendiri menggunakan IConfigurationRoot.GetSection standar.Kemudian cukup periksa kebenaran dari bagian baca.

Selanjutnya kita membaca bagian bindim dengan tipe pengaturan kita: seksi. Dapatkan Di sini kita menemukan fitur parser YAML - itu tidak membedakan antara bagian kosong (tanpa parameter, mis. Absen) dari bagian di mana semua parameter kosong.

Ini adalah kasus yang serupa:

 VirtualFeed: Names: [] 

Di sini, di bagian VirtualFeed ada parameter Nama dengan daftar nilai kosong, tetapi parser YAML, sayangnya, akan mengatakan bahwa bagian VirtualFeed umumnya benar-benar kosong. Menyedihkan.

Dan akhirnya, dalam metode ini sedikit sihir jalanan diterapkan untuk mendukung properti IEnumerable di pengaturan. Kami tidak berhasil mencapai pembacaan normal dari daftar "di luar kotak".

 ReadArrays(parsed, section); ... /// <summary> /// Manual reading of array properties in config /// </summary> private void ReadArrays(TAppSettings settings, IConfigurationSection section) { foreach (PropertyInfo propertyInfo in GetPublicProperties(typeof(TAppSettings), needSetters: true).Where(p => typeof(IEnumerable<string>).IsAssignableFrom(p.PropertyType))) { ClearDefaultArrayIfOverridenExists(section.Key, propertyInfo.Name); IConfigurationSection enumerableProperty = section.GetSection(propertyInfo.Name); propertyInfo.SetValue(settings, enumerableProperty.Get<IEnumerable<string>>()); } } /// <summary> /// Clears array property from default config to use overriden one. /// Standard implementation merges default and overriden array by indexes - this is not what we need /// </summary> private void ClearDefaultArrayIfOverridenExists(string sectionName, string propertyName) { List<IConfigurationProvider> providers = _configuration.Providers.ToList(); if (providers.Count == 0) { return; } string propertyTemplate = $"{sectionName}:{propertyName}:"; if (!providers[providers.Count - 1].TryGet($"{propertyTemplate}{0}", out _)) { //we should use array from default config, because overriden config has no overriden array return; } foreach (IConfigurationProvider provider in providers.Take(providers.Count - 1)) { for (int i = 0; ; i++) { string propertyInnerName = $"{propertyTemplate}{i}"; if (!provider.TryGet(propertyInnerName, out _)) { break; } provider.Set(propertyInnerName, null); } } } 

Seperti yang Anda lihat, kami menemukan semua properti yang tipenya diwarisi dari IEnumerable dan memberikan nilai kepada mereka dari "bagian" boneka, juga dinamai sebagai pengaturan yang kami minati. Tetapi sebelum itu, jangan lupa untuk memeriksa: apakah ada nilai override dari properti yang disebutkan ini dalam file konfigurasi kedua? Jika ada, maka kami hanya mengambilnya, dan kami menghapus pengaturan membaca dari file konfigurasi dasar. Jika ini tidak dilakukan, maka kedua properti (dari file dasar dan dari yang ditimpa) akan secara otomatis digabung menjadi satu array pada level IConfigurationSection, dan indeks array akan berfungsi sebagai kunci untuk menggabungkan. Ini akan menghasilkan beberapa jenis hash daripada nilai yang ditimpa normal.

Metode ReadSection yang ditampilkan pada akhirnya digunakan dalam metode utama kelas: FindSection.

 [CanBeNull] public object FindSection(Type sectionInterface) { string sectionName = FindSectionName(sectionInterface); if (sectionName == null) { return null; } //we must return same instance of settings for same requested section (otherwise changing of settings will lead to inconsistent state) return _cachedSections.GetOrAdd(sectionName, typeName => ReadSection(sectionName, sectionInterface)); } 

Pada prinsipnya, menjadi jelas mengapa, dengan dukungan bagian, kami tidak dapat mendukung AppSettings.Default dengan cara apa pun: setiap akses ke bagian pengaturan baru (yang belum dibaca) melalui FindSection benar-benar akan memberi kami contoh baru dari kelas AppSettings, meskipun melekat pada antarmuka yang diinginkan , dan, oleh karena itu, jika kami menggunakan AppSettings.Default, itu akan didefinisikan ulang setiap kali bagian baru dibaca dan hanya berisi pengaturan yang termasuk bagian baca terakhir (sisanya akan memiliki nilai default - NULL dan 0).

Validasi pengaturan di bagian ini dilaksanakan sebagai berikut:

 private void CheckSection(IConfigurationSection section, string sectionName, Type sectionInterface) { ICollection<PropertyInfo> properties = GetPublicProperties(sectionInterface, needSetters: false); var configProperties = new HashSet<string>(section.GetChildren().Select(c => c.Key)); foreach (PropertyInfo propertyInfo in properties) { if (!configProperties.Remove(propertyInfo.Name)) { if (propertyInfo.PropertyType != typeof(string) && typeof(IEnumerable).IsAssignableFrom(propertyInfo.PropertyType)) { //no way to distinguish absent array and empty array :( Logger.Debug("Property {0} has no valuable items in configs section {1}", propertyInfo.Name, sectionName.SafeSurround()); } else { Logger.Fatal("Property {0} not found in configs section {1}", propertyInfo.Name, sectionName.SafeSurround()); } } } if (configProperties.Any()) { Logger.Fatal("Unexpected config properties {0} in configs section {1}", configProperties.SafeSurroundAndJoin(), sectionName.SafeSurround()); } } 

Di sini, pertama-tama, semua properti publik dari antarmuka yang kami minati diekstraksi (baca pengaturan bagian). Dan untuk masing-masing properti ini ditemukan kecocokan dalam pengaturan baca: jika tidak ada kecocokan yang ditemukan, maka masalah yang terkait dicatat, karena ini berarti bahwa beberapa konfigurasi tidak ada dalam file konfigurasi. Pada akhirnya, itu juga diperiksa apakah pengaturan baca mana pun tetap tidak cocok dengan antarmuka. Jika ada, maka masalahnya dicatat lagi, karena ini berarti bahwa properti yang tidak dijelaskan dalam antarmuka ditemukan di file konfigurasi, yang juga tidak boleh dalam situasi normal.

Timbul pertanyaan - dari mana persyaratan berasal, bahwa dalam file baca semua pengaturan harus sesuai dengan yang tersedia di antarmuka pada basis satu-ke-satu? Faktanya adalah bahwa, pada kenyataannya, seperti yang disebutkan di atas, pada saat itu tidak satu file dibaca, tetapi dua sekaligus - satu dengan pengaturan default, dan yang lainnya dengan yang diganti, dan keduanya berdekatan. Dengan demikian, pada kenyataannya, kita tidak melihat pengaturan dari satu file, tetapi yang lengkap. Dan dalam hal ini, tentu saja, himpunan mereka harus sesuai dengan pengaturan yang diharapkan satu lawan satu.

Juga perhatikan sumber-sumber di atas untuk metode GetPublicProperties, yang, sepertinya, hanya mengembalikan semua properti publik dari antarmuka. Tapi itu tidak sesederhana mungkin, karena kadang-kadang kita memiliki antarmuka yang menggambarkan pengaturan server yang diwarisi dari antarmuka lain, dan, oleh karena itu, ada kebutuhan untuk melihat seluruh hierarki antarmuka untuk menemukan semua properti publik.

Mendapatkan pengaturan server


Berdasarkan hal di atas, untuk mendapatkan pengaturan server di mana-mana dengan kode, kita beralih ke antarmuka berikut:

 /// <summary> /// Provides different configurations for current server /// </summary> public interface IServerConfigurationProvider { TSettingsSectionInterface FindSection<TSettingsSectionInterface>() where TSettingsSectionInterface : class; object FindSection(Type sectionInterface); IEnumerable<Type> AllSections { get; } } 

Metode pertama dari antarmuka ini - FindSection - memungkinkan Anda untuk mengakses bagian pengaturan yang menarik. Sesuatu seperti ini:

 IThreadPoolProperties threadPoolProperties = ConfigurationProvider.FindSection<IThreadPoolProperties>(); 

Mengapa metode kedua dan ketiga diperlukan - saya akan menjelaskan lebih lanjut.

Registrasi antarmuka pengaturan


Dalam proyek kami, Castle Windsor digunakan sebagai wadah IoC. Dialah yang memasok, termasuk antarmuka pengaturan server. Karenanya, antarmuka ini harus terdaftar di dalamnya.

Untuk tujuan ini, kelas ekstensi sederhana ditulis, yang menyederhanakan prosedur ini agar tidak menulis pendaftaran seluruh rangkaian antarmuka di setiap server:

 public static class ServerConfigurationProviderExtensions { public static void RegisterAllConfigurationSections(this IWindsorContainer container, IServerConfigurationProvider configurationProvider) { Register(container, configurationProvider, configurationProvider.AllSections.ToArray()); } public static void Register(this IWindsorContainer container, IServerConfigurationProvider configurationProvider, params Type[] configSections) { var registrations = new IRegistration[configSections.Length]; for (int i = 0; i < registrations.Length; i++) { Type configSection = configSections[i]; object section = configurationProvider.FindSection(configSection); registrations[i] = Component.For(configSection).Instance(section).Named(configSection.FullName); } container.Register(registrations); } } 

Metode pertama memungkinkan Anda untuk mendaftarkan semua bagian pengaturan (untuk ini Anda memerlukan properti AllSections di antarmuka IServerConfigurationProvider).

Dan metode kedua digunakan di yang pertama, dan secara otomatis membaca bagian pengaturan yang ditentukan menggunakan ServerConfigurationProvider kami, sehingga segera menulisnya ke cache ServerConfigurationProvider dan mendaftarkannya di Windsor.
Di sinilah metode FindSection kedua, tanpa parameter, dari IServerConfigurationProvider digunakan.

Yang tersisa adalah memanggil metode Extension kami dalam kode registrasi kontainer Windsor:

 container.RegisterAllConfigurationSections(configProvider); 

Kesimpulan


Apa yang terjadi


Dengan cara yang disajikan, dimungkinkan untuk mentransfer semua pengaturan server kami dari XML ke YAML tanpa rasa sakit, sambil melakukan sedikit perubahan pada kode server yang ada.

Konfigurasi YAML, tidak seperti XML, ternyata lebih mudah dibaca karena tidak hanya keringkasan yang lebih besar, tetapi juga dukungan untuk partisi.

Kami tidak menemukan sepeda kami sendiri untuk parsing YAML, tetapi menggunakan solusi yang sudah jadi. Namun, untuk mengintegrasikannya ke dalam realitas proyek kami, diperlukan beberapa trik yang dijelaskan dalam artikel ini. Saya berharap mereka akan bermanfaat bagi pembaca.

Itu mungkin untuk mempertahankan kemampuan untuk menangkap perubahan dalam pengaturan di moncong web server kami dengan cepat. Selain itu, sebagai bonus, juga dimungkinkan untuk menangkap perubahan pada file YAML itu sendiri dengan cepat (sebelumnya, perlu untuk me-restart server untuk setiap perubahan dalam file konfigurasi).

Kami mempertahankan kemampuan untuk menggabungkan dua file konfigurasi - pengaturan default dan yang diganti, dan kami melakukan ini menggunakan solusi pihak ketiga di luar kotak.

Apa yang tidak berhasil dengan baik


Saya harus meninggalkan kemampuan yang sebelumnya tersedia untuk menyimpan perubahan yang diterapkan dari wajah web server kami untuk mengonfigurasi file, karena dukungan untuk fungsionalitas seperti itu akan membutuhkan gerak-gerik yang hebat, dan tugas bisnis sebelum kita secara umum tidak seperti itu.

Yah, saya juga harus menolak akses ke pengaturan melalui AppSettings.Default, tapi ini lebih merupakan plus daripada minus.

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


All Articles