Comments 110
Не совсем понял мотивацию для использования.

При использовании Dependency Injection объект класса не только не должен отвечать за жизненный цикл своих зависимостей, он просто физически не может это делать: зависимость может реализовать IDisposable, а может не реализовать, но при этом у нее могут быть свои зависимости и так далее.
У класса есть ссылка на зависимость, по этой ссылке можно вызвать Dispose.

class Foo : IDisposable
{
    IDependency _dependency;

    public Foo(IDependency dependency)
    {
        _dependency = dependency;
    }

    public void Dispose()
    {
        if (_dependency is IDisposable)
            ((IDisposable)_dependency).Dispose();
    }
}


Зависимостями зависимости может управлять сама зависимость. Логику освобождения ресурсов можно поместить в Dispose конкретного класса. Если логика освобождения ресурсов может различаться в разных ситуациях, можно использовать стратегию + фабрику.

В вашем примере логика освобождения ресурсов размазывается по нескольким классам. Часть логики в методе ToDisposable() одного класса (
return value.ToDisposable(Disposable.Empty);return value.ToDisposable(value);), часть логики — непосредственно в Dispose конкретного класса (к которому принадлежит value). Для чего это и в чём выигрыш?
Не стоит делать как вы предлагаете. В этом случае следующий потребитель вашей зависимости может получить disposed зависимость.
Если вы хотите сами контролировать время жизни, лучше вбрасывать фабрику и использовать ее внутри конструкции using
В случае, когда логика освобождения ресурсов может различаться в разных ситуациях, я предложил использовать стратегию. Нужно освободить разделяемый ресурс — освобождаем, не нужно — не освобождаем. Лучше увидеть внутри Dispose применение стратегии, чем увидеть внутри Dispose часть логики освобождения ресурсов (как предлагает автор статьи), а потом разбираться, почему освобождение ресурсов работает совсем не так, как описано в Dispose, и как оно работает на самом деле.

лучше вбрасывать фабрику и использовать ее
Не совсем понял, что вы предлагаете. Какую фабрику? Что она создаёт? Как инициализирует то, что создаёт?
Лучше увидеть внутри Dispose применение стратегии, чем увидеть внутри Dispose часть логики освобождения ресурсов (как предлагает автор статьи), а потом разбираться, почему освобождение ресурсов работает совсем не так, как описано в Dispose, и как оно работает на самом деле.

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

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

Не совсем понял, что вы предлагаете. Какую фабрику? Что она создаёт? Как инициализирует то, что создаёт?


Если вы работаете с DI контейнером, можно сделать так:

class Foo : IDisposable
{
    Func<IDependency> _dependencyFactory;

    public Foo(Func<IDependency> dependencyFactory)
    {
        _dependencyFactory= dependencyFactory;
    }

    public void SomMethod()
    {
          ...
         using(var dependency = _dependencyFactory())
         {
              ///используем зависимость.
         }
    }
}

Предлогаемые им обертки ничего не упрощают, при этом усложняя код или компиляцию.

Вам же не составит труда привести примеры усложнения в сравнении с более простыми аналогами?
Если вы работаете с DI контейнером, можно сделать так

Нельзя сделать так.
  1. IDependency не обязан наследоваться от IDisposable
  2. Экземпляр реализации IDependency может переиспользоваться (пример — соединения)
  3. У реализации IDependency могут быть собственные зависимости

Так что корректно определить сигнатуру фабрики можно вот так (Autofac)
Func<Owned<IDependency>> _dependencyFactory;

… или вот так:
Func<IDisposable<IDependency>> _dependencyFactory;
Я, на всякий случай, замечу, что есть разница между Owned<T> и любым другим классом (если только вы не написали свое расширение к Autofac): когда вы сделаете Dispose на Owned<T>, Autofac закроет соответствующий LifetimeScope, и все зависимости, которые он создавал под T, будут явно отпущены.
С точки зрения классов, реализующих функциональность, разница между Owned и IDisposable&ltT> только в том, что первый требует ссылку на сборку с Autofac. Семантика абсолютно одинаковая.
А с точки зрения реализации Composition Root вы правы: Autofac реализует именно такое поведение для Owned по умолчанию. Впрочем, его легко переопределить и чуть сложнее реализовать аналогичное для IDisposable&ltT>
Семантика абсолютно одинаковая.

Да разве? А мне казалось, что семантика IDisposable<T> полностью определяется тем, кто его создает, и может не делать вообще ничего.
Для того, кто ресурсами пользуется, семантика одинаковая: «Мне этот ресурс больше не нужен, можете делать с ним все, что считаете нужным».
Э нет. Семантика IDisposable — я с ресурсом закончил, освободи его немедленно.
Э нет. Семантика IDisposable — я с ресурсом закончил, освободи его немедленно.

Вы правда прочитали статью?
Там прямым текстом указаны различия в семантике IDisposable и IDisposable&ltT>
Семантика обобщенного IDisposable отличается от обычного примерно так же как «можете быть свободны» от «немедленно освободите помещение».
Так вот вопрос, зачем вы называете свой интерфейс так же как уже существующий, с устоявшейся семантикой? Чтобы было проще?
Совершенно верно. Оба интерфейса предназначены для очистки ресурсов и им логично иметь схожие имена.
У вас есть лучший вариант для имени нового интерфейса их статьи?
Ну ведь правда же легко спутать. Я от того даже плюсанул.
Ну, в этом случае release звучит более корректно, чем dispose.
Вот только у Owned семантика другая, и она означает: закройте lifetime scope, который был открыт при создании Owned. Это, только это, и ничего, кроме этого.
А вот здесь вы неправы.
закройте lifetime scope, который был открыт при создании Owned

Это всего лишь сценарий по умолчанию для автоматического разрешения зависимостей с помощью Autofac.
Данное поведение легко переопределить средствами самого Autofac как для конкретного типа, так и в общем случае.
Ваша трактовка на уровне класса-клиента совершенно избыточна и прямо противоречит паттерну Dependency Injection.
Это всего лишь сценарий по умолчанию для автоматического разрешения зависимостей с помощью Autofac.
Данное поведение легко переопределить средствами самого Autofac как для конкретного типа, так и в общем случае.

Данное поведение, наверное — я, кстати, не знаю, как, — можно переопределить, но документация описывает именно то поведение, которое я озвучил. Переопределяя его, вы нарушаете ожидания клиентского кода.

Ваша трактовка на уровне класса-клиента совершенно избыточна и прямо противоречит паттерну Dependency Injection.

Тем не менее, в Autofac она такова. Я не уверен, что это хорошая идея, поэтому я предпочитаю комбинацию Func/Disposable, но у нее есть свои недостатки. И в любом случае, это не решение для частого использования.
Данное поведение, наверное — я, кстати, не знаю, как, — можно переопределить, но документация описывает именно то поведение, которое я озвучил. Переопределяя его, вы нарушаете ожидания клиентского кода.

Неужели? Вот что написано по вашей же ссылке об ожиданиях клиентского кода:
An owned dependency can be released by the owner when it is no longer required. Owned dependencies usually correspond to some unit of work performed by the dependent component.

Сразу после — пример того самого клиентского кода.
И только потом — объяснение, как оно работает по умолчанию.
Пока клиентский код исполняет контракт «can be released by the owner when it is no longer required» никаких проблем с нарушением ожиданий нет и не будет.
Я не уверен, что это хорошая идея, поэтому я предпочитаю комбинацию Func/Disposable, но у нее есть свои недостатки.

Вот поэтому я и сделал свое решение — оно не привязывает ни к какому конкретному инструменту как Owned и не имеет проблем с зависимостями как Func/Disposable.
И только потом — объяснение, как оно работает по умолчанию.

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

Вот поэтому я и сделал свое решение — оно не привязывает ни к какому конкретному инструменту как Owned и не имеет проблем с зависимостями как Func/Disposable.

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

Полагаю, что напрасно. Такая трактовка вас же и ограничивает, при этом не давая никаких плюшек взамен.
Зато привязывает к вашему инструменту, и имеет неопределенную семантику.

Контракт в виде интерфейса с двумя членами и поведением, описываемым в одну строку, к чему-то привязывает? Ну я даже не знаю.
имеет неопределенную семантику

Так это только плюс: от клиента требуется сущий мизер, а при реализации Composition Root у вас полностью развязаны руки.
Полагаю, что напрасно. Такая трактовка вас же и ограничивает, при этом не давая никаких плюшек взамен.

Как раз наоборот. В качестве плюшки я получаю заведомо определенное поведение.

Контракт в виде интерфейса с двумя членами и поведением, описываемым в одну строку, к чему-то привязывает?

Конечно. Я должен иметь бинарную зависимость от этого интерфейса, например.

Так это только плюс: от клиента требуется сущий мизер, а при реализации Composition Root у вас полностью развязаны руки.

Это минус как раз. Контракт с неопределенной семантикой — это, по сути, не контракт, а видимость оного.
Контракт с неопределенной семантикой — это, по сути, не контракт, а видимость оного.

Где вы нашли неопределенность? На стороне клиента — «я могу известить, что этот ресурс мне нужен». На стороне Composition Root — «как только ресурс не будет нужен клиенту — я об этом узнаю». Напротив, семантика определена весьма жестко и компактно, соблюдать такой контракт проблем нет c обеих сторон.
Конечно. Я должен иметь бинарную зависимость от этого интерфейса, например.

Это только когда моя статья превратиться в готовый к установке nuget-пакет. Но даже тогда зависимость будет не столь неприятной, как от контейнера, которому место строго в Composition Root.
Как раз наоборот. В качестве плюшки я получаю заведомо определенное поведение.

Определенность, в которой указаны особенности работы с lifetimeScope для класса-клиента скорее вредна чем бесполезна.
Где вы нашли неопределенность?

Там, где вы выше написали, что неопределенная семантика — это благо.

На стороне клиента — «я могу известить, что этот ресурс мне нужен». На стороне Composition Root — «как только ресурс не будет нужен клиенту — я об этом узнаю».

А такой контракт мне просто не нужен, он не решает моих задач.

Определенность, в которой указаны особенности работы с lifetimeScope для класса-клиента скорее вредна чем бесполезна.

А вот это предоставьте решать клиенту. Вы можете не верить, но иногда клиентам нужно строго детерминированное время жизни зависимости.
Вы в этой статье написали некую реализацию
IDisposable<T>
которая является оберткой вокруг IDisposable. При этом совершенно не очевидно, как она решает проблемы вынесенные в начало статьи, а главное, не понятно зачем эти проблемы решать и проблемы ли это. Именно это я называю «обертки ничего не упрощают, при этом усложняя код или компиляцию».
Нельзя сделать так.
IDependency не обязан наследоваться от IDisposable
Экземпляр реализации IDependency может переиспользоваться (пример — соединения)
У реализации IDependency могут быть собственные зависимости


Если бы вы не вырывали из контекста, вы бы увидели, что пример кода там был в случае, если хочется больше контроля над disposable зависимостями. А значит данная конкретная зависимость наследуется от IDisposable. Наиболее правильно (и об этом я пишу выше), передать управление зависимостями DI контейнеру.
При этом совершенно не очевидно, как она решает проблемы вынесенные в начало статьи, а главное, не понятно зачем эти проблемы решать и проблемы ли это. Именно это я называю «обертки ничего не упрощают, при этом усложняя код или компиляцию».

То есть вы не готовы показать усложнение кода или компиляции на примере? Очень жаль, без них ваша оценка сильно теряет в убедительности.
Кстати, Николас Блумхард тоже почему-то с вами не согласен.
А значит данная конкретная зависимость наследуется от IDisposable

Вот именно что НЕ значит. Не может зависимость отвечать за свои зависимости и так далее, а вот Composition Root может и должен. Но есть нюанс: о том, что с момента X зависимость Y больше не нужна объекту Z этот самый объект должен как-то сообщить. Мой IDisposable&ltT>, так же как и Owned из Autofac, именно эту задачу и решает.
Если эта зависимость не диспозабл, а внутри нее какие-то диспозабл зависимости, то этот код должен переехать глубже (к диспозабл зависимостям).
Я понимаю что такое Owned из Autofac, и его использование, что характерно, оверхеда почти не добавляет (потому что весь оверхед в том, что в фабрику мне нужно добавить слово Owned). При этом я делегирую управление зависимостью Autofac. Но я продолжаю не понимать зачем мне ваша реализация без DI контейнера? Какой от нее профит?
Если эта зависимость не диспозабл, а внутри нее какие-то диспозабл зависимости, то этот код должен переехать глубже (к диспозабл зависимостям).

Не должен. По определению паттерна Dependency Injection классу-клиенту безразличны любые аспекты реализации зависимости. Значение имеет только контракт (интерфейс).
Но я продолжаю не понимать зачем мне ваша реализация без DI контейнера? Какой от нее профит?

Предположим, у вас есть такой ресурс, как соединение (IConnection)
Его контракт не допускает параллельное использование в нескольких разных задачах.
Поддержка соединения в рабочем состоянии кое-чего стоит, но создание нового.гораздо дороже.
Вы хотите полностью развязать себе руки в части выбора способа управления соединениями, при этом сохранив все возможности достичь максимальной эффективности.
Нет никакого смысла позволять клиентам делать вызов Dispose для соединения, так как вам вовсе не нужно закрывать его каждый раз.
Но нельзя давать соединение новому клиенту, если его еще использует старый.

Решение:

Давать клиентам зависимость в виде
Func<IDisposable<IConnection>>

Клиенты получают соединение, пользуются им, после чего вызывают Dispose у IDisposable&ltIConnection>

Реализовать «фабрику» соединений можно так:
() => context =>
{
    var pool = context.Resolve<IConnectionPool>();
    return pool.GetConnection().ToDisposable(connection => pool.PutConnection(connection));
}

По определению паттерна Dependency Injection классу-клиенту безразличны любые аспекты реализации зависимости. Значение имеет только контракт (интерфейс).

(не Dependency Injection, а Dependency Inversion, в данном случае, но не принципиально).

На самом деле, именно поэтому мы не должны ничего знать о том, есть ли какие-то зависимости у того, чем мы пользуемся; и если объект, которым мы пользуемся, не предоставляет семантики Release/Dispose, значит, навязывать ее ему некорректно.

Предположим, у вас есть такой ресурс, как соединение (IConnection)
Его контракт не допускает параллельное использование в нескольких разных задачах.
Поддержка соединения в рабочем состоянии кое-чего стоит, но создание нового.гораздо дороже.
Вы хотите полностью развязать себе руки в части выбора способа управления соединениями, при этом сохранив все возможности достичь максимальной эффективности.
Нет никакого смысла позволять клиентам делать вызов Dispose для соединения, так как вам вовсе не нужно закрывать его каждый раз.
Но нельзя давать соединение новому клиенту, если его еще использует старый.

Ну и типовой коннекшн пул, прекрасно реализуется без дополнительных оберток. Более того, реализуется прозрачно для клиента.
На самом деле, именно поэтому мы не должны ничего знать о том, есть ли какие-то зависимости у того, чем мы пользуемся; и если объект, которым мы пользуемся, не предоставляет семантики Release/Dispose, значит, навязывать ее ему некорректно.

А где навязывание? Есть только комбинирование основной семантики и Dispose с помощью обобщенного типа.
Вы же не отвергаете по тому же принципу обобщенные коллекции?
Ну и типовой коннекшн пул, прекрасно реализуется без дополнительных оберток. Более того, реализуется прозрачно для клиента.

Пример прозрачной для клиента реализации можете привести?
Кстати, у меня само соединение о пуле тоже ничего не знает.
Есть только комбинирование основной семантики и Dispose с помощью обобщенного типа.

Ну так странно же комбинировать Dispose с объектом, у которого его нет.

Пример прозрачной для клиента реализации можете привести?

SqlConnection.
Ну так странно же комбинировать Dispose с объектом, у которого его нет.

Не более странно чем комбинировать одиночные объекты в коллекции.
SqlConnection

Там ЕМНИП соединения очень даже хорошо знают о пуле. И в плане простоты что связей что иерархии классов далеко не положительный пример.
Не более странно чем комбинировать одиночные объекты в коллекции.

Неа. Обязанность одиночного объекта от комбинирования в коллекции не меняется.

Там ЕМНИП соединения очень даже хорошо знают о пуле. И в плане простоты что связей что иерархии классов далеко не положительный пример.

Вы просили простую для клиента. И для клиента проще придумать сложно.
Неа. Обязанность одиночного объекта от комбинирования в коллекции не меняется.

Как и обязанности одиночной реализации от заворачивания в IDisposable&ltT>
Вы просили простую для клиента. И для клиента проще придумать сложно.

Сначала вы говорите про «прекрасно реализуется», а потом приводите в качестве примера чужое и тяжеловесное? Видимо, не так уж оно и прекрасно на практике-то?
Кстати, я для клиента проще не только придумал, но и реализовал. Клиент вообще не имел дела с соединениями, только с транзакциями.
Сначала вы говорите про «прекрасно реализуется», а потом приводите в качестве примера чужое и тяжеловесное?

А какая разница, свое оно или чужое? Мне достаточно того, что оно работает с заданной семантикой.

Кстати, я для клиента проще не только придумал, но и реализовал

Пример кода в студию. Только именно кода connection (object) pool, потому что как убрать от пользователя коннекшны, я как раз прекрасно знаю, только здесь это не обсуждалось. А то мне как-то сложно себе представить что-то проще чем

using(var cn = new SqlConnection(...))
{
  cn.Open();
  //...
}


Ну да, явный Open можно бы убрать было. Но это с пулингом не связано.
Не должен. По определению паттерна Dependency Injection классу-клиенту безразличны любые аспекты реализации зависимости. Значение имеет только контракт (интерфейс).

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

Предположим, у вас есть такой ресурс, как соединение (IConnection)
Его контракт не допускает параллельное использование в нескольких разных задачах.
Поддержка соединения в рабочем состоянии кое-чего стоит, но создание нового.гораздо дороже.
Вы хотите полностью развязать себе руки в части выбора способа управления соединениями, при этом сохранив все возможности достичь максимальной эффективности.
Нет никакого смысла позволять клиентам делать вызов Dispose для соединения, так как вам вовсе не нужно закрывать его каждый раз.
Но нельзя давать соединение новому клиенту, если его еще использует старый.


Про коннекшн пул не слышали? Типовой паттерн.
Ну так либо интерфейс зависимости является IDisposable и тогда клиент знает что любая пришедшая сюда зависимость Disposable (и это не деталь реализации а контракт), либо не нужно ее диспозить.

Либо вы так и не прочитали статью.
Зависимости нельзя диспозить, так как за их время жизни отвечает Composition Root, а не клиент.
Зависимости надо диспозить, так ненужные дорогие ресурсы надо освобождать как можно быстрее, но про момент ненужности знает только клиент, а не Composition Root.
Для разрешения этого противоречия и был придуман IDisposable&ltT>
Про коннекшн пул не слышали? Типовой паттерн.

Не только слышал.
Вы точно прочитали мой комментарий? Там как раз про реализацию пула, прозрачную как для клиента, так и для самого соединения.
Нельзя у зависимости вызвать Dispose: класс-клиент ей не владеет.
  1. Одна зависимость может разделяться несколькими клиентами и за это отвечает не клиент, а CompositionRoot
  2. Зависимость сама может не быть IDisposable, в отличие от ее собственных зависимостей, зависимостей ее зависимостей и т.д.
Зависимость сама может не быть IDisposable, в отличие от ее собственных зависимостей, зависимостей ее зависимостей и т.д.
То есть объект косвенно владеет ресурсами (пусть и через свои зависимости), но его класс не реализует IDisposable? Зачем так сделано? Имхо, класс, управляющий управляемыми ресурсами, сам становится управляемым ресурсом (потому что его надо явно освободить), поэтому ему следует реализовывать IDisposable.
Например, класс использует эти ресурсы но не управляет ими.
Как тот же StreamWriter поверх переданного снаружи стрима. Смысл в том, что если ваш класс не создает внутри себя зависимость, а получает ее снаружи, он не должен ее и освобождать, потому что ему не известно что с этой зависимостью предполагается делать дальше.
Поэтому у StreamWriter есть параметр в конструкторе, чтобы стрим извне не освобождался при Dispose у StreamWriter. И то и то поведение нужное и полезное, поэтому дан выбор. Не понятно, к чему пример конкретно с ним.
var disposableViewModel = new ViewModel().ToDisposable(vm => 
        {
            observableCollection.Add(vm);
            return () => observableCollection.Remove(vm);
        });
Тут можно было просто добавить событие\делегат в ViewModel, тогда cohesion было бы выше при том же coupling.
Куда именно добавить? Из конструктора события вызывать бесполезно, IDisposable viewModel реализовывать не обязана, как и знать о своем нахождении в ObservableCollection.
Не вижу никаких препятствий, мешающих ViewModel реализовать IDisposable

    class ViewModel : IDisposable
    {
        public Action<ViewModel> DisposeStrategy;
        
        public void Dispose()
        {
            /*тут освобождение своих ресурсов*/

            try
            {
                if (DisposeStrategy != null)
                    DisposeStrategy(this);
            }
            finally
            {
                DisposeStrategy = null;
            }
        }
    }


var viewModel = new ViewModel() {DisposeStrategy = vm => observableCollection.Remove(vm)};
observableCollection.Add(viewModel);
  1. Вы добавили внутрь ViewModel ответственность, которая ей самой не нужна
  2. Вы исключили сценарий дальнейшего переиспользования конкретного экземпляра ViewModel
  3. Вам потребуется более трудоемкий рефакторинг при замене типа ViewModel на другой класс
  4. Ваш код еще не делает того, что делает мой, но уже больше и сложнее
Допустим, у вас в ViewModel «на одну ответственность меньше», а сколько тогда ответственностей у Disposable[ViewModel]? А если понадобится добавить функцию печати и импорта в Excel, вы напишите Printable<Excelable<Disposable[ViewModel]>>?
зависимость может реализовать IDisposable, а может не реализовать

Эээ, если зависимость не реализует IDisposable, то в чем проблема-то? Не реализует — не диспозь.
Так у нее свои зависимости могут быть, вполне себе диспозабельные. И если исходная зависимость с какого-то времени нам не нужна, то надо об этом как-то сообщить, чтобы CompositionRoot смог очистить все, что уже не требуется никому.
Owned в Autofac именно для этого.
Кому сообщить? DI-контейнеру? Так давайте для этого использовать ISignalToContainer (он же Owned в автофаке), а не IDisposable, семантика-то разная совершенно.
семантика-то разная совершенно.

Чем она разная-то? И там, и там «это мне больше не нужно».
Так давайте для этого использовать ISignalToContainer (он же Owned в автофаке)

… и пронесем зависимость от контейнера в обычный класс?
Чем она разная-то? И там, и там «это мне больше не нужно».

Да нет же. IDisposable означает «отпусти ресурсы». Owned означает «закрой lifetime scope». Упомянутый выше псевдоинтерфейс означает «передай контейнеру, что я все, пусть делает, что хочет».

… и пронесем зависимость от контейнера в обычный класс?

А так пронесем зависимость от вашего IDisposable<T> — оно чем-то лучше?

Ну и да, тема абстракции от DI-контейнера, конечно, интересная и плодотворная, но она совершенно не связана с тем, что вы в посте пишете.
А так пронесем зависимость от вашего IDisposable — оно чем-то лучше?
Тем, что нет никакой привязки к контейнеру и никаких лишних обязательств для реализации.
Ну и да, тема абстракции от DI-контейнера, конечно, интересная и плодотворная, но она совершенно не связана с тем, что вы в посте пишете.

Вы можете так считать. Я полагаю, что поддержка абстракции классов-клиентов от контейнеров — один из типовых вариантов использования для IDisposable&ltT> Просто в Autofac эта тема на мой взгляд раскрыта настолько полно, что я предпочел ограничиться ссылкой.
Тем, что нет никакой привязки к контейнеру и никаких лишних обязательств для реализации.

Вот именно, что нет обязательств — что означает, что мне никто не обещает, что поведет себя каким-то конкретным образом. И именно поэтому оно мне низачем не надо.
Вот именно, что нет обязательств — что означает, что мне никто не обещает, что поведет себя каким-то конкретным образом.

Не понял — CompositionRoot реализуете не вы, а кто-то другой? Если вы, то нет никакой проблемы реализовать любое нужное вам поведение.
Кроме того, а какие обещания вам нужны на стороне класса-клиента? Вам дают ресурс и просят сообщить, когда он он перестанет быть вам нужен. Чего не хватает?
CompositionRoot реализуете не вы, а кто-то другой?

Не я.

Вам дают ресурс и просят сообщить, когда он он перестанет быть вам нужен. Чего не хватает?

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

А сообщать о том, что ресурс мне больше не нужен, мне, как разработчику класса-клиента, низачем не сдалось.
Так вот, я запрашиваю зависимость

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

Понятно, в рамках Dependency Injection это в терминах Марка Симана есть чистейший Control Freak. В таком варианте IDisposable&ltT> вам и в самом деле ни к чему
Это у вас уже не Dependency Injection, а Service Locator какой-то

Нет. Параметр в конструкторе (в случае использования Dependency Injection) имеет семантику «для моей работы нужна вот такая зависимость — дай мне ее».

Понятно, в рамках Dependency Injection это в терминах Марка Симана есть чистейший Control Freak.

Не больше, чем ваш IDisposable<T>. Впрочем нет, в терминах Симана это не Control Freak, потому что «The CONTROL FREAK anti-pattern occurs every time we get an instance of a DEPENDENCY by directly or indirectly using the new keyword in any place other than a COMPOSITION ROOT». К явному управлению жизненным циклом это отношения не имеет.
К явному управлению жизненным циклом это отношения не имеет

В таком случае IDisposable&ltT> полностью соответствует заявленным вами требованиям.
Вызов Func&ltDisposable&ltT>> именно что создает зависимость для вашего объекта, а вызов IDisposable&ltT>.Dispose() ее отпускает. При этом создание и отпускание зависимости вовсе не означает обязательное конструирование или очистку объектов-реализаций.
Не соответствует. Я хочу управление lifetime scope. А поведение IDisposable<T>.Dispose(), как уже обсуждалось, не определено.
Поведение, что того, что другого определяется Compositon Root.
С вашей стороны есть только уведомление Composition Root о разрыве зависимости и не более того.
Реальное поведение зависит от реализации Composition Root (конфигурации контейнера) и никакая lifetimeScope определенности по факту не добавляет. Я могу сконфигурировать контейнер так, что при очистке lifetimeScope будет выполнен произвольный код.
Хотите узнать, что точно будет — смотрите код Composition Root и никак иначе.
Поведение, что того, что другого определяется Compositon Root.

Поведение Owned описано в документации на Autofac. Composition Root, который его переопределяет (я такого не знаю ни одного) — радикально не прав.
>CompositionRoot реализуете не вы, а кто-то другой?
Не я.

Тогда у вас никаких гарантий по определению. Кстати, очистка lifetimeScope внутри Owned на самом деле ничего конкретного вам не гарантирует. В зависимости от настроек контейнера и конкретного набора объектов на данный момент что-то может быть освобождено, а может и ничего не освободиться.

эта зависимость (вместе с деревом зависимостей) будет отпущена именно тогда, когда я попрошу.
А сообщать о том, что ресурс мне больше не нужен

«Зависимость отпущена» — это и есть всего лишь информация для Composition Root, что вы от ресурса больше не зависите, т.е. он вам не нужен.
Будут ли при разрешении зависимости создаваться какие-то объекты, а при отпускании — очищаться, вы на уровне клиента не можете гарантировать никак. Это ответственность Composition Root, а клиент может только сообщить «Мне нужно X и получить желаемое» и «мне больше не нужно X».
Тогда у вас никаких гарантий по определению

… кроме тех, которые предоставляет контейнер.

Кстати, очистка lifetimeScope внутри Owned на самом деле ничего конкретного вам не гарантирует.

Почему же. Она мне гарантирует то, что я прошу: открытие/закрытие скоупа по открытию/закрытию owned.
… кроме тех, которые предоставляет контейнер.

А в контейнере все определяется его конфигурацией. Тут хоть совой об пень, хоть пнем об сову.
Почему же. Она мне гарантирует то, что я прошу: открытие/закрытие скоупа по открытию/закрытию owned.

Во-первых даже это не гарантировано — поведение по умолчанию вполне возможно переопределить, причем не хаками, а штатными средствами. Во-вторых результат создания-очистки скоупа также определяется конфигурацией контейнера. Контейнер — это всего лишь инструмент для реализации Composition Root он специально сделан максимально гибким.
А в контейнере все определяется его конфигурацией.

Ну не все. Какие-то вещи захардкожены. И те, кто переопределяет очевидное поведение, сами за это отвечают.
Захардкожено как раз следование конфигурации, то бишь тотальный софткод :)
Вы хоть в реализацию Owned смотрели, прежде, чем такое писать?
Я-то как раз смотрел.
И мне не проблема сконфигурировать контейнер так, чтобы вообще не трогая все, что Owned-based, тем не менее добиться исполнения произвольного кода при очистке Owned-скоупа.
Мне тоже не проблема. Но вы тем самым нарушаете поведение, описанное в документации.
Не нарушаю, специально же написал:
вообще не трогая все, что Owned-based
Это, к примеру, дурная привычка StreamReader закрывать нижележащий Stream при вызове Dispose

Вообще-то у StreamReader есть параметр, который это отключает.
Вы правы, вот только и то и другое поведение может быть недостаточно гибко.
С одной стороны вариант по умолчанию закрывает то, что можно переиспользовать.
С другой — вариант без автозакрытия ничего не сообщает о том, что Stream больше не используется и им можно свободно распоряжаться.
IDisposable&ltT> позволяет гибко настроить оба варианта, причем на уровне CompositionRoot, не трогая код, использующий StreamReader.
Такое ощущение, что вы боретесь не с причиной, а со следствием. А причина — изначально плохая архитектура. Один bool вариант в StreamReader/StreamWriter покрывает все, что необходимо от этого класса — закрыть или не закрыть стрим, который ему дается. Больше он ни о чем думать не должен. Код снаружи должен сообщать, может ли стрим использоваться где-то в другом месте или он еще занят. Уже изначально такая проблема намекает, что где-то что-то не так и IDisposable тут совсем не при чем.
Один bool вариант в StreamReader/StreamWriter покрывает все, что необходимо от этого класса — закрыть или не закрыть стрим, который ему дается.

