Skip to content

Latest commit

 

History

History
305 lines (256 loc) · 7.74 KB

File metadata and controls

305 lines (256 loc) · 7.74 KB

Scope

The Scoped lifetime ensures a single instance of a dependency within a scope — a typical example is a single DbContext, unit of work, or request context per web request. This example wraps scope creation in a Scope class: each scope gets its own RequestContext, all services resolved within that scope share it, and disposing the scope disposes all scoped instances it created.

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

var composition = new Composition();
IRequestContext ctx1;
IRequestContext ctx2;

// Scope #1
using (var scope1 = composition.NewScope)
{
    var checkout11 = scope1.Checkout;
    var checkout12 = scope1.Checkout;
    ctx1 = checkout11.Context;

    // Same request => same scoped instance
    ctx1.ShouldBe(checkout12.Context);
    ctx1.IsDisposed.ShouldBeFalse();
}

// End of request #1 => scoped instance is disposed
ctx1.IsDisposed.ShouldBeTrue();

// Request #2
using (var scope1 = composition.NewScope)
{
    var checkout2 = scope1.Checkout;
    ctx2 = checkout2.Context;
}

// Different request => different scoped instance
ctx1.ShouldNotBe(ctx2);

// End of request #2 => scoped instance is disposed
ctx2.IsDisposed.ShouldBeTrue();

interface IIdGenerator
{
    Guid Generate();
}

class IdGenerator : IIdGenerator
{
    public Guid Generate() => Guid.NewGuid();
}

interface IRequestContext
{
    Guid CorrelationId { get; }

    bool IsDisposed { get; }
}

// Typically: DbContext / UnitOfWork / RequestTelemetry / Activity, etc.
sealed class RequestContext(IIdGenerator idGenerator)
    : IRequestContext, IDisposable
{
    public Guid CorrelationId { get; } = idGenerator.Generate();

    public bool IsDisposed { get; private set; }

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

interface ICheckoutService
{
    IRequestContext Context { get; }
}

// "Controller/service" that participates in request processing.
// It depends on a scoped context (per-request resource).
sealed class CheckoutService(IRequestContext context)
    : ICheckoutService
{
    public IRequestContext Context => context;
}

// Represents a scope
class Scope(Composition composition): IDisposable
{
    private readonly Composition _scope = Composition.SetupScope(composition, new Composition());

    public ICheckoutService Checkout => _scope.RequestRoot;

    public void Dispose() => _scope.Dispose();
}

partial class Composition
{
    static void Setup() =>

        DI.Setup()
            .Hint(Hint.ScopeMethodName, "SetupScope")
            // Per-request lifetime
            .Bind().As(Scoped).To<RequestContext>()

            .Bind().As(Singleton).To<IdGenerator>()

            // Regular service that consumes scoped context
            .Bind().To<CheckoutService>()

            // "Request root" (what your controller/handler resolves)
            .Root<ICheckoutService>("RequestRoot")
            .Root<Scope>("NewScope");
}
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

Note

A scope is just another composition instance bound to its parent, so singletons remain shared across scopes while scoped instances are unique per scope.

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

  private RequestContext? _singletonCompositionInOtherProject;
  private IdGenerator? _singletonCompositionWithGenericRootsAndArgsInOtherProject;

  internal static Composition SetupScope(Composition parentScope, Composition childScope)
  {
    if (Object.ReferenceEquals(parentScope, null)) throw new ArgumentNullException(nameof(parentScope));
    if (Object.ReferenceEquals(childScope, null)) throw new ArgumentNullException(nameof(childScope));
    if (Object.ReferenceEquals(parentScope, childScope)) throw new ArgumentException("The parent and child scopes must be different instances.", nameof(childScope));
    childScope._root = parentScope._root ?? parentScope;
    childScope._lock = parentScope._lock;
    childScope._disposables = new object[1];
    return childScope;
  }

  public ICheckoutService RequestRoot
  {
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    get
    {
      var root = _root ?? this;
      if (_singletonCompositionInOtherProject is null)
        lock (_lock)
          if (_singletonCompositionInOtherProject is null)
          {
            if (root._singletonCompositionWithGenericRootsAndArgsInOtherProject is null)
            {
              root._singletonCompositionWithGenericRootsAndArgsInOtherProject = new IdGenerator();
            }

            _singletonCompositionInOtherProject = new RequestContext(root._singletonCompositionWithGenericRootsAndArgsInOtherProject);
            _disposables[_disposeIndex++] = _singletonCompositionInOtherProject;
          }

      return new CheckoutService(_singletonCompositionInOtherProject);
    }
  }

  public Scope NewScope
  {
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    get
    {
      return new Scope(this);
    }
  }

  public void Dispose()
  {
    int disposeIndex;
    object[] disposables;
    lock (_lock)
    {
      disposeIndex = _disposeIndex;
      _disposeIndex = 0;
      disposables = _disposables;
      _disposables = new object[1];
      _singletonCompositionInOtherProject = null;
      _singletonCompositionWithGenericRootsAndArgsInOtherProject = 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
	RequestContext --|> IRequestContext
	IdGenerator --|> IIdGenerator
	CheckoutService --|> ICheckoutService
	Composition ..> Scope : Scope NewScope
	Composition ..> CheckoutService : ICheckoutService RequestRoot
	RequestContext o-- "Singleton" IdGenerator : IIdGenerator
	CheckoutService o-- "Scoped" RequestContext : IRequestContext
	Scope *-- Composition : Composition
	namespace Pure.DI.UsageTests.Lifetimes.ScopeScenario {
		class CheckoutService {
				<<class>>
			+CheckoutService(IRequestContext context)
		}
		class Composition {
		<<partial>>
		+Scope NewScope
		+ICheckoutService RequestRoot
		}
		class ICheckoutService {
			<<interface>>
		}
		class IdGenerator {
				<<class>>
			+IdGenerator()
		}
		class IIdGenerator {
			<<interface>>
		}
		class IRequestContext {
			<<interface>>
		}
		class RequestContext {
				<<class>>
			+RequestContext(IIdGenerator idGenerator)
		}
		class Scope {
				<<class>>
			+Scope(Composition composition)
		}
	}
	namespace System {
		class IDisposable {
			<<interface>>
		}
	}
Loading

See also: