diff --git a/My project/Assets/FlowScope/Runtime/Container/Container.cs b/My project/Assets/FlowScope/Runtime/Container/Container.cs deleted file mode 100644 index c648e25..0000000 --- a/My project/Assets/FlowScope/Runtime/Container/Container.cs +++ /dev/null @@ -1,261 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace FlowScope.Container -{ - public sealed class Container : IDisposable - { - [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) - { - ThrowIfDisposed(); - - if (!TryFindRegistration(typeof(T), out var registration)) - { - value = default; - return false; - } - - value = (T)ResolveRegistration(typeof(T), registration); - return true; - } - - 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 deleted file mode 100644 index 0ad4df9..0000000 --- a/My project/Assets/FlowScope/Runtime/Container/ContainerRegistration.cs +++ /dev/null @@ -1,50 +0,0 @@ -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/Core.meta b/My project/Assets/FlowScope/Runtime/Core.meta new file mode 100644 index 0000000..239a032 --- /dev/null +++ b/My project/Assets/FlowScope/Runtime/Core.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c35227ceb2c96da44873807b1908facf +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/My project/Assets/FlowScope/Runtime/Container.meta b/My project/Assets/FlowScope/Runtime/Core/Container.meta similarity index 64% rename from My project/Assets/FlowScope/Runtime/Container.meta rename to My project/Assets/FlowScope/Runtime/Core/Container.meta index f7d417a..1315e67 100644 --- a/My project/Assets/FlowScope/Runtime/Container.meta +++ b/My project/Assets/FlowScope/Runtime/Core/Container.meta @@ -1,5 +1,5 @@ -fileFormatVersion: 2 -guid: 8d24db89928e4010a3c1af79ffb15709 +fileFormatVersion: 2 +guid: 4bd6a11cc58df434eb82db6a3fe8362f folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/My project/Assets/FlowScope/Runtime/Core/Container/ArchitectureScope.cs b/My project/Assets/FlowScope/Runtime/Core/Container/ArchitectureScope.cs new file mode 100644 index 0000000..146e956 --- /dev/null +++ b/My project/Assets/FlowScope/Runtime/Core/Container/ArchitectureScope.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; + +namespace FlowScope.Core.Container +{ + internal sealed class ArchitectureScope : IArchitectureScope + { + private readonly ArchitectureScope _parent; + private readonly global::FlowScope.Container.Container _container; + private readonly List _children = new List(); + + public ArchitectureScope(string name) + : this(name, null, new global::FlowScope.Container.Container()) + { + } + + private ArchitectureScope( + string name, + ArchitectureScope parent, + global::FlowScope.Container.Container container) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException("Scope name cannot be empty.", nameof(name)); + } + + Name = name; + _parent = parent; + _container = container; + Path = parent == null ? name : $"{parent.Path}/{name}"; + } + + public string Name { get; } + public IScopeInfo Parent => _parent; + public string Path { get; } + public bool IsDisposed { get; private set; } + + public IArchitectureScope CreateScope(string name) + { + ThrowIfDisposed(); + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException("Scope name cannot be empty.", nameof(name)); + } + + if (_children.Exists(child => !child.IsDisposed && child.Name == name)) + { + throw new InvalidOperationException($"Scope '{name}' already exists under '{Path}'."); + } + + var child = new ArchitectureScope(name, this, _container.CreateScope(name)); + _children.Add(child); + return child; + } + + public IArchitectureScope CreateFeature() + where TFeature : IFeature, new() + { + return CreateFeature(typeof(TFeature).Name); + } + + public IArchitectureScope CreateFeature(string name) + where TFeature : IFeature, new() + { + ThrowIfDisposed(); + var featureScope = (ArchitectureScope)CreateScope(name); + try + { + var feature = new TFeature(); + feature.Install(featureScope); + return featureScope; + } + catch + { + featureScope.Dispose(); + throw; + } + } + + public IRegistrationBuilder Register() + { + ThrowIfDisposed(); + return new RegistrationBuilderAdapter(_container.Register()); + } + + public IRegistrationBuilder Register(string name) + { + ThrowIfDisposed(); + return new RegistrationBuilderAdapter(_container.Register(name)); + } + + public IRegistrationBuilder Register() + where TImplementation : TService + { + ThrowIfDisposed(); + return new RegistrationBuilderAdapter(_container.Register()); + } + + public IRegistrationBuilder Register(string name) + where TImplementation : TService + { + ThrowIfDisposed(); + return new RegistrationBuilderAdapter(_container.Register(name)); + } + + public void RegisterInstance(TService instance) + { + RegisterInstance(instance, null); + } + + public void RegisterInstance(TService instance, string name) + { + ThrowIfDisposed(); + _container.RegisterInstance(instance, name); + } + + public TService Resolve() + { + ThrowIfDisposed(); + return _container.Resolve(); + } + + public TService Resolve(string name) + { + ThrowIfDisposed(); + return _container.Resolve(name); + } + + public bool TryResolve(out TService value) + { + ThrowIfDisposed(); + return _container.TryResolve(out value); + } + + public bool TryResolve(out TService value, string name) + { + ThrowIfDisposed(); + return _container.TryResolve(out value, name); + } + + public void Dispose() + { + if (IsDisposed) + { + return; + } + + IsDisposed = true; + for (var i = _children.Count - 1; i >= 0; i--) + { + _children[i].Dispose(); + } + + _children.Clear(); + _container.Dispose(); + _parent?._children.Remove(this); + } + + private void ThrowIfDisposed() + { + if (IsDisposed) + { + throw new ObjectDisposedException(Path); + } + } + } +} diff --git a/My project/Assets/FlowScope/Runtime/Core/Container/ArchitectureScope.cs.meta b/My project/Assets/FlowScope/Runtime/Core/Container/ArchitectureScope.cs.meta new file mode 100644 index 0000000..bf5a523 --- /dev/null +++ b/My project/Assets/FlowScope/Runtime/Core/Container/ArchitectureScope.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cce1873306eafbf41b8db1123fd2bd49 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/My project/Assets/FlowScope/Runtime/Core/Container/Container.cs b/My project/Assets/FlowScope/Runtime/Core/Container/Container.cs new file mode 100644 index 0000000..7443ce8 --- /dev/null +++ b/My project/Assets/FlowScope/Runtime/Core/Container/Container.cs @@ -0,0 +1,392 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace FlowScope.Container +{ + public sealed class Container : IDisposable + { + [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, string name) + { + _parent = parent; + Name = string.IsNullOrWhiteSpace(name) ? null : name; + } + + public string Name { get; } + + public void RegisterInstance(T instance) + { + RegisterInstance(instance, null); + } + + public void RegisterInstance(T instance, string name) + { + ThrowIfDisposed(); + + if (instance == null) + { + throw new ArgumentNullException(nameof(instance)); + } + + RegisterLocal(new ServiceKey(typeof(T), name), ContainerRegistration.ForInstance(instance)); + } + + public void RegisterFactory(Func factory) + { + RegisterFactory(null, factory); + } + + public void RegisterFactory(string name, Func factory) + { + ThrowIfDisposed(); + + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + + RegisterLocal( + new ServiceKey(typeof(T), name), + ContainerRegistration.ForFactory(this, c => factory(c), ServiceLifetime.Singleton)); + } + + public ContainerRegistrationBuilder Register() + { + return Register(null); + } + + public ContainerRegistrationBuilder Register(string name) + { + ThrowIfDisposed(); + return RegisterBuiltFactory( + new ServiceKey(typeof(TImplementation), name), + typeof(TImplementation)); + } + + public ContainerRegistrationBuilder Register() + where TImplementation : TInterface + { + return Register(null); + } + + public ContainerRegistrationBuilder Register(string name) + where TImplementation : TInterface + { + ThrowIfDisposed(); + + var serviceType = typeof(TInterface); + var implementationType = typeof(TImplementation); + if (!serviceType.IsAssignableFrom(implementationType)) + { + throw new InvalidOperationException($"{FormatType(implementationType)} cannot be assigned to {FormatType(serviceType)}."); + } + + return RegisterBuiltFactory(new ServiceKey(serviceType, name), implementationType); + } + + 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() + { + return Resolve(null); + } + + public T Resolve(string name) + { + ThrowIfDisposed(); + + return (T)Resolve(typeof(T), name); + } + + public bool TryResolve(out T value) + { + return TryResolve(out value, null); + } + + public bool TryResolve(out T value, string name) + { + ThrowIfDisposed(); + + var key = new ServiceKey(typeof(T), name); + if (!TryFindRegistration(key, out var registration)) + { + value = default; + return false; + } + + value = (T)ResolveRegistration(key, registration); + return true; + } + + public Container CreateScope() + { + ThrowIfDisposed(); + + var child = new Container(this, null); + _children.Add(child); + return child; + } + + public Container CreateScope(string name) + { + ThrowIfDisposed(); + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException("Scope name cannot be empty.", nameof(name)); + } + + if (_children.Exists(child => !child._disposed && child.Name == name)) + { + throw new InvalidOperationException($"Scope '{name}' already exists in this container."); + } + + var child = new Container(this, name); + _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) + { + return Resolve(serviceType, null); + } + + internal object Resolve(Type serviceType, string name) + { + ThrowIfDisposed(); + + var key = new ServiceKey(serviceType, name); + if (!TryFindRegistration(key, out var registration)) + { + throw new InvalidOperationException($"No registration found for {key}."); + } + + return ResolveRegistration(key, 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( + new ServiceKey(serviceType, null), + ContainerRegistration.ForFactory(this, factory, ServiceLifetime.Singleton)); + } + + internal void RegisterBuilt(ServiceKey serviceKey, ContainerRegistration registration) + { + ThrowIfDisposed(); + RegisterLocal(serviceKey, registration); + } + + 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 ContainerRegistrationBuilder RegisterBuiltFactory( + ServiceKey serviceKey, + Type implementationType) + { + Func factory; + if (!GeneratedFactories.TryGetFactory(implementationType, out factory)) + { + factory = BuildFactory(implementationType); + } + + return new ContainerRegistrationBuilder(this, serviceKey, factory); + } + + private static Func BuildFactory(Type implementationType) + { + 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 void RegisterLocal(ServiceKey serviceKey, ContainerRegistration registration) + { + if (_registrations.ContainsKey(serviceKey)) + { + throw new InvalidOperationException($"{serviceKey} is already registered in this container scope."); + } + + _registrations.Add(serviceKey, registration); + } + + private object ResolveRegistration(ServiceKey serviceKey, ContainerRegistration registration) + { + _resolutionStack = _resolutionStack ?? new Stack(); + + if (_resolutionStack.Contains(serviceKey.ServiceType)) + { + var chain = _resolutionStack.Reverse() + .Concat(new[] { serviceKey.ServiceType }) + .Select(t => t.Name); + throw new InvalidOperationException($"Circular dependency detected: {string.Join(" -> ", chain)}"); + } + + _resolutionStack.Push(serviceKey.ServiceType); + try + { + return registration.Resolve(this); + } + finally + { + _resolutionStack.Pop(); + if (_resolutionStack.Count == 0) + { + _resolutionStack = null; + } + } + } + + private bool TryFindRegistration(ServiceKey serviceKey, out ContainerRegistration registration) + { + if (_registrations.TryGetValue(serviceKey, out registration)) + { + return true; + } + + if (_parent != null) + { + return _parent.TryFindRegistration(serviceKey, 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) + { + return type.FullName ?? type.Name; + } + } +} diff --git a/My project/Assets/FlowScope/Runtime/Container/Container.cs.meta b/My project/Assets/FlowScope/Runtime/Core/Container/Container.cs.meta similarity index 100% rename from My project/Assets/FlowScope/Runtime/Container/Container.cs.meta rename to My project/Assets/FlowScope/Runtime/Core/Container/Container.cs.meta diff --git a/My project/Assets/FlowScope/Runtime/Core/Container/ContainerRegistration.cs b/My project/Assets/FlowScope/Runtime/Core/Container/ContainerRegistration.cs new file mode 100644 index 0000000..eec5ad0 --- /dev/null +++ b/My project/Assets/FlowScope/Runtime/Core/Container/ContainerRegistration.cs @@ -0,0 +1,193 @@ +using System; +using System.Collections.Generic; + +namespace FlowScope.Container +{ + public enum ServiceLifetime + { + Singleton, + Scoped, + Transient + } + + public readonly struct ServiceKey : IEquatable + { + public ServiceKey(Type serviceType, string name) + { + ServiceType = serviceType ?? throw new ArgumentNullException(nameof(serviceType)); + Name = string.IsNullOrWhiteSpace(name) ? null : name; + } + + public Type ServiceType { get; } + public string Name { get; } + + public bool Equals(ServiceKey other) + { + return ServiceType == other.ServiceType && string.Equals(Name, other.Name, StringComparison.Ordinal); + } + + public override bool Equals(object obj) + { + return obj is ServiceKey other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + return ((ServiceType != null ? ServiceType.GetHashCode() : 0) * 397) ^ + (Name != null ? StringComparer.Ordinal.GetHashCode(Name) : 0); + } + } + + public override string ToString() + { + var typeName = ServiceType.FullName ?? ServiceType.Name; + return Name == null ? typeName : $"{typeName} named '{Name}'"; + } + } + + public sealed class ContainerRegistrationBuilder + { + private readonly Container _owner; + private readonly ServiceKey _serviceKey; + private readonly Func _factory; + private bool _committed; + + internal ContainerRegistrationBuilder( + Container owner, + ServiceKey serviceKey, + Func factory) + { + _owner = owner ?? throw new ArgumentNullException(nameof(owner)); + _serviceKey = serviceKey; + _factory = factory ?? throw new ArgumentNullException(nameof(factory)); + } + + public void AsSingleton() + { + Commit(ServiceLifetime.Singleton); + } + + public void PerScope() + { + Commit(ServiceLifetime.Scoped); + } + + public void AsTransient() + { + Commit(ServiceLifetime.Transient); + } + + private void Commit(ServiceLifetime lifetime) + { + if (_committed) + { + throw new InvalidOperationException($"{_serviceKey} registration has already been committed."); + } + + _committed = true; + _owner.RegisterBuilt(_serviceKey, ContainerRegistration.ForFactory(_owner, _factory, lifetime)); + } + } + + internal sealed class ContainerRegistration + { + private readonly Container _owner; + private readonly Func _factory; + private readonly ServiceLifetime _lifetime; + private readonly Dictionary _scopedInstances; + private object _singletonInstance; + private bool _singletonCreated; + + private ContainerRegistration( + Container owner, + Func factory, + object instance, + bool created, + ServiceLifetime lifetime) + { + _owner = owner; + _factory = factory; + _singletonInstance = instance; + _singletonCreated = created; + _lifetime = lifetime; + _scopedInstances = lifetime == ServiceLifetime.Scoped + ? new Dictionary() + : null; + } + + public static ContainerRegistration ForInstance(object instance) + { + return new ContainerRegistration(null, null, instance, true, ServiceLifetime.Singleton); + } + + public static ContainerRegistration ForFactory(Container owner, Func factory) + { + return ForFactory(owner, factory, ServiceLifetime.Singleton); + } + + public static ContainerRegistration ForFactory( + Container owner, + Func factory, + ServiceLifetime lifetime) + { + return new ContainerRegistration(owner, factory, null, false, lifetime); + } + + public object Resolve(Container requester) + { + switch (_lifetime) + { + case ServiceLifetime.Singleton: + return ResolveSingleton(); + case ServiceLifetime.Scoped: + return ResolveScoped(requester); + case ServiceLifetime.Transient: + return CreateAndTrack(requester); + default: + throw new InvalidOperationException($"Unknown service lifetime {_lifetime}."); + } + } + + private object ResolveSingleton() + { + if (_singletonCreated) + { + return _singletonInstance; + } + + _singletonInstance = CreateAndTrack(_owner); + _singletonCreated = true; + return _singletonInstance; + } + + private object ResolveScoped(Container requester) + { + if (requester == null) + { + requester = _owner; + } + + if (_scopedInstances.TryGetValue(requester, out var instance)) + { + return instance; + } + + instance = CreateAndTrack(requester); + _scopedInstances.Add(requester, instance); + return instance; + } + + private object CreateAndTrack(Container owner) + { + var instance = _factory(owner); + if (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/Core/Container/ContainerRegistration.cs.meta similarity index 100% rename from My project/Assets/FlowScope/Runtime/Container/ContainerRegistration.cs.meta rename to My project/Assets/FlowScope/Runtime/Core/Container/ContainerRegistration.cs.meta diff --git a/My project/Assets/FlowScope/Runtime/Container/GeneratedFactories.cs b/My project/Assets/FlowScope/Runtime/Core/Container/GeneratedFactories.cs similarity index 100% rename from My project/Assets/FlowScope/Runtime/Container/GeneratedFactories.cs rename to My project/Assets/FlowScope/Runtime/Core/Container/GeneratedFactories.cs diff --git a/My project/Assets/FlowScope/Runtime/Container/GeneratedFactories.cs.meta b/My project/Assets/FlowScope/Runtime/Core/Container/GeneratedFactories.cs.meta similarity index 100% rename from My project/Assets/FlowScope/Runtime/Container/GeneratedFactories.cs.meta rename to My project/Assets/FlowScope/Runtime/Core/Container/GeneratedFactories.cs.meta diff --git a/My project/Assets/FlowScope/Runtime/Container/InjectableAttribute.cs b/My project/Assets/FlowScope/Runtime/Core/Container/InjectableAttribute.cs similarity index 100% rename from My project/Assets/FlowScope/Runtime/Container/InjectableAttribute.cs rename to My project/Assets/FlowScope/Runtime/Core/Container/InjectableAttribute.cs diff --git a/My project/Assets/FlowScope/Runtime/Container/InjectableAttribute.cs.meta b/My project/Assets/FlowScope/Runtime/Core/Container/InjectableAttribute.cs.meta similarity index 100% rename from My project/Assets/FlowScope/Runtime/Container/InjectableAttribute.cs.meta rename to My project/Assets/FlowScope/Runtime/Core/Container/InjectableAttribute.cs.meta diff --git a/My project/Assets/FlowScope/Runtime/Container/ReflectionFactoryBuilder.cs b/My project/Assets/FlowScope/Runtime/Core/Container/ReflectionFactoryBuilder.cs similarity index 100% rename from My project/Assets/FlowScope/Runtime/Container/ReflectionFactoryBuilder.cs rename to My project/Assets/FlowScope/Runtime/Core/Container/ReflectionFactoryBuilder.cs diff --git a/My project/Assets/FlowScope/Runtime/Container/ReflectionFactoryBuilder.cs.meta b/My project/Assets/FlowScope/Runtime/Core/Container/ReflectionFactoryBuilder.cs.meta similarity index 100% rename from My project/Assets/FlowScope/Runtime/Container/ReflectionFactoryBuilder.cs.meta rename to My project/Assets/FlowScope/Runtime/Core/Container/ReflectionFactoryBuilder.cs.meta diff --git a/My project/Assets/FlowScope/Runtime/Core/Container/RegistrationBuilderAdapter.cs b/My project/Assets/FlowScope/Runtime/Core/Container/RegistrationBuilderAdapter.cs new file mode 100644 index 0000000..0a0e39d --- /dev/null +++ b/My project/Assets/FlowScope/Runtime/Core/Container/RegistrationBuilderAdapter.cs @@ -0,0 +1,18 @@ +using FlowScope.Container; + +namespace FlowScope.Core.Container +{ + internal sealed class RegistrationBuilderAdapter : IRegistrationBuilder + { + private readonly ContainerRegistrationBuilder _inner; + + public RegistrationBuilderAdapter(ContainerRegistrationBuilder inner) + { + _inner = inner; + } + + public void AsSingleton() => _inner.AsSingleton(); + public void PerScope() => _inner.PerScope(); + public void AsTransient() => _inner.AsTransient(); + } +} diff --git a/My project/Assets/FlowScope/Runtime/Core/Container/RegistrationBuilderAdapter.cs.meta b/My project/Assets/FlowScope/Runtime/Core/Container/RegistrationBuilderAdapter.cs.meta new file mode 100644 index 0000000..1b455e2 --- /dev/null +++ b/My project/Assets/FlowScope/Runtime/Core/Container/RegistrationBuilderAdapter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 988d85a7d7fa65f4786596348f72e053 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/My project/Assets/FlowScope/Runtime/Core/FlowScope.cs b/My project/Assets/FlowScope/Runtime/Core/FlowScope.cs new file mode 100644 index 0000000..ea7670a --- /dev/null +++ b/My project/Assets/FlowScope/Runtime/Core/FlowScope.cs @@ -0,0 +1,17 @@ +using FlowScope.Core.Container; + +namespace FlowScope +{ + public static class FlowScope + { + public static IArchitectureScope Create() + { + return Create("Root"); + } + + public static IArchitectureScope Create(string rootName) + { + return new ArchitectureScope(rootName); + } + } +} diff --git a/My project/Assets/FlowScope/Runtime/Core/FlowScope.cs.meta b/My project/Assets/FlowScope/Runtime/Core/FlowScope.cs.meta new file mode 100644 index 0000000..61e9cf5 --- /dev/null +++ b/My project/Assets/FlowScope/Runtime/Core/FlowScope.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c3102de921ecf4a40acc99c9bd099adc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/My project/Assets/FlowScope/Runtime/Core/IArchitectureScope.cs b/My project/Assets/FlowScope/Runtime/Core/IArchitectureScope.cs new file mode 100644 index 0000000..e109131 --- /dev/null +++ b/My project/Assets/FlowScope/Runtime/Core/IArchitectureScope.cs @@ -0,0 +1,24 @@ +using System; + +namespace FlowScope +{ + public interface IArchitectureScope : IScopeInfo, IDisposable + { + IArchitectureScope CreateScope(string name); + IArchitectureScope CreateFeature() where TFeature : IFeature, new(); + IArchitectureScope CreateFeature(string name) where TFeature : IFeature, new(); + + IRegistrationBuilder Register(); + IRegistrationBuilder Register(string name); + IRegistrationBuilder Register() where TImplementation : TService; + IRegistrationBuilder Register(string name) where TImplementation : TService; + + void RegisterInstance(TService instance); + void RegisterInstance(TService instance, string name); + + TService Resolve(); + TService Resolve(string name); + bool TryResolve(out TService value); + bool TryResolve(out TService value, string name); + } +} diff --git a/My project/Assets/FlowScope/Runtime/Core/IArchitectureScope.cs.meta b/My project/Assets/FlowScope/Runtime/Core/IArchitectureScope.cs.meta new file mode 100644 index 0000000..8df6460 --- /dev/null +++ b/My project/Assets/FlowScope/Runtime/Core/IArchitectureScope.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4aac2cbe9a0b55b4a847cc7eb58e21e5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/My project/Assets/FlowScope/Runtime/Core/IFeature.cs b/My project/Assets/FlowScope/Runtime/Core/IFeature.cs new file mode 100644 index 0000000..2eed1ab --- /dev/null +++ b/My project/Assets/FlowScope/Runtime/Core/IFeature.cs @@ -0,0 +1,7 @@ +namespace FlowScope +{ + public interface IFeature + { + void Install(IArchitectureScope scope); + } +} diff --git a/My project/Assets/FlowScope/Runtime/Core/IFeature.cs.meta b/My project/Assets/FlowScope/Runtime/Core/IFeature.cs.meta new file mode 100644 index 0000000..6516bcf --- /dev/null +++ b/My project/Assets/FlowScope/Runtime/Core/IFeature.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d65ef41768038504fb7b3c5b5712d3ad +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/My project/Assets/FlowScope/Runtime/Core/IRegistrationBuilder.cs b/My project/Assets/FlowScope/Runtime/Core/IRegistrationBuilder.cs new file mode 100644 index 0000000..36071ad --- /dev/null +++ b/My project/Assets/FlowScope/Runtime/Core/IRegistrationBuilder.cs @@ -0,0 +1,9 @@ +namespace FlowScope +{ + public interface IRegistrationBuilder + { + void AsSingleton(); + void PerScope(); + void AsTransient(); + } +} diff --git a/My project/Assets/FlowScope/Runtime/Core/IRegistrationBuilder.cs.meta b/My project/Assets/FlowScope/Runtime/Core/IRegistrationBuilder.cs.meta new file mode 100644 index 0000000..98817dd --- /dev/null +++ b/My project/Assets/FlowScope/Runtime/Core/IRegistrationBuilder.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6e26efad6333ec6469df54dbf85637e0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/My project/Assets/FlowScope/Runtime/Core/IScopeInfo.cs b/My project/Assets/FlowScope/Runtime/Core/IScopeInfo.cs new file mode 100644 index 0000000..6ccc94d --- /dev/null +++ b/My project/Assets/FlowScope/Runtime/Core/IScopeInfo.cs @@ -0,0 +1,10 @@ +namespace FlowScope +{ + public interface IScopeInfo + { + string Name { get; } + IScopeInfo Parent { get; } + string Path { get; } + bool IsDisposed { get; } + } +} diff --git a/My project/Assets/FlowScope/Runtime/Core/IScopeInfo.cs.meta b/My project/Assets/FlowScope/Runtime/Core/IScopeInfo.cs.meta new file mode 100644 index 0000000..bd8c1dc --- /dev/null +++ b/My project/Assets/FlowScope/Runtime/Core/IScopeInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a005e79c7b865c4458eb72135769a42f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/My project/Assets/FlowScope/Tests/EditMode/Core.meta b/My project/Assets/FlowScope/Tests/EditMode/Core.meta new file mode 100644 index 0000000..1415c4b --- /dev/null +++ b/My project/Assets/FlowScope/Tests/EditMode/Core.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1f19d44b51ad9214693ce4ebaa68a5fa +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeDisposeTests.cs b/My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeDisposeTests.cs new file mode 100644 index 0000000..7208b0f --- /dev/null +++ b/My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeDisposeTests.cs @@ -0,0 +1,109 @@ +using NUnit.Framework; + +namespace FlowScope.Tests.EditMode.Core +{ + public sealed class ArchitectureScopeDisposeTests + { + [Test] + public void Singleton_FromParentIsSharedByChildrenAndDisposedByParent() + { + using var root = global::FlowScope.FlowScope.Create(); + var first = root.CreateScope("First"); + var second = root.CreateScope("Second"); + + root.Register().AsSingleton(); + + var rootInstance = root.Resolve(); + Assert.That(first.Resolve(), Is.SameAs(rootInstance)); + Assert.That(second.Resolve(), Is.SameAs(rootInstance)); + + first.Dispose(); + Assert.That(rootInstance.IsDisposed, Is.False); + + root.Dispose(); + Assert.That(rootInstance.IsDisposed, Is.True); + } + + [Test] + public void PerScope_FromParentCreatesDifferentInstancesPerChildScope() + { + using var root = global::FlowScope.FlowScope.Create(); + var first = root.CreateScope("First"); + var second = root.CreateScope("Second"); + + root.Register().PerScope(); + + var firstInstance = first.Resolve(); + var secondInstance = second.Resolve(); + + Assert.That(first.Resolve(), Is.SameAs(firstInstance)); + Assert.That(second.Resolve(), Is.SameAs(secondInstance)); + Assert.That(firstInstance, Is.Not.SameAs(secondInstance)); + + first.Dispose(); + Assert.That(firstInstance.IsDisposed, Is.True); + Assert.That(secondInstance.IsDisposed, Is.False); + } + + [Test] + public void Dispose_ParentDisposesChildrenBeforeOwnInstancesAndIsIdempotent() + { + DisposeRecorder.Order.Clear(); + var root = global::FlowScope.FlowScope.Create(); + var child = root.CreateScope("Child"); + + root.Register().AsSingleton(); + child.Register().AsSingleton(); + + root.Resolve(); + child.Resolve(); + + root.Dispose(); + root.Dispose(); + + CollectionAssert.AreEqual(new[] { "child", "root" }, DisposeRecorder.Order); + } + + [Test] + public void RegisterInstance_DefaultDoesNotDisposeExternalInstance() + { + var root = global::FlowScope.FlowScope.Create(); + var service = new DisposableService(); + + root.RegisterInstance(service); + root.Dispose(); + + Assert.That(service.IsDisposed, Is.False); + } + + private interface IDisposableService + { + bool IsDisposed { get; } + } + + private sealed class DisposableService : IDisposableService, System.IDisposable + { + public bool IsDisposed { get; private set; } + public void Dispose() => IsDisposed = true; + } + + private interface ITracked { } + private interface IChildTracked { } + + private static class DisposeRecorder + { + public static readonly System.Collections.Generic.List Order = + new System.Collections.Generic.List(); + } + + private sealed class RootTracked : ITracked, System.IDisposable + { + public void Dispose() => DisposeRecorder.Order.Add("root"); + } + + private sealed class ChildTracked : IChildTracked, System.IDisposable + { + public void Dispose() => DisposeRecorder.Order.Add("child"); + } + } +} diff --git a/My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeDisposeTests.cs.meta b/My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeDisposeTests.cs.meta new file mode 100644 index 0000000..8b23a54 --- /dev/null +++ b/My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeDisposeTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7e708e4fc0c860b46b4c51d589dca9ef +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeFeatureTests.cs b/My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeFeatureTests.cs new file mode 100644 index 0000000..2921266 --- /dev/null +++ b/My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeFeatureTests.cs @@ -0,0 +1,114 @@ +using System; +using NUnit.Framework; + +namespace FlowScope.Tests.EditMode.Core +{ + public sealed class ArchitectureScopeFeatureTests + { + [Test] + public void CreateFeature_UsesFeatureTypeNameAndCallsInstall() + { + using var root = global::FlowScope.FlowScope.Create(); + + using var feature = root.CreateFeature(); + + Assert.That(feature.Name, Is.EqualTo(nameof(BossFightFeature))); + Assert.That(feature.Path, Is.EqualTo("Root/BossFightFeature")); + Assert.That(feature.Resolve(), Is.Not.Null); + } + + [Test] + public void CreateFeature_WithNameUsesExplicitName() + { + using var root = global::FlowScope.FlowScope.Create(); + + using var feature = root.CreateFeature("BossFight"); + + Assert.That(feature.Name, Is.EqualTo("BossFight")); + Assert.That(feature.Resolve(), Is.Not.Null); + } + + [Test] + public void CreateFeature_WhenInstallFailsDisposesCreatedScope() + { + using var root = global::FlowScope.FlowScope.Create(); + + Assert.Throws(() => root.CreateFeature()); + + using var feature = root.CreateFeature(nameof(BrokenFeature)); + Assert.That(feature.Name, Is.EqualTo(nameof(BrokenFeature))); + } + + [Test] + public void FeatureScope_CanResolveRootServices() + { + using var root = global::FlowScope.FlowScope.Create(); + var clock = new RootClock(10); + root.RegisterInstance(clock); + + using var feature = root.CreateFeature(); + + Assert.That(feature.Resolve(), Is.SameAs(clock)); + Assert.That(feature.Resolve().Value, Is.EqualTo(10)); + } + + [Test] + public void TwoFeatureScopes_DoNotSharePerScopeFeatureServices() + { + using var root = global::FlowScope.FlowScope.Create(); + root.Register().PerScope(); + + using var first = root.CreateFeature("FirstActivity"); + using var second = root.CreateFeature("SecondActivity"); + + Assert.That(first.Resolve(), Is.SameAs(first.Resolve())); + Assert.That(first.Resolve(), Is.Not.SameAs(second.Resolve())); + } + + private sealed class BossFightFeature : IFeature + { + public void Install(IArchitectureScope scope) + { + scope.Register().PerScope(); + } + } + + private sealed class BrokenFeature : IFeature + { + public void Install(IArchitectureScope scope) + { + throw new InvalidOperationException("install failed"); + } + } + + private sealed class BossFightModel { } + + private interface IRootClock { int Value { get; } } + private sealed class RootClock : IRootClock + { + public RootClock(int value) => Value = value; + public int Value { get; } + } + + private sealed class ClockFeature : IFeature + { + public void Install(IArchitectureScope scope) + { + scope.Register().PerScope(); + } + } + + private sealed class ClockModel + { + public ClockModel(IRootClock clock) => Value = clock.Value; + public int Value { get; } + } + + private interface IFeatureService { } + private sealed class FeatureService : IFeatureService { } + private sealed class EmptyFeature : IFeature + { + public void Install(IArchitectureScope scope) { } + } + } +} diff --git a/My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeFeatureTests.cs.meta b/My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeFeatureTests.cs.meta new file mode 100644 index 0000000..7d89864 --- /dev/null +++ b/My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeFeatureTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8c5d54b1595c9e4488ca5e4558e87b60 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeRegistrationTests.cs b/My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeRegistrationTests.cs new file mode 100644 index 0000000..70e4bd5 --- /dev/null +++ b/My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeRegistrationTests.cs @@ -0,0 +1,46 @@ +using System; +using NUnit.Framework; + +namespace FlowScope.Tests.EditMode.Core +{ + public sealed class ArchitectureScopeRegistrationTests + { + [Test] + public void Create_ReturnsRootArchitectureScope() + { + using var root = global::FlowScope.FlowScope.Create(); + + Assert.That(root.Name, Is.EqualTo("Root")); + Assert.That(root.Parent, Is.Null); + Assert.That(root.Path, Is.EqualTo("Root")); + Assert.That(root.IsDisposed, Is.False); + } + + [Test] + public void Register_WritesOnlyCurrentScope() + { + using var root = global::FlowScope.FlowScope.Create(); + using var child = root.CreateScope("Child"); + + root.Register().AsSingleton(); + child.Register().AsSingleton(); + + Assert.That(root.Resolve(), Is.TypeOf()); + Assert.That(child.Resolve(), Is.TypeOf()); + } + + [Test] + public void Register_DuplicateKeyInSameScopeThrows() + { + using var root = global::FlowScope.FlowScope.Create(); + root.Register().AsSingleton(); + + Assert.Throws(() => + root.Register().AsSingleton()); + } + + private interface IService { } + private sealed class RootService : IService { } + private sealed class ChildService : IService { } + } +} diff --git a/My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeRegistrationTests.cs.meta b/My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeRegistrationTests.cs.meta new file mode 100644 index 0000000..66e2533 --- /dev/null +++ b/My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeRegistrationTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 853e2ec507a40b743b8c415496908b3a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeResolveTests.cs b/My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeResolveTests.cs new file mode 100644 index 0000000..6a37341 --- /dev/null +++ b/My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeResolveTests.cs @@ -0,0 +1,116 @@ +using System; +using System.Reflection; +using NUnit.Framework; + +namespace FlowScope.Tests.EditMode.Core +{ + public sealed class ArchitectureScopeResolveTests + { + [Test] + public void CreateScope_CreatesChildWithParentAndPath() + { + using var root = global::FlowScope.FlowScope.Create(); + using var feature = root.CreateScope("BossFightFeature"); + using var child = feature.CreateScope("RevivePanel"); + + Assert.That(feature.Parent, Is.SameAs(root)); + Assert.That(child.Parent, Is.SameAs(feature)); + Assert.That(feature.Path, Is.EqualTo("Root/BossFightFeature")); + Assert.That(child.Path, Is.EqualTo("Root/BossFightFeature/RevivePanel")); + } + + [Test] + public void CreateScope_DuplicateSiblingNameThrows() + { + using var root = global::FlowScope.FlowScope.Create(); + using var first = root.CreateScope("BossFightFeature"); + + Assert.Throws(() => root.CreateScope("BossFightFeature")); + } + + [Test] + public void Resolve_FallsBackToParentWhenCurrentMissing() + { + using var root = global::FlowScope.FlowScope.Create(); + using var feature = root.CreateScope("BossFightFeature"); + + var clock = new RootClock(42); + root.RegisterInstance(clock); + + Assert.That(feature.Resolve(), Is.SameAs(clock)); + } + + [Test] + public void Resolve_ChildRegistrationOverridesParent() + { + using var root = global::FlowScope.FlowScope.Create(); + using var feature = root.CreateScope("BossFightFeature"); + + root.Register().AsSingleton(); + feature.Register().AsSingleton(); + + Assert.That(root.Resolve(), Is.TypeOf()); + Assert.That(feature.Resolve(), Is.TypeOf()); + } + + [Test] + public void TryResolve_MissingRegistrationReturnsFalse() + { + using var root = global::FlowScope.FlowScope.Create(); + + Assert.That(root.TryResolve(out var value), Is.False); + Assert.That(value, Is.Null); + } + + [Test] + public void TryResolve_ConstructorFailureThrows() + { + using var root = global::FlowScope.FlowScope.Create(); + root.Register().AsTransient(); + + Assert.Throws(() => + root.TryResolve(out _)); + } + + [Test] + public void NamedRegistration_ResolvesByTypeAndNameFromFeature() + { + using var root = global::FlowScope.FlowScope.Create(); + root.Register("boss").PerScope(); + root.Register("scratch").PerScope(); + + using var boss = root.CreateFeature("BossFight"); + using var scratch = root.CreateFeature("ScratchTicket"); + + Assert.That(boss.Resolve("boss"), Is.TypeOf()); + Assert.That(scratch.Resolve("scratch"), Is.TypeOf()); + Assert.That(boss.Resolve("boss"), Is.SameAs(boss.Resolve("boss"))); + Assert.That(boss.Resolve("boss"), Is.Not.SameAs(scratch.Resolve("boss"))); + } + + private interface IRootClock { int Value { get; } } + private sealed class RootClock : IRootClock + { + public RootClock(int value) => Value = value; + public int Value { get; } + } + + private interface IRewardService { } + private sealed class GlobalRewardService : IRewardService { } + private sealed class BossRewardService : IRewardService { } + private sealed class ScratchRewardService : IRewardService { } + + private sealed class EmptyFeature : IFeature + { + public void Install(IArchitectureScope scope) { } + } + + private sealed class BrokenService + { + public BrokenService() + { + throw new InvalidOperationException("broken"); + } + } + } +} diff --git a/My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeResolveTests.cs.meta b/My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeResolveTests.cs.meta new file mode 100644 index 0000000..6e0f1a9 --- /dev/null +++ b/My project/Assets/FlowScope/Tests/EditMode/Core/ArchitectureScopeResolveTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0723a2e5a83c0c04bb6ba5aee064485a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/My project/Assets/FlowScope/Tests/EditMode/Container.meta b/My project/Assets/FlowScope/Tests/EditMode/Core/Container.meta similarity index 86% rename from My project/Assets/FlowScope/Tests/EditMode/Container.meta rename to My project/Assets/FlowScope/Tests/EditMode/Core/Container.meta index 24a339e..1b27ba6 100644 --- a/My project/Assets/FlowScope/Tests/EditMode/Container.meta +++ b/My project/Assets/FlowScope/Tests/EditMode/Core/Container.meta @@ -1,4 +1,4 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: 0bea5f9c12b040369bc322e19d230e50 folderAsset: yes DefaultImporter: diff --git a/My project/Assets/FlowScope/Tests/EditMode/Container/ContainerTests.cs b/My project/Assets/FlowScope/Tests/EditMode/Core/Container/ContainerTests.cs similarity index 65% rename from My project/Assets/FlowScope/Tests/EditMode/Container/ContainerTests.cs rename to My project/Assets/FlowScope/Tests/EditMode/Core/Container/ContainerTests.cs index 7b1e492..5865367 100644 --- a/My project/Assets/FlowScope/Tests/EditMode/Container/ContainerTests.cs +++ b/My project/Assets/FlowScope/Tests/EditMode/Core/Container/ContainerTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using NUnit.Framework; using FlowScope.Container; @@ -10,7 +10,7 @@ namespace FlowScope.Tests.EditMode.Container [Test] public void RegisterInstance_Resolve_ReturnsSameInstanceAndDoesNotDisposeExternalInstance() { - var container = new FlowScope.Container.Container(); + var container = new global::FlowScope.Container.Container(); var service = new DisposableService(); container.RegisterInstance(service); @@ -28,7 +28,7 @@ namespace FlowScope.Tests.EditMode.Container public void RegisterFactory_Resolve_CreatesOnceAndDisposesInReverseCreationOrder() { var disposeOrder = new List(); - var container = new FlowScope.Container.Container(); + var container = new global::FlowScope.Container.Container(); container.RegisterFactory(_ => new FirstDisposable(disposeOrder)); container.RegisterFactory(_ => new SecondDisposable(disposeOrder)); @@ -47,7 +47,7 @@ namespace FlowScope.Tests.EditMode.Container [Test] public void CreateScope_Resolve_ReadsParentAndCanOverrideWithoutPollutingParent() { - var parent = new FlowScope.Container.Container(); + var parent = new global::FlowScope.Container.Container(); var parentService = new NamedService("parent"); var childService = new NamedService("child"); parent.RegisterInstance(parentService); @@ -62,10 +62,81 @@ namespace FlowScope.Tests.EditMode.Container Assert.AreSame(parentService, parent.Resolve()); } + [Test] + public void RegisterBuilder_AsSingleton_ReusesOneRootOwnedInstanceAcrossScopes() + { + var parent = new global::FlowScope.Container.Container(); + parent.Register().AsSingleton(); + var firstScope = parent.CreateScope("first"); + var secondScope = parent.CreateScope("second"); + + var rootResolved = parent.Resolve(); + var firstResolved = firstScope.Resolve(); + var secondResolved = secondScope.Resolve(); + + Assert.AreSame(rootResolved, firstResolved); + Assert.AreSame(rootResolved, secondResolved); + + firstScope.Dispose(); + Assert.IsFalse(((DisposableService)rootResolved).IsDisposed); + + parent.Dispose(); + Assert.IsTrue(((DisposableService)rootResolved).IsDisposed); + } + + [Test] + public void RegisterBuilder_PerScope_CreatesOneInstancePerResolvingScope() + { + var parent = new global::FlowScope.Container.Container(); + parent.Register().PerScope(); + var firstScope = parent.CreateScope("first"); + var secondScope = parent.CreateScope("second"); + + var firstResolved = firstScope.Resolve(); + var secondResolved = secondScope.Resolve(); + + Assert.AreSame(firstResolved, firstScope.Resolve()); + Assert.AreSame(secondResolved, secondScope.Resolve()); + Assert.AreNotSame(firstResolved, secondResolved); + + firstScope.Dispose(); + Assert.IsTrue(((DisposableService)firstResolved).IsDisposed); + Assert.IsFalse(((DisposableService)secondResolved).IsDisposed); + } + + [Test] + public void RegisterBuilder_AsTransient_CreatesNewInstanceOnEveryResolve() + { + var container = new global::FlowScope.Container.Container(); + container.Register().AsTransient(); + + var first = container.Resolve(); + var second = container.Resolve(); + + Assert.AreNotSame(first, second); + + container.Dispose(); + Assert.IsTrue(((DisposableService)first).IsDisposed); + Assert.IsTrue(((DisposableService)second).IsDisposed); + } + + [Test] + public void NamedRegistration_ResolvesByTypeAndName() + { + var container = new global::FlowScope.Container.Container(); + container.Register("boss").AsSingleton(); + container.Register("scratch").AsSingleton(); + + Assert.AreEqual("named", container.Resolve("boss").Name); + Assert.AreEqual("other", container.Resolve("scratch").Name); + + Assert.Throws(() => container.Resolve()); + } + [Test] public void RegisterInstance_WhenSameScopeAlreadyRegistered_Throws() { - var container = new FlowScope.Container.Container(); + var container = new global::FlowScope.Container.Container(); container.RegisterInstance(new NamedService("first")); var ex = Assert.Throws( @@ -78,7 +149,7 @@ namespace FlowScope.Tests.EditMode.Container public void Dispose_ParentDisposesChildScopesBeforeOwnInstances() { var disposeOrder = new List(); - var parent = new FlowScope.Container.Container(); + var parent = new global::FlowScope.Container.Container(); parent.RegisterFactory(_ => new FirstDisposable(disposeOrder)); var child = parent.CreateScope(); child.RegisterFactory(_ => new SecondDisposable(disposeOrder)); @@ -94,7 +165,7 @@ namespace FlowScope.Tests.EditMode.Container [Test] public void RegisterType_Resolve_UsesGeneratedFactoryEntryPointForConstructorInjection() { - var container = new FlowScope.Container.Container(); + var container = new global::FlowScope.Container.Container(); container.RegisterInstance(new NamedService("dependency")); GeneratedFactories.Register( c => new ConstructorInjectedService(c.Resolve())); @@ -109,7 +180,7 @@ namespace FlowScope.Tests.EditMode.Container [Test] public void ReflectionFactoryBuilder_RegisterType_ResolvesConstructorDependencies() { - var container = new FlowScope.Container.Container(); + var container = new global::FlowScope.Container.Container(); container.RegisterInstance(new NamedService("reflection")); ReflectionFactoryBuilder.RegisterType(container); @@ -122,7 +193,7 @@ namespace FlowScope.Tests.EditMode.Container [Test] public void RegisterType_WhenFactoryEntryPointMissing_ThrowsClearError() { - var container = new FlowScope.Container.Container(); + var container = new global::FlowScope.Container.Container(); var ex = Assert.Throws(() => container.RegisterType()); @@ -132,7 +203,7 @@ namespace FlowScope.Tests.EditMode.Container [Test] public void Resolve_WhenDependencyCycleExists_ThrowsWithCompleteDependencyChain() { - var container = new FlowScope.Container.Container(); + var container = new global::FlowScope.Container.Container(); ReflectionFactoryBuilder.RegisterType(container); ReflectionFactoryBuilder.RegisterType(container); @@ -146,7 +217,7 @@ namespace FlowScope.Tests.EditMode.Container [Test] public void DisposedContainer_RegisterAndResolveThrowObjectDisposedException() { - var container = new FlowScope.Container.Container(); + var container = new global::FlowScope.Container.Container(); container.Dispose(); @@ -163,10 +234,20 @@ namespace FlowScope.Tests.EditMode.Container private sealed class NamedService : IService { + public NamedService() + : this("named") + { + } + public NamedService(string name) => Name = name; public string Name { get; } } + private sealed class OtherNamedService : IService + { + public string Name => "other"; + } + private sealed class DisposableService : IService, IDisposable { public string Name => "disposable"; diff --git a/My project/Assets/FlowScope/Tests/EditMode/Container/ContainerTests.cs.meta b/My project/Assets/FlowScope/Tests/EditMode/Core/Container/ContainerTests.cs.meta similarity index 100% rename from My project/Assets/FlowScope/Tests/EditMode/Container/ContainerTests.cs.meta rename to My project/Assets/FlowScope/Tests/EditMode/Core/Container/ContainerTests.cs.meta diff --git a/My project/Assets/FlowScope/Tests/EditMode/Flow/GameFlowTests.cs b/My project/Assets/FlowScope/Tests/EditMode/Flow/GameFlowTests.cs index 8f3ac58..230af05 100644 --- a/My project/Assets/FlowScope/Tests/EditMode/Flow/GameFlowTests.cs +++ b/My project/Assets/FlowScope/Tests/EditMode/Flow/GameFlowTests.cs @@ -233,7 +233,7 @@ namespace FlowScope.Tests.EditMode.Flow } } - private abstract class RecordingFeature : IFeature + private abstract class RecordingFeature : global::FlowScope.Flow.IFeature { public List Calls { get; } = new(); public FeatureContext Context { get; private set; }