合并 P0 容器实现
This commit is contained in:
8
My project/Assets/FlowScope/Runtime.meta
Normal file
8
My project/Assets/FlowScope/Runtime.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: eab40aadc4ea4632930155f91c6faca4
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -1,21 +1,261 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace FlowScope.Container
|
namespace FlowScope.Container
|
||||||
{
|
{
|
||||||
public sealed class Container : IDisposable
|
public sealed class Container : IDisposable
|
||||||
{
|
{
|
||||||
public void RegisterInstance<T>(T instance) => throw new NotImplementedException();
|
[ThreadStatic]
|
||||||
public void RegisterFactory<T>(Func<Container, T> factory) => throw new NotImplementedException();
|
private static Stack<Type> _resolutionStack;
|
||||||
public void RegisterType<TInterface, TImplementation>() where TImplementation : TInterface => throw new NotImplementedException();
|
|
||||||
public void RegisterType<TImplementation>() => throw new NotImplementedException();
|
private readonly Container _parent;
|
||||||
public void RegisterAssembly() => throw new NotImplementedException();
|
private readonly Dictionary<Type, ContainerRegistration> _registrations = new Dictionary<Type, ContainerRegistration>();
|
||||||
public T Resolve<T>() => throw new NotImplementedException();
|
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)
|
public bool TryResolve<T>(out T value)
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
|
||||||
|
if (!TryFindRegistration(typeof(T), out var registration))
|
||||||
{
|
{
|
||||||
value = default;
|
value = default;
|
||||||
throw new NotImplementedException();
|
return false;
|
||||||
}
|
}
|
||||||
public Container CreateScope() => throw new NotImplementedException();
|
|
||||||
public void Dispose() => throw new NotImplementedException();
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d024d7ed8fd642b6948a216b3a676e22
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace FlowScope.Container
|
||||||
|
{
|
||||||
|
public static class GeneratedFactories
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<Type, Func<Container, object>> Factories = new Dictionary<Type, Func<Container, object>>();
|
||||||
|
|
||||||
|
public static void Register<TImplementation>(Func<Container, TImplementation> factory)
|
||||||
|
{
|
||||||
|
if (factory == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(factory));
|
||||||
|
}
|
||||||
|
|
||||||
|
Factories[typeof(TImplementation)] = c => factory(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryGetFactory(Type implementationType, out Func<Container, object> factory)
|
||||||
|
{
|
||||||
|
if (implementationType == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(implementationType));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Factories.TryGetValue(implementationType, out factory);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RegisterAssembly(Container container)
|
||||||
|
{
|
||||||
|
if (container == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(container));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var pair in Factories)
|
||||||
|
{
|
||||||
|
container.RegisterFactory(pair.Key, pair.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: fc50e3c76ee147b59d2663e872ced1a0
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace FlowScope.Container
|
||||||
|
{
|
||||||
|
public static class ReflectionFactoryBuilder
|
||||||
|
{
|
||||||
|
public static void RegisterType<TImplementation>(Container container)
|
||||||
|
{
|
||||||
|
if (container == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(container));
|
||||||
|
}
|
||||||
|
|
||||||
|
container.RegisterFactory(typeof(TImplementation), BuildFactory(typeof(TImplementation)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RegisterType<TInterface, TImplementation>(Container container)
|
||||||
|
where TImplementation : TInterface
|
||||||
|
{
|
||||||
|
if (container == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(container));
|
||||||
|
}
|
||||||
|
|
||||||
|
container.RegisterFactory(typeof(TInterface), BuildFactory(typeof(TImplementation)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Func<Container, object> BuildFactory(Type implementationType)
|
||||||
|
{
|
||||||
|
if (implementationType == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(implementationType));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Attribute.IsDefined(implementationType, typeof(InjectableAttribute)))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"{FormatType(implementationType)} must be marked with InjectableAttribute for reflection registration.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var constructors = implementationType.GetConstructors(BindingFlags.Public | BindingFlags.Instance);
|
||||||
|
if (constructors.Length != 1)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"{FormatType(implementationType)} must declare exactly one public constructor.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var constructor = constructors[0];
|
||||||
|
var parameters = constructor.GetParameters();
|
||||||
|
|
||||||
|
return container =>
|
||||||
|
{
|
||||||
|
var arguments = parameters
|
||||||
|
.Select(parameter => container.Resolve(parameter.ParameterType))
|
||||||
|
.ToArray();
|
||||||
|
return constructor.Invoke(arguments);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatType(Type type) => type.FullName ?? type.Name;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 1d2827e4c16e4b4ea60a84f709d54665
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
My project/Assets/FlowScope/Tests.meta
Normal file
8
My project/Assets/FlowScope/Tests.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 283379933e6c4e008981e1de556732cd
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
My project/Assets/FlowScope/Tests/EditMode.meta
Normal file
8
My project/Assets/FlowScope/Tests/EditMode.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 1d4bc61cf30a4b319edd9c885eb07341
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 0bea5f9c12b040369bc322e19d230e50
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using FlowScope.Container;
|
||||||
|
|
||||||
|
namespace FlowScope.Tests.EditMode.Container
|
||||||
|
{
|
||||||
|
public sealed class ContainerTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void RegisterInstance_Resolve_ReturnsSameInstanceAndDoesNotDisposeExternalInstance()
|
||||||
|
{
|
||||||
|
var container = new FlowScope.Container.Container();
|
||||||
|
var service = new DisposableService();
|
||||||
|
|
||||||
|
container.RegisterInstance<IService>(service);
|
||||||
|
|
||||||
|
Assert.AreSame(service, container.Resolve<IService>());
|
||||||
|
Assert.IsTrue(container.TryResolve<IService>(out var resolved));
|
||||||
|
Assert.AreSame(service, resolved);
|
||||||
|
|
||||||
|
container.Dispose();
|
||||||
|
|
||||||
|
Assert.IsFalse(service.IsDisposed);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void RegisterFactory_Resolve_CreatesOnceAndDisposesInReverseCreationOrder()
|
||||||
|
{
|
||||||
|
var disposeOrder = new List<string>();
|
||||||
|
var container = new FlowScope.Container.Container();
|
||||||
|
container.RegisterFactory<FirstDisposable>(_ => new FirstDisposable(disposeOrder));
|
||||||
|
container.RegisterFactory<SecondDisposable>(_ => new SecondDisposable(disposeOrder));
|
||||||
|
|
||||||
|
var first = container.Resolve<FirstDisposable>();
|
||||||
|
var second = container.Resolve<SecondDisposable>();
|
||||||
|
|
||||||
|
Assert.AreSame(first, container.Resolve<FirstDisposable>());
|
||||||
|
Assert.AreSame(second, container.Resolve<SecondDisposable>());
|
||||||
|
|
||||||
|
container.Dispose();
|
||||||
|
container.Dispose();
|
||||||
|
|
||||||
|
CollectionAssert.AreEqual(new[] { "second", "first" }, disposeOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void CreateScope_Resolve_ReadsParentAndCanOverrideWithoutPollutingParent()
|
||||||
|
{
|
||||||
|
var parent = new FlowScope.Container.Container();
|
||||||
|
var parentService = new NamedService("parent");
|
||||||
|
var childService = new NamedService("child");
|
||||||
|
parent.RegisterInstance<IService>(parentService);
|
||||||
|
|
||||||
|
var child = parent.CreateScope();
|
||||||
|
|
||||||
|
Assert.AreSame(parentService, child.Resolve<IService>());
|
||||||
|
|
||||||
|
child.RegisterInstance<IService>(childService);
|
||||||
|
|
||||||
|
Assert.AreSame(childService, child.Resolve<IService>());
|
||||||
|
Assert.AreSame(parentService, parent.Resolve<IService>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void RegisterInstance_WhenSameScopeAlreadyRegistered_Throws()
|
||||||
|
{
|
||||||
|
var container = new FlowScope.Container.Container();
|
||||||
|
container.RegisterInstance<IService>(new NamedService("first"));
|
||||||
|
|
||||||
|
var ex = Assert.Throws<InvalidOperationException>(
|
||||||
|
() => container.RegisterInstance<IService>(new NamedService("second")));
|
||||||
|
|
||||||
|
StringAssert.Contains("already registered", ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Dispose_ParentDisposesChildScopesBeforeOwnInstances()
|
||||||
|
{
|
||||||
|
var disposeOrder = new List<string>();
|
||||||
|
var parent = new FlowScope.Container.Container();
|
||||||
|
parent.RegisterFactory<FirstDisposable>(_ => new FirstDisposable(disposeOrder));
|
||||||
|
var child = parent.CreateScope();
|
||||||
|
child.RegisterFactory<SecondDisposable>(_ => new SecondDisposable(disposeOrder));
|
||||||
|
|
||||||
|
parent.Resolve<FirstDisposable>();
|
||||||
|
child.Resolve<SecondDisposable>();
|
||||||
|
|
||||||
|
parent.Dispose();
|
||||||
|
|
||||||
|
CollectionAssert.AreEqual(new[] { "second", "first" }, disposeOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void RegisterType_Resolve_UsesGeneratedFactoryEntryPointForConstructorInjection()
|
||||||
|
{
|
||||||
|
var container = new FlowScope.Container.Container();
|
||||||
|
container.RegisterInstance<IService>(new NamedService("dependency"));
|
||||||
|
GeneratedFactories.Register<ConstructorInjectedService>(
|
||||||
|
c => new ConstructorInjectedService(c.Resolve<IService>()));
|
||||||
|
|
||||||
|
container.RegisterType<ConstructorInjectedService>();
|
||||||
|
|
||||||
|
var resolved = container.Resolve<ConstructorInjectedService>();
|
||||||
|
|
||||||
|
Assert.AreEqual("dependency", resolved.Service.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ReflectionFactoryBuilder_RegisterType_ResolvesConstructorDependencies()
|
||||||
|
{
|
||||||
|
var container = new FlowScope.Container.Container();
|
||||||
|
container.RegisterInstance<IService>(new NamedService("reflection"));
|
||||||
|
|
||||||
|
ReflectionFactoryBuilder.RegisterType<ConstructorInjectedService>(container);
|
||||||
|
|
||||||
|
var resolved = container.Resolve<ConstructorInjectedService>();
|
||||||
|
|
||||||
|
Assert.AreEqual("reflection", resolved.Service.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void RegisterType_WhenFactoryEntryPointMissing_ThrowsClearError()
|
||||||
|
{
|
||||||
|
var container = new FlowScope.Container.Container();
|
||||||
|
|
||||||
|
var ex = Assert.Throws<InvalidOperationException>(() => container.RegisterType<UnregisteredGeneratedService>());
|
||||||
|
|
||||||
|
StringAssert.Contains("RegisterFactory", ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Resolve_WhenDependencyCycleExists_ThrowsWithCompleteDependencyChain()
|
||||||
|
{
|
||||||
|
var container = new FlowScope.Container.Container();
|
||||||
|
ReflectionFactoryBuilder.RegisterType<CycleA>(container);
|
||||||
|
ReflectionFactoryBuilder.RegisterType<CycleB>(container);
|
||||||
|
|
||||||
|
var ex = Assert.Throws<InvalidOperationException>(() => container.Resolve<CycleA>());
|
||||||
|
|
||||||
|
StringAssert.Contains(nameof(CycleA), ex.Message);
|
||||||
|
StringAssert.Contains(nameof(CycleB), ex.Message);
|
||||||
|
StringAssert.Contains($"{nameof(CycleA)} -> {nameof(CycleB)} -> {nameof(CycleA)}", ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void DisposedContainer_RegisterAndResolveThrowObjectDisposedException()
|
||||||
|
{
|
||||||
|
var container = new FlowScope.Container.Container();
|
||||||
|
|
||||||
|
container.Dispose();
|
||||||
|
|
||||||
|
Assert.Throws<ObjectDisposedException>(() => container.RegisterInstance<IService>(new NamedService("late")));
|
||||||
|
Assert.Throws<ObjectDisposedException>(() => container.RegisterFactory<IService>(_ => new NamedService("late")));
|
||||||
|
Assert.Throws<ObjectDisposedException>(() => container.Resolve<IService>());
|
||||||
|
Assert.Throws<ObjectDisposedException>(() => container.CreateScope());
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface IService
|
||||||
|
{
|
||||||
|
string Name { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class NamedService : IService
|
||||||
|
{
|
||||||
|
public NamedService(string name) => Name = name;
|
||||||
|
public string Name { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class DisposableService : IService, IDisposable
|
||||||
|
{
|
||||||
|
public string Name => "disposable";
|
||||||
|
public bool IsDisposed { get; private set; }
|
||||||
|
public void Dispose() => IsDisposed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class FirstDisposable : IDisposable
|
||||||
|
{
|
||||||
|
private readonly List<string> _disposeOrder;
|
||||||
|
public FirstDisposable(List<string> disposeOrder) => _disposeOrder = disposeOrder;
|
||||||
|
public void Dispose() => _disposeOrder.Add("first");
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class SecondDisposable : IDisposable
|
||||||
|
{
|
||||||
|
private readonly List<string> _disposeOrder;
|
||||||
|
public SecondDisposable(List<string> disposeOrder) => _disposeOrder = disposeOrder;
|
||||||
|
public void Dispose() => _disposeOrder.Add("second");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Injectable]
|
||||||
|
private sealed class ConstructorInjectedService
|
||||||
|
{
|
||||||
|
public ConstructorInjectedService(IService service) => Service = service;
|
||||||
|
public IService Service { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Injectable]
|
||||||
|
private sealed class UnregisteredGeneratedService
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[Injectable]
|
||||||
|
private sealed class CycleA
|
||||||
|
{
|
||||||
|
public CycleA(CycleB dependency) => Dependency = dependency;
|
||||||
|
public CycleB Dependency { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Injectable]
|
||||||
|
private sealed class CycleB
|
||||||
|
{
|
||||||
|
public CycleB(CycleA dependency) => Dependency = dependency;
|
||||||
|
public CycleA Dependency { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d5d96cf26ce24c8988c911eb9e73f1fb
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
Reference in New Issue
Block a user