Каковы затраты и возможные побочные эффекты вызова BuildServiceProvider () в ConfigureServices () - PullRequest
2 голосов
/ 08 мая 2019

Иногда при регистрации службы мне нужно разрешить другие (уже зарегистрированные) службы из контейнера DI. С такими контейнерами, как Autofac или DryIoc, это не составило особого труда, поскольку вы могли зарегистрировать службу в одной строке, а в следующей - сразу разрешить ее.

Но с помощью контейнера DI от Microsoft вам необходимо зарегистрировать службу, затем создать поставщика услуг, и только тогда вы сможете разрешить службы из этого IServiceProvider экземпляра.

См. Принятый ответ на этот вопрос SO: Сообщения об ошибках привязки базовой модели ASP.NET

public void ConfigureServices(IServiceCollection services)
{
    services.AddLocalization(options => { options.ResourcesPath = "Resources"; });
    services.AddMvc(options =>
    {
        var F = services.BuildServiceProvider().GetService<IStringLocalizerFactory>();
        var L = F.Create("ModelBindingMessages", "AspNetCoreLocalizationSample");
        options.ModelBindingMessageProvider.ValueIsInvalidAccessor =
            (x) => L["The value '{0}' is invalid."];

        // omitted the rest of the snippet
    })
}

Чтобы иметь возможность локализовать сообщение ModelBindingMessageProvider.ValueIsInvalidAccessor, ответ предлагает разрешить IStringLocalizerFactory через поставщика услуг, построенного на основе текущего набора услуг.

Какова стоимость "построения" поставщика услуг в этот момент и есть ли какие-либо побочные эффекты от этого, поскольку поставщик услуг будет создан по крайней мере еще один раз (после добавления всех услуг)?

1 Ответ

5 голосов
/ 09 мая 2019

Каждый поставщик услуг имеет свой собственный кэш. Следовательно, создание нескольких экземпляров поставщика услуг может привести к проблеме под названием Torn Lifestyles :

Когда несколько [регистраций] с одной и той же картой образа жизни относятся к одному и тому же компоненту, компонент, как говорят, имеет разорванный образ жизни. Компонент считается порванным, поскольку каждая [регистрация] будет иметь свой собственный кэш данного компонента, что потенциально может привести к нескольким экземплярам компонента в одной области действия. При разрыве регистраций приложение может быть неправильно подключено, что может привести к неожиданному поведению.

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

Но есть и другие, такие же тонкие ошибки, которые могут появиться. Например, при разрешении графов объектов, которые содержат зависимые области. Создание отдельного временного поставщика услуг для создания графа объектов, который хранится в следующем контейнере, может привести к тому, что эти зависимые области будут поддерживаться в течение всего времени приложения. Эту проблему обычно называют Зависимые значения .

С такими контейнерами, как Autofac или DryIoc, это не составило особого труда, поскольку вы могли зарегистрировать службу в одной строке, а в следующей - сразу разрешить ее.

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

Особенно из-за трудных для отслеживания ошибок контейнеров DI, таких как Autofac, Simple Injector и Microsoft.Extensions.DependencyInjection (MS.DI), в первую очередь мешают вам сделать это. Autofac и MS.DI делают это путем регистрации в «конструкторе контейнеров» (AutoFac ContainerBuilder и MS.DI ServiceCollection). Простой Injector, с другой стороны, не делает это разделение. Вместо этого он блокирует контейнер от любых изменений после разрешения первого экземпляра. Эффект, однако, похож; после разрешения вы не сможете добавлять регистрации.

Документация Simple Injector на самом деле содержит достойное объяснение о том, почему этот шаблон Register-Resolve-Register проблематичен:

Представьте себе сценарий, в котором вы хотите заменить некоторый компонент FileLogger для другой реализации с тем же интерфейсом ILogger. Если есть компонент, который прямо или косвенно зависит от ILogger, замена реализации ILogger может работать не так, как вы ожидаете. Например, если потребительский компонент зарегистрирован как одноэлементный, контейнер должен гарантировать, что будет создан только один экземпляр этого компонента. Когда вам разрешено изменить реализацию ILogger после того, как экземпляр-одиночка уже содержит ссылку на «старую» зарегистрированную реализацию, у контейнера есть два варианта выбора, ни один из которых не является правильным:

  • Возвращает кэшированный экземпляр потребляющего компонента, который имеет ссылку на «неправильную» реализацию ILogger.
  • Создайте и кэшируйте новый экземпляр этого компонента и при этом нарушите обещание регистрации типа в качестве одиночного и гарантию, что контейнер всегда будет возвращать один и тот же экземпляр.

По этой же причине вы видите, что класс ASP.NET Core Startup определяет две отдельные фазы:

  • Фото «Добавить»ase (метод ConfigureServices), где вы добавляете регистрации в «контейнерный конструктор» (a.k.a. IServiceCollection)
  • Фаза «Использование» (метод Configure), где вы заявляете, что хотите использовать MVC, устанавливая маршруты. На этом этапе IServiceCollection был превращен в IServiceProvider, и эти службы могут быть даже внедрены в метод Configure.

Таким образом, общее решение заключается в том, чтобы отложить разрешающие сервисы (например, IStringLocalizerFactory) до фазы «Использование», а вместе с этим отложить окончательную настройку вещей, зависящих от разрешения сервисов.

Это, к сожалению, кажется причиной курицы или яйца дилемма причинности, когда дело доходит до настройки ModelBindingMessageProvider, потому что:

  • Настройка ModelBindingMessageProvider требует использования класса MvcOptions.
  • Класс MvcOptions доступен только во время фазы «Добавить» (Configure).
  • На этапе «Добавить» нет доступа к IStringLocalizerFactory и нет доступа к контейнеру или поставщику услуг, и его разрешение нельзя отложить, создав такое значение с помощью Lazy<IStringLocalizerFactory>.
  • Во время фазы «Использования» доступно IStringLocalizerFactory, но на этом этапе MvcOptions больше не может использоваться для настройки ModelBindingMessageProvider.

Единственный выход из этого тупика - использовать закрытые поля внутри класса Startup и использовать их при закрытии AddOptions. Например:

public void ConfigureServices(IServiceCollection services)
{
    services.AddLocalization();
    services.AddMvc(options =>
    {
        options.ModelBindingMessageProvider.SetValueIsInvalidAccessor(
            _ => this.localizer["The value '{0}' is invalid."]);
    });
}

private IStringLocalizer localizer;

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    this.localizer = app.ApplicationServices
        .GetRequiredService<IStringLocalizerFactory>()
        .Create("ModelBindingMessages", "AspNetCoreLocalizationSample");
}

Недостатком этого решения является то, что оно вызывает временную связь , что является собственным запахом кода.

Можно, конечно, утверждать, что это уродливый обходной путь для проблемы, которая может даже не существовать при работе с IStringLocalizerFactory; создание временного поставщика услуг для разрешения фабрики локализации может работать в этом конкретном случае просто отлично. Дело, однако, в том, что на самом деле довольно сложно проанализировать, попадете ли вы в беду или нет. Например:

  • Несмотря на то, что ResourceManagerStringLocalizerFactory, фабрика локализатора по умолчанию, не содержит никакого состояния, она зависит от других служб, а именно IOptions<LocalizationOptions> и ILoggerFactory. Оба из которых настроены как синглтоны.
  • Реализация ILoggerFactory по умолчанию (т. Е. LoggerFactory) создается поставщиком услуг, и экземпляры ILoggerProvider могут быть добавлены впоследствии к этой фабрике. Что произойдет, если ваш второй ResourceManagerStringLocalizerFactory зависит от собственной реализации ILoggerFactory? Будет ли это работать правильно?
  • То же самое относится к IOptions<T> - реализуется OptionsManager<T>. Это синглтон, но OptionsManager<T> сам по себе зависит от IOptionsFactory<T> и содержит свой собственный кеш. Что произойдет, если для определенного T есть секунда OptionsManager<T>? И может ли это измениться в будущем?
  • Что, если ResourceManagerStringLocalizerFactory заменить другой реализацией? Это не маловероятный сценарий. Как будет выглядеть график зависимости, и будет ли это причиной проблем, если образ жизни порвется?
  • В целом, даже если вы сможете сделать вывод, что все работает правильно, вы уверены, что так будет и в любой будущей версии ASP.NET Core? Нетрудно представить, что обновление до будущей версии ASP.NET Core будет ломать ваше приложение очень тонкими и странными способами, потому что вы неявно зависите от этого конкретного поведения. Эти ошибки будет довольно сложно отследить.

К сожалению, когда дело доходит до настройки ModelBindingMessageProvider, кажется, нет простого выхода. Это IMO недостаток дизайна в ASP.NET Core MVC. Надеюсь, Microsoft исправит это в следующем выпуске.

...