auto-scoped.md

July 5, 2026 · View on GitHub

Auto scoped

You can use the following example to automatically create a session when creating instances of a particular type:

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

var composition = new Composition();
var musicApp = composition.MusicAppRoot;

// Session #1: user starts listening on "Living Room Speaker"
var session1 = musicApp.StartListeningSession();
session1.Enqueue("Daft Punk - One More Time");
session1.Enqueue("Massive Attack - Teardrop");

// Session #2: user starts listening on "Headphones"
var session2 = musicApp.StartListeningSession();
session2.Enqueue("Radiohead - Weird Fishes/Arpeggi");

// Different sessions -> different scoped queue instances
session1.Queue.ShouldNotBe(session2.Queue);

// But inside one session, the same queue is used everywhere within that scope
session1.Queue.Items.Count.ShouldBe(2);
session2.Queue.Items.Count.ShouldBe(1);

// Domain abstractions

interface IPlaybackQueue
{
    IReadOnlyList<string> Items { get; }
    void Add(string trackTitle);
}

sealed class PlaybackQueue : IPlaybackQueue
{
    private readonly List<string> _items = [];

    public IReadOnlyList<string> Items => _items;

    public void Add(string trackTitle) => _items.Add(trackTitle);
}

interface IListeningSession
{
    IPlaybackQueue Queue { get; }

    void Enqueue(string trackTitle);
}

sealed class ListeningSession(IPlaybackQueue queue) : IListeningSession
{
    public IPlaybackQueue Queue => queue;

    public void Enqueue(string trackTitle) => queue.Add(trackTitle);
}

// Implements a "session boundary" for listening
class MusicApp(Func<IListeningSession> sessionFactory)
{
    // Each call creates a new DI scope under the hood (new "listening session").
    public IListeningSession StartListeningSession() => sessionFactory();
}

partial class Composition
{
    static void Setup() =>

        DI.Setup()
            // Scoped: one queue per listening session
            .Bind().As(Scoped).To<PlaybackQueue>()

            // Session composition root (private root used only to build sessions)
            .Root<ListeningSession>("Session", kind: RootKinds.Private)

            // Auto scoped factory: creates a new scope for each listening session
            .Bind().To(IListeningSession (Composition parentScope) => {
                // Create a child scope so scoped services (PlaybackQueue) are unique per session.
                var scope = new Composition(parentScope);
                return scope.Session;
            })

            // App-level root
            .Root<MusicApp>("MusicAppRoot");
}
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

Important

The method Inject()cannot be used outside of the binding setup.

The following partial class will be generated
partial class Composition
{
  private Composition _root;
#if NET9_0_OR_GREATER
  private readonly Lock _lock;
#else
  private readonly Object _lock;
#endif

  private PlaybackQueue? _singletonCompositionInOtherProject;

  [OrdinalAttribute(256)]
  public Composition()
  {
    _root = this;
#if NET9_0_OR_GREATER
    _lock = new Lock();
#else
    _lock = new Object();
#endif
  }

  internal Composition(Composition parentScope)
  {
    if (Object.ReferenceEquals(parentScope, null)) throw new ArgumentNullException(nameof(parentScope));
    _root = parentScope._root ?? parentScope;
    _lock = parentScope._lock;
  }

  public MusicApp MusicAppRoot
  {
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    get
    {
      Func<IListeningSession> perBlockFuncIListeningSession = new Func<IListeningSession>(
      [MethodImpl(MethodImplOptions.AggressiveInlining)]
      () =>
      {
        // Creates a deferred value
        IListeningSession transientIListeningSession;
        Composition localParentScope = this;
        // Create a child scope so scoped services (PlaybackQueue) are unique per session.
        var localScope = new Composition(localParentScope);
        transientIListeningSession = localScope.Session;
        return transientIListeningSession;
      });
      return new MusicApp(perBlockFuncIListeningSession);
    }
  }

  private ListeningSession Session
  {
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    get
    {
      if (_singletonCompositionInOtherProject is null)
        lock (_lock)
          if (_singletonCompositionInOtherProject is null)
          {
            _singletonCompositionInOtherProject = new PlaybackQueue();
          }

      return new ListeningSession(_singletonCompositionInOtherProject);
    }
  }
}

Class diagram:

---
 config:
  class:
   hideEmptyMembersBox: true
---
classDiagram
	PlaybackQueue --|> IPlaybackQueue
	Composition ..> MusicApp : MusicApp MusicAppRoot
	Composition ..> ListeningSession : ListeningSession Session
	IListeningSession *-- Composition : Composition
	MusicApp o-- "PerBlock" FuncᐸIListeningSessionᐳ : FuncᐸIListeningSessionᐳ
	ListeningSession o-- "Scoped" PlaybackQueue : IPlaybackQueue
	FuncᐸIListeningSessionᐳ *-- IListeningSession : IListeningSession
	namespace Pure.DI.UsageTests.Lifetimes.AutoScopedScenario {
		class Composition {
		<<partial>>
		+MusicApp MusicAppRoot
		-ListeningSession Session
		}
		class IListeningSession {
				<<interface>>
		}
		class IPlaybackQueue {
			<<interface>>
		}
		class ListeningSession {
				<<class>>
			+ListeningSession(IPlaybackQueue queue)
		}
		class MusicApp {
				<<class>>
			+MusicApp(FuncᐸIListeningSessionᐳ sessionFactory)
		}
		class PlaybackQueue {
				<<class>>
			+PlaybackQueue()
		}
	}
	namespace System {
		class FuncᐸIListeningSessionᐳ {
				<<delegate>>
		}
	}

See also: