feat(core): 实现架构作用域容器核心

This commit is contained in:
JSD\13999
2026-06-18 10:25:32 +08:00
parent 6e45152250
commit 76016d35c1
41 changed files with 1455 additions and 326 deletions

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 8d24db89928e4010a3c1af79ffb15709
fileFormatVersion: 2
guid: 4bd6a11cc58df434eb82db6a3fe8362f
folderAsset: yes
DefaultImporter:
externalObjects: {}

View File

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

View File

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

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
namespace FlowScope
{
public interface IFeature
{
void Install(IArchitectureScope scope);
}
}

View File

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

View File

@@ -0,0 +1,9 @@
namespace FlowScope
{
public interface IRegistrationBuilder
{
void AsSingleton();
void PerScope();
void AsTransient();
}
}

View File

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

View File

@@ -0,0 +1,10 @@
namespace FlowScope
{
public interface IScopeInfo
{
string Name { get; }
IScopeInfo Parent { get; }
string Path { get; }
bool IsDisposed { get; }
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
fileFormatVersion: 2
fileFormatVersion: 2
guid: 0bea5f9c12b040369bc322e19d230e50
folderAsset: yes
DefaultImporter:

View File

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

View File

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