Skip to content

Commit

Permalink
Adds AddSingleton overload that accepts a factory
Browse files Browse the repository at this point in the history
  • Loading branch information
gustavopsantos committed Dec 1, 2023
1 parent df59529 commit b8ae8ce
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 24 deletions.
7 changes: 3 additions & 4 deletions Assets/Reflex.Tests/CallbackAssertion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,15 @@
public class CallbackAssertion
{
private int _calls;
private readonly Action _call;

public CallbackAssertion()
public void Invoke()
{
_call = () => _calls++;
_calls++;
}

public static implicit operator Action(CallbackAssertion callbackAssertion)
{
return callbackAssertion._call;
return callbackAssertion.Invoke;
}

public void ShouldHaveBeenCalledOnce()
Expand Down
44 changes: 35 additions & 9 deletions Assets/Reflex.Tests/ContainerDescriptorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,22 @@ private class Valuable : IValuable
public int Value { get; set; }
}

[Test]
public void AddSingletonFromType_ValuableWithIDisposableAsContract_ShouldThrow()
{
var builder = new ContainerDescriptor("");
Action addSingleton = () => builder.AddSingleton(typeof(Valuable), typeof(IDisposable));
addSingleton.Should().ThrowExactly<ContractDefinitionException>();
}

[Test]
public void AddSingletonFromType_ValuableWithObjectAndValuableAndIValuableAsContract_ShouldNotThrow()
{
var builder = new ContainerDescriptor("");
Action addSingleton = () => builder.AddSingleton(typeof(Valuable), typeof(object), typeof(Valuable), typeof(IValuable));
addSingleton.Should().NotThrow();
}

[Test]
public void AddSingletonFromValue_ValuableWithIDisposableAsContract_ShouldThrow()
{
Expand All @@ -36,34 +52,44 @@ public void AddSingletonFromValue_ValuableWithObjectAndValuableAndIValuableAsCon
}

[Test]
public void AddTransient_ValuableWithIDisposableAsContract_ShouldThrow()
public void AddSingletonFromFactory_ValuableWithIDisposableAsContract_ShouldThrow()
{
Valuable Factory(Container container)
{
return new Valuable();
}

var builder = new ContainerDescriptor("");
Action addSingleton = () => builder.AddTransient(typeof(Valuable), typeof(IDisposable));
Action addSingleton = () => builder.AddSingleton(Factory, typeof(IDisposable));
addSingleton.Should().ThrowExactly<ContractDefinitionException>();
}

[Test]
public void AddTransient_ValuableWithObjectAndValuableAndIValuableAsContract_ShouldNotThrow()
public void AddSingletonFromFactory_ValuableWithObjectAndValuableAndIValuableAsContract_ShouldNotThrow()
{
Valuable Factory(Container container)
{
return new Valuable();
}

var builder = new ContainerDescriptor("");
Action addSingleton = () => builder.AddTransient(typeof(Valuable), typeof(object), typeof(Valuable), typeof(IValuable));
Action addSingleton = () => builder.AddSingleton(Factory, typeof(object), typeof(Valuable), typeof(IValuable));
addSingleton.Should().NotThrow();
}

[Test]
public void AddSingleton_ValuableWithIDisposableAsContract_ShouldThrow()
public void AddTransient_ValuableWithIDisposableAsContract_ShouldThrow()
{
var builder = new ContainerDescriptor("");
Action addSingleton = () => builder.AddSingleton(typeof(Valuable), typeof(IDisposable));
Action addSingleton = () => builder.AddTransient(typeof(Valuable), typeof(IDisposable));
addSingleton.Should().ThrowExactly<ContractDefinitionException>();
}

[Test]
public void AddSingleton_ValuableWithObjectAndValuableAndIValuableAsContract_ShouldNotThrow()
public void AddTransient_ValuableWithObjectAndValuableAndIValuableAsContract_ShouldNotThrow()
{
var builder = new ContainerDescriptor("");
Action addSingleton = () => builder.AddSingleton(typeof(Valuable), typeof(object), typeof(Valuable), typeof(IValuable));
Action addSingleton = () => builder.AddTransient(typeof(Valuable), typeof(object), typeof(Valuable), typeof(IValuable));
addSingleton.Should().NotThrow();
}

Expand Down
80 changes: 76 additions & 4 deletions Assets/Reflex.Tests/ContainerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public void Resolve_AsTransient_ShouldReturnAlwaysANewInstance()
}

[Test]
public void Resolve_AsSingleton_ShouldReturnAlwaysSameInstance()
public void Resolve_AsSingletonFromType_ShouldReturnAlwaysSameInstance()
{
var container = new ContainerDescriptor("")
.AddSingleton(typeof(Valuable), typeof(IValuable))
Expand All @@ -71,6 +71,27 @@ public void Resolve_AsSingleton_ShouldReturnAlwaysSameInstance()
container.Single<IValuable>().Value = 123;
container.Single<IValuable>().Value.Should().Be(123);
}

[Test]
public void Resolve_AsSingletonFromFactory_ShouldRunFactoryOnce()
{
var callbackAssertion = new CallbackAssertion();

string Factory(Container container)
{
callbackAssertion.Invoke();
return "Hello World!";
}

var container = new ContainerDescriptor("")
.AddSingleton(Factory)
.Build();

container.Single<string>().Should().Be("Hello World!");
container.Single<string>().Should().Be("Hello World!");
container.Single<string>().Should().Be("Hello World!");
callbackAssertion.ShouldHaveBeenCalledOnce();
}

[Test]
public void Resolve_UnknownDependency_ShouldThrowUnknownContractException()
Expand Down Expand Up @@ -237,7 +258,7 @@ public void Start()
}

[Test]
public void NonStartableSingleton_ShouldNotBeConstructedAfterContainerBuild()
public void NonStartableSingletonFromType_ShouldNotBeConstructedAfterContainerBuild()
{
var callbackAssertion = new CallbackAssertion();
StartableSingleton.OnConstructed = callbackAssertion;
Expand All @@ -250,7 +271,7 @@ public void NonStartableSingleton_ShouldNotBeConstructedAfterContainerBuild()
}

[Test]
public void StartableSingleton_ShouldBeConstructedAfterContainerBuild()
public void StartableSingletonFromType_ShouldBeConstructedAfterContainerBuild()
{
var callbackAssertion = new CallbackAssertion();
StartableSingleton.OnConstructed = callbackAssertion;
Expand All @@ -263,7 +284,7 @@ public void StartableSingleton_ShouldBeConstructedAfterContainerBuild()
}

[Test]
public void StartableSingleton_ShouldBeStartedAfterContainerBuild()
public void StartableSingletonFromType_ShouldBeStartedAfterContainerBuild()
{
var container = new ContainerDescriptor("")
.AddSingleton(typeof(StartableSingleton), typeof(StartableSingleton), typeof(IStartable))
Expand All @@ -272,6 +293,57 @@ public void StartableSingleton_ShouldBeStartedAfterContainerBuild()
var startable = container.Single<StartableSingleton>().WasStarted.Should().BeTrue();
}

[Test]
public void NonStartableSingletonFromFactory_ShouldNotBeConstructedAfterContainerBuild()
{
StartableSingleton Factory(Container container)
{
return new StartableSingleton();
}

var callbackAssertion = new CallbackAssertion();
StartableSingleton.OnConstructed = callbackAssertion;

var container = new ContainerDescriptor("")
.AddSingleton(Factory)
.Build();

callbackAssertion.ShouldNotHaveBeenCalled();
}

[Test]
public void StartableSingletonFromFactory_ShouldBeConstructedAfterContainerBuild()
{
StartableSingleton Factory(Container container)
{
return new StartableSingleton();
}

var callbackAssertion = new CallbackAssertion();
StartableSingleton.OnConstructed = callbackAssertion;

var container = new ContainerDescriptor("")
.AddSingleton(Factory, typeof(IStartable))
.Build();

callbackAssertion.ShouldHaveBeenCalledOnce();
}

[Test]
public void StartableSingletonFromFactory_ShouldBeStartedAfterContainerBuild()
{
StartableSingleton Factory(Container container)
{
return new StartableSingleton();
}

var container = new ContainerDescriptor("")
.AddSingleton(Factory, typeof(StartableSingleton), typeof(IStartable))
.Build();

var startable = container.Single<StartableSingleton>().WasStarted.Should().BeTrue();
}

[Test]
public void All_OnParentShouldNotBeAffectedByScoped()
{
Expand Down
31 changes: 24 additions & 7 deletions Assets/Reflex.Tests/DisposeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public void Dispose()
}

[Test]
public void Singleton_ShouldBeDisposed_WhenOwnerIsDisposed()
public void SingletonFromType_ShouldBeDisposed_WhenOwnerIsDisposed()
{
var container = new ContainerDescriptor("")
.AddSingleton(typeof(Service), typeof(Service))
Expand All @@ -28,27 +28,44 @@ public void Singleton_ShouldBeDisposed_WhenOwnerIsDisposed()
container.Dispose();
service.Disposed.Should().Be(1);
}

[Test]
public void Transient_ShouldBeDisposed_WhenOwnerIsDisposed()
public void SingletonFromValue_ShouldBeDisposed_WhenOwnerIsDisposed()
{
var service = new Service();
var container = new ContainerDescriptor("")
.AddTransient(typeof(Service), typeof(Service))
.AddSingleton(service, typeof(Service))
.Build();

container.Dispose();
service.Disposed.Should().Be(1);
}

[Test]
public void SingletonFromFactory_ShouldBeDisposed_WhenOwnerIsDisposed()
{
Service Factory(Container container)
{
return new Service();
}

var container = new ContainerDescriptor("")
.AddSingleton(Factory, typeof(Service))
.Build();

var service = container.Single<Service>();
container.Dispose();
service.Disposed.Should().Be(1);
}

[Test]
public void SingletonFromValue_ShouldBeDisposed_WhenOwnerIsDisposed()
public void Transient_ShouldBeDisposed_WhenOwnerIsDisposed()
{
var service = new Service();
var container = new ContainerDescriptor("")
.AddSingleton(service, typeof(Service))
.AddTransient(typeof(Service), typeof(Service))
.Build();

var service = container.Single<Service>();
container.Dispose();
service.Disposed.Should().Be(1);
}
Expand Down
16 changes: 16 additions & 0 deletions Assets/Reflex/Core/ContainerDescriptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,22 @@ public ContainerDescriptor AddSingleton(object instance)
return AddSingleton(instance, instance.GetType());
}

public ContainerDescriptor AddSingleton<T>(Func<Container, T> factory, params Type[] contracts)
{
var resolver = new SingletonFactoryResolver(Proxy, typeof(T));
return Add(typeof(T), contracts, resolver);

object Proxy(Container container)
{
return factory.Invoke(container);
}
}

public ContainerDescriptor AddSingleton<T>(Func<Container, T> factory)
{
return AddSingleton(factory, typeof(T));
}

public ContainerDescriptor AddTransient(Type concrete, params Type[] contracts)
{
return Add(concrete, contracts, new TransientTypeResolver(concrete));
Expand Down
31 changes: 31 additions & 0 deletions Assets/Reflex/Resolvers/SingletonFactoryResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System;
using Reflex.Core;

namespace Reflex.Resolvers
{
internal sealed class SingletonFactoryResolver : Resolver
{
private readonly Func<Container, object> _factory;
private object _instance;

public SingletonFactoryResolver(Func<Container, object> factory, Type concrete)
{
RegisterCallSite();
_factory = factory;
Concrete = concrete;
}

public override object Resolve(Container container)
{
IncrementResolutions();

if (_instance == null)
{
_instance = _factory.Invoke(container);
Disposables.TryAdd(_instance);
}

return _instance;
}
}
}
3 changes: 3 additions & 0 deletions Assets/Reflex/Resolvers/SingletonFactoryResolver.cs.meta

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

11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,17 @@ Adds an object already contructed by the user to the container as a singleton, e
If object implements `IDisposable` it will be disposed when its parent Container are disposed.
Theres no need to pass `IDisposable` as contract to have your object disposed, howerver, if you want to retrieve all `IDisposable` by any API `Single<TContract>`, `Resolve<TContract>` or `All<TContract>` then yes, you have to specify it.

### AddSingleton (From Factory)
```csharp
ContainerDescriptor::AddSingleton<T>(Func<Container, T> factory, params Type[] contracts)
```
Adds a defered object creation based on the given factory and its contracts.
The object will be constructed lazyli, once first request to resolve any of its contracts is called.
The factory will be ran once, and then the **same** object will always be returned.
If you want your singleton to be constructed just after container build (non-lazyli), add `typeof(IStartable)` as one of your contracts.
If object implements `IDisposable` it will be disposed when its parent Container are disposed.
Theres no need to pass `IDisposable` as contract to have your object disposed, howerver, if you want to retrieve all `IDisposable` by any API `Single<TContract>`, `Resolve<TContract>` or `All<TContract>` then yes, you have to specify it.

### AddTransient (From Type)
```csharp
ContainerDescriptor::AddTransient(Type concrete, params Type[] contracts)
Expand Down

0 comments on commit b8ae8ce

Please sign in to comment.