Skip to content

Latest commit

 

History

History
218 lines (183 loc) · 6.14 KB

disposable-singleton.md

File metadata and controls

218 lines (183 loc) · 6.14 KB

Disposable singleton

To dispose all created singleton instances, simply dispose the composition instance. The composition itself becomes disposable as soon as it can create at least one disposable singleton, so using around it is the whole disposal strategy.

using Shouldly;
using Pure.DI;
using static Pure.DI.Lifetime;

DI.Setup(nameof(Composition))
    // Disable Resolve methods to keep the public API minimal
    .Hint(Hint.Resolve, "Off")

    // A realistic example:
    // a submarine has a shared hardware bus to onboard sensors.
    // It should be created once and disposed when the "mission scope"
    // (the composition instance) ends.
    .Bind().As(Singleton).To<AcousticSensorBus>()
    .Bind().To<SubmarineCombatSystem>()
    .Root<ICombatSystem>("CombatSystem");

IAcousticSensorBus bus;
using (var composition = new Composition())
{
    var combatSystem = composition.CombatSystem;

    // Store the singleton instance to verify that it gets disposed
    // when composition is disposed.
    bus = combatSystem.SensorBus;

    // In real usage you would call methods like:
    // combatSystem.ScanForContacts();
}

// When the mission scope ends, all disposable singletons created by it
// must be disposed.
bus.IsDisposed.ShouldBeTrue();

interface IAcousticSensorBus
{
    bool IsDisposed { get; }
}

// Represents a shared connection to submarine sensors (sonar, hydrophones, etc.).
// This is a singleton because the hardware bus is typically a single shared resource,
// and it must be cleaned up properly.
class AcousticSensorBus : IAcousticSensorBus, IDisposable
{
    public bool IsDisposed { get; private set; }

    public void Dispose() => IsDisposed = true;
}

interface ICombatSystem
{
    IAcousticSensorBus SensorBus { get; }
}

// A "combat system" is a typical high-level service that uses shared hardware resources.
class SubmarineCombatSystem(IAcousticSensorBus sensorBus) : ICombatSystem
{
    public IAcousticSensorBus SensorBus { get; } = sensorBus;
}
Running this code sample locally
dotnet --list-sdk
  • Create a net10.0 (or later) console application
dotnet new console -n Sample
dotnet add package Pure.DI
dotnet add package Shouldly
  • Copy the example code into the Program.cs file

You are ready to run the example 🚀

dotnet run

A composition class becomes disposable if it creates at least one disposable singleton instance.

This is the disposal path for process-wide resources a server or a desktop shell owns: message brokers, hardware handles, file writers, caches with a background timer. They are created lazily on first use and released when the host shuts the composition down.

Warning

Only what the composition created is disposed, and only in a reverse order of creation. Transient and PerResolve instances, and objects built up by a builder, are not in that list and stay the responsibility of their owner.

Exceptions thrown by one Dispose are caught and passed to the OnDisposeException partial method, so one faulty resource does not stop the rest. Override it to log, otherwise the failure is silent. Prefer IAsyncDisposable (see Async disposable singleton) when a release must await I/O; a synchronous Dispose that blocks hides the cost in shutdown.

The following partial class will be generated
partial class Composition: IDisposable
{
#if NET9_0_OR_GREATER
  private readonly Lock _lock = new Lock();
#else
  private readonly Object _lock = new Object();
#endif
  private object[] _disposables = new object[1];
  private int _disposeIndex;

  private AcousticSensorBus? _singletonWcfLikeChannel;

  public ICombatSystem CombatSystem
  {
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    get
    {
      if (_singletonWcfLikeChannel is null)
        lock (_lock)
          if (_singletonWcfLikeChannel is null)
          {
            _singletonWcfLikeChannel = new AcousticSensorBus();
            _disposables[_disposeIndex++] = _singletonWcfLikeChannel;
          }

      return new SubmarineCombatSystem(_singletonWcfLikeChannel);
    }
  }

  public void Dispose()
  {
    int disposeIndex;
    object[] disposables;
    lock (_lock)
    {
      disposeIndex = _disposeIndex;
      _disposeIndex = 0;
      disposables = _disposables;
      _disposables = new object[1];
      _singletonWcfLikeChannel = null;
    }

    while (disposeIndex-- > 0)
    {
      switch (disposables[disposeIndex])
      {
        case IDisposable disposableInstance:
          try
          {
            disposableInstance.Dispose();
          }
          catch (Exception exception)
          {
            OnDisposeException(disposableInstance, exception);
          }
          break;
      }
    }
  }

  partial void OnDisposeException<T>(T disposableInstance, Exception exception) where T : IDisposable;
}

Class diagram:

---
 config:
  class:
   hideEmptyMembersBox: true
---
classDiagram
	Composition --|> IDisposable
	AcousticSensorBus --|> IAcousticSensorBus
	SubmarineCombatSystem --|> ICombatSystem
	Composition ..> SubmarineCombatSystem : ICombatSystem CombatSystem
	SubmarineCombatSystem o-- "Singleton" AcousticSensorBus : IAcousticSensorBus
	namespace Pure.DI.UsageTests.Lifetimes.DisposableSingletonScenario {
		class AcousticSensorBus {
				<<class>>
			+AcousticSensorBus()
		}
		class Composition {
		<<partial>>
		+ICombatSystem CombatSystem
		}
		class IAcousticSensorBus {
			<<interface>>
		}
		class ICombatSystem {
			<<interface>>
		}
		class SubmarineCombatSystem {
				<<class>>
			+SubmarineCombatSystem(IAcousticSensorBus sensorBus)
		}
	}
	namespace System {
		class IDisposable {
			<<interface>>
		}
	}
Loading

See also: