实现 P0 容器核心能力

This commit is contained in:
JSD\13999
2026-05-18 11:49:00 +08:00
parent 49318c50a4
commit b1018c120d
13 changed files with 698 additions and 10 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: eab40aadc4ea4632930155f91c6faca4
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,21 +1,261 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace FlowScope.Container
{
public sealed class Container : IDisposable
{
public void RegisterInstance<T>(T instance) => throw new NotImplementedException();
public void RegisterFactory<T>(Func<Container, T> factory) => throw new NotImplementedException();
public void RegisterType<TInterface, TImplementation>() where TImplementation : TInterface => throw new NotImplementedException();
public void RegisterType<TImplementation>() => throw new NotImplementedException();
public void RegisterAssembly() => throw new NotImplementedException();
public T Resolve<T>() => throw new NotImplementedException();
[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)
{
value = default;
throw new NotImplementedException();
ThrowIfDisposed();
if (!TryFindRegistration(typeof(T), out var registration))
{
value = default;
return false;
}
value = (T)ResolveRegistration(typeof(T), registration);
return true;
}
public Container CreateScope() => throw new NotImplementedException();
public void Dispose() => throw new NotImplementedException();
public Container CreateScope()
{
ThrowIfDisposed();
var child = new Container(this);
_children.Add(child);
return child;
}
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
var exceptions = new List<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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d024d7ed8fd642b6948a216b3a676e22
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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);
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fc50e3c76ee147b59d2663e872ced1a0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1d2827e4c16e4b4ea60a84f709d54665
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 283379933e6c4e008981e1de556732cd
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1d4bc61cf30a4b319edd9c885eb07341
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 0bea5f9c12b040369bc322e19d230e50
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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; }
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d5d96cf26ce24c8988c911eb9e73f1fb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: