diff --git a/My project/Assets/FlowScope/Runtime.meta b/My project/Assets/FlowScope/Runtime.meta new file mode 100644 index 0000000..42907a3 --- /dev/null +++ b/My project/Assets/FlowScope/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: eab40aadc4ea4632930155f91c6faca4 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/My project/Assets/FlowScope/Runtime/Container/Container.cs b/My project/Assets/FlowScope/Runtime/Container/Container.cs index e00aa7d..c648e25 100644 --- a/My project/Assets/FlowScope/Runtime/Container/Container.cs +++ b/My project/Assets/FlowScope/Runtime/Container/Container.cs @@ -1,21 +1,261 @@ using System; +using System.Collections.Generic; +using System.Linq; namespace FlowScope.Container { public sealed class Container : IDisposable { - public void RegisterInstance(T instance) => throw new NotImplementedException(); - public void RegisterFactory(Func factory) => throw new NotImplementedException(); - public void RegisterType() where TImplementation : TInterface => throw new NotImplementedException(); - public void RegisterType() => throw new NotImplementedException(); - public void RegisterAssembly() => throw new NotImplementedException(); - public T Resolve() => throw new NotImplementedException(); + [ThreadStatic] + private static Stack _resolutionStack; + + private readonly Container _parent; + private readonly Dictionary _registrations = new Dictionary(); + private readonly List _children = new List(); + private readonly List _ownedDisposables = new List(); + private bool _disposed; + + public Container() + { + } + + private Container(Container parent) + { + _parent = parent; + } + + public void RegisterInstance(T instance) + { + ThrowIfDisposed(); + + if (instance == null) + { + throw new ArgumentNullException(nameof(instance)); + } + + RegisterLocal(typeof(T), ContainerRegistration.ForInstance(instance)); + } + + public void RegisterFactory(Func factory) + { + ThrowIfDisposed(); + + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + RegisterLocal(typeof(T), ContainerRegistration.ForFactory(this, c => factory(c))); + } + + public void RegisterType() where TImplementation : TInterface + { + ThrowIfDisposed(); + + RegisterType(typeof(TInterface), typeof(TImplementation)); + } + + public void RegisterType() + { + ThrowIfDisposed(); + + RegisterType(typeof(TImplementation), typeof(TImplementation)); + } + + public void RegisterAssembly() + { + ThrowIfDisposed(); + + GeneratedFactories.RegisterAssembly(this); + } + + public T Resolve() + { + ThrowIfDisposed(); + + return (T)Resolve(typeof(T)); + } + public bool TryResolve(out T value) { - value = default; - throw new NotImplementedException(); + ThrowIfDisposed(); + + if (!TryFindRegistration(typeof(T), out var registration)) + { + value = default; + return false; + } + + value = (T)ResolveRegistration(typeof(T), registration); + return true; } - public Container CreateScope() => throw new NotImplementedException(); - public void Dispose() => throw new NotImplementedException(); + + public Container CreateScope() + { + ThrowIfDisposed(); + + var child = new Container(this); + _children.Add(child); + return child; + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + var exceptions = new List(); + + for (var i = _children.Count - 1; i >= 0; i--) + { + TryDispose(_children[i], exceptions); + } + + for (var i = _ownedDisposables.Count - 1; i >= 0; i--) + { + TryDispose(_ownedDisposables[i], exceptions); + } + + _children.Clear(); + _ownedDisposables.Clear(); + _registrations.Clear(); + + if (_parent != null) + { + _parent._children.Remove(this); + } + + if (exceptions.Count > 0) + { + throw new AggregateException("One or more container-owned instances failed to dispose.", exceptions); + } + } + + internal object Resolve(Type serviceType) + { + ThrowIfDisposed(); + + if (!TryFindRegistration(serviceType, out var registration)) + { + throw new InvalidOperationException($"No registration found for {FormatType(serviceType)}."); + } + + return ResolveRegistration(serviceType, registration); + } + + internal void RegisterFactory(Type serviceType, Func factory) + { + ThrowIfDisposed(); + + if (serviceType == null) + { + throw new ArgumentNullException(nameof(serviceType)); + } + + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + RegisterLocal(serviceType, ContainerRegistration.ForFactory(this, factory)); + } + + internal void TrackOwnedDisposable(IDisposable disposable) + { + _ownedDisposables.Add(disposable); + } + + private void RegisterType(Type serviceType, Type implementationType) + { + if (!serviceType.IsAssignableFrom(implementationType)) + { + throw new InvalidOperationException($"{FormatType(implementationType)} cannot be assigned to {FormatType(serviceType)}."); + } + + if (!GeneratedFactories.TryGetFactory(implementationType, out var factory)) + { + throw new InvalidOperationException( + $"No generated factory registered for {FormatType(implementationType)}. Use GeneratedFactories.Register, ReflectionFactoryBuilder.RegisterType, or RegisterFactory before resolving this type."); + } + + RegisterFactory(serviceType, factory); + } + + private void RegisterLocal(Type serviceType, ContainerRegistration registration) + { + if (_registrations.ContainsKey(serviceType)) + { + throw new InvalidOperationException($"{FormatType(serviceType)} is already registered in this container scope."); + } + + _registrations.Add(serviceType, registration); + } + + private object ResolveRegistration(Type serviceType, ContainerRegistration registration) + { + _resolutionStack = _resolutionStack ?? new Stack(); + + if (_resolutionStack.Contains(serviceType)) + { + var chain = _resolutionStack.Reverse() + .Concat(new[] { serviceType }) + .Select(t => t.Name); + throw new InvalidOperationException($"Circular dependency detected: {string.Join(" -> ", chain)}"); + } + + _resolutionStack.Push(serviceType); + try + { + return registration.Resolve(); + } + finally + { + _resolutionStack.Pop(); + if (_resolutionStack.Count == 0) + { + _resolutionStack = null; + } + } + } + + private bool TryFindRegistration(Type serviceType, out ContainerRegistration registration) + { + if (_registrations.TryGetValue(serviceType, out registration)) + { + return true; + } + + if (_parent != null) + { + return _parent.TryFindRegistration(serviceType, out registration); + } + + return false; + } + + private void ThrowIfDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(Container)); + } + } + + private static void TryDispose(IDisposable disposable, List exceptions) + { + try + { + disposable.Dispose(); + } + catch (Exception ex) + { + exceptions.Add(ex); + } + } + + private static string FormatType(Type type) => type.FullName ?? type.Name; } } diff --git a/My project/Assets/FlowScope/Runtime/Container/ContainerRegistration.cs b/My project/Assets/FlowScope/Runtime/Container/ContainerRegistration.cs new file mode 100644 index 0000000..0ad4df9 --- /dev/null +++ b/My project/Assets/FlowScope/Runtime/Container/ContainerRegistration.cs @@ -0,0 +1,50 @@ +using System; + +namespace FlowScope.Container +{ + internal sealed class ContainerRegistration + { + private readonly Container _owner; + private readonly Func _factory; + private readonly bool _ownsCreatedInstance; + private object _instance; + private bool _created; + + private ContainerRegistration(Container owner, Func factory, object instance, bool created, bool ownsCreatedInstance) + { + _owner = owner; + _factory = factory; + _instance = instance; + _created = created; + _ownsCreatedInstance = ownsCreatedInstance; + } + + public static ContainerRegistration ForInstance(object instance) + { + return new ContainerRegistration(null, null, instance, true, false); + } + + public static ContainerRegistration ForFactory(Container owner, Func factory) + { + return new ContainerRegistration(owner, factory, null, false, true); + } + + public object Resolve() + { + if (_created) + { + return _instance; + } + + _instance = _factory(_owner); + _created = true; + + if (_ownsCreatedInstance && _instance is IDisposable disposable) + { + _owner.TrackOwnedDisposable(disposable); + } + + return _instance; + } + } +} diff --git a/My project/Assets/FlowScope/Runtime/Container/ContainerRegistration.cs.meta b/My project/Assets/FlowScope/Runtime/Container/ContainerRegistration.cs.meta new file mode 100644 index 0000000..4af7558 --- /dev/null +++ b/My project/Assets/FlowScope/Runtime/Container/ContainerRegistration.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d024d7ed8fd642b6948a216b3a676e22 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/My project/Assets/FlowScope/Runtime/Container/GeneratedFactories.cs b/My project/Assets/FlowScope/Runtime/Container/GeneratedFactories.cs new file mode 100644 index 0000000..c786b9c --- /dev/null +++ b/My project/Assets/FlowScope/Runtime/Container/GeneratedFactories.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; + +namespace FlowScope.Container +{ + public static class GeneratedFactories + { + private static readonly Dictionary> Factories = new Dictionary>(); + + public static void Register(Func factory) + { + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + Factories[typeof(TImplementation)] = c => factory(c); + } + + public static bool TryGetFactory(Type implementationType, out Func factory) + { + if (implementationType == null) + { + throw new ArgumentNullException(nameof(implementationType)); + } + + return Factories.TryGetValue(implementationType, out factory); + } + + public static void RegisterAssembly(Container container) + { + if (container == null) + { + throw new ArgumentNullException(nameof(container)); + } + + foreach (var pair in Factories) + { + container.RegisterFactory(pair.Key, pair.Value); + } + } + } +} diff --git a/My project/Assets/FlowScope/Runtime/Container/GeneratedFactories.cs.meta b/My project/Assets/FlowScope/Runtime/Container/GeneratedFactories.cs.meta new file mode 100644 index 0000000..4a88501 --- /dev/null +++ b/My project/Assets/FlowScope/Runtime/Container/GeneratedFactories.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fc50e3c76ee147b59d2663e872ced1a0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/My project/Assets/FlowScope/Runtime/Container/ReflectionFactoryBuilder.cs b/My project/Assets/FlowScope/Runtime/Container/ReflectionFactoryBuilder.cs new file mode 100644 index 0000000..6f802d4 --- /dev/null +++ b/My project/Assets/FlowScope/Runtime/Container/ReflectionFactoryBuilder.cs @@ -0,0 +1,62 @@ +using System; +using System.Linq; +using System.Reflection; + +namespace FlowScope.Container +{ + public static class ReflectionFactoryBuilder + { + public static void RegisterType(Container container) + { + if (container == null) + { + throw new ArgumentNullException(nameof(container)); + } + + container.RegisterFactory(typeof(TImplementation), BuildFactory(typeof(TImplementation))); + } + + public static void RegisterType(Container container) + where TImplementation : TInterface + { + if (container == null) + { + throw new ArgumentNullException(nameof(container)); + } + + container.RegisterFactory(typeof(TInterface), BuildFactory(typeof(TImplementation))); + } + + public static Func BuildFactory(Type implementationType) + { + if (implementationType == null) + { + throw new ArgumentNullException(nameof(implementationType)); + } + + if (!Attribute.IsDefined(implementationType, typeof(InjectableAttribute))) + { + throw new InvalidOperationException($"{FormatType(implementationType)} must be marked with InjectableAttribute for reflection registration."); + } + + var constructors = implementationType.GetConstructors(BindingFlags.Public | BindingFlags.Instance); + if (constructors.Length != 1) + { + throw new InvalidOperationException($"{FormatType(implementationType)} must declare exactly one public constructor."); + } + + var constructor = constructors[0]; + var parameters = constructor.GetParameters(); + + return container => + { + var arguments = parameters + .Select(parameter => container.Resolve(parameter.ParameterType)) + .ToArray(); + return constructor.Invoke(arguments); + }; + } + + private static string FormatType(Type type) => type.FullName ?? type.Name; + } +} diff --git a/My project/Assets/FlowScope/Runtime/Container/ReflectionFactoryBuilder.cs.meta b/My project/Assets/FlowScope/Runtime/Container/ReflectionFactoryBuilder.cs.meta new file mode 100644 index 0000000..68d1a48 --- /dev/null +++ b/My project/Assets/FlowScope/Runtime/Container/ReflectionFactoryBuilder.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1d2827e4c16e4b4ea60a84f709d54665 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/My project/Assets/FlowScope/Tests.meta b/My project/Assets/FlowScope/Tests.meta new file mode 100644 index 0000000..2981e10 --- /dev/null +++ b/My project/Assets/FlowScope/Tests.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 283379933e6c4e008981e1de556732cd +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/My project/Assets/FlowScope/Tests/EditMode.meta b/My project/Assets/FlowScope/Tests/EditMode.meta new file mode 100644 index 0000000..bbd2c55 --- /dev/null +++ b/My project/Assets/FlowScope/Tests/EditMode.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1d4bc61cf30a4b319edd9c885eb07341 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/My project/Assets/FlowScope/Tests/EditMode/Container.meta b/My project/Assets/FlowScope/Tests/EditMode/Container.meta new file mode 100644 index 0000000..24a339e --- /dev/null +++ b/My project/Assets/FlowScope/Tests/EditMode/Container.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0bea5f9c12b040369bc322e19d230e50 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/My project/Assets/FlowScope/Tests/EditMode/Container/ContainerTests.cs b/My project/Assets/FlowScope/Tests/EditMode/Container/ContainerTests.cs new file mode 100644 index 0000000..7b1e492 --- /dev/null +++ b/My project/Assets/FlowScope/Tests/EditMode/Container/ContainerTests.cs @@ -0,0 +1,217 @@ +using System; +using System.Collections.Generic; +using NUnit.Framework; +using FlowScope.Container; + +namespace FlowScope.Tests.EditMode.Container +{ + public sealed class ContainerTests + { + [Test] + public void RegisterInstance_Resolve_ReturnsSameInstanceAndDoesNotDisposeExternalInstance() + { + var container = new FlowScope.Container.Container(); + var service = new DisposableService(); + + container.RegisterInstance(service); + + Assert.AreSame(service, container.Resolve()); + Assert.IsTrue(container.TryResolve(out var resolved)); + Assert.AreSame(service, resolved); + + container.Dispose(); + + Assert.IsFalse(service.IsDisposed); + } + + [Test] + public void RegisterFactory_Resolve_CreatesOnceAndDisposesInReverseCreationOrder() + { + var disposeOrder = new List(); + var container = new FlowScope.Container.Container(); + container.RegisterFactory(_ => new FirstDisposable(disposeOrder)); + container.RegisterFactory(_ => new SecondDisposable(disposeOrder)); + + var first = container.Resolve(); + var second = container.Resolve(); + + Assert.AreSame(first, container.Resolve()); + Assert.AreSame(second, container.Resolve()); + + container.Dispose(); + container.Dispose(); + + CollectionAssert.AreEqual(new[] { "second", "first" }, disposeOrder); + } + + [Test] + public void CreateScope_Resolve_ReadsParentAndCanOverrideWithoutPollutingParent() + { + var parent = new FlowScope.Container.Container(); + var parentService = new NamedService("parent"); + var childService = new NamedService("child"); + parent.RegisterInstance(parentService); + + var child = parent.CreateScope(); + + Assert.AreSame(parentService, child.Resolve()); + + child.RegisterInstance(childService); + + Assert.AreSame(childService, child.Resolve()); + Assert.AreSame(parentService, parent.Resolve()); + } + + [Test] + public void RegisterInstance_WhenSameScopeAlreadyRegistered_Throws() + { + var container = new FlowScope.Container.Container(); + container.RegisterInstance(new NamedService("first")); + + var ex = Assert.Throws( + () => container.RegisterInstance(new NamedService("second"))); + + StringAssert.Contains("already registered", ex.Message); + } + + [Test] + public void Dispose_ParentDisposesChildScopesBeforeOwnInstances() + { + var disposeOrder = new List(); + var parent = new FlowScope.Container.Container(); + parent.RegisterFactory(_ => new FirstDisposable(disposeOrder)); + var child = parent.CreateScope(); + child.RegisterFactory(_ => new SecondDisposable(disposeOrder)); + + parent.Resolve(); + child.Resolve(); + + parent.Dispose(); + + CollectionAssert.AreEqual(new[] { "second", "first" }, disposeOrder); + } + + [Test] + public void RegisterType_Resolve_UsesGeneratedFactoryEntryPointForConstructorInjection() + { + var container = new FlowScope.Container.Container(); + container.RegisterInstance(new NamedService("dependency")); + GeneratedFactories.Register( + c => new ConstructorInjectedService(c.Resolve())); + + container.RegisterType(); + + var resolved = container.Resolve(); + + Assert.AreEqual("dependency", resolved.Service.Name); + } + + [Test] + public void ReflectionFactoryBuilder_RegisterType_ResolvesConstructorDependencies() + { + var container = new FlowScope.Container.Container(); + container.RegisterInstance(new NamedService("reflection")); + + ReflectionFactoryBuilder.RegisterType(container); + + var resolved = container.Resolve(); + + Assert.AreEqual("reflection", resolved.Service.Name); + } + + [Test] + public void RegisterType_WhenFactoryEntryPointMissing_ThrowsClearError() + { + var container = new FlowScope.Container.Container(); + + var ex = Assert.Throws(() => container.RegisterType()); + + StringAssert.Contains("RegisterFactory", ex.Message); + } + + [Test] + public void Resolve_WhenDependencyCycleExists_ThrowsWithCompleteDependencyChain() + { + var container = new FlowScope.Container.Container(); + ReflectionFactoryBuilder.RegisterType(container); + ReflectionFactoryBuilder.RegisterType(container); + + var ex = Assert.Throws(() => container.Resolve()); + + StringAssert.Contains(nameof(CycleA), ex.Message); + StringAssert.Contains(nameof(CycleB), ex.Message); + StringAssert.Contains($"{nameof(CycleA)} -> {nameof(CycleB)} -> {nameof(CycleA)}", ex.Message); + } + + [Test] + public void DisposedContainer_RegisterAndResolveThrowObjectDisposedException() + { + var container = new FlowScope.Container.Container(); + + container.Dispose(); + + Assert.Throws(() => container.RegisterInstance(new NamedService("late"))); + Assert.Throws(() => container.RegisterFactory(_ => new NamedService("late"))); + Assert.Throws(() => container.Resolve()); + Assert.Throws(() => container.CreateScope()); + } + + private interface IService + { + string Name { get; } + } + + private sealed class NamedService : IService + { + public NamedService(string name) => Name = name; + public string Name { get; } + } + + private sealed class DisposableService : IService, IDisposable + { + public string Name => "disposable"; + public bool IsDisposed { get; private set; } + public void Dispose() => IsDisposed = true; + } + + private sealed class FirstDisposable : IDisposable + { + private readonly List _disposeOrder; + public FirstDisposable(List disposeOrder) => _disposeOrder = disposeOrder; + public void Dispose() => _disposeOrder.Add("first"); + } + + private sealed class SecondDisposable : IDisposable + { + private readonly List _disposeOrder; + public SecondDisposable(List disposeOrder) => _disposeOrder = disposeOrder; + public void Dispose() => _disposeOrder.Add("second"); + } + + [Injectable] + private sealed class ConstructorInjectedService + { + public ConstructorInjectedService(IService service) => Service = service; + public IService Service { get; } + } + + [Injectable] + private sealed class UnregisteredGeneratedService + { + } + + [Injectable] + private sealed class CycleA + { + public CycleA(CycleB dependency) => Dependency = dependency; + public CycleB Dependency { get; } + } + + [Injectable] + private sealed class CycleB + { + public CycleB(CycleA dependency) => Dependency = dependency; + public CycleA Dependency { get; } + } + } +} diff --git a/My project/Assets/FlowScope/Tests/EditMode/Container/ContainerTests.cs.meta b/My project/Assets/FlowScope/Tests/EditMode/Container/ContainerTests.cs.meta new file mode 100644 index 0000000..69931bd --- /dev/null +++ b/My project/Assets/FlowScope/Tests/EditMode/Container/ContainerTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d5d96cf26ce24c8988c911eb9e73f1fb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: