Skip to content

Commit d3a1afe

Browse files
Add scoped api
1 parent ad0c6ac commit d3a1afe

11 files changed

+271
-1
lines changed

Assets/Reflex.EditModeTests/GarbageCollectionTests.cs

+7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Diagnostics;
34
using System.Linq;
45
using FluentAssertions;
56
using NUnit.Framework;
@@ -21,6 +22,12 @@ public static void ForceGarbageCollection()
2122
GC.WaitForPendingFinalizers();
2223
}
2324

25+
[Conditional("REFLEX_DEBUG")]
26+
public static void MarkAsInconclusiveWhenReflexDebugIsEnabled()
27+
{
28+
Assert.Inconclusive("Disable REFLEX_DEBUG symbol when running garbage collection tests!");
29+
}
30+
2431
[Test, Retry(3)]
2532
public void Singleton_ShouldBeFinalized_WhenOwnerIsDisposed()
2633
{
+144
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using FluentAssertions;
4+
using NUnit.Framework;
5+
using Reflex.Core;
6+
using UnityEditor;
7+
8+
namespace Reflex.EditModeTests
9+
{
10+
public class ScopedTests
11+
{
12+
private class Service : IDisposable
13+
{
14+
public bool IsDisposed { get; private set; }
15+
16+
public void Dispose()
17+
{
18+
IsDisposed = true;
19+
}
20+
}
21+
22+
[Test]
23+
public void ScopedFromType_ShouldReturnAlwaysSameInstance_WhenCalledFromSameContainer()
24+
{
25+
var parentContainer = new ContainerBuilder().AddScoped(typeof(Service)).Build();
26+
var childContainer = parentContainer.Scope();
27+
parentContainer.Resolve<Service>().Should().Be(parentContainer.Resolve<Service>());
28+
childContainer.Resolve<Service>().Should().Be(childContainer.Resolve<Service>());
29+
}
30+
31+
[Test]
32+
public void ScopedFromFactory_ShouldReturnAlwaysSameInstance_WhenCalledFromSameContainer()
33+
{
34+
var parentContainer = new ContainerBuilder().AddScoped(_ => new Service()).Build();
35+
var childContainer = parentContainer.Scope();
36+
parentContainer.Resolve<Service>().Should().Be(parentContainer.Resolve<Service>());
37+
childContainer.Resolve<Service>().Should().Be(childContainer.Resolve<Service>());
38+
}
39+
40+
[Test]
41+
public void ScopedFromType_NewInstanceShouldBeConstructed_ForEveryNewContainer()
42+
{
43+
var instances = new HashSet<Service>();
44+
var parentContainer = new ContainerBuilder().AddScoped(typeof(Service)).Build();
45+
var childContainer = parentContainer.Scope();
46+
instances.Add(parentContainer.Resolve<Service>());
47+
instances.Add(childContainer.Resolve<Service>());
48+
instances.Add(parentContainer.Resolve<Service>());
49+
instances.Add(childContainer.Resolve<Service>());
50+
instances.Count.Should().Be(2);
51+
}
52+
53+
[Test]
54+
public void ScopedFromFactory_NewInstanceShouldBeConstructed_ForEveryNewContainer()
55+
{
56+
var instances = new HashSet<Service>();
57+
var parentContainer = new ContainerBuilder().AddScoped(_ => new Service()).Build();
58+
var childContainer = parentContainer.Scope();
59+
instances.Add(parentContainer.Resolve<Service>());
60+
instances.Add(childContainer.Resolve<Service>());
61+
instances.Add(parentContainer.Resolve<Service>());
62+
instances.Add(childContainer.Resolve<Service>());
63+
instances.Count.Should().Be(2);
64+
}
65+
66+
[Test]
67+
public void ScopedFromType_ConstructedInstances_ShouldBeDisposed_WithinConstructingContainer()
68+
{
69+
var parentContainer = new ContainerBuilder().AddScoped(typeof(Service)).Build();
70+
var childContainer = parentContainer.Scope();
71+
72+
var instanceConstructedByChild = childContainer.Resolve<Service>();
73+
var instanceConstructedByParent = parentContainer.Resolve<Service>();
74+
75+
childContainer.Dispose();
76+
77+
instanceConstructedByChild.IsDisposed.Should().BeTrue();
78+
instanceConstructedByParent.IsDisposed.Should().BeFalse();
79+
}
80+
81+
[Test]
82+
public void ScopedFromFactory_ConstructedInstances_ShouldBeDisposed_WithinConstructingContainer()
83+
{
84+
var parentContainer = new ContainerBuilder().AddScoped(_ => new Service()).Build();
85+
var childContainer = parentContainer.Scope();
86+
87+
var instanceConstructedByChild = childContainer.Resolve<Service>();
88+
var instanceConstructedByParent = parentContainer.Resolve<Service>();
89+
90+
childContainer.Dispose();
91+
92+
instanceConstructedByChild.IsDisposed.Should().BeTrue();
93+
instanceConstructedByParent.IsDisposed.Should().BeFalse();
94+
}
95+
96+
[Test, Retry(3)]
97+
public void ScopedFromType_ConstructedInstances_ShouldBeCollected_WhenConstructingContainerIsDisposed()
98+
{
99+
GarbageCollectionTests.MarkAsInconclusiveWhenReflexDebugIsEnabled();
100+
101+
WeakReference instanceConstructedByChild;
102+
WeakReference instanceConstructedByParent;
103+
var parentContainer = new ContainerBuilder().AddScoped(typeof(Service)).Build();
104+
105+
void Act()
106+
{
107+
using (var childContainer = parentContainer.Scope())
108+
{
109+
instanceConstructedByChild = new WeakReference(childContainer.Resolve<Service>());
110+
instanceConstructedByParent = new WeakReference(parentContainer.Resolve<Service>());
111+
}
112+
}
113+
114+
Act();
115+
GarbageCollectionTests.ForceGarbageCollection();
116+
instanceConstructedByChild.IsAlive.Should().BeFalse();
117+
instanceConstructedByParent.IsAlive.Should().BeTrue();
118+
}
119+
120+
[Test, Retry(3)]
121+
public void ScopedFromFactory_ConstructedInstances_ShouldBeCollected_WhenConstructingContainerIsDisposed()
122+
{
123+
GarbageCollectionTests.MarkAsInconclusiveWhenReflexDebugIsEnabled();
124+
125+
WeakReference instanceConstructedByChild;
126+
WeakReference instanceConstructedByParent;
127+
var parentContainer = new ContainerBuilder().AddScoped(_ => new Service()).Build();
128+
129+
void Act()
130+
{
131+
using (var childContainer = parentContainer.Scope())
132+
{
133+
instanceConstructedByChild = new WeakReference(childContainer.Resolve<Service>());
134+
instanceConstructedByParent = new WeakReference(parentContainer.Resolve<Service>());
135+
}
136+
}
137+
138+
Act();
139+
GarbageCollectionTests.ForceGarbageCollection();
140+
instanceConstructedByChild.IsAlive.Should().BeFalse();
141+
instanceConstructedByParent.IsAlive.Should().BeTrue();
142+
}
143+
}
144+
}

Assets/Reflex.EditModeTests/ScopedTests.cs.meta

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Assets/Reflex.EditModeTests/TransientTests.cs

+4
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ public void TransientFromFactory_ConstructedInstances_ShouldBeDisposed_WithinCon
5050
[Test, Retry(3)]
5151
public void TransientFromType_ConstructedInstances_ShouldBeCollected_WhenConstructingContainerIsDisposed()
5252
{
53+
GarbageCollectionTests.MarkAsInconclusiveWhenReflexDebugIsEnabled();
54+
5355
WeakReference instanceConstructedByChild;
5456
WeakReference instanceConstructedByParent;
5557
var parentContainer = new ContainerBuilder().AddTransient(typeof(Service)).Build();
@@ -72,6 +74,8 @@ void Act()
7274
[Test, Retry(3)]
7375
public void TransientFromFactory_ConstructedInstances_ShouldBeCollected_WhenConstructingContainerIsDisposed()
7476
{
77+
GarbageCollectionTests.MarkAsInconclusiveWhenReflexDebugIsEnabled();
78+
7579
WeakReference instanceConstructedByChild;
7680
WeakReference instanceConstructedByParent;
7781
var parentContainer = new ContainerBuilder().AddTransient(c => new Service()).Build();

Assets/Reflex/Core/Container.cs

-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ namespace Reflex.Core
1212
{
1313
public sealed class Container : IDisposable
1414
{
15-
1615
public string Name { get; }
1716
internal Container Parent { get; }
1817
internal List<Container> Children { get; } = new();

Assets/Reflex/Core/ContainerBuilder.cs

+28
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,34 @@ public ContainerBuilder AddTransient<T>(Func<Container, T> factory)
117117
{
118118
return AddTransient(factory, typeof(T));
119119
}
120+
121+
// Scoped
122+
123+
public ContainerBuilder AddScoped(Type concrete, params Type[] contracts)
124+
{
125+
return Add(concrete, contracts, new ScopedTypeResolver(concrete));
126+
}
127+
128+
public ContainerBuilder AddScoped(Type concrete)
129+
{
130+
return AddScoped(concrete, concrete);
131+
}
132+
133+
public ContainerBuilder AddScoped<T>(Func<Container, T> factory, params Type[] contracts)
134+
{
135+
var resolver = new ScopedFactoryResolver(Proxy);
136+
return Add(typeof(T), contracts, resolver);
137+
138+
object Proxy(Container container)
139+
{
140+
return factory.Invoke(container);
141+
}
142+
}
143+
144+
public ContainerBuilder AddScoped<T>(Func<Container, T> factory)
145+
{
146+
return AddScoped(factory, typeof(T));
147+
}
120148

121149
public bool HasBinding(Type type)
122150
{

Assets/Reflex/Enums/Lifetime.cs

+1
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ public enum Lifetime
44
{
55
Singleton,
66
Transient,
7+
Scoped,
78
}
89
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using System;
2+
using System.Runtime.CompilerServices;
3+
using Reflex.Core;
4+
using Reflex.Enums;
5+
6+
namespace Reflex.Resolvers
7+
{
8+
internal sealed class ScopedFactoryResolver : IResolver
9+
{
10+
private readonly Func<Container, object> _factory;
11+
private readonly ConditionalWeakTable<Container, object> _instances = new();
12+
public Lifetime Lifetime => Lifetime.Scoped;
13+
14+
public ScopedFactoryResolver(Func<Container, object> factory)
15+
{
16+
Diagnosis.RegisterCallSite(this);
17+
_factory = factory;
18+
}
19+
20+
public object Resolve(Container container)
21+
{
22+
Diagnosis.IncrementResolutions(this);
23+
24+
if (!_instances.TryGetValue(container, out var instance))
25+
{
26+
instance = _factory.Invoke(container);
27+
_instances.Add(container, instance);
28+
container.Disposables.TryAdd(instance);
29+
Diagnosis.RegisterInstance(this, instance);
30+
}
31+
32+
return instance;
33+
}
34+
35+
public void Dispose()
36+
{
37+
}
38+
}
39+
}

Assets/Reflex/Resolvers/ScopedFactoryResolver.cs.meta

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using System;
2+
using System.Runtime.CompilerServices;
3+
using Reflex.Core;
4+
using Reflex.Enums;
5+
6+
namespace Reflex.Resolvers
7+
{
8+
internal sealed class ScopedTypeResolver : IResolver
9+
{
10+
private readonly Type _concreteType;
11+
private readonly ConditionalWeakTable<Container, object> _instances = new();
12+
public Lifetime Lifetime => Lifetime.Scoped;
13+
14+
public ScopedTypeResolver(Type concreteType)
15+
{
16+
Diagnosis.RegisterCallSite(this);
17+
_concreteType = concreteType;
18+
}
19+
20+
public object Resolve(Container container)
21+
{
22+
Diagnosis.IncrementResolutions(this);
23+
24+
if (!_instances.TryGetValue(container, out var instance))
25+
{
26+
instance = container.Construct(_concreteType);
27+
_instances.Add(container, instance);
28+
container.Disposables.TryAdd(instance);
29+
Diagnosis.RegisterInstance(this, instance);
30+
}
31+
32+
return instance;
33+
}
34+
35+
public void Dispose()
36+
{
37+
}
38+
}
39+
}

Assets/Reflex/Resolvers/ScopedTypeResolver.cs.meta

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)