Skip to content

Commit e35df65

Browse files
author
Nikolay Pianikov
committed
Add UnitySceneScopesScenario test to demonstrate Unity-style scoped lifetimes
1 parent 8fffe12 commit e35df65

1 file changed

Lines changed: 184 additions & 0 deletions

File tree

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
/*
2+
$v=true
3+
$p=3
4+
$i=false
5+
$d=Unity scene scopes
6+
$h=Demonstrates Unity-style scoped lifetime boundaries where Unity creates MonoBehaviour instances and Pure.DI builds them up without constructors.
7+
$h=Each loaded scene has its own scope, so scoped services are shared inside one scene and isolated from another scene.
8+
$f=>[!NOTE]
9+
$f=>In a real Unity project the scene objects are created by Unity. The sample uses constructors only to simulate serialized references in a test.
10+
$r=Shouldly
11+
*/
12+
13+
// ReSharper disable ClassNeverInstantiated.Local
14+
// ReSharper disable CheckNamespace
15+
// ReSharper disable UnusedParameter.Local
16+
// ReSharper disable ArrangeTypeModifiers
17+
// ReSharper disable UnusedTypeParameter
18+
// ReSharper disable UnusedParameterInPartialMethod
19+
// ReSharper disable UnusedMember.Local
20+
// ReSharper disable ArrangeTypeMemberModifiers
21+
// ReSharper disable InconsistentNaming
22+
// ReSharper disable UnusedMember.Global
23+
// ReSharper disable ConvertToPrimaryConstructor
24+
// ReSharper disable UnassignedField.Global
25+
// ReSharper disable FieldCanBeMadeReadOnly.Local
26+
// ReSharper disable ConvertConstructorToMemberInitializers
27+
#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value
28+
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
29+
namespace Pure.DI.UsageTests.Unity.UnitySceneScopesScenario;
30+
31+
using Shouldly;
32+
using UnityEngine;
33+
using Xunit;
34+
using static Lifetime;
35+
36+
// {
37+
//# using Pure.DI;
38+
//# using UnityEngine;
39+
//# using static Pure.DI.Lifetime;
40+
// }
41+
42+
public class Scenario
43+
{
44+
[Fact]
45+
public void Run()
46+
{
47+
// {
48+
// Application scope: one root for shared singletons.
49+
var application = new Scope("Application");
50+
51+
// Unity loads two scenes. Each scene has its own MonoBehaviour scope object.
52+
var menuScene = Scope.SetupScope(application, new Scope("Menu"));
53+
var levelScene = Scope.SetupScope(application, new Scope("Level"));
54+
55+
// Unity creates MonoBehaviour instances. Pure.DI only builds them up.
56+
var menuClock1 = new Clock(menuScene);
57+
var menuClock2 = new Clock(menuScene);
58+
var levelClock = new Clock(levelScene);
59+
60+
menuClock1.Awake();
61+
menuClock2.Awake();
62+
levelClock.Awake();
63+
64+
// Same scene => same scoped dependency.
65+
menuClock1.Session.ShouldBe(menuClock2.Session);
66+
67+
// Different scenes => different scoped dependencies.
68+
menuClock1.Session.ShouldNotBe(levelClock.Session);
69+
70+
// Singleton dependency is still shared from the application scope.
71+
menuClock1.ClockService.ShouldBe(levelClock.ClockService);
72+
73+
menuScene.Dispose();
74+
menuClock1.Session.IsDisposed.ShouldBeTrue();
75+
levelClock.Session.IsDisposed.ShouldBeFalse();
76+
77+
levelScene.Dispose();
78+
levelClock.Session.IsDisposed.ShouldBeTrue();
79+
80+
application.Dispose();
81+
menuClock1.ClockService.IsDisposed.ShouldBeTrue();
82+
// }
83+
}
84+
}
85+
86+
// {
87+
public class Clock : MonoBehaviour
88+
{
89+
[SerializeField] Scope scope;
90+
// }
91+
public Clock(Scope scope)
92+
{
93+
this.scope = scope;
94+
}
95+
// {
96+
[Dependency]
97+
public IClockService ClockService { get; set; }
98+
99+
[Dependency]
100+
public IClockSession Session { get; set; }
101+
102+
public void Awake()
103+
{
104+
scope.BuildUp(this);
105+
}
106+
}
107+
108+
public interface IClockConfig
109+
{
110+
TimeSpan Offset { get; }
111+
}
112+
113+
[CreateAssetMenu(fileName = "ClockConfig", menuName = "Clock/Config")]
114+
public class ClockConfig : ScriptableObject, IClockConfig
115+
{
116+
[SerializeField] int offsetHours;
117+
// }
118+
public ClockConfig()
119+
{
120+
offsetHours = 3;
121+
}
122+
// {
123+
public TimeSpan Offset => TimeSpan.FromHours(offsetHours);
124+
}
125+
126+
public interface IClockService
127+
{
128+
DateTime Now { get; }
129+
130+
bool IsDisposed { get; }
131+
}
132+
133+
public class ClockService(IClockConfig config) : IClockService, IDisposable
134+
{
135+
public DateTime Now => DateTime.UtcNow + config.Offset;
136+
137+
public bool IsDisposed { get; private set; }
138+
139+
public void Dispose() => IsDisposed = true;
140+
}
141+
142+
public interface IClockSession
143+
{
144+
string SceneName { get; }
145+
146+
bool IsDisposed { get; }
147+
}
148+
149+
public class ClockSession([Tag("scene name")] string sceneName) : IClockSession, IDisposable
150+
{
151+
public string SceneName { get; } = sceneName;
152+
153+
public bool IsDisposed { get; private set; }
154+
155+
public void Dispose() => IsDisposed = true;
156+
}
157+
158+
public partial class Scope : MonoBehaviour
159+
{
160+
[SerializeField] ClockConfig clockConfig;
161+
[SerializeField] string sceneName;
162+
// }
163+
public Scope(string sceneName)
164+
{
165+
clockConfig = new ClockConfig();
166+
this.sceneName = sceneName;
167+
}
168+
// Resolve = Off
169+
// ToString = Off
170+
// {
171+
void Setup() => DI.Setup()
172+
.Hint(Hint.ScopeMethodName, "SetupScope")
173+
.Bind().To(() => clockConfig)
174+
.Bind("scene name").To(_ => sceneName)
175+
.Bind<IClockService>().As(Singleton).To<ClockService>()
176+
.Bind<IClockSession>().As(Scoped).To<ClockSession>()
177+
.Builders<MonoBehaviour>();
178+
179+
void OnDestroy()
180+
{
181+
Dispose();
182+
}
183+
}
184+
// }

0 commit comments

Comments
 (0)