Исследуем .NET 6. Часть 1

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

В этой серии статей я собираюсь взглянуть на некоторые из новых функций, которые появились в .NET 6. Про .NET 6 уже написано много контента, в том числе множество постов непосредственно от команд .NET и ASP.NET. Я же собираюсь рассмотреть код некоторых из этих новых функций.

Заглянем в ConfigurationManager

В этом первом посте я рассмотрю класс ConfigurationManager, почему он был добавлен, а также часть кода, использованного для его реализации.

Погодите-ка, что за ConfigurationManager?

Если у вас сразу возник этот вопрос, не волнуйтесь, вы не пропустили ничего важного!

ConfigurationManager был добавлен для поддержки новой модели WebApplication в ASP.NET Core*, используемой для упрощения кода запуска приложений ASP.NET Core. Однако ConfigurationManager во многом является деталью реализации. Он был введён для оптимизации определённого сценария (который я вкратце опишу), но в большинстве случаев вы не будете (да это и не нужно) знать, что вы его используете.

Прежде чем мы перейдём, собственно, к ConfigurationManager, рассмотрим, что он заменяет и почему.

Конфигурация приложений в .NET 5

.NET 5 предоставляет несколько типов конфигурации, но два основных из них, которые вы используете непосредственно в своих приложениях, приведены ниже:

  • IConfigurationBuilder — используется для добавления источников конфигурации. Вызов Build() в построителе считывает каждый из источников конфигурации и строит окончательную конфигурацию.

  • IConfigurationRoot — представляет собой окончательную «построенную» конфигурацию.

Интерфейс IConfigurationBuilder, по сути, представляет собой обёртку для списка источников конфигурации. Поставщики конфигурации обычно включают методы расширения (например, AddJsonFile() и AddAzureKeyVault()), которые добавляют источник конфигурации в список источников.

public interface IConfigurationBuilder
{
  IDictionary<string, object> Properties { get; }
  IList<IConfigurationSource> Sources { get; }
  IConfigurationBuilder Add(IConfigurationSource source);
  IConfigurationRoot Build();
}

IConfigurationRoot в свою очередь представляет окончательные «многоуровневые» значения конфигурации, объединяя все значения из каждого из источников конфигурации, чтобы дать окончательное «плоское» представление всех значений.

