HTML Diff
0 added 0 removed
Original 2026-01-01
Modified 2026-03-10
1 <p>Однажды в компании, где я работаю, было принято решение перейти на конфигурирование через модный YAML. Какие проблемы при этом перед нами встали, и как мы их решили - в этой статье.</p>
1 <p>Однажды в компании, где я работаю, было принято решение перейти на конфигурирование через модный YAML. Какие проблемы при этом перед нами встали, и как мы их решили - в этой статье.</p>
2 <h2>Предыстория вопроса</h2>
2 <h2>Предыстория вопроса</h2>
3 <p>Нашей компанией, среди прочего, разработаны несколько сервисов (точнее - 12), работающих бэкендом наших систем. Каждый из сервисов представляет собой Windows-службу и выполняет свои специфические задачи.</p>
3 <p>Нашей компанией, среди прочего, разработаны несколько сервисов (точнее - 12), работающих бэкендом наших систем. Каждый из сервисов представляет собой Windows-службу и выполняет свои специфические задачи.</p>
4 <p>Хочется все эти сервисы перенести под *nix-ОС. Для этого надо отказываться от обёртки в виде Windows-служб и переходить с .NET Framework на .NET Standard.</p>
4 <p>Хочется все эти сервисы перенести под *nix-ОС. Для этого надо отказываться от обёртки в виде Windows-служб и переходить с .NET Framework на .NET Standard.</p>
5 <p>Последнее требование приводит к необходимости избавиться от некоторого Legacy-кода, который не поддерживается в .NET Standard, в т. ч. от поддержки конфигурирования наших серверов через XML, реализованного с использованием классов из System.Configuration. Заодно таким образом решается и давняя проблема, связанная с тем, что в XML-конфигах мы время от времени ошибались при изменении настроек (например, иногда не туда ставили закрывающий тэг или забывали его вовсе), а замечательная читалка XML-конфигов System.Xml.XmlDocument молча проглатывает такие конфиги, выдавая совсем непредсказуемый результат.</p>
5 <p>Последнее требование приводит к необходимости избавиться от некоторого Legacy-кода, который не поддерживается в .NET Standard, в т. ч. от поддержки конфигурирования наших серверов через XML, реализованного с использованием классов из System.Configuration. Заодно таким образом решается и давняя проблема, связанная с тем, что в XML-конфигах мы время от времени ошибались при изменении настроек (например, иногда не туда ставили закрывающий тэг или забывали его вовсе), а замечательная читалка XML-конфигов System.Xml.XmlDocument молча проглатывает такие конфиги, выдавая совсем непредсказуемый результат.</p>
6 <p>Таким образом и было решено перейти на конфигурирование через модный YAML.</p>
6 <p>Таким образом и было решено перейти на конфигурирование через модный YAML.</p>
7 <h2>Что имеем</h2>
7 <h2>Что имеем</h2>
8 <h3>Как мы читаем конфигурацию из XML</h3>
8 <h3>Как мы читаем конфигурацию из XML</h3>
9 <p>Читаем XML стандартным и для большинства других проектов способом.</p>
9 <p>Читаем XML стандартным и для большинства других проектов способом.</p>
10 <p>В каждом сервисе есть файл настроек .NET-проектов, называется AppSettings.cs, содержит все требующиеся сервису настройки. Примерно так:</p>
10 <p>В каждом сервисе есть файл настроек .NET-проектов, называется AppSettings.cs, содержит все требующиеся сервису настройки. Примерно так:</p>
11 [System.Configuration.SettingsProvider(typeof(PortableSettingsProvider))] internal sealed partial class AppSettings : IServerManagerConfigStorage, IWebSettingsStorage, IServerSettingsStorage, IGraphiteAddressStorage, IDatabaseConfigStorage, IBlackListStorage, IKeyCloackConfigFilePathProvider, IPrometheusSettingsStorage, IMetricsConfig { }<p>Подобная техника разделения настроек на интерфейсы позволяет удобно использовать их в дальнейшем через DI-контейнер.</p>
11 [System.Configuration.SettingsProvider(typeof(PortableSettingsProvider))] internal sealed partial class AppSettings : IServerManagerConfigStorage, IWebSettingsStorage, IServerSettingsStorage, IGraphiteAddressStorage, IDatabaseConfigStorage, IBlackListStorage, IKeyCloackConfigFilePathProvider, IPrometheusSettingsStorage, IMetricsConfig { }<p>Подобная техника разделения настроек на интерфейсы позволяет удобно использовать их в дальнейшем через DI-контейнер.</p>
12 <p>Вся основная магия по хранению настроек на самом деле скрыта в PortableSettingsProvider (см. атрибут класса), а также в файле дизайнера AppSettings.Designer.cs:</p>
12 <p>Вся основная магия по хранению настроек на самом деле скрыта в PortableSettingsProvider (см. атрибут класса), а также в файле дизайнера AppSettings.Designer.cs:</p>
13 [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; } } ...<p>Как видно, "за кулисами" скрыты все те свойства, которые мы добавляем в конфигурацию сервера, когда редактируем ее через дизайнер настроек в Visual Studio.</p>
13 [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; } } ...<p>Как видно, "за кулисами" скрыты все те свойства, которые мы добавляем в конфигурацию сервера, когда редактируем ее через дизайнер настроек в Visual Studio.</p>
14 <p>Наш класс PortableSettingsProvider, упомянутый выше, занимается непосредственно чтением XML-файла, а прочитанный результат уже используется в SettingsProvider для записи настроек в свойства AppSettings.</p>
14 <p>Наш класс PortableSettingsProvider, упомянутый выше, занимается непосредственно чтением XML-файла, а прочитанный результат уже используется в SettingsProvider для записи настроек в свойства AppSettings.</p>
15 <p>Пример XML-конфига, который мы читаем (бОльшая часть настроек скрыта из соображений безопасности):</p>
15 <p>Пример XML-конфига, который мы читаем (бОльшая часть настроек скрыта из соображений безопасности):</p>
16 &lt;?xml version="1.0" encoding="utf-8"?&gt; &lt;configuration&gt; &lt;configSections&gt; &lt;sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup"&gt; &lt;section name="MetricServer.Properties.Settings" type="System.Configuration.ClientSettingsSection" /&gt; &lt;/sectionGroup&gt; &lt;/configSections&gt; &lt;userSettings&gt; &lt;MetricServer.Properties.Settings&gt; &lt;setting name="MCXSettings" serializeAs="String"&gt; &lt;value&gt;Inactive, ChartLen: 1000, PrintLen: 50, UseProxy: False&lt;/value&gt; &lt;/setting&gt; &lt;setting name="KickUnknownAfter" serializeAs="String"&gt; &lt;value&gt;00:00:10&lt;/value&gt; &lt;/setting&gt; ... &lt;/MetricServer.Properties.Settings&gt; &lt;/userSettings&gt; &lt;/configuration&gt;<h3>Какие YAML-файлы хотелось бы читать</h3>
16 &lt;?xml version="1.0" encoding="utf-8"?&gt; &lt;configuration&gt; &lt;configSections&gt; &lt;sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup"&gt; &lt;section name="MetricServer.Properties.Settings" type="System.Configuration.ClientSettingsSection" /&gt; &lt;/sectionGroup&gt; &lt;/configSections&gt; &lt;userSettings&gt; &lt;MetricServer.Properties.Settings&gt; &lt;setting name="MCXSettings" serializeAs="String"&gt; &lt;value&gt;Inactive, ChartLen: 1000, PrintLen: 50, UseProxy: False&lt;/value&gt; &lt;/setting&gt; &lt;setting name="KickUnknownAfter" serializeAs="String"&gt; &lt;value&gt;00:00:10&lt;/value&gt; &lt;/setting&gt; ... &lt;/MetricServer.Properties.Settings&gt; &lt;/userSettings&gt; &lt;/configuration&gt;<h3>Какие YAML-файлы хотелось бы читать</h3>
17 <p>Примерно такие:</p>
17 <p>Примерно такие:</p>
18 VirtualFeed: MaxChartHistoryLength: 10 Port: 35016 UseThrottling: True ThrottlingIntervalMs: 50000 UseHistoryBroadcast: True CalendarName: "EmptyCalendar" UsMarketFeed: UseImbalances: True<h2>Проблемы перехода</h2>
18 VirtualFeed: MaxChartHistoryLength: 10 Port: 35016 UseThrottling: True ThrottlingIntervalMs: 50000 UseHistoryBroadcast: True CalendarName: "EmptyCalendar" UsMarketFeed: UseImbalances: True<h2>Проблемы перехода</h2>
19 <p><strong>Во-первых</strong>, конфиги в XML - "плоские", а в YAML - нет (поддерживаются секции и подсекции). Это хорошо видно в приведенных выше примерах. При использовании XML мы решали проблему плоских настроек вводом собственных парсеров, которые умеют строки определенного вида преобразовывать в наши более сложные классы. Пример такой сложной строки:</p>
19 <p><strong>Во-первых</strong>, конфиги в XML - "плоские", а в YAML - нет (поддерживаются секции и подсекции). Это хорошо видно в приведенных выше примерах. При использовании XML мы решали проблему плоских настроек вводом собственных парсеров, которые умеют строки определенного вида преобразовывать в наши более сложные классы. Пример такой сложной строки:</p>
20 &lt;setting name="MCXSettings" serializeAs="String"&gt; &lt;value&gt;Inactive, ChartLen: 1000, PrintLen: 50, UseProxy: False&lt;/value&gt; &lt;/setting&gt;<p>Заниматься такими преобразованиями при работе с YAML совсем не хочется. Но при этом мы ограничены существующей "плоской" структурой класса AppSettings: все свойства настроек в нем свалены в одну кучу.</p>
20 &lt;setting name="MCXSettings" serializeAs="String"&gt; &lt;value&gt;Inactive, ChartLen: 1000, PrintLen: 50, UseProxy: False&lt;/value&gt; &lt;/setting&gt;<p>Заниматься такими преобразованиями при работе с YAML совсем не хочется. Но при этом мы ограничены существующей "плоской" структурой класса AppSettings: все свойства настроек в нем свалены в одну кучу.</p>
21 <p><strong>Во-вторых</strong>, конфиги наших серверов - это не статичный монолит, мы их время от времени меняем прямо по ходу работы сервера, т.е. эти изменения надо уметь отлавливать "на лету", в рантайме. Для этого в XML-реализации мы наследуем наш AppSettings от INotifyPropertyChanged (на самом деле от него унаследован каждый интерфейс, который реализует AppSettings) и подписываемся на события обновления свойств настроек. Работает такой подход от того, что базовый класс System.Configuration.ApplicationSettingsBase "из коробки" реализует INotifyPropertyChanged. Подобное поведение надо сохранить и после перехода на YAML.</p>
21 <p><strong>Во-вторых</strong>, конфиги наших серверов - это не статичный монолит, мы их время от времени меняем прямо по ходу работы сервера, т.е. эти изменения надо уметь отлавливать "на лету", в рантайме. Для этого в XML-реализации мы наследуем наш AppSettings от INotifyPropertyChanged (на самом деле от него унаследован каждый интерфейс, который реализует AppSettings) и подписываемся на события обновления свойств настроек. Работает такой подход от того, что базовый класс System.Configuration.ApplicationSettingsBase "из коробки" реализует INotifyPropertyChanged. Подобное поведение надо сохранить и после перехода на YAML.</p>
22 <p><strong>В-третьих</strong>, конфигов по каждому серверу у нас, на самом деле, не один, а целых два: один с дефолтными настройками, другой - с переопределенными. Это требуется для того, чтобы в каждом из нескольких инстансов серверов одного типа, слушающих разные порты и имеющих немного отличающиеся настройки, не приходилось полностью копировать весь набор настроек.</p>
22 <p><strong>В-третьих</strong>, конфигов по каждому серверу у нас, на самом деле, не один, а целых два: один с дефолтными настройками, другой - с переопределенными. Это требуется для того, чтобы в каждом из нескольких инстансов серверов одного типа, слушающих разные порты и имеющих немного отличающиеся настройки, не приходилось полностью копировать весь набор настроек.</p>
23 <p>И еще одна проблема - доступ к настройкам идет не только через интерфейсы, но и прямым обращением к AppSettings.Default. Напомню как он объявлен в закулисном AppSettings.Designer.cs:</p>
23 <p>И еще одна проблема - доступ к настройкам идет не только через интерфейсы, но и прямым обращением к AppSettings.Default. Напомню как он объявлен в закулисном AppSettings.Designer.cs:</p>
24 private static AppSettings defaultInstance = ((AppSettings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new AppSettings()))); public static AppSettings Default { get { return defaultInstance; } }<p>С учетом изложенного требовалось придумать новый подход к хранению настроек в AppSettings.</p>
24 private static AppSettings defaultInstance = ((AppSettings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new AppSettings()))); public static AppSettings Default { get { return defaultInstance; } }<p>С учетом изложенного требовалось придумать новый подход к хранению настроек в AppSettings.</p>
25 <h2>Решение</h2>
25 <h2>Решение</h2>
26 <h3>Инструментарий</h3>
26 <h3>Инструментарий</h3>
27 <p>Непосредственно для чтения YAML решили использовать готовые библиотеки, доступные через NuGet:</p>
27 <p>Непосредственно для чтения YAML решили использовать готовые библиотеки, доступные через NuGet:</p>
28 <ol><li><strong>YamlDotNet</strong>-<em>github.com/aaubry/YamlDotNet</em>. Из описания библиотеки (перевод):<em>YamlDotNet - это .NET библиотека для YAML. YamlDotNet предоставляет низкоуровневые парсер и генератор YAML, а также высокоуровневую объектную модель, схожую с XmlDocument. Также сюда включена библиотека сериализации, которая позволяет читать и записывать объекты из/в YAML-потоков</em>.</li>
28 <ol><li><strong>YamlDotNet</strong>-<em>github.com/aaubry/YamlDotNet</em>. Из описания библиотеки (перевод):<em>YamlDotNet - это .NET библиотека для YAML. YamlDotNet предоставляет низкоуровневые парсер и генератор YAML, а также высокоуровневую объектную модель, схожую с XmlDocument. Также сюда включена библиотека сериализации, которая позволяет читать и записывать объекты из/в YAML-потоков</em>.</li>
29 <li><strong>NetEscapades.Configuration</strong>-<em>github.com/andrewlock/NetEscapades.Configuration</em>. Это непосредственно провайдер конфигураций (в смысле Microsoft.Extensions.Configuration.IConfigurationSource, активно используемого в ASP.NET Core приложениях), который читает YAML-файлы, используя как раз, упомянутый выше YamlDotNet.</li>
29 <li><strong>NetEscapades.Configuration</strong>-<em>github.com/andrewlock/NetEscapades.Configuration</em>. Это непосредственно провайдер конфигураций (в смысле Microsoft.Extensions.Configuration.IConfigurationSource, активно используемого в ASP.NET Core приложениях), который читает YAML-файлы, используя как раз, упомянутый выше YamlDotNet.</li>
30 </ol><p>Подробнее о том, как использовать указанные библиотеки можно почитать вот тут:<em>https://andrewlock.net/creating-a-custom-iconfigurationprovider-in-asp-net-core-to-parse-yaml/.</em></p>
30 </ol><p>Подробнее о том, как использовать указанные библиотеки можно почитать вот тут:<em>https://andrewlock.net/creating-a-custom-iconfigurationprovider-in-asp-net-core-to-parse-yaml/.</em></p>
31 <h3>Переход к YAML</h3>
31 <h3>Переход к YAML</h3>
32 <p>Сам переход мы осуществили в два этапа: сначала просто перешли от XML к YAML, но сохранив плоскую иерархию конфиг-файлов, а затем уже ввели секции в YAML-файлах. Эти этапы можно было, в принципе, объединить в один, и для простоты изложения я именно так и сделаю. Все описываемые далее действия применялись последовательно к каждому сервису.</p>
32 <p>Сам переход мы осуществили в два этапа: сначала просто перешли от XML к YAML, но сохранив плоскую иерархию конфиг-файлов, а затем уже ввели секции в YAML-файлах. Эти этапы можно было, в принципе, объединить в один, и для простоты изложения я именно так и сделаю. Все описываемые далее действия применялись последовательно к каждому сервису.</p>
33 <h3>Подготовка YML-файла</h3>
33 <h3>Подготовка YML-файла</h3>
34 <p>Сперва требуется подготовить сам YAML-файл. Назовем его именем проекта (полезно для будущих интеграционных тестов, которые должны уметь работать с разными серверами и различать их конфиги между собой), положим файлик прямо в корне проекта, рядом с AppSettings:</p>
34 <p>Сперва требуется подготовить сам YAML-файл. Назовем его именем проекта (полезно для будущих интеграционных тестов, которые должны уметь работать с разными серверами и различать их конфиги между собой), положим файлик прямо в корне проекта, рядом с AppSettings:</p>
35 <p>В самом YML-файле для начала сохраним "плоскую" структуру:</p>
35 <p>В самом YML-файле для начала сохраним "плоскую" структуру:</p>
36 VirtualFeed: "MaxChartHistoryLength: 10, UseThrottling: True, ThrottlingIntervalMs: 50000, UseHistoryBroadcast: True, CalendarName: EmptyCalendar" VirtualFeedPort: 35016 UsMarketFeedUseImbalances: True<h3>Наполнение AppSettings свойствами настроек</h3>
36 VirtualFeed: "MaxChartHistoryLength: 10, UseThrottling: True, ThrottlingIntervalMs: 50000, UseHistoryBroadcast: True, CalendarName: EmptyCalendar" VirtualFeedPort: 35016 UsMarketFeedUseImbalances: True<h3>Наполнение AppSettings свойствами настроек</h3>
37 <p>Перенесем все свойства из AppSettings.Designer.cs в AppSettings.cs, попутно избавляясь от ставших лишними атрибутов дизайнера и самого кода в get/set-частях.</p>
37 <p>Перенесем все свойства из AppSettings.Designer.cs в AppSettings.cs, попутно избавляясь от ставших лишними атрибутов дизайнера и самого кода в get/set-частях.</p>
38 <p>Было:</p>
38 <p>Было:</p>
39 [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; } }<p>Стало:</p>
39 [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; } }<p>Стало:</p>
40 public int VirtualFeedPort { get; set; }<p>Удалим полностью AppSettings.<strong>Designer</strong>.cs за ненадобностью. Теперь, кстати говоря, можно полностью избавиться от секции userSettings в файле app.config, если он есть в проекте - там хранятся те самые дефолтные настройки, которые мы прописываем через дизайнер настроек. Идем дальше.</p>
40 public int VirtualFeedPort { get; set; }<p>Удалим полностью AppSettings.<strong>Designer</strong>.cs за ненадобностью. Теперь, кстати говоря, можно полностью избавиться от секции userSettings в файле app.config, если он есть в проекте - там хранятся те самые дефолтные настройки, которые мы прописываем через дизайнер настроек. Идем дальше.</p>
41 <h3>Контроль изменения настроек "на лету"</h3>
41 <h3>Контроль изменения настроек "на лету"</h3>
42 <p>Так как нам надо уметь ловить обновления наших настроек в рантайме, то требуется реализовать INotifyPropertyChanged в нашем AppSettings. Базового System.Configuration.ApplicationSettingsBase больше нет, соответственно, рассчитывать на какую-то магию не приходится.</p>
42 <p>Так как нам надо уметь ловить обновления наших настроек в рантайме, то требуется реализовать INotifyPropertyChanged в нашем AppSettings. Базового System.Configuration.ApplicationSettingsBase больше нет, соответственно, рассчитывать на какую-то магию не приходится.</p>
43 <p>Можно реализовать "в лоб": добавив имплементацию метода, выкидывающего нужное событие, и вызывая его в сеттере каждого свойства. Но это лишние строки кода, которые к тому же надо будет копировать по всем сервисам.</p>
43 <p>Можно реализовать "в лоб": добавив имплементацию метода, выкидывающего нужное событие, и вызывая его в сеттере каждого свойства. Но это лишние строки кода, которые к тому же надо будет копировать по всем сервисам.</p>
44 <p>Поступим красивее - введем вспомогательный базовый класс AutoNotifier, который фактически делает то же самое, но "за кулисами", прямо как делал ранее System.Configuration.ApplicationSettingsBase:</p>
44 <p>Поступим красивее - введем вспомогательный базовый класс AutoNotifier, который фактически делает то же самое, но "за кулисами", прямо как делал ранее System.Configuration.ApplicationSettingsBase:</p>
45 /// &lt;summary&gt; /// Implements &lt;see cref="INotifyPropertyChanged"/&gt; for classes with a lot of public properties (i.e. 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 -&gt; Inherit1 -&gt; AutoNotifier; good idea: sealed Inherit -&gt; AutoNotifier) /// &lt;/summary&gt; public abstract class AutoNotifier : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; private readonly ConcurrentDictionary&lt;string, object&gt; _wrappedValues = new ConcurrentDictionary&lt;string, object&gt;(); //just to avoid manual writing a lot of fields protected T Get&lt;T&gt;([CallerMemberName] string propertyName = null) { return (T)_wrappedValues.GetValueOrDefault(propertyName, () =&gt; default(T)); } protected void Set&lt;T&gt;(T value, [CallerMemberName] string propertyName = null) { // ReSharper disable once AssignNullToNotNullAttribute _wrappedValues.AddOrUpdate(propertyName, value, (s, o) =&gt; value); OnPropertyChanged(propertyName); } public object this[string propertyName] { get { return Get&lt;object&gt;(propertyName); } set { Set(value, propertyName); } } protected void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }<p>На этом все, окончание читайте в моей статье на Хабре:<em>https://habr.com/ru/company/utex/blog/438362/</em>. Там же полностью выложен и итоговый код, который сюда не поместился :-).</p>
45 /// &lt;summary&gt; /// Implements &lt;see cref="INotifyPropertyChanged"/&gt; for classes with a lot of public properties (i.e. 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 -&gt; Inherit1 -&gt; AutoNotifier; good idea: sealed Inherit -&gt; AutoNotifier) /// &lt;/summary&gt; public abstract class AutoNotifier : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; private readonly ConcurrentDictionary&lt;string, object&gt; _wrappedValues = new ConcurrentDictionary&lt;string, object&gt;(); //just to avoid manual writing a lot of fields protected T Get&lt;T&gt;([CallerMemberName] string propertyName = null) { return (T)_wrappedValues.GetValueOrDefault(propertyName, () =&gt; default(T)); } protected void Set&lt;T&gt;(T value, [CallerMemberName] string propertyName = null) { // ReSharper disable once AssignNullToNotNullAttribute _wrappedValues.AddOrUpdate(propertyName, value, (s, o) =&gt; value); OnPropertyChanged(propertyName); } public object this[string propertyName] { get { return Get&lt;object&gt;(propertyName); } set { Set(value, propertyName); } } protected void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }<p>На этом все, окончание читайте в моей статье на Хабре:<em>https://habr.com/ru/company/utex/blog/438362/</em>. Там же полностью выложен и итоговый код, который сюда не поместился :-).</p>
46  
46