Skip to content

Commit 61918e1

Browse files
Add scope example
1 parent 640611c commit 61918e1

3 files changed

Lines changed: 192 additions & 43 deletions

File tree

tests/Pure.DI.UsageTests/Lifetimes/ScopeFactoryScenario.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public class Scenario
3333
public void Run()
3434
{
3535
// {
36-
var composition = new Composition(desc: "Checkout");
36+
var composition = new Composition();
3737
IRequestContext ctx1;
3838
IRequestContext ctx2;
3939

@@ -61,6 +61,7 @@ public void Run()
6161

6262
// Different request => different scoped instance
6363
ctx1.ShouldNotBe(ctx2);
64+
6465
// End of request #2 => scoped instance is disposed
6566
ctx2.IsDisposed.ShouldBeTrue();
6667
// }
@@ -104,7 +105,8 @@ interface ICheckoutService
104105

105106
// "Controller/service" that participates in request processing.
106107
// It depends on a scoped context (per-request resource).
107-
sealed class CheckoutService(string description, IRequestContext context) : ICheckoutService
108+
sealed class CheckoutService(IRequestContext context)
109+
: ICheckoutService
108110
{
109111
public IRequestContext Context => context;
110112
}
@@ -118,7 +120,6 @@ static void Setup() =>
118120
// {
119121
DI.Setup()
120122
.Hint(Hint.ScopeFactoryName, "CreateScope")
121-
.Arg<string>("desc")
122123
// Per-request lifetime
123124
.Bind().As(Scoped).To<RequestContext>()
124125

Lines changed: 55 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
/*
1+
/*
22
$v=true
33
$p=4
44
$d=Scope
5-
$h=The `Scoped` lifetime ensures that there will be a single instance of the dependency for each scope.
5+
$h=Demonstrates scoped lifetime with `Hint(Hint.ScopeFactory, "on")` where scopes are represented by generated `Scope` objects created via `CreateScope()`.
66
$f=>[!NOTE]
7-
$f=>`Scoped` lifetime is essential for request-based or session-based scenarios where instances should be shared within a scope but isolated between scopes.
7+
$f=>This approach is useful when you need runtime scope creation without deriving a child composition type.
88
$r=Shouldly
99
*/
1010

@@ -33,45 +33,53 @@ public class Scenario
3333
public void Run()
3434
{
3535
// {
36-
var composition = new Composition();
37-
var app = composition.AppRoot;
36+
var composition = new Composition(desc: "Checkout");
37+
IRequestContext ctx1;
38+
IRequestContext ctx2;
3839

39-
// Real-world analogy:
40-
// each HTTP request (or message consumer handling) creates its own scope.
41-
// Scoped services live exactly as long as the request is being processed.
40+
// Scope #1
41+
using (var scope1 = composition.NewScope)
42+
{
43+
var checkout11 = scope1.Checkout;
44+
var checkout12 = scope1.Checkout;
45+
ctx1 = checkout11.Context;
4246

43-
// Request #1
44-
var request1 = app.CreateRequestScope();
45-
var checkout1 = request1.RequestRoot;
47+
// Same request => same scoped instance
48+
ctx1.ShouldBe(checkout12.Context);
49+
ctx1.IsDisposed.ShouldBeFalse();
50+
}
4651

47-
var ctx11 = checkout1.Context;
48-
var ctx12 = checkout1.Context;
49-
50-
// Same request => same scoped instance
51-
ctx11.ShouldBe(ctx12);
52+
// End of request #1 => scoped instance is disposed
53+
ctx1.IsDisposed.ShouldBeTrue();
5254

5355
// Request #2
54-
var request2 = app.CreateRequestScope();
55-
var checkout2 = request2.RequestRoot;
56-
57-
var ctx2 = checkout2.Context;
56+
using (var scope1 = composition.NewScope)
57+
{
58+
var checkout2 = scope1.Checkout;
59+
ctx2 = checkout2.Context;
60+
}
5861

5962
// Different request => different scoped instance
60-
ctx11.ShouldNotBe(ctx2);
63+
ctx1.ShouldNotBe(ctx2);
6164

62-
// End of Request #1 => scoped instance is disposed
63-
request1.Dispose();
64-
ctx11.IsDisposed.ShouldBeTrue();
65-
66-
// End of Request #2 => scoped instance is disposed
67-
request2.Dispose();
65+
// End of request #2 => scoped instance is disposed
6866
ctx2.IsDisposed.ShouldBeTrue();
6967
// }
7068
composition.SaveClassDiagram();
7169
}
7270
}
7371

7472
// {
73+
interface IIdGenerator
74+
{
75+
Guid Generate();
76+
}
77+
78+
class IdGenerator : IIdGenerator
79+
{
80+
public Guid Generate() => Guid.NewGuid();
81+
}
82+
7583
interface IRequestContext
7684
{
7785
Guid CorrelationId { get; }
@@ -80,9 +88,10 @@ interface IRequestContext
8088
}
8189

8290
// Typically: DbContext / UnitOfWork / RequestTelemetry / Activity, etc.
83-
sealed class RequestContext : IRequestContext, IDisposable
91+
sealed class RequestContext(IIdGenerator idGenerator)
92+
: IRequestContext, IDisposable
8493
{
85-
public Guid CorrelationId { get; } = Guid.NewGuid();
94+
public Guid CorrelationId { get; } = idGenerator.Generate();
8695

8796
public bool IsDisposed { get; private set; }
8897

@@ -96,18 +105,22 @@ interface ICheckoutService
96105

97106
// "Controller/service" that participates in request processing.
98107
// It depends on a scoped context (per-request resource).
99-
sealed class CheckoutService(IRequestContext context) : ICheckoutService
108+
sealed class CheckoutService(
109+
string description,
110+
IRequestContext context)
111+
: ICheckoutService
100112
{
101113
public IRequestContext Context => context;
102114
}
103115

104-
// Implements a request scope (per-request composition)
105-
sealed class RequestScope(Composition parent) : Composition(parent);
106-
107-
partial class App(Func<RequestScope> requestScopeFactory)
116+
// Represents a scope
117+
class Scope(Composition composition): IDisposable
108118
{
109-
// In a web app this would roughly map to: "create scope for request"
110-
public RequestScope CreateRequestScope() => requestScopeFactory();
119+
private readonly Composition _scope = composition.CreateScope();
120+
121+
public ICheckoutService Checkout => _scope.RequestRoot;
122+
123+
public void Dispose() => _scope.Dispose();
111124
}
112125

113126
partial class Composition
@@ -118,16 +131,18 @@ static void Setup() =>
118131
// Resolve = Off
119132
// {
120133
DI.Setup()
134+
.Hint(Hint.ScopeFactoryName, "CreateScope")
135+
.Arg<string>("desc")
121136
// Per-request lifetime
122137
.Bind().As(Scoped).To<RequestContext>()
123138

139+
.Bind().As(Singleton).To<IdGenerator>()
140+
124141
// Regular service that consumes scoped context
125142
.Bind().To<CheckoutService>()
126143

127144
// "Request root" (what your controller/handler resolves)
128145
.Root<ICheckoutService>("RequestRoot")
129-
130-
// "Application root" (what creates request scopes)
131-
.Root<App>("AppRoot");
146+
.Root<Scope>("NewScope");
132147
}
133-
// }
148+
// }
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
$v=true
3+
$p=4
4+
$d=Scoped
5+
$h=The `Scoped` lifetime ensures that there will be a single instance of the dependency for each scope.
6+
$f=>[!NOTE]
7+
$f=>`Scoped` lifetime is essential for request-based or session-based scenarios where instances should be shared within a scope but isolated between scopes.
8+
$r=Shouldly
9+
*/
10+
11+
// ReSharper disable ClassNeverInstantiated.Local
12+
// ReSharper disable CheckNamespace
13+
// ReSharper disable ClassNeverInstantiated.Global
14+
// ReSharper disable ArrangeTypeModifiers
15+
// ReSharper disable UnusedMember.Local
16+
// ReSharper disable ArrangeTypeMemberModifiers
17+
// ReSharper disable PartialTypeWithSinglePart
18+
// ReSharper disable UnusedMember.Global
19+
#pragma warning disable CS9113 // Parameter is unread.
20+
namespace Pure.DI.UsageTests.Lifetimes.ScopedScenario;
21+
22+
using Xunit;
23+
using static Lifetime;
24+
25+
// {
26+
//# using Pure.DI;
27+
//# using static Pure.DI.Lifetime;
28+
// }
29+
30+
public class Scenario
31+
{
32+
[Fact]
33+
public void Run()
34+
{
35+
// {
36+
var composition = new Composition();
37+
var app = composition.AppRoot;
38+
39+
// Real-world analogy:
40+
// each HTTP request (or message consumer handling) creates its own scope.
41+
// Scoped services live exactly as long as the request is being processed.
42+
43+
// Request #1
44+
var request1 = app.CreateRequestScope();
45+
var checkout1 = request1.RequestRoot;
46+
47+
var ctx11 = checkout1.Context;
48+
var ctx12 = checkout1.Context;
49+
50+
// Same request => same scoped instance
51+
ctx11.ShouldBe(ctx12);
52+
53+
// Request #2
54+
var request2 = app.CreateRequestScope();
55+
var checkout2 = request2.RequestRoot;
56+
57+
var ctx2 = checkout2.Context;
58+
59+
// Different request => different scoped instance
60+
ctx11.ShouldNotBe(ctx2);
61+
62+
// End of Request #1 => scoped instance is disposed
63+
request1.Dispose();
64+
ctx11.IsDisposed.ShouldBeTrue();
65+
66+
// End of Request #2 => scoped instance is disposed
67+
request2.Dispose();
68+
ctx2.IsDisposed.ShouldBeTrue();
69+
// }
70+
composition.SaveClassDiagram();
71+
}
72+
}
73+
74+
// {
75+
interface IRequestContext
76+
{
77+
Guid CorrelationId { get; }
78+
79+
bool IsDisposed { get; }
80+
}
81+
82+
// Typically: DbContext / UnitOfWork / RequestTelemetry / Activity, etc.
83+
sealed class RequestContext : IRequestContext, IDisposable
84+
{
85+
public Guid CorrelationId { get; } = Guid.NewGuid();
86+
87+
public bool IsDisposed { get; private set; }
88+
89+
public void Dispose() => IsDisposed = true;
90+
}
91+
92+
interface ICheckoutService
93+
{
94+
IRequestContext Context { get; }
95+
}
96+
97+
// "Controller/service" that participates in request processing.
98+
// It depends on a scoped context (per-request resource).
99+
sealed class CheckoutService(IRequestContext context) : ICheckoutService
100+
{
101+
public IRequestContext Context => context;
102+
}
103+
104+
// Implements a request scope (per-request composition)
105+
sealed class RequestScope(Composition parent) : Composition(parent);
106+
107+
partial class App(Func<RequestScope> requestScopeFactory)
108+
{
109+
// In a web app this would roughly map to: "create scope for request"
110+
public RequestScope CreateRequestScope() => requestScopeFactory();
111+
}
112+
113+
partial class Composition
114+
{
115+
static void Setup() =>
116+
// }
117+
// Disable Resolve methods to keep the public API minimal
118+
// Resolve = Off
119+
// {
120+
DI.Setup()
121+
// Per-request lifetime
122+
.Bind().As(Scoped).To<RequestContext>()
123+
124+
// Regular service that consumes scoped context
125+
.Bind().To<CheckoutService>()
126+
127+
// "Request root" (what your controller/handler resolves)
128+
.Root<ICheckoutService>("RequestRoot")
129+
130+
// "Application root" (what creates request scopes)
131+
.Root<App>("AppRoot");
132+
}
133+
// }

0 commit comments

Comments
 (0)