Более поздние поставщики конфигурации (переменные среды) перезаписывают значения, добавленные более ранними поставщиками конфигурации (appsettings.json, sharedsettings.json). Взято из моей книги ASP.NET в Действии (https://dmkpress.com/catalog/computer/web/978-5-97060-550-9/)
Более поздние поставщики конфигурации (переменные среды) перезаписывают значения, добавленные более ранними поставщиками конфигурации (appsettings.json, sharedsettings.json). Взято из моей книги ASP.NET в Действии (https://dmkpress.com/catalog/computer/web/978-5-97060-550-9/)

В .NET 5 и ранее интерфейсы IConfigurationBuilder и IConfigurationRoot реализуются с помощью ConfigurationBuilder и ConfigurationRoot соответственно. Если бы вы использовали эти типы напрямую, вы могли бы сделать что-то вроде этого:

var builder = new ConfigurationBuilder();
// добавляем статические значения builder.AddInMemoryCollection(new Dictionary<string, string>
{
  { "MyKey", "MyValue" },
});

// добавляем значения из json файла
builder.AddJsonFile("appsettings.json");
// создаём экземпляр IConfigurationRoot
IConfigurationRoot config = builder.Build();

// получаем значение
string value = config["MyKey"];
// получаем секцию
IConfigurationSection section = config.GetSection("SubSection");

В типичном приложении ASP.NET Core вы не создаёте ConfigurationBuilder самостоятельно или не вызываете Build(), однако это то, что происходит за кулисами. Между этими двумя типами существует чёткое разделение, и в большинстве случаев эта система конфигурации работает хорошо. Так зачем нам новый тип в .NET 6?

Проблема "частичной сборки конфигурации" в .NET 5

Основная проблема с этим подходом проявляется, когда вам нужно построить конфигурацию «частично». Это распространённая проблема, когда вы храните свою конфигурацию в таком сервисе, как Azure Key Vault, или даже в базе данных.

Например, ниже приведён рекомендуемый способ чтения секретов из Azure Key Vault внутри ConfigureAppConfiguration() в ASP.NET Core:

.ConfigureAppConfiguration((context, config) =>
{
  // "нормальная" конфигурация
config.AddJsonFile("appsettings.json");
  config.AddEnvironmentVariables();

  if (context.HostingEnvironment.IsProduction())
  {
    // построение частичной конфигурации
    IConfigurationRoot partialConfig = config.Build();
    // чтение значения из конфигурации
    string keyVaultName = partialConfig["KeyVaultName"];
    var secretClient = new SecretClient(
      new Uri($"https://{keyVaultName}.vault.azure.net/"),
      new DefaultAzureCredential());
    // добавляем ещё один источник конфигурации
    config.AddAzureKeyVault(secretClient, new KeyVaultSecretManager());

    // Фреймворк СНОВА вызывает config.Build(),
    // чтобы построить окончательный IConfigurationRoot
  }
})

Для настройки поставщика Azure Key Vault требуется значение конфигурации, поэтому вы столкнулись с проблемой курицы и яйца: вы не можете добавить источник конфигурации, пока не создадите конфигурацию!

Решение состоит в следующем:

  • Добавить «начальные» значения конфигурации.

  • Создать «частичный» результат конфигурации, вызвав IConfigurationBuilder.Build()

  • Получить необходимые значения конфигурации из построенного IConfigurationRoot

  • Использовать эти значения, чтобы добавить оставшиеся источники конфигурации.

  • Фреймворк неявно вызывает IConfigurationBuilder.Build(), генерируя окончательный IConfigurationRoot и используя его для окончательной конфигурации приложения.

Этот танец с бубном немного странный, но формально в нём нет ничего неправильного. Тогда в чём же проблема?

Проблемой является то, что нам нужно вызвать Build() дважды: один раз для создания IConfigurationRoot с использованием только начальных источников, а затем ещё раз для создания IConfiguartionRoot с использованием всех источников, включая источник Azure Key Vault.

В реализации ConfigurationBuilder по умолчанию вызов Build() выполняет итерацию по всем источникам, загружает поставщиков и передаёт их новому экземпляру ConfigurationRoot:

public IConfigurationRoot Build()
{
  var providers = new List<IConfigurationProvider>();
  foreach (IConfigurationSource source in Sources)
  {
    IConfigurationProvider provider = source.Build(this);
    providers.Add(provider);
  }
  return new ConfigurationRoot(providers);
}

Затем ConfigurationRoot по очереди перебирает каждого из этих поставщиков и загружает значения конфигурации.

public class ConfigurationRoot : IConfigurationRoot, IDisposable
{
  private readonly IList<IConfigurationProvider> _providers;
  private readonly IList<IDisposable> _changeTokenRegistrations;

  public ConfigurationRoot(IList<IConfigurationProvider> providers)
  {
    _providers = providers;
    _changeTokenRegistrations = new List<IDisposable>(providers.Count);

    foreach (IConfigurationProvider p in providers)
    {
      p.Load();
      _changeTokenRegistrations.Add(ChangeToken.OnChange(() => p.GetReloadToken(), () => RaiseChanged()));
    }
  }
  // ... остальная реализация
}

Если вы вызовете Build() дважды во время запуска приложения, всё это выполнится дважды.

Строго говоря, нет ничего плохого в том, чтобы получить данные из источника конфигурации более одного раза, но это ненужная работа и часто включает (относительно медленное) чтение файлов и т. п.

Это настолько распространённый сценарий, что в .NET 6 был введён новый тип ConfigurationManager, позволяющий избежать этого «перепостроения».

Менеджер Конфигурации в .NET 6

В .NET 6 разработчики .NET добавили новый тип конфигурации, ConfigurationManager, как часть «упрощённой» модели приложения. Этот тип реализует как IConfigurationBuilder, так и IConfigurationRoot. Объединив обе реализации в одном типе, .NET 6 может оптимизировать этот распространённый сценарий, показанный в предыдущем разделе.

В ConfigurationManager, когда добавляется IConfigurationSource (например, когда вы вызываете AddJsonFile()), поставщик сразу загружается, и конфигурация обновляется. Это позволяет избежать загрузки источников конфигурации более одного раза в сценарии частичной сборки.

Реализовать это немного сложнее, чем кажется, из-за интерфейса IConfigurationBuilder, хранящего источники в виде IList<IConfigurationSource>:

public interface IConfigurationBuilder
{
  IList<IConfigurationSource> Sources { get; }
  // … другие члены
}

Проблема с этим с точки зрения ConfigurationManager заключается в том, что IList<> предоставляет методы Add() и Remove(). Если бы использовался простой List<>, потребители могли бы добавлять и удалять поставщиков конфигурации, а ConfigurationManager об этом бы не знал.

Чтобы обойти это, ConfigurationManager использует свою реализацию IList<>. Она содержит ссылку на экземпляр ConfigurationManager, чтобы любые изменения могли быть отражены в конфигурации:

private class ConfigurationSources : IList<IConfigurationSource>
{
  private readonly List<IConfigurationSource> _sources = new();
  private readonly ConfigurationManager _config;

  public ConfigurationSources(ConfigurationManager config)
  {
    _config = config;
  }

  public void Add(IConfigurationSource source)
  {
    _sources.Add(source);
    // добавляет источник в ConfigurationManager
    _config.AddSource(source);
  }

  public bool Remove(IConfigurationSource source)
  {
    var removed = _sources.Remove(source);
    // перезагрузка источников в ConfigurationManager
    _config.ReloadSources();
    return removed;
  }

  // ... остальная реализация
}

Используя собственную реализацию IList<>, ConfigurationManager гарантирует, что AddSource() вызывается всякий раз, когда добавляется новый источник. В этом заключается преимущество ConfigurationManager: вызов AddSource() немедленно загружает источник.

public class ConfigurationManager
{
  private void AddSource(IConfigurationSource source)
  {
    lock (_providerLock)
    {
      IConfigurationProvider provider = source.Build(this);
      _providers.Add(provider);
      provider.Load();
      _changeTokenRegistrations.Add(ChangeToken.OnChange(() => provider.GetReloadToken(), () => RaiseChanged()));
    }

    RaiseChanged();
  }
}

Этот метод немедленно вызывает Build на IConfigurationSource для создания IConfigurationProvider и добавляет его в список поставщиков.

Затем вызывается метод IConfigurationProvider.Load(). Он загружает данные в поставщик (например, из переменных среды, файла JSON или Azure Key Vault) и является «дорогостоящим» шагом, для которого всё это и затевалось! В «нормальном» случае, когда вы просто добавляете источники в IConfigurationBuilder и в случае, когда вам требуется построить его несколько раз, это более «оптимальный» подход: источники загружаются один раз, и только один раз.

Реализация Build() в ConfigurationManager теперь пустая, просто возвращает себя.

IConfigurationRoot IConfigurationBuilder.Build () => this;

Конечно, разработка программного обеспечения — это всегда компромиссы. Инкрементное создание источников при их добавлении хорошо работает, если вы только добавляете источники. Однако, если вы вызываете любую из других функций IList<>, таких как Clear(), Remove() или индексатор, ConfigurationManager должен вызвать ReloadSources()

private void ReloadSources()
{
  lock (_providerLock)
  {
    DisposeRegistrationsAndProvidersUnsynchronized();

    _changeTokenRegistrations.Clear();
    _providers.Clear();

    foreach (var source in _sources)
    {
      _providers.Add(source.Build(this));
    }

    foreach (var p in _providers)
    {
      p.Load();
      _changeTokenRegistrations.Add(ChangeToken.OnChange(() => p.GetReloadToken(), () => RaiseChanged()));
    }
  }

  RaiseChanged();
}

Как видите, если какой-либо из источников изменится, ConfigurationManager должен удалить всё и начать заново, перебирая каждый из источников и перезагружая их. Если вы много манипулируете источниками конфигурации, это может дорого вам обойтись, и полностью сведёт на нет первоначальное преимущество ConfigurationManager.

Конечно, удаление источников довольно странная операция: обычно нет причин делать что-либо, кроме добавления, — поэтому ConfigurationManager оптимизирован для наиболее распространенных случаев. Кто бы мог подумать?

Источник: https://habr.com/ru/post/594423/


Интересные статьи

Интересные статьи

Привет, я Дарья Двоеглазова, менеджер продукта в Quadcode. В своей предыдущей статье я рассказала об инструментах и методах, которые использовала для планирования и тайм-менеджмента последние 3,5 года...
В предыдущей части статьи, было рассказано об основных принципах работы и особенностях проектирования радаров для применения в дорожной инфраструктуре. Теперь попробуем разобраться с радарами для авто...
Доспехи от гризли, дианетика, чай, эффект Даннинга—Крюгера, антихристы, драки, вкус головастиков и, конечно же, секс любовь. Мне попалась книга по давно интересующей меня теме. В книге описаны шн...
Технологические гиганты при помощи денег инвестиционных фондов контролируют всё большую часть новых разработчиков и продуктов, перекрывая тем самым путь для новых програм...
С чего начать? Кто такой вовлеченный сотрудник? Это человек, который максимально лоялен к Компании, в которой он работает. Он категорически не хочет уволится, а, наоборот, помогает с ...