Вообще-то не покрывает. Закрыть или не закрыть Stream означает «переиспользовать или не переиспользовать» уровнем выше.
И если все-таки переиспользовать, то ответ на вопрос «с какого момента?» может оказаться очень важным.
Код снаружи тут не при делах: только тот, кто использует Reader может сообщить о том, что зависимости Reader'а больше не нужны и могут быть переиспользованы где-то еще.
Аналог — переиспользование соединений посредством пула: чтобы вернуть соединение в пул и сделать его доступным для повторного использования, необходимо знать, когда оно больше не нужно текущему клиенту.
Имхо, автор открыл (ну, или поспособствовал открытию) замечательный способ множественного наследования из мира ненормального программирования. Вместо того, чтобы класс реализовывал IPrintable, IExcelable, IDisposable можно написать три универсальных класса Printable, Excelable, Disposable, способных печатать и диспозить что угодно. Всю логику передаём в универсальные классы через делегаты. Поскольку в C#/Java нет множественного наследования, то делаем Printable[Excelable[Disposable[ViewModel]]].
Можно вообще сделать просто один класс Doer[T], делающий вообще всё что угодно. Пусть у класса 3 метода, тогда переменная будет иметь тип Doer[Doer[Doer[StreamReader]]](). Код вызова 1го метода: doer.Do(). Второго: doer.Value.Do(). Третьего: doer.Value.Value.Do(). Логику методов можно передавать через делегаты: new StreamReader.ToDoer(x => x.Dispose()).ToDoer(x => x.Value.ToString()).ToDoer(x => x.Value.Value.GetHashCode()). Чудесненько.

Имхо, в данном случае автор предлагает своеобразный способ, как «переопределить» Dispose в StreamReader, не реализуя наследника от StreamReader. Это незначительно экономит время: вместо объявления класса и метода нужно просто передать тело метода через делегат в ToDisposable(Action[T] delegate). Цена: использование нестандартного подхода, замена переменных типа StreamReader на Disposable[StreamReader], возможность переопределить только один метод (иначе приходим к Printable[Excelable[Disposable[StreamReader]]], а это уже явный перебор). Этот способ имеет смысл, когда нужно переопределить метод в запечатанном классе без публичных конструкторов. Однако автор предлагает использовать его повсеместно при любом освобождении ресурсов. Непонятно, почему бы просто не реализовать наследника StreamReader, раз уж так сильно хочется переопределить Dispose. Хотя не до конца понятно, а зачем его переопределять. Взяли из пула соединение, передали в функцию, функция отработала, вернули соединение в пул.
IDisposable<T> позволяет гибко настроить оба варианта

Не в данном случае. StreamReader принимает на вход Stream, а не IDisposable.

Ну и да, отдельно хочу заметить, что — учитывая стандартные сценарии использования StreamReader/Writer, — я совершенно не понимаю, зачем куда-то о чем-то сигнализировать. Этот флаг покрывал все случавшиеся в моей практике варианты работы.
Я вот в этом совершенно не уверен. Ничего особо плохого в нем нет (ну кроме общей болезни флаговых параметров), а при этом типовые сценарии использования покрыты.
Я бы сделал так, что если мы стрим передали снаружи — его не закрывать. Если создали внутри то закрывать. А иначе, поведение не очевидное.
Овер 90% использования ридера — с переданным наружи стримом. И в ощутимой части из них его закрывать совершенно не надо.
Внесу свои 5 копеек. У меня один процесс от другого получает данные в таких объемах, что приходится переводить GC в режим Sustained Low Latency. Внутри цикла, который иногда вызывается аж раз в 60 микросекунд, создаются и уничтожаются достаточно большие managed и unmanaged массивы, т.к. новые данные приходят через MemoryMap (ага, и StreamReader есть, точнее 8). Чтобы это не отъедало оперативную память со скоростью в 500мб в минуту у всего что там временно используется вызывается Dispose, чтобы наверняка. А еще чтобы не тормозило там куча unsafe и unchecked кода, доступ к коллекциям из локальных переменных, сознательное использование структур и их boxing, ни одной лямбды или замыкания, вобщем все грабли C# вплоть до ручного контроля Capacity коллекций. Короче, код где реально без нормального Dispose вообще никак, еще и с кучей unmanaged ресурсов. И ни разу мне не понадобилось ничего, кроме канонической реализации IDisposable и пары классов оберток. А в вашем варианте и множественное наследование не реализовать толком, и вызовы виртуальных функций и не sealed классы до сих пор такой пенальти по производительности дают, хоть стой хоть падай. В реальном коде который доводит систему до under memory pressure ваша реализация ИМХО гарантированно даст отрицательную эффективность. Я не говорю что ваниль спасет человечество, но такие велосипеды говорят о кривой архитектуре. Да, сознаюсь, я наверное в 100500 классов уже скопировал реализацию IDisposable, IComparable и т.д., но это особенность C#, и с ней не надо воевать, тем более есть тот же Решарпер. А все попытки добавить свой сахар и сделать «поудобнее» очень сильно пинают по производительности.
Да, сознаюсь, я наверное в 100500 классов уже скопировал реализацию IDisposable, IComparable и т.д., но это особенность C#, и с ней не надо воевать, тем более есть тот же Решарпер. А все попытки добавить свой сахар и сделать «поудобнее» очень сильно пинают по производительности.

Полагаю, что ваши требования к производительности актуальны далеко не для всех.
Также полагаю, что даже там, где они актуальны, проще оптимизировать корректный код, чем корректировать оптимизированный.
А в вашем варианте и множественное наследование не реализовать толком, и вызовы виртуальных функций и не sealed классы до сих пор такой пенальти по производительности дают, хоть стой хоть падай.

Не могу понять где вы у меня нашли наследование классов, вызовы виртуальный функций и т.п.
Ну у вас как бы два варианта развития событий, код используете только вы, и там где код вызывается редко, можно все залить сахаром в виде LINQ, Rx и все такое. А второй вариант это если код использую другие люди, или он является ядром системы. И в итоге есть когда пара солюшенов, каждый на 500 файлов не считая тестов, и в одном что-то тормозит из-за косяков в другом начинаются проблемы, из серии «билд не может закончится т.к. таргет dll занята в мокапе для интегрейшн теста другого солюшена, а перезапускать сервер на каждый билд мы задолбались уже». И очень быстро приучаешься писать с минимумом наследования, абстракт классами и seal'ом всего и вся. И инициализация ресурсов, и финализация, и Dispose начинают прописываться вообще до бизнес логики. А после продакшена вообще оказывается что кастом методы Dispose прописанные для каждого класса это мана небесная, вроде «Так, а здесь для экономии ресурсов и увеличения скорости мы вообще откажемся от GC и лог замаршаллим прямо строками в heap», когда внезапно боттлнеком оказывается синхронное логгирование в одном из тредлупов из-за загруженности тома другим процессом на сервере.

И как раз «наследование классов, вызовы виртуальный функций»: у вас есть класс с достаточно базовой функциональностью, и нам нужно запилить наследника с еще какой-то базовой функциональностью и начинается пляска с кучей интерфейсов на разных этажах. А по изначальной логике C# есть набор базовых функций например IDisposable, IComparable и все такое, далее делается несколько абстракт классов с нужными наборами функциональности, и потом в идеальном случае один финальный класс где уже все функции определены и по максимуму финализированы. И этажерка virtual calls минимальная, и дженерики генерируются очень эффективно. А вы пытаетесь какой-то свой стиль привить, и в итоге что там в стеке MSIL получится мне страшно представить.
И как раз «наследование классов, вызовы виртуальный функций»: у вас есть класс с достаточно базовой функциональностью, и нам нужно запилить наследника с еще какой-то базовой функциональностью и начинается пляска с кучей интерфейсов на разных этажах.

Дело в том, что я не использую наследование классов и, соответственно, не использую ни виртуальные методы, ни абстрактные классы.
Тут дело в том что вы на функциональность которая идет «снизу» хотя бы в рамках GC и всего такого пытаетесь навесить инвершен и зайти «сверху». И архитектура трещит, и абсолютно реально появление конструкций «Printable<Excelable<Disposable[ViewModel]>>» если лепить концепт IoC на все подряд. Хотя тут старый добрый ООП отлично подошел бы, а чтобы не поощрять программиста устраивать вложенные virtual calls C# еще и по рукам бьет.
Видите ли, то, что я делаю, это и есть старый добрый ООП.
Что значит «архитектура трещит»?
Те оптимизации, про которые вы говорите — всего лишь издержки конкретной реализации платформы и не надо выдавать нужду за добродетель.

Зачем, зачем все это? Если ты сам не знаешь, о используемых объектах, что, как, где, зачем и как долго, то никакие велосипеды тебя не спасут, только добавят сложности в твой код, больше, больше и еще больше!
При использовании Dependency Injection объект класса не только не должен отвечать за жизненный цикл своих зависимостей, он просто физически не может это делать: зависимость может разделяться между несколькими клиентами

Несколько владельцев у одного disposable объекта? Просто не надо так делать.
Вы в курсе про паттерн Dependency Injection? Объект НЕ владеет своими зависимостями, владелец у них один — Composition Root.
Несколько классов зависят от одного disposable класса? Просто не надо так делать.
Лучше? )
Не лучше.
Допустим, у вас есть есть очень дорогой объект. Он долго создается и жрет четверть всей доступной вам памяти за счет требующих очистки ресурсов. Реализуемый им интерфейс допускает параллельную работу без ограничений. Также у вас есть 10 дешевых объектов, которые зависят от интерфейса, реализованного дорогим.
Теперь попробуйте последовать собственному совету, подумайте, что будет и как исправить ситуацию.
Когда эти ресурсы требуют освобождения? Обычно такие объекты регистрируются как синглтон и умирают вместе с процессом. Соответственно, такой объект не должен быть idisposable.
В подходящий момент. Вместе с процессом создавать и уничтожать нельзя.
Не стоит пытаться подогнать условия под ответ.
Кстати, даже если регистрировать объект как синглтон — он обязан быть Disposable, если имеет ресурсы, не очищаемые сборщиком мусора.
Кстати, даже если регистрировать объект как синглтон — он обязан быть Disposable, если имеет ресурсы, не очищаемые сборщиком мусора.

Это почему?
> Это почему?
Потому что иначе класс будет содержать утечки ресурсов by design
Потому что иначе класс будет содержать утечки ресурсов by design

И в какой момент будут утекать ресурсы, учитывая, что экземпляр класса ровно один на процесс?
А вот это на уровне класса неизвестно. И не должно быть известно.
Вот еще. Если я проектирую и класс с ресурсами, и приложение, его использующее, зачем мне добавлять избыточный код?
Вам, возможно, и незачем.
Я же предпочитаю не иметь такой сильной и неявной связности нигде и никогда.
Это сильно помогает при проектировании, изменениях, повторном использовании и рефакторинге. Экономия на спичках на мой взгляд здесь совершенно не нужна.
Понимаете ли, избыточный код — это тоже оверхед. Его надо осознавать, его надо поддерживать.

Ну и да, получается, что нет никакого «объект обязан быть Disposable, если он имеет ресурсы, не очищаемые сборщиком мусора», есть «я предпочитаю делать такие объекты Disposable, потому что я никогда не знаю, как они будут использованы».
Понимаете ли, избыточный код — это тоже оверхед. Его надо осознавать, его надо поддерживать.

Недостающий код — оверхед много больший. Класс, который сам по себе течет как слониха в красный день календаря, требует дополнительных усилий для поддержки и сопровождения, ибо надо знать, почему это так реализовано и каждый раз заново убеждаться, что причина выбора столь своеобразного метода очистки ресурсов до сих пор актуальна.
Ну и да, получается, что нет никакого «объект обязан быть Disposable, если он имеет ресурсы, не очищаемые сборщиком мусора»

Не получается. Какой-нибудь очередной ad hoc означает пренебрежение обязанностью, а не ее отсутствие.
«я предпочитаю делать такие объекты Disposable, потому что я никогда не знаю, как они будут использованы»

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

Как вы определяете, что его не достает?

Класс, который сам по себе течет как слониха в красный день календаря… причина выбора столь своеобразного метода очистки ресурсов до сих пор актуальна.

O, srsly? Давайте на примерах. Вот, значит, класс «с ресурсами»:

public class DisposableResourceHolder : IDisposable {

    private SafeHandle resource; // handle to a resource

    public DisposableResourceHolder(){
        this.resource = ... // allocates the resource
    }

    public void Dispose(){
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing){
        if (disposing){
            if (resource!= null) resource.Dispose();
        }
    }
}


А вот регистрация:
container.Register<DisposableResourceHolder>().AsSingleInstance();


Два вопроса:
— «течет» ли этот класс?
— в какой момент будет гарантированно вызван resource.Dispose?

А теперь давайте сделаем вот так:
public class DisposableResourceHolder {

    private SafeHandle resource; // handle to a resource

    public DisposableResourceHolder(){
        this.resource = ... // allocates the resource
    }
}

container.Register<DisposableResourceHolder>().AsSingleInstance();


Что изменилось в ответах на поставленные выше вопросы?

Не получается. Какой-нибудь очередной ad hoc означает пренебрежение обязанностью, а не ее отсутствие.

Так почему же объект, имеющий не-GC-ресурсы, обязан иметь IDisposable? Что изменится, если он не будет его иметь?

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

А зря. Короткоживущие и долгоживущие объекты могут иметь сильно разные внутреннюю реализацию.
Не стоит пытаться подогнать условия под ответ.

«Дорогой объект, который жрет четверть всей доступной вам памяти за счет требующих очистки ( в подходящий момент !) ресурсов». Это условие? )) В таком случае реализация будет зависеть от того, что является «подходящим моментом» или кто его определяет.

Кстати, даже если регистрировать объект как синглтон — он обязан быть Disposable, если имеет ресурсы, не очищаемые сборщиком мусора.


На самом деле IDisposable — это аналог RAII. Отличие в том, что там деструктор вызывается гарантировано и детерминировано, а в c# для этого используется связка using/dispose. Т.е. когда класс проектируется как IDisposable, то предполагается, что его инстанцирование будет производиться в using. Если он является частью агрегата, то корень этого агрегат должен вызывать его Dispose в своём Dispose (в случае RAII деструкторы частей агрегата вызываются при разрушении корня агрегата). В DI-контейнерах такие объекты регистрируются как transient и освобождаются явно, как конкретно — зависит от контейнера.
В таком случае реализация будет зависеть от того, что является «подходящим моментом» или кто его определяет.

Не будет. Класс, владеющий ресурсами, которые нельзя освободить автоматически, обязан дать возможность сделать это вручную. В какой именно момент освобождать — ответственность не объекта, а его владельца.
Т.е. когда класс проектируется как IDisposable, то предполагается, что его инстанцирование будет производиться в using.

Нет. using всего лишь удобный синтаксический сахар для использование IDisposable в пределах одного метода.
Если он является частью агрегата, то корень этого агрегат должен вызывать его Dispose в своём Dispose (в случае RAII деструкторы частей агрегата вызываются при разрушении корня агрегата). В DI-контейнерах такие объекты регистрируются как transient и освобождаются явно, как конкретно — зависит от контейнера.

Autofac позволяет не заморачиваться с явным освобождением. Вызов Dispose у LifetimeScope автоматически вызовет Dispose у всех созданных в ней объектов.

Так что вы будете делать в условиях задачи?
Не будет. Класс, владеющий ресурсами, которые нельзя освободить автоматически, обязан дать возможность сделать это вручную. В какой именно момент освобождать — ответственность не объекта, а его владельца.

То, как класс следует использовать, определяется при проектировании класса. И если класс реализует IDisposable, то это означает, что для его объектов должен быть обязательно вызван Dispose(). Через using, finally или ещё как-нибудь, но вызван. Это не значит, что Dispose нам дали, а мы решаем, дёргать его или нет.

Например, если я реализую класс SingleInstance, задача которого — держать открытым некий файл на протяжении работы процесса для контроля единственности запущенного экземпляра, я вызову Dispose() у файлового потока (он обязан быть вызван по задумке его разработчиков) в ~SingleInstance. А SingleInstance.IDisposable реализовывать, на всякий случай, я не буду. Потому что так класс задуман, такая у него задача.

Так что вы будете делать в условиях задачи?

Приведите условие конкретной задачи, как я привёл выше. «Сделайте класс, который может освобождать ресурсы в произвольный момент» = «Реализуйте IDisposable», да. Но необходимость освобождать ресурсы в произвольный момент должна быть чем-то вызвана. Она не безусловна, как вы утверждаете. И уж тем более такая необходимость крайне сомнительна, когда речь идёт об очень большом ресурсе, разделяемом между большим количеством потребителей.

Вызов Dispose у LifetimeScope автоматически вызовет Dispose у всех созданных в ней объектов.

Кто вызывает Dispose у LifetimeScope?

То, как класс следует использовать, определяется при проектировании класса.

И является его контрактом.
Включать в контракт класса, владеющего ресурсами, реализацию IDisposable — это паттерн.
А вот включать в контракт класса контроль его единственности на процесс — это антипаттерн, нарушающий, для начала, принцип единственной ответственности.
Такая практика чревата размножением копипасты, непригодной для автономного тестирования и повторного использования.
Например, если я реализую класс SingleInstance, задача которого — держать открытым некий файл на протяжении работы процесса для контроля единственности запущенного экземпляра, я вызову Dispose() у файлового потока (он обязан быть вызван по задумке его разработчиков) в ~SingleInstance. А SingleInstance.IDisposable реализовывать, на всякий случай, я не буду. Потому что так класс задуман, такая у него задача.

Класс не должен контролировать количество своих экземпляров. Для этого есть коллекции, пулы, контейнеры и т.п.
Если мне надо держать файл открытым — это обеспечит один класс. А единственность его экземпляра в рамках процесса — другой класс.
Приведите условие конкретной задачи

В задаче достаточно информации для решения.
Но необходимость освобождать ресурсы в произвольный момент должна быть чем-то вызвана. Она не безусловна, как вы утверждаете. И уж тем более такая необходимость крайне сомнительна, когда речь идёт об очень большом ресурсе, разделяемом между большим количеством потребителей.

В рамках поставленной задачи эта необходимость безусловна. Ваш код должен работать без изменений вне зависимости от того, когда владелец захочет освободить ресурсы. Этот момент вы не контролируете.
Вы можете с этим справиться?
Кто вызывает Dispose у LifetimeScope?

Composition Root.
Включать в контракт класса, владеющего ресурсами, реализацию IDisposable — это паттерн.

Пруф?

А вот включать в контракт класса контроль его единственности на процесс — это антипаттерн

А где вы у меня это увидели? Впрочем, споры о том, чем является синглтон, паттерном ли, антипаттерном, не утихают по сей день :)

В задаче достаточно информации для решения.

В задаче недостаточно задачи :) Вы мне диаграмму классов словами описали, которая сама уже является решением некой абстрактной задачи. Причём сомнительным решением.
Но раз уж на то пошло, что предлагаете вы?

Only those users with full accounts are able to leave comments. Log in, please.