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
|
||||
guid: 8d24db89928e4010a3c1af79ffb15709
|
||||
fileFormatVersion: 2
|
||||
guid: 4bd6a11cc58df434eb82db6a3fe8362f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
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
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using FlowScope.Container;
|
||||
@@ -10,7 +10,7 @@ namespace FlowScope.Tests.EditMode.Container
|
||||
[Test]
|
||||
public void RegisterInstance_Resolve_ReturnsSameInstanceAndDoesNotDisposeExternalInstance()
|
||||
{
|
||||
var container = new FlowScope.Container.Container();
|
||||
var container = new global::FlowScope.Container.Container();
|
||||
var service = new DisposableService();
|
||||
|
||||
container.RegisterInstance<IService>(service);
|
||||
@@ -28,7 +28,7 @@ namespace FlowScope.Tests.EditMode.Container
|
||||
public void RegisterFactory_Resolve_CreatesOnceAndDisposesInReverseCreationOrder()
|
||||
{
|
||||
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<SecondDisposable>(_ => new SecondDisposable(disposeOrder));
|
||||
|
||||
@@ -47,7 +47,7 @@ namespace FlowScope.Tests.EditMode.Container
|
||||
[Test]
|
||||
public void CreateScope_Resolve_ReadsParentAndCanOverrideWithoutPollutingParent()
|
||||
{
|
||||
var parent = new FlowScope.Container.Container();
|
||||
var parent = new global::FlowScope.Container.Container();
|
||||
var parentService = new NamedService("parent");
|
||||
var childService = new NamedService("child");
|
||||
parent.RegisterInstance<IService>(parentService);
|
||||
@@ -62,10 +62,81 @@ namespace FlowScope.Tests.EditMode.Container
|
||||
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]
|
||||
public void RegisterInstance_WhenSameScopeAlreadyRegistered_Throws()
|
||||
{
|
||||
var container = new FlowScope.Container.Container();
|
||||
var container = new global::FlowScope.Container.Container();
|
||||
container.RegisterInstance<IService>(new NamedService("first"));
|
||||
|
||||
var ex = Assert.Throws<InvalidOperationException>(
|
||||
@@ -78,7 +149,7 @@ namespace FlowScope.Tests.EditMode.Container
|
||||
public void Dispose_ParentDisposesChildScopesBeforeOwnInstances()
|
||||
{
|
||||
var disposeOrder = new List<string>();
|
||||
var parent = new FlowScope.Container.Container();
|
||||
var parent = new global::FlowScope.Container.Container();
|
||||
parent.RegisterFactory<FirstDisposable>(_ => new FirstDisposable(disposeOrder));
|
||||
var child = parent.CreateScope();
|
||||
child.RegisterFactory<SecondDisposable>(_ => new SecondDisposable(disposeOrder));
|
||||
@@ -94,7 +165,7 @@ namespace FlowScope.Tests.EditMode.Container
|
||||
[Test]
|
||||
public void RegisterType_Resolve_UsesGeneratedFactoryEntryPointForConstructorInjection()
|
||||
{
|
||||
var container = new FlowScope.Container.Container();
|
||||
var container = new global::FlowScope.Container.Container();
|
||||
container.RegisterInstance<IService>(new NamedService("dependency"));
|
||||
GeneratedFactories.Register<ConstructorInjectedService>(
|
||||
c => new ConstructorInjectedService(c.Resolve<IService>()));
|
||||
@@ -109,7 +180,7 @@ namespace FlowScope.Tests.EditMode.Container
|
||||
[Test]
|
||||
public void ReflectionFactoryBuilder_RegisterType_ResolvesConstructorDependencies()
|
||||
{
|
||||
var container = new FlowScope.Container.Container();
|
||||
var container = new global::FlowScope.Container.Container();
|
||||
container.RegisterInstance<IService>(new NamedService("reflection"));
|
||||
|
||||
ReflectionFactoryBuilder.RegisterType<ConstructorInjectedService>(container);
|
||||
@@ -122,7 +193,7 @@ namespace FlowScope.Tests.EditMode.Container
|
||||
[Test]
|
||||
public void RegisterType_WhenFactoryEntryPointMissing_ThrowsClearError()
|
||||
{
|
||||
var container = new FlowScope.Container.Container();
|
||||
var container = new global::FlowScope.Container.Container();
|
||||
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => container.RegisterType<UnregisteredGeneratedService>());
|
||||
|
||||
@@ -132,7 +203,7 @@ namespace FlowScope.Tests.EditMode.Container
|
||||
[Test]
|
||||
public void Resolve_WhenDependencyCycleExists_ThrowsWithCompleteDependencyChain()
|
||||
{
|
||||
var container = new FlowScope.Container.Container();
|
||||
var container = new global::FlowScope.Container.Container();
|
||||
ReflectionFactoryBuilder.RegisterType<CycleA>(container);
|
||||
ReflectionFactoryBuilder.RegisterType<CycleB>(container);
|
||||
|
||||
@@ -146,7 +217,7 @@ namespace FlowScope.Tests.EditMode.Container
|
||||
[Test]
|
||||
public void DisposedContainer_RegisterAndResolveThrowObjectDisposedException()
|
||||
{
|
||||
var container = new FlowScope.Container.Container();
|
||||
var container = new global::FlowScope.Container.Container();
|
||||
|
||||
container.Dispose();
|
||||
|
||||
@@ -163,10 +234,20 @@ namespace FlowScope.Tests.EditMode.Container
|
||||
|
||||
private sealed class NamedService : IService
|
||||
{
|
||||
public NamedService()
|
||||
: this("named")
|
||||
{
|
||||
}
|
||||
|
||||
public NamedService(string name) => Name = name;
|
||||
public string Name { get; }
|
||||
}
|
||||
|
||||
private sealed class OtherNamedService : IService
|
||||
{
|
||||
public string Name => "other";
|
||||
}
|
||||
|
||||
private sealed class DisposableService : IService, IDisposable
|
||||
{
|
||||
public string Name => "disposable";
|
||||
@@ -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 FeatureContext Context { get; private set; }
|
||||
|
||||
Reference in New Issue
Block a user