diff --git a/Assets/Reflex.Tests/CallbackAssertion.cs b/Assets/Reflex.Tests/CallbackAssertion.cs index 56fd4cc2..cf35c0cc 100644 --- a/Assets/Reflex.Tests/CallbackAssertion.cs +++ b/Assets/Reflex.Tests/CallbackAssertion.cs @@ -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() diff --git a/Assets/Reflex.Tests/ContainerDescriptorTests.cs b/Assets/Reflex.Tests/ContainerDescriptorTests.cs index e8f67f1b..5f7e5428 100644 --- a/Assets/Reflex.Tests/ContainerDescriptorTests.cs +++ b/Assets/Reflex.Tests/ContainerDescriptorTests.cs @@ -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(); + } + + [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() { @@ -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(); } - + [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(); } [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(); } diff --git a/Assets/Reflex.Tests/ContainerTests.cs b/Assets/Reflex.Tests/ContainerTests.cs index 01c02ae6..d34dd77e 100644 --- a/Assets/Reflex.Tests/ContainerTests.cs +++ b/Assets/Reflex.Tests/ContainerTests.cs @@ -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)) @@ -71,6 +71,27 @@ public void Resolve_AsSingleton_ShouldReturnAlwaysSameInstance() container.Single().Value = 123; container.Single().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().Should().Be("Hello World!"); + container.Single().Should().Be("Hello World!"); + container.Single().Should().Be("Hello World!"); + callbackAssertion.ShouldHaveBeenCalledOnce(); + } [Test] public void Resolve_UnknownDependency_ShouldThrowUnknownContractException() @@ -237,7 +258,7 @@ public void Start() } [Test] - public void NonStartableSingleton_ShouldNotBeConstructedAfterContainerBuild() + public void NonStartableSingletonFromType_ShouldNotBeConstructedAfterContainerBuild() { var callbackAssertion = new CallbackAssertion(); StartableSingleton.OnConstructed = callbackAssertion; @@ -250,7 +271,7 @@ public void NonStartableSingleton_ShouldNotBeConstructedAfterContainerBuild() } [Test] - public void StartableSingleton_ShouldBeConstructedAfterContainerBuild() + public void StartableSingletonFromType_ShouldBeConstructedAfterContainerBuild() { var callbackAssertion = new CallbackAssertion(); StartableSingleton.OnConstructed = callbackAssertion; @@ -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)) @@ -272,6 +293,57 @@ public void StartableSingleton_ShouldBeStartedAfterContainerBuild() var startable = container.Single().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().WasStarted.Should().BeTrue(); + } + [Test] public void All_OnParentShouldNotBeAffectedByScoped() { diff --git a/Assets/Reflex.Tests/DisposeTests.cs b/Assets/Reflex.Tests/DisposeTests.cs index 2fcd6aa4..b114bd11 100644 --- a/Assets/Reflex.Tests/DisposeTests.cs +++ b/Assets/Reflex.Tests/DisposeTests.cs @@ -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)) @@ -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(); 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(); container.Dispose(); service.Disposed.Should().Be(1); } diff --git a/Assets/Reflex/Core/ContainerDescriptor.cs b/Assets/Reflex/Core/ContainerDescriptor.cs index 4b6baee3..d30424bc 100644 --- a/Assets/Reflex/Core/ContainerDescriptor.cs +++ b/Assets/Reflex/Core/ContainerDescriptor.cs @@ -62,6 +62,22 @@ public ContainerDescriptor AddSingleton(object instance) return AddSingleton(instance, instance.GetType()); } + public ContainerDescriptor AddSingleton(Func 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(Func factory) + { + return AddSingleton(factory, typeof(T)); + } + public ContainerDescriptor AddTransient(Type concrete, params Type[] contracts) { return Add(concrete, contracts, new TransientTypeResolver(concrete)); diff --git a/Assets/Reflex/Resolvers/SingletonFactoryResolver.cs b/Assets/Reflex/Resolvers/SingletonFactoryResolver.cs new file mode 100644 index 00000000..dc29bfd9 --- /dev/null +++ b/Assets/Reflex/Resolvers/SingletonFactoryResolver.cs @@ -0,0 +1,31 @@ +using System; +using Reflex.Core; + +namespace Reflex.Resolvers +{ + internal sealed class SingletonFactoryResolver : Resolver + { + private readonly Func _factory; + private object _instance; + + public SingletonFactoryResolver(Func 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; + } + } +} \ No newline at end of file diff --git a/Assets/Reflex/Resolvers/SingletonFactoryResolver.cs.meta b/Assets/Reflex/Resolvers/SingletonFactoryResolver.cs.meta new file mode 100644 index 00000000..7d613f1b --- /dev/null +++ b/Assets/Reflex/Resolvers/SingletonFactoryResolver.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 897d5a72dbd44c3f87886aba13bcf838 +timeCreated: 1701434900 \ No newline at end of file diff --git a/README.md b/README.md index 78add2bc..c196fa29 100644 --- a/README.md +++ b/README.md @@ -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`, `Resolve` or `All` then yes, you have to specify it. +### AddSingleton (From Factory) +```csharp +ContainerDescriptor::AddSingleton(Func 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`, `Resolve` or `All` then yes, you have to specify it. + ### AddTransient (From Type) ```csharp ContainerDescriptor::AddTransient(Type concrete, params Type[] contracts)