Почему контейнер DI удаляет службу с заданной областью дважды?

Обслуживание:

    public interface IMessageWriter
    {
        void Write(string message);
    }

    public class ConsoleMessageWriter:IMessageWriter, IDisposable
    {
        public void Dispose()
        {
            Console.WriteLine($"The {GetType().Name} instanse is disposed.");
        }

        public void Write(string message)
        {
            Console.WriteLine(message);
        }
    }

Теперь я использую его в области видимости:

using DI_Sandbox.Models;
using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();
services
    .AddScoped<ConsoleMessageWriter>()
    .AddScoped<IMessageWriter>(provider => provider.GetRequiredService<ConsoleMessageWriter>());

var container = services.BuildServiceProvider(validateScopes:true);
using (var serviceScope = container.CreateScope())
{
    var messageWriter1 = serviceScope.ServiceProvider.GetRequiredService<IMessageWriter>();
    var messageWriter2 = serviceScope.ServiceProvider.GetRequiredService<IMessageWriter>();

    Console.WriteLine($"messageWriter1 == messageWriter2: {messageWriter1 == messageWriter2}");

    messageWriter1.Write("Hello DI!");
}
Console.Write("THE END");

Выход:

messageWriter1 == messageWriter2: True
Hello DI!
The ConsoleMessageWriter instanse is disposed.
The ConsoleMessageWriter instanse is disposed.
THE END

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

Если ваш Dispose идемпотентен (как рекомендуется здесь ), то это раздражает, но, конечно, не блокирует.... «Чтобы обеспечить правильную очистку ресурсов, метод Dispose должен быть идемпотентным, чтобы он вызывается несколько раз без возникновения исключения. Кроме того, последующие вызовы Dispose не должны ничего делать».

spender 29.04.2024 18:26
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
1
60
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Вы сделали две регистрации. Один для ConsoleMessageWriter и один для IMessageWriter:

services.AddScoped<ConsoleMessageWriter>()
services.AddScoped<IMessageWriter>(p =>
    p.GetRequiredService<ConsoleMessageWriter>());

Хотя регистрация для IMessageWriter вызывает обратную связь с регистрацией ConsoleMessageWriter, MS.DI не знает об этом. Однако он знает, что обе регистрации приводят к созданию объекта IDisposable. Это происходит даже несмотря на то, что IMessageWriter не реализуется IDisposable. Проверка выполняется во время принятия решения. Каждая регистрация отслеживает свой возвращенный объект, не зная, что это тот же самый объект.

Вы можете считать это ошибкой дизайна, потому что, по крайней мере, MS.DI мог бы убедиться, что список одноразовых экземпляров дедуплицирован, прежде чем вызывать Dispose на них. Но это, вероятно, компромисс с производительностью, потому что:

  • Дедупликация списка может замедлить работу контейнера.
  • Не часто такая ситуация возникает
  • Методы Dispose в любом случае должны быть идемпотентными (как упоминалось в комментариях @spender), поэтому не должно быть проблем, когда метод Dispose вызывается дважды.

Если бы вы выполнили регистрацию следующим образом, это привело бы к проблеме Torn Lifestyle, приводящей к появлению двух экземпляров ConsoleMessageWriter в одной области:

// Incorrect registration. Leads to a Torn Lifestyle
services.AddScoped<ConsoleMessageWriter>()
services.AddScoped<IMessageWriter, ConsoleMessageWriter>();

Дизайн MS.DI прост (некоторые скажут наивный), и единственный способ предотвратить появление Torn Lifestyle компонентов - это выполнить регистрацию, которую вы сделали в своем вопросе:

services.AddScoped<ConsoleMessageWriter>()
services.AddScoped<IMessageWriter>(p =>
    p.GetRequiredService<ConsoleMessageWriter>());

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

Более зрелые DI-контейнеры имеют более сложные способы предотвращения разрыва образа жизни. На ум приходят два: Unity и Simple Injector. Например, Simple Injector позволит вам выполнить регистрацию следующим образом, сохраняя при этом один экземпляр ConsoleMessageWriter для каждой области:

// Registrations using Simple Injector. No Torn Lifestyle here.
container.Register<ConsoleMessageWriter>(Lifestyle.Scoped);
container.Register<IMessageWriter, ConsoleMessageWriter>(Lifestyle.Scoped);

Unity IoC ведет себя аналогично, в то время как для других DI-контейнеров может потребоваться конструкция, аналогичная той, что вам нужна для MS.DI.

Если дубликат удаления вызывает проблему, способ обойти это — создать одноразовый класс-оболочку для интерфейса IMessageWriter, например:

public sealed class NonDisposableMessageWriterWrapper : IMessageWriter
{
    private readonly IMessageWriter decoratee;

    public NonDisposableMessageWriterWrapper(IMessageWriter decoratee) =>
       this.decoratee = decoratee;

    public void Write(string message) => this.decoratee.Write(message);
}

И измените регистрации на следующие:

services.AddScoped<ConsoleMessageWriter>()
services.AddScoped<IMessageWriter>(p =>
    new NonDisposableMessageWriterWrapper(
        p.GetRequiredService<ConsoleMessageWriter>()));

Таким образом ConsoleMessageWriter больше не утилизируется дважды.

Но стоит ли эта дополнительная сложность того? Возможно нет.

Хороший! Никогда раньше не слышал о Torn Lifestyle, столкнусь ли я с той же проблемой, используя MS.DI .AddDbContext<C>() и .AddScoped<IDbContext, C>()?

Norcino 01.05.2024 17:19

Да, ты бы сделал это. Это может вызвать ту же проблему: вы получите два экземпляра C в одной области.

Steven 01.05.2024 18:56

Другие вопросы по теме