feat(core): 实现架构作用域容器核心
This commit is contained in:
@@ -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<Type> _resolutionStack;
|
|
||||||
|
|
||||||
private readonly Container _parent;
|
|
||||||
private readonly Dictionary<Type, ContainerRegistration> _registrations = new Dictionary<Type, ContainerRegistration>();
|
|
||||||
private readonly List<Container> _children = new List<Container>();
|
|
||||||
private readonly List<IDisposable> _ownedDisposables = new List<IDisposable>();
|
|
||||||
private bool _disposed;
|
|
||||||
|
|
||||||
public Container()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
private Container(Container parent)
|
|
||||||
{
|
|
||||||
_parent = parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RegisterInstance<T>(T instance)
|
|
||||||
{
|
|
||||||
ThrowIfDisposed();
|
|
||||||
|
|
||||||
if (instance == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(instance));
|
|
||||||
}
|
|
||||||
|
|
||||||
RegisterLocal(typeof(T), ContainerRegistration.ForInstance(instance));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RegisterFactory<T>(Func<Container, T> factory)
|
|
||||||
{
|
|
||||||
ThrowIfDisposed();
|
|
||||||
|
|
||||||
if (factory == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(factory));
|
|
||||||
}
|
|
||||||
|
|
||||||
RegisterLocal(typeof(T), ContainerRegistration.ForFactory(this, c => factory(c)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RegisterType<TInterface, TImplementation>() where TImplementation : TInterface
|
|
||||||
{
|
|
||||||
ThrowIfDisposed();
|
|
||||||
|
|
||||||
RegisterType(typeof(TInterface), typeof(TImplementation));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RegisterType<TImplementation>()
|
|
||||||
{
|
|
||||||
ThrowIfDisposed();
|
|
||||||
|
|
||||||
RegisterType(typeof(TImplementation), typeof(TImplementation));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RegisterAssembly()
|
|
||||||
{
|
|
||||||
ThrowIfDisposed();
|
|
||||||
|
|
||||||
GeneratedFactories.RegisterAssembly(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public T Resolve<T>()
|
|
||||||
{
|
|
||||||
ThrowIfDisposed();
|
|
||||||
|
|
||||||
return (T)Resolve(typeof(T));
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryResolve<T>(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<Exception>();
|
|
||||||
|
|
||||||
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<Container, object> 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<Type>();
|
|
||||||
|
|
||||||
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<Exception> exceptions)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
disposable.Dispose();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
exceptions.Add(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string FormatType(Type type) => type.FullName ?? type.Name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace FlowScope.Container
|
|
||||||
{
|
|
||||||
internal sealed class ContainerRegistration
|
|
||||||
{
|
|
||||||
private readonly Container _owner;
|
|
||||||
private readonly Func<Container, object> _factory;
|
|
||||||
private readonly bool _ownsCreatedInstance;
|
|
||||||
private object _instance;
|
|
||||||
private bool _created;
|
|
||||||
|
|
||||||
private ContainerRegistration(Container owner, Func<Container, object> 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<Container, object> 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
8
My project/Assets/FlowScope/Runtime/Core.meta
Normal file
8
My project/Assets/FlowScope/Runtime/Core.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c35227ceb2c96da44873807b1908facf
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 8d24db89928e4010a3c1af79ffb15709
|
guid: 4bd6a11cc58df434eb82db6a3fe8362f
|
||||||
folderAsset: yes
|
folderAsset: yes
|
||||||
DefaultImporter:
|
DefaultImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
@@ -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<ArchitectureScope> _children = new List<ArchitectureScope>();
|
||||||
|
|
||||||
|
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<TFeature>()
|
||||||
|
where TFeature : IFeature, new()
|
||||||
|
{
|
||||||
|
return CreateFeature<TFeature>(typeof(TFeature).Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IArchitectureScope CreateFeature<TFeature>(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<TImplementation>()
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
return new RegistrationBuilderAdapter(_container.Register<TImplementation>());
|
||||||
|
}
|
||||||
|
|
||||||
|
public IRegistrationBuilder Register<TImplementation>(string name)
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
return new RegistrationBuilderAdapter(_container.Register<TImplementation>(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
public IRegistrationBuilder Register<TService, TImplementation>()
|
||||||
|
where TImplementation : TService
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
return new RegistrationBuilderAdapter(_container.Register<TService, TImplementation>());
|
||||||
|
}
|
||||||
|
|
||||||
|
public IRegistrationBuilder Register<TService, TImplementation>(string name)
|
||||||
|
where TImplementation : TService
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
return new RegistrationBuilderAdapter(_container.Register<TService, TImplementation>(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterInstance<TService>(TService instance)
|
||||||
|
{
|
||||||
|
RegisterInstance(instance, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterInstance<TService>(TService instance, string name)
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
_container.RegisterInstance(instance, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TService Resolve<TService>()
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
return _container.Resolve<TService>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public TService Resolve<TService>(string name)
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
return _container.Resolve<TService>(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryResolve<TService>(out TService value)
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
return _container.TryResolve(out value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryResolve<TService>(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: cce1873306eafbf41b8db1123fd2bd49
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
392
My project/Assets/FlowScope/Runtime/Core/Container/Container.cs
Normal file
392
My project/Assets/FlowScope/Runtime/Core/Container/Container.cs
Normal file
@@ -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<Type> _resolutionStack;
|
||||||
|
|
||||||
|
private readonly Container _parent;
|
||||||
|
private readonly Dictionary<ServiceKey, ContainerRegistration> _registrations =
|
||||||
|
new Dictionary<ServiceKey, ContainerRegistration>();
|
||||||
|
private readonly List<Container> _children = new List<Container>();
|
||||||
|
private readonly List<IDisposable> _ownedDisposables = new List<IDisposable>();
|
||||||
|
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>(T instance)
|
||||||
|
{
|
||||||
|
RegisterInstance(instance, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterInstance<T>(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<T>(Func<Container, T> factory)
|
||||||
|
{
|
||||||
|
RegisterFactory(null, factory);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterFactory<T>(string name, Func<Container, T> 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<TImplementation>()
|
||||||
|
{
|
||||||
|
return Register<TImplementation>(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ContainerRegistrationBuilder Register<TImplementation>(string name)
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
return RegisterBuiltFactory(
|
||||||
|
new ServiceKey(typeof(TImplementation), name),
|
||||||
|
typeof(TImplementation));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ContainerRegistrationBuilder Register<TInterface, TImplementation>()
|
||||||
|
where TImplementation : TInterface
|
||||||
|
{
|
||||||
|
return Register<TInterface, TImplementation>(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ContainerRegistrationBuilder Register<TInterface, TImplementation>(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<TInterface, TImplementation>()
|
||||||
|
where TImplementation : TInterface
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
|
||||||
|
RegisterType(typeof(TInterface), typeof(TImplementation));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterType<TImplementation>()
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
|
||||||
|
RegisterType(typeof(TImplementation), typeof(TImplementation));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterAssembly()
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
|
||||||
|
GeneratedFactories.RegisterAssembly(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public T Resolve<T>()
|
||||||
|
{
|
||||||
|
return Resolve<T>(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public T Resolve<T>(string name)
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
|
||||||
|
return (T)Resolve(typeof(T), name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryResolve<T>(out T value)
|
||||||
|
{
|
||||||
|
return TryResolve(out value, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryResolve<T>(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<Exception>();
|
||||||
|
|
||||||
|
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<Container, object> 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<Container, object> factory;
|
||||||
|
if (!GeneratedFactories.TryGetFactory(implementationType, out factory))
|
||||||
|
{
|
||||||
|
factory = BuildFactory(implementationType);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ContainerRegistrationBuilder(this, serviceKey, factory);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Func<Container, object> 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<Type>();
|
||||||
|
|
||||||
|
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<Exception> exceptions)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
disposable.Dispose();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
exceptions.Add(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatType(Type type)
|
||||||
|
{
|
||||||
|
return type.FullName ?? type.Name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace FlowScope.Container
|
||||||
|
{
|
||||||
|
public enum ServiceLifetime
|
||||||
|
{
|
||||||
|
Singleton,
|
||||||
|
Scoped,
|
||||||
|
Transient
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly struct ServiceKey : IEquatable<ServiceKey>
|
||||||
|
{
|
||||||
|
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<Container, object> _factory;
|
||||||
|
private bool _committed;
|
||||||
|
|
||||||
|
internal ContainerRegistrationBuilder(
|
||||||
|
Container owner,
|
||||||
|
ServiceKey serviceKey,
|
||||||
|
Func<Container, object> 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<Container, object> _factory;
|
||||||
|
private readonly ServiceLifetime _lifetime;
|
||||||
|
private readonly Dictionary<Container, object> _scopedInstances;
|
||||||
|
private object _singletonInstance;
|
||||||
|
private bool _singletonCreated;
|
||||||
|
|
||||||
|
private ContainerRegistration(
|
||||||
|
Container owner,
|
||||||
|
Func<Container, object> factory,
|
||||||
|
object instance,
|
||||||
|
bool created,
|
||||||
|
ServiceLifetime lifetime)
|
||||||
|
{
|
||||||
|
_owner = owner;
|
||||||
|
_factory = factory;
|
||||||
|
_singletonInstance = instance;
|
||||||
|
_singletonCreated = created;
|
||||||
|
_lifetime = lifetime;
|
||||||
|
_scopedInstances = lifetime == ServiceLifetime.Scoped
|
||||||
|
? new Dictionary<Container, object>()
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ContainerRegistration ForInstance(object instance)
|
||||||
|
{
|
||||||
|
return new ContainerRegistration(null, null, instance, true, ServiceLifetime.Singleton);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ContainerRegistration ForFactory(Container owner, Func<Container, object> factory)
|
||||||
|
{
|
||||||
|
return ForFactory(owner, factory, ServiceLifetime.Singleton);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ContainerRegistration ForFactory(
|
||||||
|
Container owner,
|
||||||
|
Func<Container, object> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 988d85a7d7fa65f4786596348f72e053
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
17
My project/Assets/FlowScope/Runtime/Core/FlowScope.cs
Normal file
17
My project/Assets/FlowScope/Runtime/Core/FlowScope.cs
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
My project/Assets/FlowScope/Runtime/Core/FlowScope.cs.meta
Normal file
11
My project/Assets/FlowScope/Runtime/Core/FlowScope.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c3102de921ecf4a40acc99c9bd099adc
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace FlowScope
|
||||||
|
{
|
||||||
|
public interface IArchitectureScope : IScopeInfo, IDisposable
|
||||||
|
{
|
||||||
|
IArchitectureScope CreateScope(string name);
|
||||||
|
IArchitectureScope CreateFeature<TFeature>() where TFeature : IFeature, new();
|
||||||
|
IArchitectureScope CreateFeature<TFeature>(string name) where TFeature : IFeature, new();
|
||||||
|
|
||||||
|
IRegistrationBuilder Register<TImplementation>();
|
||||||
|
IRegistrationBuilder Register<TImplementation>(string name);
|
||||||
|
IRegistrationBuilder Register<TService, TImplementation>() where TImplementation : TService;
|
||||||
|
IRegistrationBuilder Register<TService, TImplementation>(string name) where TImplementation : TService;
|
||||||
|
|
||||||
|
void RegisterInstance<TService>(TService instance);
|
||||||
|
void RegisterInstance<TService>(TService instance, string name);
|
||||||
|
|
||||||
|
TService Resolve<TService>();
|
||||||
|
TService Resolve<TService>(string name);
|
||||||
|
bool TryResolve<TService>(out TService value);
|
||||||
|
bool TryResolve<TService>(out TService value, string name);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 4aac2cbe9a0b55b4a847cc7eb58e21e5
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
7
My project/Assets/FlowScope/Runtime/Core/IFeature.cs
Normal file
7
My project/Assets/FlowScope/Runtime/Core/IFeature.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace FlowScope
|
||||||
|
{
|
||||||
|
public interface IFeature
|
||||||
|
{
|
||||||
|
void Install(IArchitectureScope scope);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
My project/Assets/FlowScope/Runtime/Core/IFeature.cs.meta
Normal file
11
My project/Assets/FlowScope/Runtime/Core/IFeature.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d65ef41768038504fb7b3c5b5712d3ad
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace FlowScope
|
||||||
|
{
|
||||||
|
public interface IRegistrationBuilder
|
||||||
|
{
|
||||||
|
void AsSingleton();
|
||||||
|
void PerScope();
|
||||||
|
void AsTransient();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 6e26efad6333ec6469df54dbf85637e0
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
10
My project/Assets/FlowScope/Runtime/Core/IScopeInfo.cs
Normal file
10
My project/Assets/FlowScope/Runtime/Core/IScopeInfo.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace FlowScope
|
||||||
|
{
|
||||||
|
public interface IScopeInfo
|
||||||
|
{
|
||||||
|
string Name { get; }
|
||||||
|
IScopeInfo Parent { get; }
|
||||||
|
string Path { get; }
|
||||||
|
bool IsDisposed { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
11
My project/Assets/FlowScope/Runtime/Core/IScopeInfo.cs.meta
Normal file
11
My project/Assets/FlowScope/Runtime/Core/IScopeInfo.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a005e79c7b865c4458eb72135769a42f
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
My project/Assets/FlowScope/Tests/EditMode/Core.meta
Normal file
8
My project/Assets/FlowScope/Tests/EditMode/Core.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 1f19d44b51ad9214693ce4ebaa68a5fa
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -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<IDisposableService, DisposableService>().AsSingleton();
|
||||||
|
|
||||||
|
var rootInstance = root.Resolve<IDisposableService>();
|
||||||
|
Assert.That(first.Resolve<IDisposableService>(), Is.SameAs(rootInstance));
|
||||||
|
Assert.That(second.Resolve<IDisposableService>(), 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<IDisposableService, DisposableService>().PerScope();
|
||||||
|
|
||||||
|
var firstInstance = first.Resolve<IDisposableService>();
|
||||||
|
var secondInstance = second.Resolve<IDisposableService>();
|
||||||
|
|
||||||
|
Assert.That(first.Resolve<IDisposableService>(), Is.SameAs(firstInstance));
|
||||||
|
Assert.That(second.Resolve<IDisposableService>(), 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<ITracked, RootTracked>().AsSingleton();
|
||||||
|
child.Register<IChildTracked, ChildTracked>().AsSingleton();
|
||||||
|
|
||||||
|
root.Resolve<ITracked>();
|
||||||
|
child.Resolve<IChildTracked>();
|
||||||
|
|
||||||
|
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<IDisposableService>(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<string> Order =
|
||||||
|
new System.Collections.Generic.List<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7e708e4fc0c860b46b4c51d589dca9ef
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -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<BossFightFeature>();
|
||||||
|
|
||||||
|
Assert.That(feature.Name, Is.EqualTo(nameof(BossFightFeature)));
|
||||||
|
Assert.That(feature.Path, Is.EqualTo("Root/BossFightFeature"));
|
||||||
|
Assert.That(feature.Resolve<BossFightModel>(), Is.Not.Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void CreateFeature_WithNameUsesExplicitName()
|
||||||
|
{
|
||||||
|
using var root = global::FlowScope.FlowScope.Create();
|
||||||
|
|
||||||
|
using var feature = root.CreateFeature<BossFightFeature>("BossFight");
|
||||||
|
|
||||||
|
Assert.That(feature.Name, Is.EqualTo("BossFight"));
|
||||||
|
Assert.That(feature.Resolve<BossFightModel>(), Is.Not.Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void CreateFeature_WhenInstallFailsDisposesCreatedScope()
|
||||||
|
{
|
||||||
|
using var root = global::FlowScope.FlowScope.Create();
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(() => root.CreateFeature<BrokenFeature>());
|
||||||
|
|
||||||
|
using var feature = root.CreateFeature<BossFightFeature>(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<IRootClock>(clock);
|
||||||
|
|
||||||
|
using var feature = root.CreateFeature<ClockFeature>();
|
||||||
|
|
||||||
|
Assert.That(feature.Resolve<IRootClock>(), Is.SameAs(clock));
|
||||||
|
Assert.That(feature.Resolve<ClockModel>().Value, Is.EqualTo(10));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TwoFeatureScopes_DoNotSharePerScopeFeatureServices()
|
||||||
|
{
|
||||||
|
using var root = global::FlowScope.FlowScope.Create();
|
||||||
|
root.Register<IFeatureService, FeatureService>().PerScope();
|
||||||
|
|
||||||
|
using var first = root.CreateFeature<EmptyFeature>("FirstActivity");
|
||||||
|
using var second = root.CreateFeature<EmptyFeature>("SecondActivity");
|
||||||
|
|
||||||
|
Assert.That(first.Resolve<IFeatureService>(), Is.SameAs(first.Resolve<IFeatureService>()));
|
||||||
|
Assert.That(first.Resolve<IFeatureService>(), Is.Not.SameAs(second.Resolve<IFeatureService>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class BossFightFeature : IFeature
|
||||||
|
{
|
||||||
|
public void Install(IArchitectureScope scope)
|
||||||
|
{
|
||||||
|
scope.Register<BossFightModel>().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<ClockModel>().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) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 8c5d54b1595c9e4488ca5e4558e87b60
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -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<IService, RootService>().AsSingleton();
|
||||||
|
child.Register<IService, ChildService>().AsSingleton();
|
||||||
|
|
||||||
|
Assert.That(root.Resolve<IService>(), Is.TypeOf<RootService>());
|
||||||
|
Assert.That(child.Resolve<IService>(), Is.TypeOf<ChildService>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Register_DuplicateKeyInSameScopeThrows()
|
||||||
|
{
|
||||||
|
using var root = global::FlowScope.FlowScope.Create();
|
||||||
|
root.Register<IService, RootService>().AsSingleton();
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(() =>
|
||||||
|
root.Register<IService, ChildService>().AsSingleton());
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface IService { }
|
||||||
|
private sealed class RootService : IService { }
|
||||||
|
private sealed class ChildService : IService { }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 853e2ec507a40b743b8c415496908b3a
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -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<InvalidOperationException>(() => 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<IRootClock>(clock);
|
||||||
|
|
||||||
|
Assert.That(feature.Resolve<IRootClock>(), Is.SameAs(clock));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Resolve_ChildRegistrationOverridesParent()
|
||||||
|
{
|
||||||
|
using var root = global::FlowScope.FlowScope.Create();
|
||||||
|
using var feature = root.CreateScope("BossFightFeature");
|
||||||
|
|
||||||
|
root.Register<IRewardService, GlobalRewardService>().AsSingleton();
|
||||||
|
feature.Register<IRewardService, BossRewardService>().AsSingleton();
|
||||||
|
|
||||||
|
Assert.That(root.Resolve<IRewardService>(), Is.TypeOf<GlobalRewardService>());
|
||||||
|
Assert.That(feature.Resolve<IRewardService>(), Is.TypeOf<BossRewardService>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TryResolve_MissingRegistrationReturnsFalse()
|
||||||
|
{
|
||||||
|
using var root = global::FlowScope.FlowScope.Create();
|
||||||
|
|
||||||
|
Assert.That(root.TryResolve<IRewardService>(out var value), Is.False);
|
||||||
|
Assert.That(value, Is.Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TryResolve_ConstructorFailureThrows()
|
||||||
|
{
|
||||||
|
using var root = global::FlowScope.FlowScope.Create();
|
||||||
|
root.Register<BrokenService>().AsTransient();
|
||||||
|
|
||||||
|
Assert.Throws<TargetInvocationException>(() =>
|
||||||
|
root.TryResolve<BrokenService>(out _));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void NamedRegistration_ResolvesByTypeAndNameFromFeature()
|
||||||
|
{
|
||||||
|
using var root = global::FlowScope.FlowScope.Create();
|
||||||
|
root.Register<IRewardService, BossRewardService>("boss").PerScope();
|
||||||
|
root.Register<IRewardService, ScratchRewardService>("scratch").PerScope();
|
||||||
|
|
||||||
|
using var boss = root.CreateFeature<EmptyFeature>("BossFight");
|
||||||
|
using var scratch = root.CreateFeature<EmptyFeature>("ScratchTicket");
|
||||||
|
|
||||||
|
Assert.That(boss.Resolve<IRewardService>("boss"), Is.TypeOf<BossRewardService>());
|
||||||
|
Assert.That(scratch.Resolve<IRewardService>("scratch"), Is.TypeOf<ScratchRewardService>());
|
||||||
|
Assert.That(boss.Resolve<IRewardService>("boss"), Is.SameAs(boss.Resolve<IRewardService>("boss")));
|
||||||
|
Assert.That(boss.Resolve<IRewardService>("boss"), Is.Not.SameAs(scratch.Resolve<IRewardService>("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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 0723a2e5a83c0c04bb6ba5aee064485a
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 0bea5f9c12b040369bc322e19d230e50
|
guid: 0bea5f9c12b040369bc322e19d230e50
|
||||||
folderAsset: yes
|
folderAsset: yes
|
||||||
DefaultImporter:
|
DefaultImporter:
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using FlowScope.Container;
|
using FlowScope.Container;
|
||||||
@@ -10,7 +10,7 @@ namespace FlowScope.Tests.EditMode.Container
|
|||||||
[Test]
|
[Test]
|
||||||
public void RegisterInstance_Resolve_ReturnsSameInstanceAndDoesNotDisposeExternalInstance()
|
public void RegisterInstance_Resolve_ReturnsSameInstanceAndDoesNotDisposeExternalInstance()
|
||||||
{
|
{
|
||||||
var container = new FlowScope.Container.Container();
|
var container = new global::FlowScope.Container.Container();
|
||||||
var service = new DisposableService();
|
var service = new DisposableService();
|
||||||
|
|
||||||
container.RegisterInstance<IService>(service);
|
container.RegisterInstance<IService>(service);
|
||||||
@@ -28,7 +28,7 @@ namespace FlowScope.Tests.EditMode.Container
|
|||||||
public void RegisterFactory_Resolve_CreatesOnceAndDisposesInReverseCreationOrder()
|
public void RegisterFactory_Resolve_CreatesOnceAndDisposesInReverseCreationOrder()
|
||||||
{
|
{
|
||||||
var disposeOrder = new List<string>();
|
var disposeOrder = new List<string>();
|
||||||
var container = new FlowScope.Container.Container();
|
var container = new global::FlowScope.Container.Container();
|
||||||
container.RegisterFactory<FirstDisposable>(_ => new FirstDisposable(disposeOrder));
|
container.RegisterFactory<FirstDisposable>(_ => new FirstDisposable(disposeOrder));
|
||||||
container.RegisterFactory<SecondDisposable>(_ => new SecondDisposable(disposeOrder));
|
container.RegisterFactory<SecondDisposable>(_ => new SecondDisposable(disposeOrder));
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ namespace FlowScope.Tests.EditMode.Container
|
|||||||
[Test]
|
[Test]
|
||||||
public void CreateScope_Resolve_ReadsParentAndCanOverrideWithoutPollutingParent()
|
public void CreateScope_Resolve_ReadsParentAndCanOverrideWithoutPollutingParent()
|
||||||
{
|
{
|
||||||
var parent = new FlowScope.Container.Container();
|
var parent = new global::FlowScope.Container.Container();
|
||||||
var parentService = new NamedService("parent");
|
var parentService = new NamedService("parent");
|
||||||
var childService = new NamedService("child");
|
var childService = new NamedService("child");
|
||||||
parent.RegisterInstance<IService>(parentService);
|
parent.RegisterInstance<IService>(parentService);
|
||||||
@@ -62,10 +62,81 @@ namespace FlowScope.Tests.EditMode.Container
|
|||||||
Assert.AreSame(parentService, parent.Resolve<IService>());
|
Assert.AreSame(parentService, parent.Resolve<IService>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void RegisterBuilder_AsSingleton_ReusesOneRootOwnedInstanceAcrossScopes()
|
||||||
|
{
|
||||||
|
var parent = new global::FlowScope.Container.Container();
|
||||||
|
parent.Register<IService, DisposableService>().AsSingleton();
|
||||||
|
var firstScope = parent.CreateScope("first");
|
||||||
|
var secondScope = parent.CreateScope("second");
|
||||||
|
|
||||||
|
var rootResolved = parent.Resolve<IService>();
|
||||||
|
var firstResolved = firstScope.Resolve<IService>();
|
||||||
|
var secondResolved = secondScope.Resolve<IService>();
|
||||||
|
|
||||||
|
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<IService, DisposableService>().PerScope();
|
||||||
|
var firstScope = parent.CreateScope("first");
|
||||||
|
var secondScope = parent.CreateScope("second");
|
||||||
|
|
||||||
|
var firstResolved = firstScope.Resolve<IService>();
|
||||||
|
var secondResolved = secondScope.Resolve<IService>();
|
||||||
|
|
||||||
|
Assert.AreSame(firstResolved, firstScope.Resolve<IService>());
|
||||||
|
Assert.AreSame(secondResolved, secondScope.Resolve<IService>());
|
||||||
|
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<IService, DisposableService>().AsTransient();
|
||||||
|
|
||||||
|
var first = container.Resolve<IService>();
|
||||||
|
var second = container.Resolve<IService>();
|
||||||
|
|
||||||
|
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<IService, NamedService>("boss").AsSingleton();
|
||||||
|
container.Register<IService, OtherNamedService>("scratch").AsSingleton();
|
||||||
|
|
||||||
|
Assert.AreEqual("named", container.Resolve<IService>("boss").Name);
|
||||||
|
Assert.AreEqual("other", container.Resolve<IService>("scratch").Name);
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(() => container.Resolve<IService>());
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void RegisterInstance_WhenSameScopeAlreadyRegistered_Throws()
|
public void RegisterInstance_WhenSameScopeAlreadyRegistered_Throws()
|
||||||
{
|
{
|
||||||
var container = new FlowScope.Container.Container();
|
var container = new global::FlowScope.Container.Container();
|
||||||
container.RegisterInstance<IService>(new NamedService("first"));
|
container.RegisterInstance<IService>(new NamedService("first"));
|
||||||
|
|
||||||
var ex = Assert.Throws<InvalidOperationException>(
|
var ex = Assert.Throws<InvalidOperationException>(
|
||||||
@@ -78,7 +149,7 @@ namespace FlowScope.Tests.EditMode.Container
|
|||||||
public void Dispose_ParentDisposesChildScopesBeforeOwnInstances()
|
public void Dispose_ParentDisposesChildScopesBeforeOwnInstances()
|
||||||
{
|
{
|
||||||
var disposeOrder = new List<string>();
|
var disposeOrder = new List<string>();
|
||||||
var parent = new FlowScope.Container.Container();
|
var parent = new global::FlowScope.Container.Container();
|
||||||
parent.RegisterFactory<FirstDisposable>(_ => new FirstDisposable(disposeOrder));
|
parent.RegisterFactory<FirstDisposable>(_ => new FirstDisposable(disposeOrder));
|
||||||
var child = parent.CreateScope();
|
var child = parent.CreateScope();
|
||||||
child.RegisterFactory<SecondDisposable>(_ => new SecondDisposable(disposeOrder));
|
child.RegisterFactory<SecondDisposable>(_ => new SecondDisposable(disposeOrder));
|
||||||
@@ -94,7 +165,7 @@ namespace FlowScope.Tests.EditMode.Container
|
|||||||
[Test]
|
[Test]
|
||||||
public void RegisterType_Resolve_UsesGeneratedFactoryEntryPointForConstructorInjection()
|
public void RegisterType_Resolve_UsesGeneratedFactoryEntryPointForConstructorInjection()
|
||||||
{
|
{
|
||||||
var container = new FlowScope.Container.Container();
|
var container = new global::FlowScope.Container.Container();
|
||||||
container.RegisterInstance<IService>(new NamedService("dependency"));
|
container.RegisterInstance<IService>(new NamedService("dependency"));
|
||||||
GeneratedFactories.Register<ConstructorInjectedService>(
|
GeneratedFactories.Register<ConstructorInjectedService>(
|
||||||
c => new ConstructorInjectedService(c.Resolve<IService>()));
|
c => new ConstructorInjectedService(c.Resolve<IService>()));
|
||||||
@@ -109,7 +180,7 @@ namespace FlowScope.Tests.EditMode.Container
|
|||||||
[Test]
|
[Test]
|
||||||
public void ReflectionFactoryBuilder_RegisterType_ResolvesConstructorDependencies()
|
public void ReflectionFactoryBuilder_RegisterType_ResolvesConstructorDependencies()
|
||||||
{
|
{
|
||||||
var container = new FlowScope.Container.Container();
|
var container = new global::FlowScope.Container.Container();
|
||||||
container.RegisterInstance<IService>(new NamedService("reflection"));
|
container.RegisterInstance<IService>(new NamedService("reflection"));
|
||||||
|
|
||||||
ReflectionFactoryBuilder.RegisterType<ConstructorInjectedService>(container);
|
ReflectionFactoryBuilder.RegisterType<ConstructorInjectedService>(container);
|
||||||
@@ -122,7 +193,7 @@ namespace FlowScope.Tests.EditMode.Container
|
|||||||
[Test]
|
[Test]
|
||||||
public void RegisterType_WhenFactoryEntryPointMissing_ThrowsClearError()
|
public void RegisterType_WhenFactoryEntryPointMissing_ThrowsClearError()
|
||||||
{
|
{
|
||||||
var container = new FlowScope.Container.Container();
|
var container = new global::FlowScope.Container.Container();
|
||||||
|
|
||||||
var ex = Assert.Throws<InvalidOperationException>(() => container.RegisterType<UnregisteredGeneratedService>());
|
var ex = Assert.Throws<InvalidOperationException>(() => container.RegisterType<UnregisteredGeneratedService>());
|
||||||
|
|
||||||
@@ -132,7 +203,7 @@ namespace FlowScope.Tests.EditMode.Container
|
|||||||
[Test]
|
[Test]
|
||||||
public void Resolve_WhenDependencyCycleExists_ThrowsWithCompleteDependencyChain()
|
public void Resolve_WhenDependencyCycleExists_ThrowsWithCompleteDependencyChain()
|
||||||
{
|
{
|
||||||
var container = new FlowScope.Container.Container();
|
var container = new global::FlowScope.Container.Container();
|
||||||
ReflectionFactoryBuilder.RegisterType<CycleA>(container);
|
ReflectionFactoryBuilder.RegisterType<CycleA>(container);
|
||||||
ReflectionFactoryBuilder.RegisterType<CycleB>(container);
|
ReflectionFactoryBuilder.RegisterType<CycleB>(container);
|
||||||
|
|
||||||
@@ -146,7 +217,7 @@ namespace FlowScope.Tests.EditMode.Container
|
|||||||
[Test]
|
[Test]
|
||||||
public void DisposedContainer_RegisterAndResolveThrowObjectDisposedException()
|
public void DisposedContainer_RegisterAndResolveThrowObjectDisposedException()
|
||||||
{
|
{
|
||||||
var container = new FlowScope.Container.Container();
|
var container = new global::FlowScope.Container.Container();
|
||||||
|
|
||||||
container.Dispose();
|
container.Dispose();
|
||||||
|
|
||||||
@@ -163,10 +234,20 @@ namespace FlowScope.Tests.EditMode.Container
|
|||||||
|
|
||||||
private sealed class NamedService : IService
|
private sealed class NamedService : IService
|
||||||
{
|
{
|
||||||
|
public NamedService()
|
||||||
|
: this("named")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
public NamedService(string name) => Name = name;
|
public NamedService(string name) => Name = name;
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class OtherNamedService : IService
|
||||||
|
{
|
||||||
|
public string Name => "other";
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class DisposableService : IService, IDisposable
|
private sealed class DisposableService : IService, IDisposable
|
||||||
{
|
{
|
||||||
public string Name => "disposable";
|
public string Name => "disposable";
|
||||||
@@ -233,7 +233,7 @@ namespace FlowScope.Tests.EditMode.Flow
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private abstract class RecordingFeature : IFeature
|
private abstract class RecordingFeature : global::FlowScope.Flow.IFeature
|
||||||
{
|
{
|
||||||
public List<string> Calls { get; } = new();
|
public List<string> Calls { get; } = new();
|
||||||
public FeatureContext Context { get; private set; }
|
public FeatureContext Context { get; private set; }
|
||||||
|
|||||||
Reference in New Issue
Block a user