الخلفية
قامت شركتنا ، من بين أشياء أخرى ، بتطوير العديد من الخدمات (بشكل أكثر دقة - 12) تعمل كخلفية لأنظمتنا. كل من الخدمات هي خدمة Windows وتؤدي مهامها المحددة.
أود نقل جميع هذه الخدمات إلى * nix-OS. للقيام بذلك ، التخلي عن المجمع في شكل خدمات Windows والتبديل من .NET Framework إلى .NET Standard.
يؤدي الشرط الأخير إلى الحاجة إلى التخلص من بعض رمز Legacy ، وهو غير معتمد في .NET Standard ، بما في ذلك من دعم تكوين خوادمنا عبر XML ، يتم تنفيذها باستخدام فئات من System.Configuration. في الوقت نفسه ، يعمل هذا على حل المشكلة الطويلة الأمد المتعلقة بحقيقة أننا في ملفات XML-config ارتكبنا أخطاء من وقت لآخر عند تغيير الإعدادات (على سبيل المثال ، في بعض الأحيان نضع علامة الإغلاق في المكان الخطأ أو ننساه على الإطلاق) ، ولكن قارئ رائع لـ System.Xml XML-configs. XmlDocument يبتلع بصمت مثل هذه التكوينات ، مما يؤدي إلى نتيجة غير متوقعة تمامًا.
تقرر التبديل إلى التكوين من خلال YAML العصرية. ما هي المشكلات التي واجهناها وكيف حلها - في هذه المقالة.
ماذا لدينا
كيف يمكننا قراءة التكوين من XML
نقرأ XML بطريقة قياسية لمعظم المشاريع الأخرى.
تحتوي كل خدمة على ملف إعدادات لمشاريع .NET ، يسمى AppSettings.cs ، والذي يحتوي على كافة الإعدادات المطلوبة بواسطة الخدمة. شيء مثل هذا:
[System.Configuration.SettingsProvider(typeof(PortableSettingsProvider))] internal sealed partial class AppSettings : IServerManagerConfigStorage, IWebSettingsStorage, IServerSettingsStorage, IGraphiteAddressStorage, IDatabaseConfigStorage, IBlackListStorage, IKeyCloackConfigFilePathProvider, IPrometheusSettingsStorage, IMetricsConfig { }
تقنية مشابهة لفصل الإعدادات إلى واجهات تجعلها ملائمة لاستخدامها لاحقًا عبر حاوية DI.
يتم إخفاء كل السحر الرئيسي لتخزين الإعدادات في PortableSettingsProvider (انظر السمة class) ، وكذلك في 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; } } ...
كما ترون ، "وراء الكواليس" يتم إخفاء كل تلك الخصائص التي نضيفها إلى تكوين الخادم عندما نقوم بتحريرها من خلال مصمم الإعدادات في Visual Studio.
تقوم فئة PortableSettingsProvider ، المذكورة أعلاه ، بقراءة ملف XML مباشرةً ، ويتم استخدام نتيجة القراءة بالفعل في SettingsProvider لكتابة الإعدادات إلى خصائص AppSettings.
مثال على تهيئة XML التي نقرأها (معظم الإعدادات مخفية لأسباب أمنية):
<?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>
ما YAML الملفات أود أن أقرأ
شيء مثل هذا:
VirtualFeed: MaxChartHistoryLength: 10 Port: 35016 UseThrottling: True ThrottlingIntervalMs: 50000 UseHistoryBroadcast: True CalendarName: "EmptyCalendar" UsMarketFeed: UseImbalances: True
مشاكل الانتقال
أولاً ، تكون التكوينات في XML "مسطحة" ، لكن في YAML لا تكون (الأقسام والأقسام الفرعية مدعومة). وينظر إلى هذا بوضوح في الأمثلة المذكورة أعلاه. باستخدام XML ، قمنا بحل مشكلة الإعدادات المسطحة من خلال تقديم موزعات خاصة بنا يمكنها تحويل سلاسل من نوع معين إلى فئات أكثر تعقيدًا لدينا. مثال على مثل هذه السلسلة المعقدة:
<setting name="MCXSettings" serializeAs="String"> <value>Inactive, ChartLen: 1000, PrintLen: 50, UseProxy: False</value> </setting>
لا أشعر برغبة في إجراء مثل هذه التحويلات عند العمل مع YAML. ولكن في الوقت نفسه ، نحن مقيدون بالهيكل "المسطح" الحالي لفئة AppSettings: تتراكم جميع خصائص الإعدادات الموجودة به في كومة واحدة.
ثانياً ، إن تكوينات خوادمنا ليست متراصة ثابتة ، فنحن نغيرها من وقت لآخر في سياق الخادم ، أي هذه التغييرات تحتاج إلى أن تكون قادرة على اللحاق ، في وقت التشغيل. للقيام بذلك ، في تطبيق XML ، نرث AppSettings الخاصة بنا من INotifyPropertyChanged (في الواقع ، كل واجهة تقوم بتطبيق AppSettings موروثة منه) ونشترك في تحديث أحداث إعدادات الخصائص. يعمل هذا النهج لأن الفئة الأساسية System.Configuration.ApplicationSettingsBase خارج المربع تنفذ INotifyPropertyChanged. يجب الحفاظ على سلوك مماثل بعد الانتقال إلى YAML.
ثالثًا ، ليس لدينا في الواقع ملف تهيئة واحد لكل خادم ، ولكن يوجد ملفان به: واحد مع الإعدادات الافتراضية ، والآخر بإعدادات تجاوز. هذا مطلوب حتى لا يكون عليك نسخ مجموعة الإعدادات بالكامل بالكامل في كل حالة من الحالات المتعددة للخوادم من نفس النوع ، والاستماع إلى منافذ مختلفة ولديها إعدادات مختلفة قليلاً.
ومشكلة أخرى - الوصول إلى الإعدادات لا يمر عبر واجهات فقط ، ولكن أيضًا عن طريق الوصول المباشر إلى AppSettings.Default. اسمحوا لي أن أذكركم كيف تم الإعلان عنها في AppSettings.Designer.cs خلف الكواليس:
private static AppSettings defaultInstance = ((AppSettings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new AppSettings()))); public static AppSettings Default { get { return defaultInstance; } }
بالنظر إلى ما سبق ، كان من الضروري التوصل إلى مقاربة جديدة لتخزين الإعدادات في AppSettings.
الحل
مجموعة الأدوات
للقراءة المباشرة ، قررت YAML استخدام المكتبات الجاهزة المتوفرة من خلال NuGet:
- YamlDotNet - github.com/aaubry/YamlDotNet. من وصف المكتبة (ترجمة):
YamlDotNet هي مكتبة .NET لـ YAML. يوفر YamlDotNet محلل منخفض المستوى ومنشئ YAML ، بالإضافة إلى طراز كائن رفيع المستوى يشبه XmlDocument. يتضمن أيضًا مكتبة التسلسل التي تتيح لك قراءة وكتابة الكائنات من / إلى تدفقات YAML.
- NetEscapades.Configuration - github.com/andrewlock/NetEscapades.Configuration. هذا هو موفر التهيئة نفسه (بمعنى Microsoft.Extensions.Configuration.IConfigurationSource ، والذي يستخدم بنشاط في تطبيقات ASP.NET Core) ، الذي يقرأ ملفات YAML باستخدام فقط المذكورة أعلاه YamlDotNet.
اقرأ المزيد حول كيفية استخدام هذه المكتبات
هنا .
الانتقال إلى YAML
لقد قمنا بالانتقال على مرحلتين: في البداية ، قمنا ببساطة بالتبديل من XML إلى YAML ، مع الاحتفاظ بتسلسل هرمي مسطح لملفات التكوين ، ثم قدمنا أقسامًا في ملفات YAML. يمكن دمج هذه المراحل ، من حيث المبدأ ، في مرحلة واحدة ، وبساطة العرض سأقوم بذلك. تم تطبيق جميع الإجراءات الموضحة أدناه بالتتابع على كل خدمة.
تحضير ملف YML
تحتاج أولاً إلى إعداد ملف YAML نفسه. نسميها اسم المشروع (مفيد لاختبارات التكامل في المستقبل ، والتي يجب أن تكون قادرة على العمل مع خوادم مختلفة وتمييز التكوينات الخاصة بهم فيما بينها) ، ضع الملف مباشرة في جذر المشروع ، بجانب AppSettings:

في ملف YML ، بالنسبة للمبتدئين ، دعنا نحفظ بنية "مسطحة":
VirtualFeed: "MaxChartHistoryLength: 10, UseThrottling: True, ThrottlingIntervalMs: 50000, UseHistoryBroadcast: True, CalendarName: EmptyCalendar" VirtualFeedPort: 35016 UsMarketFeedUseImbalances: True
ملء AppSettings مع خصائص الإعدادات
ننقل جميع الخصائص من AppSettings.Designer.cs إلى AppSettings.cs ، ونتخلص في وقت واحد من السمات الزائدة عن المصمم والرمز نفسه في get / set-parts.
كان:
[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; } }
أصبح:
public int VirtualFeedPort { get; set; }
سنقوم بإزالة AppSettings
.Designer .cs بشكل غير ضروري. الآن ، بالمناسبة ، يمكنك التخلص تمامًا من قسم إعدادات المستخدم في ملف app.config ، إذا كان في المشروع ، فإنه يخزن نفس الإعدادات الافتراضية التي نحددها من خلال مصمم الإعدادات.
المضي قدما.
إعدادات التحكم على الطاير
نظرًا لأننا نحتاج إلى الاطلاع على تحديثات إعداداتنا في وقت التشغيل ، فإننا نحتاج إلى تطبيق INotifyPropertyChanged في AppSettings لدينا. قاعدة System.Configuration.ApplicationSettingsBase لم تعد موجودة ، على التوالي ، لا يمكنك الاعتماد على أي سحر.
يمكنك تنفيذه "على الجبهة": عن طريق إضافة تطبيق لطريقة ترمي الحدث المرغوب وتدعوه في واضعة كل خاصية. ولكن هذه هي سطور إضافية من التعليمات البرمجية ، والتي ستحتاج بالإضافة إلى نسخها في جميع الخدمات.
دعونا نفعل ذلك بشكل أفضل - تقديم AutoNotifier أساسي من الدرجة الأساسية ، وهو ما يفعل الشيء نفسه بالفعل ، ولكن خلف الكواليس ، تمامًا مثل System.Configuration.ApplicationSettingsBase ، قام بما يلي:
هنا تسمح لك السمة [CallerMemberName] بالحصول تلقائيًا على اسم خاصية كائن الاتصال ، أي AppSettings
الآن يمكننا أن نرث AppSettings لدينا من هذا AutoNotifier الأساسي ، ثم يتم تعديل كل خاصية قليلاً:
public int VirtualFeedPort { get { return Get<int>(); } set { Set(value); } }
من خلال هذا النهج ، تبدو فئات AppSettings الخاصة بنا ، حتى التي تحتوي على الكثير من الإعدادات ، مضغوطة ، وفي الوقت نفسه تنفذ INotifyPropertyChanged بالكامل.
نعم ، أعلم أنه سيكون من الممكن تقديم المزيد من السحر ، باستخدام ، على سبيل المثال ، Castle.DynamicProxy.IInterceptor ، اعتراض التغييرات في الخصائص الضرورية وإثارة الأحداث هناك. لكن هذا القرار بدا لي مثقلاً للغاية.
قراءة الإعدادات من ملف YAML
الخطوة التالية هي إضافة قارئ لتكوين YAML نفسه. يحدث هذا في مكان ما أقرب إلى بداية الخدمة. بإخفاء التفاصيل غير الضرورية التي لا تتعلق بالموضوع قيد المناقشة ، نحصل على شيء مماثل:
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))
في التعليمة البرمجية المقدمة ، ربما لا يكون ConfigurationBuilder ذا أهمية خاصة - كل العمل معها يشبه العمل مع التكوينات في ASP.NET Core. لكن النقاط التالية تهمك. أولاً ، "خارج الصندوق" ، سنحت لنا أيضًا فرصة لدمج الإعدادات من عدة ملفات. يوفر هذا شرط وجود ملفين للتكوين على الأقل لكل خادم ، كما ذكرت أعلاه. ثانياً ، نقوم بتمرير كل تكوين القراءة إلى ServerConfigurationProvider معين. لماذا؟
المقاطع في ملف YAML
سنقوم بالرد على هذا السؤال لاحقًا ، ونعود الآن إلى متطلبات تخزين الإعدادات المهيكلة في ملف YML.
من حيث المبدأ ، تنفيذ هذا بسيط للغاية. أولاً ، في ملف YML ، نقدم البنية التي نحتاجها:
VirtualFeed: MaxChartHistoryLength: 10 Port: 35016 UseThrottling: True ThrottlingIntervalMs: 50000 UseHistoryBroadcast: True CalendarName: "EmptyCalendar" UsMarketFeed: UseImbalances: True
الآن دعنا نذهب إلى AppSettings ونعلمه كيفية تقسيم فنادقنا إلى أقسام. شيء مثل هذا:
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"} }; ...
كما ترى ، أضفنا قاموسًا مباشرةً إلى AppSettings ، حيث تمثل المفاتيح أنواع الواجهات التي تنفذها فئة AppSettings ، والقيم هي رؤوس الأقسام المقابلة. الآن يمكننا مقارنة التسلسل الهرمي في ملف YML مع التسلسل الهرمي للخصائص في AppSettings (على الرغم من أنه ليس أعمق من مستوى واحد من التداخل ، ولكن في حالتنا كان هذا كافياً).
لماذا نفعل هذا هنا - في AppSettings؟ لأننا بهذه الطريقة لا ننشر المعلومات حول الإعدادات للكيانات المختلفة ، وبالإضافة إلى ذلك ، هذا هو المكان الأكثر طبيعية ، لأن في كل خدمة ، ووفقًا لذلك ، في كل AppSettings ، قسم الإعدادات الخاص به.
إذا كنت لا تحتاج إلى تسلسل هرمي في الإعدادات؟
من حيث المبدأ ، إنها حالة غريبة ، لكن كان لدينا في المرحلة الأولى بالضبط ، عندما انتقلنا ببساطة من XML إلى YAML ، دون استخدام مزايا YAML.
في هذه الحالة ، لا يمكن تخزين قائمة الأقسام بأكملها ، وسيكون ServerConfigurationProvider أبسط بكثير (تمت مناقشته لاحقًا).
ولكن النقطة المهمة هي أنه إذا قررنا ترك تسلسل هرمي مسطح ، فيمكننا تلبية متطلبات الحفاظ على القدرة على الوصول إلى الإعدادات من خلال AppSettings.Default. للقيام بذلك ، أضف هنا مُنشئًا عامًا بسيطًا في AppSettings:
public static AppSettings Default { get; } public AppSettings() { Default = this; }
الآن يمكننا متابعة الوصول إلى فئة الإعدادات في كل مكان من خلال AppSettings.Default (شريطة أن تكون الإعدادات قد تمت قراءتها بالفعل من خلال IConfigurationRoot في ServerConfigurationProvider وبالتالي ، تم إنشاء مثيل لـ AppSettings).
إذا كان التسلسل الهرمي المسطح غير مقبول ، إذن ، على أي حال ، يجب عليك التخلص من AppSettings.Default في كل مكان عن طريق الرمز والعمل مع الإعدادات فقط من خلال واجهات (وهو أمر جيد من حيث المبدأ). لماذا ذلك - سوف تصبح أكثر وضوحا.
ServerConfigurationProvider
تتعامل فئة ServerConfigurationProvider الخاصة المذكورة سابقًا مع السحر الذي يتيح لك العمل بشكل كامل مع تكوين YAML الهرمي الجديد مع AppSettings مسطح فقط.
إذا كنت لا تستطيع الانتظار ، فهذه هي.
ServerConfigurationProvider رمز الكامل تتم المعلمة ServerConfigurationProvider بواسطة فئة إعدادات AppSettings:
public class ServerConfigurationProvider<TAppSettings> : IServerConfigurationProvider where TAppSettings : new()
هذا ، كما تعتقد ، يتيح لك استخدامه على الفور في جميع الخدمات.
يتم تمرير تكوين القراءة نفسه (IConfigurationRoot) ، وكذلك قاموس القسم المذكور أعلاه (AppSettings.Sections) إلى المُنشئ. هناك اشتراك في تحديثات الملفات (هل نريد سحب هذه التغييرات إلينا على الفور في حالة حدوث تغيير في ملف 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); } } }
كما ترون ، هنا ، في حالة تحديث ملف YML ، نذهب إلى جميع الأقسام التي نعرفها ونقرأ كل منها. بعد ذلك ، إذا كان قد تم بالفعل قراءة القسم مسبقًا في ذاكرة التخزين المؤقت (أي أنه تم طلبه بالفعل في مكان ما في الكود بواسطة بعض الفئات) ، فإننا نعيد كتابة القيم القديمة في ذاكرة التخزين المؤقت بقيم جديدة.
يبدو - لماذا تقرأ كل قسم ، لماذا لا تقرأ فقط تلك الموجودة في ذاكرة التخزين المؤقت (على سبيل المثال)؟ لأنه في قراءة القسم قمنا بتنفيذ التحقق من التكوين الصحيح. وفي حالة الإعدادات غير الصحيحة ، يتم طرح التنبيهات المقابلة ، وتسجيل المشاكل. من الأفضل معرفة مشاكل تغييرات التكوين في أسرع وقت ممكن ، والتي نقرأ منها جميع الأقسام على الفور.
تحديث القيم القديمة في ذاكرة التخزين المؤقت بقيم جديدة أمر تافه بما فيه الكفاية:
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)); } }
لكن أقسام القراءة ليست بهذه البساطة:
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) {
هنا أولاً وقبل كل شيء نقرأ القسم نفسه باستخدام IConfigurationRoot.GetSection القياسي. - .
: section.Get YAML- — ( , .. ) , .
:
VirtualFeed: Names: []
VirtualFeed Names , YAML-, , , VirtualFeed . إنه لأمر محزن.
IEnumerable- . « » .
ReadArrays(parsed, section); ...
, , IEnumerable «», . : -? — , , -, . , ( ) IConfigurationSection, . - .
ReadSection : FindSection.
[CanBeNull] public object FindSection(Type sectionInterface) { string sectionName = FindSectionName(sectionInterface); if (sectionName == null) { return null; }
, , AppSettings.Default: ( ) FindSection AppSettings, , , , AppSettings.Default, , ( — NULL 0).
:
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)) {
( — ). : , , , - . , - . , , .. , , , .
— , «--»? , , , , — , , . , , . , , .
GetPublicProperties, , , . , , , , , , , , , .
:
— FindSection — . شيء مثل هذا:
IThreadPoolProperties threadPoolProperties = ConfigurationProvider.FindSection<IThreadPoolProperties>();
— .
IoC- Castle Windsor. . , .
Extension-, , :
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); } }
( AllSections IServerConfigurationProvider).
, ServerConfigurationProvider, ServerConfigurationProvider Windsor.
, , FindSection IServerConfigurationProvider.
Windsor Extension-:
container.RegisterAllConfigurationSections(configProvider);
الخاتمة
XML YAML, .
YAML-, XML, , .
YAML, . , , . , .
- « ». , YAML- ( -).
— , « ».
, - , -, .. , - - .
AppSettings.Default, , .