Skip to content

Commit

Permalink
Add scoped api
Browse files Browse the repository at this point in the history
  • Loading branch information
gustavopsantos committed Aug 29, 2024
1 parent ad0c6ac commit d3a1afe
Show file tree
Hide file tree
Showing 11 changed files with 271 additions and 1 deletion.
7 changes: 7 additions & 0 deletions Assets/Reflex.EditModeTests/GarbageCollectionTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using FluentAssertions;
using NUnit.Framework;
Expand All @@ -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()
{
Expand Down
144 changes: 144 additions & 0 deletions Assets/Reflex.EditModeTests/ScopedTests.cs
Original file line number Diff line number Diff line change
@@ -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<Service>().Should().Be(parentContainer.Resolve<Service>());
childContainer.Resolve<Service>().Should().Be(childContainer.Resolve<Service>());
}

[Test]
public void ScopedFromFactory_ShouldReturnAlwaysSameInstance_WhenCalledFromSameContainer()
{
var parentContainer = new ContainerBuilder().AddScoped(_ => new Service()).Build();
var childContainer = parentContainer.Scope();
parentContainer.Resolve<Service>().Should().Be(parentContainer.Resolve<Service>());
childContainer.Resolve<Service>().Should().Be(childContainer.Resolve<Service>());
}

[Test]
public void ScopedFromType_NewInstanceShouldBeConstructed_ForEveryNewContainer()
{
var instances = new HashSet<Service>();
var parentContainer = new ContainerBuilder().AddScoped(typeof(Service)).Build();
var childContainer = parentContainer.Scope();
instances.Add(parentContainer.Resolve<Service>());
instances.Add(childContainer.Resolve<Service>());
instances.Add(parentContainer.Resolve<Service>());
instances.Add(childContainer.Resolve<Service>());
instances.Count.Should().Be(2);
}

[Test]
public void ScopedFromFactory_NewInstanceShouldBeConstructed_ForEveryNewContainer()
{
var instances = new HashSet<Service>();
var parentContainer = new ContainerBuilder().AddScoped(_ => new Service()).Build();
var childContainer = parentContainer.Scope();
instances.Add(parentContainer.Resolve<Service>());
instances.Add(childContainer.Resolve<Service>());
instances.Add(parentContainer.Resolve<Service>());
instances.Add(childContainer.Resolve<Service>());
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<Service>();
var instanceConstructedByParent = parentContainer.Resolve<Service>();

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<Service>();
var instanceConstructedByParent = parentContainer.Resolve<Service>();

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<Service>());
instanceConstructedByParent = new WeakReference(parentContainer.Resolve<Service>());
}
}

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<Service>());
instanceConstructedByParent = new WeakReference(parentContainer.Resolve<Service>());
}
}

Act();
GarbageCollectionTests.ForceGarbageCollection();
instanceConstructedByChild.IsAlive.Should().BeFalse();
instanceConstructedByParent.IsAlive.Should().BeTrue();
}
}
}
3 changes: 3 additions & 0 deletions Assets/Reflex.EditModeTests/ScopedTests.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Assets/Reflex.EditModeTests/TransientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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();
Expand Down
1 change: 0 additions & 1 deletion Assets/Reflex/Core/Container.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ namespace Reflex.Core
{
public sealed class Container : IDisposable
{

public string Name { get; }
internal Container Parent { get; }
internal List<Container> Children { get; } = new();
Expand Down
28 changes: 28 additions & 0 deletions Assets/Reflex/Core/ContainerBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,34 @@ public ContainerBuilder AddTransient<T>(Func<Container, T> 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<T>(Func<Container, T> 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<T>(Func<Container, T> factory)
{
return AddScoped(factory, typeof(T));
}

public bool HasBinding(Type type)
{
Expand Down
1 change: 1 addition & 0 deletions Assets/Reflex/Enums/Lifetime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ public enum Lifetime
{
Singleton,
Transient,
Scoped,
}
}
39 changes: 39 additions & 0 deletions Assets/Reflex/Resolvers/ScopedFactoryResolver.cs
Original file line number Diff line number Diff line change
@@ -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<Container, object> _factory;
private readonly ConditionalWeakTable<Container, object> _instances = new();
public Lifetime Lifetime => Lifetime.Scoped;

public ScopedFactoryResolver(Func<Container, object> 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()
{
}
}
}
3 changes: 3 additions & 0 deletions Assets/Reflex/Resolvers/ScopedFactoryResolver.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 39 additions & 0 deletions Assets/Reflex/Resolvers/ScopedTypeResolver.cs
Original file line number Diff line number Diff line change
@@ -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<Container, object> _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()
{
}
}
}
3 changes: 3 additions & 0 deletions Assets/Reflex/Resolvers/ScopedTypeResolver.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit d3a1afe

Please sign in to comment.