From d3a1afe626252a6307d4d6b03b29aab5e1f07a35 Mon Sep 17 00:00:00 2001 From: Gustavo Santos Date: Thu, 29 Aug 2024 09:12:27 -0300 Subject: [PATCH] Add scoped api --- .../GarbageCollectionTests.cs | 7 + Assets/Reflex.EditModeTests/ScopedTests.cs | 144 ++++++++++++++++++ .../Reflex.EditModeTests/ScopedTests.cs.meta | 3 + Assets/Reflex.EditModeTests/TransientTests.cs | 4 + Assets/Reflex/Core/Container.cs | 1 - Assets/Reflex/Core/ContainerBuilder.cs | 28 ++++ Assets/Reflex/Enums/Lifetime.cs | 1 + .../Reflex/Resolvers/ScopedFactoryResolver.cs | 39 +++++ .../Resolvers/ScopedFactoryResolver.cs.meta | 3 + Assets/Reflex/Resolvers/ScopedTypeResolver.cs | 39 +++++ .../Resolvers/ScopedTypeResolver.cs.meta | 3 + 11 files changed, 271 insertions(+), 1 deletion(-) create mode 100644 Assets/Reflex.EditModeTests/ScopedTests.cs create mode 100644 Assets/Reflex.EditModeTests/ScopedTests.cs.meta create mode 100644 Assets/Reflex/Resolvers/ScopedFactoryResolver.cs create mode 100644 Assets/Reflex/Resolvers/ScopedFactoryResolver.cs.meta create mode 100644 Assets/Reflex/Resolvers/ScopedTypeResolver.cs create mode 100644 Assets/Reflex/Resolvers/ScopedTypeResolver.cs.meta diff --git a/Assets/Reflex.EditModeTests/GarbageCollectionTests.cs b/Assets/Reflex.EditModeTests/GarbageCollectionTests.cs index ce3b799b..d0a48956 100644 --- a/Assets/Reflex.EditModeTests/GarbageCollectionTests.cs +++ b/Assets/Reflex.EditModeTests/GarbageCollectionTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using FluentAssertions; using NUnit.Framework; @@ -21,6 +22,12 @@ public static void ForceGarbageCollection() GC.WaitForPendingFinalizers(); } + [Conditional("REFLEX_DEBUG")] + public static void MarkAsInconclusiveWhenReflexDebugIsEnabled() + { + Assert.Inconclusive("Disable REFLEX_DEBUG symbol when running garbage collection tests!"); + } + [Test, Retry(3)] public void Singleton_ShouldBeFinalized_WhenOwnerIsDisposed() { diff --git a/Assets/Reflex.EditModeTests/ScopedTests.cs b/Assets/Reflex.EditModeTests/ScopedTests.cs new file mode 100644 index 00000000..62d242d7 --- /dev/null +++ b/Assets/Reflex.EditModeTests/ScopedTests.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using FluentAssertions; +using NUnit.Framework; +using Reflex.Core; +using UnityEditor; + +namespace Reflex.EditModeTests +{ + public class ScopedTests + { + private class Service : IDisposable + { + public bool IsDisposed { get; private set; } + + public void Dispose() + { + IsDisposed = true; + } + } + + [Test] + public void ScopedFromType_ShouldReturnAlwaysSameInstance_WhenCalledFromSameContainer() + { + var parentContainer = new ContainerBuilder().AddScoped(typeof(Service)).Build(); + var childContainer = parentContainer.Scope(); + parentContainer.Resolve().Should().Be(parentContainer.Resolve()); + childContainer.Resolve().Should().Be(childContainer.Resolve()); + } + + [Test] + public void ScopedFromFactory_ShouldReturnAlwaysSameInstance_WhenCalledFromSameContainer() + { + var parentContainer = new ContainerBuilder().AddScoped(_ => new Service()).Build(); + var childContainer = parentContainer.Scope(); + parentContainer.Resolve().Should().Be(parentContainer.Resolve()); + childContainer.Resolve().Should().Be(childContainer.Resolve()); + } + + [Test] + public void ScopedFromType_NewInstanceShouldBeConstructed_ForEveryNewContainer() + { + var instances = new HashSet(); + var parentContainer = new ContainerBuilder().AddScoped(typeof(Service)).Build(); + var childContainer = parentContainer.Scope(); + instances.Add(parentContainer.Resolve()); + instances.Add(childContainer.Resolve()); + instances.Add(parentContainer.Resolve()); + instances.Add(childContainer.Resolve()); + instances.Count.Should().Be(2); + } + + [Test] + public void ScopedFromFactory_NewInstanceShouldBeConstructed_ForEveryNewContainer() + { + var instances = new HashSet(); + var parentContainer = new ContainerBuilder().AddScoped(_ => new Service()).Build(); + var childContainer = parentContainer.Scope(); + instances.Add(parentContainer.Resolve()); + instances.Add(childContainer.Resolve()); + instances.Add(parentContainer.Resolve()); + instances.Add(childContainer.Resolve()); + instances.Count.Should().Be(2); + } + + [Test] + public void ScopedFromType_ConstructedInstances_ShouldBeDisposed_WithinConstructingContainer() + { + var parentContainer = new ContainerBuilder().AddScoped(typeof(Service)).Build(); + var childContainer = parentContainer.Scope(); + + var instanceConstructedByChild = childContainer.Resolve(); + var instanceConstructedByParent = parentContainer.Resolve(); + + childContainer.Dispose(); + + instanceConstructedByChild.IsDisposed.Should().BeTrue(); + instanceConstructedByParent.IsDisposed.Should().BeFalse(); + } + + [Test] + public void ScopedFromFactory_ConstructedInstances_ShouldBeDisposed_WithinConstructingContainer() + { + var parentContainer = new ContainerBuilder().AddScoped(_ => new Service()).Build(); + var childContainer = parentContainer.Scope(); + + var instanceConstructedByChild = childContainer.Resolve(); + var instanceConstructedByParent = parentContainer.Resolve(); + + childContainer.Dispose(); + + instanceConstructedByChild.IsDisposed.Should().BeTrue(); + instanceConstructedByParent.IsDisposed.Should().BeFalse(); + } + + [Test, Retry(3)] + public void ScopedFromType_ConstructedInstances_ShouldBeCollected_WhenConstructingContainerIsDisposed() + { + GarbageCollectionTests.MarkAsInconclusiveWhenReflexDebugIsEnabled(); + + WeakReference instanceConstructedByChild; + WeakReference instanceConstructedByParent; + var parentContainer = new ContainerBuilder().AddScoped(typeof(Service)).Build(); + + void Act() + { + using (var childContainer = parentContainer.Scope()) + { + instanceConstructedByChild = new WeakReference(childContainer.Resolve()); + instanceConstructedByParent = new WeakReference(parentContainer.Resolve()); + } + } + + Act(); + GarbageCollectionTests.ForceGarbageCollection(); + instanceConstructedByChild.IsAlive.Should().BeFalse(); + instanceConstructedByParent.IsAlive.Should().BeTrue(); + } + + [Test, Retry(3)] + public void ScopedFromFactory_ConstructedInstances_ShouldBeCollected_WhenConstructingContainerIsDisposed() + { + GarbageCollectionTests.MarkAsInconclusiveWhenReflexDebugIsEnabled(); + + WeakReference instanceConstructedByChild; + WeakReference instanceConstructedByParent; + var parentContainer = new ContainerBuilder().AddScoped(_ => new Service()).Build(); + + void Act() + { + using (var childContainer = parentContainer.Scope()) + { + instanceConstructedByChild = new WeakReference(childContainer.Resolve()); + instanceConstructedByParent = new WeakReference(parentContainer.Resolve()); + } + } + + Act(); + GarbageCollectionTests.ForceGarbageCollection(); + instanceConstructedByChild.IsAlive.Should().BeFalse(); + instanceConstructedByParent.IsAlive.Should().BeTrue(); + } + } +} \ No newline at end of file diff --git a/Assets/Reflex.EditModeTests/ScopedTests.cs.meta b/Assets/Reflex.EditModeTests/ScopedTests.cs.meta new file mode 100644 index 00000000..2b9eece8 --- /dev/null +++ b/Assets/Reflex.EditModeTests/ScopedTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 144cc2ca764c4174bfc325ab7ca20517 +timeCreated: 1724861052 \ No newline at end of file diff --git a/Assets/Reflex.EditModeTests/TransientTests.cs b/Assets/Reflex.EditModeTests/TransientTests.cs index b1798482..07a5fb8f 100644 --- a/Assets/Reflex.EditModeTests/TransientTests.cs +++ b/Assets/Reflex.EditModeTests/TransientTests.cs @@ -50,6 +50,8 @@ public void TransientFromFactory_ConstructedInstances_ShouldBeDisposed_WithinCon [Test, Retry(3)] public void TransientFromType_ConstructedInstances_ShouldBeCollected_WhenConstructingContainerIsDisposed() { + GarbageCollectionTests.MarkAsInconclusiveWhenReflexDebugIsEnabled(); + WeakReference instanceConstructedByChild; WeakReference instanceConstructedByParent; var parentContainer = new ContainerBuilder().AddTransient(typeof(Service)).Build(); @@ -72,6 +74,8 @@ void Act() [Test, Retry(3)] public void TransientFromFactory_ConstructedInstances_ShouldBeCollected_WhenConstructingContainerIsDisposed() { + GarbageCollectionTests.MarkAsInconclusiveWhenReflexDebugIsEnabled(); + WeakReference instanceConstructedByChild; WeakReference instanceConstructedByParent; var parentContainer = new ContainerBuilder().AddTransient(c => new Service()).Build(); diff --git a/Assets/Reflex/Core/Container.cs b/Assets/Reflex/Core/Container.cs index c47c0371..9a6c3f52 100644 --- a/Assets/Reflex/Core/Container.cs +++ b/Assets/Reflex/Core/Container.cs @@ -12,7 +12,6 @@ namespace Reflex.Core { public sealed class Container : IDisposable { - public string Name { get; } internal Container Parent { get; } internal List Children { get; } = new(); diff --git a/Assets/Reflex/Core/ContainerBuilder.cs b/Assets/Reflex/Core/ContainerBuilder.cs index 384ecf97..46b13e5c 100644 --- a/Assets/Reflex/Core/ContainerBuilder.cs +++ b/Assets/Reflex/Core/ContainerBuilder.cs @@ -117,6 +117,34 @@ public ContainerBuilder AddTransient(Func factory) { return AddTransient(factory, typeof(T)); } + + // Scoped + + public ContainerBuilder AddScoped(Type concrete, params Type[] contracts) + { + return Add(concrete, contracts, new ScopedTypeResolver(concrete)); + } + + public ContainerBuilder AddScoped(Type concrete) + { + return AddScoped(concrete, concrete); + } + + public ContainerBuilder AddScoped(Func factory, params Type[] contracts) + { + var resolver = new ScopedFactoryResolver(Proxy); + return Add(typeof(T), contracts, resolver); + + object Proxy(Container container) + { + return factory.Invoke(container); + } + } + + public ContainerBuilder AddScoped(Func factory) + { + return AddScoped(factory, typeof(T)); + } public bool HasBinding(Type type) { diff --git a/Assets/Reflex/Enums/Lifetime.cs b/Assets/Reflex/Enums/Lifetime.cs index 4bb1e29a..e25d302d 100644 --- a/Assets/Reflex/Enums/Lifetime.cs +++ b/Assets/Reflex/Enums/Lifetime.cs @@ -4,5 +4,6 @@ public enum Lifetime { Singleton, Transient, + Scoped, } } \ No newline at end of file diff --git a/Assets/Reflex/Resolvers/ScopedFactoryResolver.cs b/Assets/Reflex/Resolvers/ScopedFactoryResolver.cs new file mode 100644 index 00000000..8d97ead8 --- /dev/null +++ b/Assets/Reflex/Resolvers/ScopedFactoryResolver.cs @@ -0,0 +1,39 @@ +using System; +using System.Runtime.CompilerServices; +using Reflex.Core; +using Reflex.Enums; + +namespace Reflex.Resolvers +{ + internal sealed class ScopedFactoryResolver : IResolver + { + private readonly Func _factory; + private readonly ConditionalWeakTable _instances = new(); + public Lifetime Lifetime => Lifetime.Scoped; + + public ScopedFactoryResolver(Func factory) + { + Diagnosis.RegisterCallSite(this); + _factory = factory; + } + + public object Resolve(Container container) + { + Diagnosis.IncrementResolutions(this); + + if (!_instances.TryGetValue(container, out var instance)) + { + instance = _factory.Invoke(container); + _instances.Add(container, instance); + container.Disposables.TryAdd(instance); + Diagnosis.RegisterInstance(this, instance); + } + + return instance; + } + + public void Dispose() + { + } + } +} \ No newline at end of file diff --git a/Assets/Reflex/Resolvers/ScopedFactoryResolver.cs.meta b/Assets/Reflex/Resolvers/ScopedFactoryResolver.cs.meta new file mode 100644 index 00000000..72d6fd3f --- /dev/null +++ b/Assets/Reflex/Resolvers/ScopedFactoryResolver.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 4bffd02047644dd6ba47502e39df1a00 +timeCreated: 1724860551 \ No newline at end of file diff --git a/Assets/Reflex/Resolvers/ScopedTypeResolver.cs b/Assets/Reflex/Resolvers/ScopedTypeResolver.cs new file mode 100644 index 00000000..1c8c5144 --- /dev/null +++ b/Assets/Reflex/Resolvers/ScopedTypeResolver.cs @@ -0,0 +1,39 @@ +using System; +using System.Runtime.CompilerServices; +using Reflex.Core; +using Reflex.Enums; + +namespace Reflex.Resolvers +{ + internal sealed class ScopedTypeResolver : IResolver + { + private readonly Type _concreteType; + private readonly ConditionalWeakTable _instances = new(); + public Lifetime Lifetime => Lifetime.Scoped; + + public ScopedTypeResolver(Type concreteType) + { + Diagnosis.RegisterCallSite(this); + _concreteType = concreteType; + } + + public object Resolve(Container container) + { + Diagnosis.IncrementResolutions(this); + + if (!_instances.TryGetValue(container, out var instance)) + { + instance = container.Construct(_concreteType); + _instances.Add(container, instance); + container.Disposables.TryAdd(instance); + Diagnosis.RegisterInstance(this, instance); + } + + return instance; + } + + public void Dispose() + { + } + } +} \ No newline at end of file diff --git a/Assets/Reflex/Resolvers/ScopedTypeResolver.cs.meta b/Assets/Reflex/Resolvers/ScopedTypeResolver.cs.meta new file mode 100644 index 00000000..e31e7e76 --- /dev/null +++ b/Assets/Reflex/Resolvers/ScopedTypeResolver.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f8c31485169f4016bbc2f949fb67b825 +timeCreated: 1724860542 \ No newline at end of file