2 Commits

Author SHA1 Message Date
JSD\13999
b87465d80f refactor(scope): 收束架构作用域容器边界 2026-06-18 12:20:20 +08:00
JSD\13999
76016d35c1 feat(core): 实现架构作用域容器核心 2026-06-18 10:25:32 +08:00
214 changed files with 3870 additions and 8815 deletions

View File

@@ -1,47 +0,0 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using FlowScope.Resources;
using UnityAddressables = UnityEngine.AddressableAssets.Addressables;
namespace FlowScope.Addressables
{
public sealed class AddressablesResourceBackend : IResourceBackend
{
public string Name => "Addressables";
public async Task<T> LoadAsync<T>(string key, CancellationToken cancellationToken)
where T : class
{
var handle = UnityAddressables.LoadAssetAsync<T>(key);
await WaitWithCancellation(handle.Task, cancellationToken);
return handle.Result;
}
public void Release<T>(string key, T asset)
where T : class
{
UnityAddressables.Release(asset);
}
private static async Task WaitWithCancellation(Task task, CancellationToken cancellationToken)
{
if (!cancellationToken.CanBeCanceled)
{
await task;
return;
}
var cancellation = new TaskCompletionSource<bool>();
using (cancellationToken.Register(() => cancellation.TrySetResult(true)))
{
if (await Task.WhenAny(task, cancellation.Task) == cancellation.Task)
{
throw new OperationCanceledException(cancellationToken);
}
}
await task;
}
}
}

View File

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

View File

@@ -1,38 +0,0 @@
using System.Threading;
using System.Threading.Tasks;
using FlowScope.Addressables;
namespace FlowScope.Resources
{
public sealed class AddressablesResourceService : IResourceService, ICompletedResourceService
{
private readonly ResourceService _inner;
public AddressablesResourceService()
: this(new AddressablesResourceBackend())
{
}
public AddressablesResourceService(IResourceBackend backend)
{
_inner = new ResourceService(backend);
}
public Task<IResourceHandle<T>> LoadAsync<T>(string key, CancellationToken cancellationToken)
where T : class
{
return _inner.LoadAsync<T>(key, cancellationToken);
}
public IResourceGroup CreateGroup()
{
return _inner.CreateGroup();
}
public bool TryLoadCompleted<T>(string key, out IResourceHandle<T> handle)
where T : class
{
return _inner.TryLoadCompleted(key, out handle);
}
}
}

View File

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

View File

@@ -1,18 +0,0 @@
{
"name": "FlowScope.Addressables",
"rootNamespace": "FlowScope.Addressables",
"references": [
"FlowScope.Runtime",
"Unity.Addressables",
"Unity.ResourceManager"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 917e69d4247b4f548a5951d84e7d35db
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -1,25 +0,0 @@
using System;
namespace FlowScope.Audio
{
public sealed class AudioHandle : IAudioHandle
{
private readonly Func<bool> _isPlaying;
private readonly Action<float> _stop;
internal AudioHandle(Func<bool> isPlaying, Action<float> stop)
{
_isPlaying = isPlaying ?? throw new ArgumentNullException(nameof(isPlaying));
_stop = stop ?? throw new ArgumentNullException(nameof(stop));
}
public bool IsPlaying => _isPlaying();
public void Stop(float fadeOut = 0f)
{
_stop(fadeOut);
}
internal static AudioHandle Stopped { get; } = new AudioHandle(() => false, _ => { });
}
}

View File

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

View File

@@ -1,392 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using FlowScope.Resources;
using UnityEngine;
namespace FlowScope.Audio
{
public sealed class AudioService : MonoBehaviour, IAudioService
{
private readonly List<SfxSlot> _sfxSlots = new();
private IResourceService _resources;
private AudioServiceConfig _config;
private AudioSource _bgmSource;
private IResourceHandle<AudioClip> _bgmResource;
private Coroutine _bgmFade;
private float _bgmVolume = 1f;
private float _sfxVolume = 1f;
private bool _mute;
private int _playSequence;
public AudioService Initialize(IResourceService resources, AudioServiceConfig config = null)
{
_resources = resources ?? throw new ArgumentNullException(nameof(resources));
_config = config ?? new AudioServiceConfig();
if (_config.SfxPoolSize < 0)
{
_config.SfxPoolSize = 0;
}
return this;
}
public IAudioHandle PlayBgm(string key, bool loop = true, float fadeIn = 0f)
{
EnsureInitialized();
var clipHandle = LoadClipIfCompleted(key);
return PlayBgm(clipHandle, loop, fadeIn);
}
public async Task<IAudioHandle> PlayBgmAsync(
string key,
bool loop = true,
float fadeIn = 0f,
CancellationToken cancellationToken = default)
{
EnsureInitialized();
var clipHandle = await LoadClipAsync(key, cancellationToken);
return PlayBgm(clipHandle, loop, fadeIn);
}
public IAudioHandle PlaySfx(string key)
{
EnsureInitialized();
var clipHandle = LoadClipIfCompleted(key);
var slot = GetSfxSlot(key);
if (slot == null)
{
clipHandle.Dispose();
return AudioHandle.Stopped;
}
return PlaySfx(slot, clipHandle);
}
public async Task<IAudioHandle> PlaySfxAsync(
string key,
CancellationToken cancellationToken = default)
{
EnsureInitialized();
var clipHandle = await LoadClipAsync(key, cancellationToken);
var slot = GetSfxSlot(key);
if (slot == null)
{
clipHandle.Dispose();
return AudioHandle.Stopped;
}
return PlaySfx(slot, clipHandle);
}
public void StopBgm(float fadeOut = 0f)
{
StopBgmSource(_bgmSource, fadeOut);
}
public void SetBgmVolume(float volume)
{
_bgmVolume = Mathf.Clamp01(volume);
if (_bgmSource != null)
{
_bgmSource.volume = EffectiveBgmVolume;
}
}
public void SetSfxVolume(float volume)
{
_sfxVolume = Mathf.Clamp01(volume);
foreach (var slot in _sfxSlots)
{
if (slot.Source != null)
{
slot.Source.volume = EffectiveSfxVolume;
}
}
}
public void SetMute(bool mute)
{
_mute = mute;
if (_bgmSource != null)
{
_bgmSource.volume = EffectiveBgmVolume;
}
foreach (var slot in _sfxSlots)
{
if (slot.Source != null)
{
slot.Source.volume = EffectiveSfxVolume;
}
}
}
private IAudioHandle PlayBgm(IResourceHandle<AudioClip> clipHandle, bool loop, float fadeIn)
{
StopBgm();
_bgmSource = gameObject.AddComponent<AudioSource>();
_bgmResource = clipHandle;
_bgmSource.clip = clipHandle.Asset;
_bgmSource.loop = loop;
_bgmSource.volume = fadeIn > 0f ? 0f : EffectiveBgmVolume;
_bgmSource.Play();
if (fadeIn > 0f)
{
_bgmFade = StartCoroutine(FadeVolume(_bgmSource, 0f, EffectiveBgmVolume, fadeIn, null));
}
var source = _bgmSource;
return new AudioHandle(
() => source != null && source.isPlaying,
fadeOut => StopBgmSource(source, fadeOut));
}
private IAudioHandle PlaySfx(SfxSlot slot, IResourceHandle<AudioClip> clipHandle)
{
slot.Release();
slot.Resource = clipHandle;
slot.Source.clip = clipHandle.Asset;
slot.Source.loop = false;
slot.Source.volume = EffectiveSfxVolume;
slot.Sequence = ++_playSequence;
slot.Source.Play();
return new AudioHandle(
() => slot.Source != null && slot.Source.isPlaying,
fadeOut => StopSfxSlot(slot, fadeOut));
}
private float EffectiveBgmVolume => _mute ? 0f : _bgmVolume;
private float EffectiveSfxVolume => _mute ? 0f : _sfxVolume;
private void Update()
{
foreach (var slot in _sfxSlots)
{
if (slot.Source != null && !slot.Source.isPlaying)
{
slot.Release();
}
}
}
private void OnDestroy()
{
StopBgm();
foreach (var slot in _sfxSlots)
{
slot.Release();
}
}
private IResourceHandle<AudioClip> LoadClipIfCompleted(string key)
{
try
{
if (!(_resources is ICompletedResourceService completedResources) ||
!completedResources.TryLoadCompleted(key, out IResourceHandle<AudioClip> handle))
{
throw new InvalidOperationException(
$"Audio clip is not loaded synchronously: {key}. Use PlayBgmAsync or PlaySfxAsync.");
}
return ValidateLoadedClip(key, handle);
}
catch (Exception exception)
{
throw new InvalidOperationException($"Failed to load audio clip '{key}'.", exception);
}
}
private async Task<IResourceHandle<AudioClip>> LoadClipAsync(
string key,
CancellationToken cancellationToken)
{
try
{
var handle = await _resources.LoadAsync<AudioClip>(key, cancellationToken);
return ValidateLoadedClip(key, handle);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception exception)
{
throw new InvalidOperationException($"Failed to load audio clip '{key}'.", exception);
}
}
private static IResourceHandle<AudioClip> ValidateLoadedClip(
string key,
IResourceHandle<AudioClip> handle)
{
if (handle == null || handle.Asset == null)
{
handle?.Dispose();
throw new InvalidOperationException($"Audio clip is missing: {key}");
}
return handle;
}
private SfxSlot GetSfxSlot(string key)
{
foreach (var slot in _sfxSlots)
{
if (slot.Source != null && !slot.Source.isPlaying)
{
return slot;
}
}
if (_sfxSlots.Count < _config.SfxPoolSize)
{
var source = gameObject.AddComponent<AudioSource>();
var slot = new SfxSlot(source);
_sfxSlots.Add(slot);
return slot;
}
if (_config.ReuseOldestWhenExhausted && _sfxSlots.Count > 0)
{
var oldest = _sfxSlots[0];
foreach (var slot in _sfxSlots)
{
if (slot.Sequence < oldest.Sequence)
{
oldest = slot;
}
}
oldest.Source.Stop();
return oldest;
}
Debug.LogWarning($"SFX pool exhausted, skipped key: {key}");
return null;
}
private void StopBgmSource(AudioSource source, float fadeOut)
{
if (source == null || source != _bgmSource)
{
return;
}
if (_bgmFade != null)
{
StopCoroutine(_bgmFade);
_bgmFade = null;
}
if (fadeOut > 0f && source.isPlaying)
{
_bgmFade = StartCoroutine(FadeVolume(
source,
source.volume,
0f,
fadeOut,
() => CompleteBgmStop(source)));
return;
}
CompleteBgmStop(source);
}
private void CompleteBgmStop(AudioSource source)
{
if (source == null || source != _bgmSource)
{
return;
}
source.Stop();
source.volume = 0f;
Destroy(source);
_bgmSource = null;
_bgmFade = null;
_bgmResource?.Dispose();
_bgmResource = null;
}
private void StopSfxSlot(SfxSlot slot, float fadeOut)
{
if (slot?.Source == null || !slot.Source.isPlaying)
{
return;
}
if (fadeOut > 0f)
{
StartCoroutine(FadeVolume(
slot.Source,
slot.Source.volume,
0f,
fadeOut,
() =>
{
slot.Source.Stop();
slot.Release();
}));
return;
}
slot.Source.Stop();
slot.Release();
}
private IEnumerator FadeVolume(
AudioSource source,
float from,
float to,
float duration,
Action complete)
{
var elapsed = 0f;
while (elapsed < duration && source != null)
{
elapsed += Time.deltaTime;
source.volume = Mathf.Lerp(from, to, Mathf.Clamp01(elapsed / duration));
yield return null;
}
if (source != null)
{
source.volume = to;
}
complete?.Invoke();
}
private void EnsureInitialized()
{
if (_resources == null)
{
throw new InvalidOperationException("AudioService must be initialized before use.");
}
}
private sealed class SfxSlot
{
public SfxSlot(AudioSource source)
{
Source = source;
}
public AudioSource Source { get; }
public IResourceHandle<AudioClip> Resource { get; set; }
public int Sequence { get; set; }
public void Release()
{
Resource?.Dispose();
Resource = null;
}
}
}
}

View File

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

View File

@@ -1,8 +0,0 @@
namespace FlowScope.Audio
{
public sealed class AudioServiceConfig
{
public int SfxPoolSize = 10;
public bool ReuseOldestWhenExhausted = true;
}
}

View File

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

View File

@@ -1,8 +0,0 @@
namespace FlowScope.Audio
{
public interface IAudioHandle
{
void Stop(float fadeOut = 0f);
bool IsPlaying { get; }
}
}

View File

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

View File

@@ -1,25 +0,0 @@
using System.Threading;
using System.Threading.Tasks;
namespace FlowScope.Audio
{
public interface IAudioService
{
IAudioHandle PlayBgm(string key, bool loop = true, float fadeIn = 0f);
Task<IAudioHandle> PlayBgmAsync(
string key,
bool loop = true,
float fadeIn = 0f,
CancellationToken cancellationToken = default);
IAudioHandle PlaySfx(string key);
Task<IAudioHandle> PlaySfxAsync(
string key,
CancellationToken cancellationToken = default);
void StopBgm(float fadeOut = 0f);
void SetBgmVolume(float volume);
void SetSfxVolume(float volume);
void SetMute(bool mute);
}
}

View File

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

View File

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

View File

@@ -1,22 +0,0 @@
using System.Collections.Generic;
namespace FlowScope.Common
{
public sealed class ResultLog
{
private readonly List<string> _warnings = new();
private readonly List<string> _errors = new();
public IReadOnlyList<string> Warnings => _warnings;
public IReadOnlyList<string> Errors => _errors;
public bool HasErrors => _errors.Count > 0;
public void AddWarning(string message) => _warnings.Add(message);
public void AddError(string message) => _errors.Add(message);
public void Clear()
{
_warnings.Clear();
_errors.Clear();
}
}
}

View File

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

View File

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

View File

@@ -1,31 +0,0 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace FlowScope.Config
{
public sealed class FileConfigSource : IConfigSource
{
private readonly string _rootPath;
public FileConfigSource(string rootPath, string name = "File")
{
_rootPath = string.IsNullOrWhiteSpace(rootPath)
? throw new ArgumentException("Config root path is required.", nameof(rootPath))
: rootPath;
Name = string.IsNullOrWhiteSpace(name)
? throw new ArgumentException("Config source name is required.", nameof(name))
: name;
}
public string Name { get; }
public Task<string> LoadTextAsync(string fileName, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var path = Path.Combine(_rootPath, fileName);
return Task.Run(() => File.ReadAllText(path), cancellationToken);
}
}
}

View File

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

View File

@@ -1,14 +0,0 @@
using System.Collections.Generic;
namespace FlowScope.Config
{
public interface IConfigParser
{
string Format { get; }
IReadOnlyList<T> ParseRows<T>(
string fileName,
string text)
where T : class, IConfigRow;
}
}

View File

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

View File

@@ -1,13 +0,0 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace FlowScope.Config
{
public interface IConfigProvider
{
Task LoadAllAsync(CancellationToken cancellationToken);
T Get<T>(int id) where T : class, IConfigRow;
IReadOnlyList<T> GetAll<T>() where T : class, IConfigRow;
}
}

View File

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

View File

@@ -1,7 +0,0 @@
namespace FlowScope.Config
{
public interface IConfigRow
{
int Id { get; }
}
}

View File

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

View File

@@ -1,14 +0,0 @@
using System.Threading;
using System.Threading.Tasks;
namespace FlowScope.Config
{
public interface IConfigSource
{
string Name { get; }
Task<string> LoadTextAsync(
string fileName,
CancellationToken cancellationToken);
}
}

View File

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

View File

@@ -1,37 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace FlowScope.Config
{
public sealed class InMemoryConfigSource : IConfigSource
{
private readonly Dictionary<string, string> _files;
public InMemoryConfigSource(
IReadOnlyDictionary<string, string> files,
string name = "InMemory")
{
_files = files != null
? new Dictionary<string, string>(files)
: throw new ArgumentNullException(nameof(files));
Name = string.IsNullOrWhiteSpace(name)
? throw new ArgumentException("Config source name is required.", nameof(name))
: name;
}
public string Name { get; }
public Task<string> LoadTextAsync(string fileName, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (!_files.TryGetValue(fileName, out var text))
{
throw new InvalidOperationException($"Config file is missing: {fileName}");
}
return Task.FromResult(text);
}
}
}

View File

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

View File

@@ -1,16 +0,0 @@
using System.Collections.Generic;
using FlowScope.Data;
namespace FlowScope.Config
{
public sealed class JsonConfigParser : IConfigParser
{
public string Format => "json";
public IReadOnlyList<T> ParseRows<T>(string fileName, string text)
where T : class, IConfigRow
{
return ReactivePropertyJsonConverter.DeserializeArray<T>(text);
}
}
}

View File

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

View File

@@ -1,166 +0,0 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
namespace FlowScope.Config
{
public sealed class JsonConfigProvider : IConfigProvider
{
private readonly IReadOnlyList<ConfigMapping> _mappings;
private readonly Dictionary<Type, object> _rowsByType = new();
public JsonConfigProvider(IReadOnlyList<ConfigMapping> mappings)
{
_mappings = mappings ?? throw new ArgumentNullException(nameof(mappings));
}
public static ConfigMapping Mapping<T>(string fileName, Func<CancellationToken, Task<string>> loadTextAsync)
where T : class, IConfigRow
{
return Mapping<T>(
fileName,
new DelegateConfigSource(fileName, loadTextAsync),
new JsonConfigParser());
}
public static ConfigMapping Mapping<T>(
string fileName,
IConfigSource source,
IConfigParser parser)
where T : class, IConfigRow
{
return new ConfigMapping(typeof(T), fileName, source, parser);
}
public async Task LoadAllAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var loadedRows = new Dictionary<Type, object>();
foreach (var mapping in _mappings)
{
cancellationToken.ThrowIfCancellationRequested();
string json;
try
{
json = await mapping.Source.LoadTextAsync(mapping.FileName, cancellationToken);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception exception)
{
throw new InvalidOperationException(
$"Failed to load config file '{mapping.FileName}' from source '{mapping.Source.Name}'.",
exception);
}
try
{
loadedRows[mapping.RowType] = mapping.Parse(json);
}
catch (Exception exception) when (exception is not OperationCanceledException)
{
throw new InvalidOperationException(
$"Failed to parse config file '{mapping.FileName}': {exception.GetBaseException().Message}",
exception);
}
}
_rowsByType.Clear();
foreach (var pair in loadedRows)
{
_rowsByType[pair.Key] = pair.Value;
}
}
public T Get<T>(int id) where T : class, IConfigRow
{
return _rowsByType.TryGetValue(typeof(T), out var table) &&
table is Dictionary<int, T> rows &&
rows.TryGetValue(id, out var row)
? row
: null;
}
public IReadOnlyList<T> GetAll<T>() where T : class, IConfigRow
{
if (!_rowsByType.TryGetValue(typeof(T), out var table) || table is not Dictionary<int, T> rows)
{
return Array.Empty<T>();
}
return new List<T>(rows.Values);
}
public sealed class ConfigMapping
{
private readonly MethodInfo _parseRowsMethod;
internal ConfigMapping(
Type rowType,
string fileName,
IConfigSource source,
IConfigParser parser)
{
RowType = rowType ?? throw new ArgumentNullException(nameof(rowType));
FileName = string.IsNullOrWhiteSpace(fileName)
? throw new ArgumentException("Config file name is required.", nameof(fileName))
: fileName;
Source = source ?? throw new ArgumentNullException(nameof(source));
Parser = parser ?? throw new ArgumentNullException(nameof(parser));
_parseRowsMethod = typeof(IConfigParser)
.GetMethod(nameof(IConfigParser.ParseRows))
.MakeGenericMethod(rowType);
}
public Type RowType { get; }
public string FileName { get; }
public IConfigSource Source { get; }
public IConfigParser Parser { get; }
internal object Parse(string json)
{
var rows = (System.Collections.IEnumerable)_parseRowsMethod.Invoke(
Parser,
new object[] { FileName, json });
var byIdType = typeof(Dictionary<,>).MakeGenericType(typeof(int), RowType);
var byId = (System.Collections.IDictionary)Activator.CreateInstance(byIdType);
foreach (IConfigRow row in rows)
{
if (byId.Contains(row.Id))
{
throw new InvalidOperationException($"Duplicate config id {row.Id} for {RowType.Name}.");
}
byId.Add(row.Id, row);
}
return byId;
}
}
private sealed class DelegateConfigSource : IConfigSource
{
private readonly Func<CancellationToken, Task<string>> _loadTextAsync;
public DelegateConfigSource(
string fileName,
Func<CancellationToken, Task<string>> loadTextAsync)
{
Name = fileName;
_loadTextAsync = loadTextAsync ?? throw new ArgumentNullException(nameof(loadTextAsync));
}
public string Name { get; }
public Task<string> LoadTextAsync(string fileName, CancellationToken cancellationToken)
{
return _loadTextAsync(cancellationToken);
}
}
}
}

View File

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

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

@@ -1,43 +0,0 @@
using System;
using System.Collections.Generic;
namespace FlowScope.Container
{
public static class GeneratedFactories
{
private static readonly Dictionary<Type, Func<Container, object>> Factories = new Dictionary<Type, Func<Container, object>>();
public static void Register<TImplementation>(Func<Container, TImplementation> factory)
{
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}
Factories[typeof(TImplementation)] = c => factory(c);
}
public static bool TryGetFactory(Type implementationType, out Func<Container, object> factory)
{
if (implementationType == null)
{
throw new ArgumentNullException(nameof(implementationType));
}
return Factories.TryGetValue(implementationType, out factory);
}
public static void RegisterAssembly(Container container)
{
if (container == null)
{
throw new ArgumentNullException(nameof(container));
}
foreach (var pair in Factories)
{
container.RegisterFactory(pair.Key, pair.Value);
}
}
}
}

View File

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

View File

@@ -1,9 +0,0 @@
using System;
namespace FlowScope.Container
{
[AttributeUsage(AttributeTargets.Class)]
public sealed class InjectableAttribute : Attribute
{
}
}

View File

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

View File

@@ -1,62 +0,0 @@
using System;
using System.Linq;
using System.Reflection;
namespace FlowScope.Container
{
public static class ReflectionFactoryBuilder
{
public static void RegisterType<TImplementation>(Container container)
{
if (container == null)
{
throw new ArgumentNullException(nameof(container));
}
container.RegisterFactory(typeof(TImplementation), BuildFactory(typeof(TImplementation)));
}
public static void RegisterType<TInterface, TImplementation>(Container container)
where TImplementation : TInterface
{
if (container == null)
{
throw new ArgumentNullException(nameof(container));
}
container.RegisterFactory(typeof(TInterface), BuildFactory(typeof(TImplementation)));
}
public static Func<Container, object> BuildFactory(Type implementationType)
{
if (implementationType == null)
{
throw new ArgumentNullException(nameof(implementationType));
}
if (!Attribute.IsDefined(implementationType, typeof(InjectableAttribute)))
{
throw new InvalidOperationException($"{FormatType(implementationType)} must be marked with InjectableAttribute for reflection registration.");
}
var constructors = implementationType.GetConstructors(BindingFlags.Public | BindingFlags.Instance);
if (constructors.Length != 1)
{
throw new InvalidOperationException($"{FormatType(implementationType)} must declare exactly one public constructor.");
}
var constructor = constructors[0];
var parameters = constructor.GetParameters();
return container =>
{
var arguments = parameters
.Select(parameter => container.Resolve(parameter.ParameterType))
.ToArray();
return constructor.Invoke(arguments);
};
}
private static string FormatType(Type type) => type.FullName ?? type.Name;
}
}

View File

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

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: 67eb0ac45b8b4cf8bafcb604dd6e0e11 guid: c35227ceb2c96da44873807b1908facf
folderAsset: yes folderAsset: yes
DefaultImporter: DefaultImporter:
externalObjects: {} externalObjects: {}

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: e74ee61e34d742b9b44057032c6431bb guid: 4bd6a11cc58df434eb82db6a3fe8362f
folderAsset: yes folderAsset: yes
DefaultImporter: DefaultImporter:
externalObjects: {} 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

@@ -1,5 +1,5 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: 0b6c05d7a1d74798901d3efcc51cb7c4 guid: cce1873306eafbf41b8db1123fd2bd49
MonoImporter: MonoImporter:
externalObjects: {} externalObjects: {}
serializedVersion: 2 serializedVersion: 2

View File

@@ -0,0 +1,330 @@
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 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 RegisterBuilt(ServiceKey serviceKey, ContainerRegistration registration)
{
ThrowIfDisposed();
RegisterLocal(serviceKey, registration);
}
internal void TrackOwnedDisposable(IDisposable disposable)
{
_ownedDisposables.Add(disposable);
}
private ContainerRegistrationBuilder RegisterBuiltFactory(
ServiceKey serviceKey,
Type implementationType)
{
var 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

@@ -1,5 +1,5 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: 0c9f18f342e54f8d9a4dbdc61b74a2d1 guid: 988d85a7d7fa65f4786596348f72e053
MonoImporter: MonoImporter:
externalObjects: {} externalObjects: {}
serializedVersion: 2 serializedVersion: 2

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

@@ -1,5 +1,5 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: a5f4e3fb56394e719bff30e8edd4abaa guid: c3102de921ecf4a40acc99c9bd099adc
MonoImporter: MonoImporter:
externalObjects: {} externalObjects: {}
serializedVersion: 2 serializedVersion: 2

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

@@ -1,5 +1,5 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: a533908611b643259dbfcb681369d012 guid: 4aac2cbe9a0b55b4a847cc7eb58e21e5
MonoImporter: MonoImporter:
externalObjects: {} externalObjects: {}
serializedVersion: 2 serializedVersion: 2

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

@@ -1,731 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Reflection;
using System.Text;
namespace FlowScope.Data
{
public static class ReactivePropertyJsonConverter
{
public static string SerializeObject(object value)
{
return SimpleJson.Write(ToJsonValue(value));
}
public static T DeserializeObject<T>(string json)
{
var instance = (T)Activator.CreateInstance(typeof(T), true);
PopulateObject(json, instance);
return instance;
}
public static void PopulateObject(string json, object target)
{
if (target == null)
{
throw new ArgumentNullException(nameof(target));
}
if (SimpleJson.Parse(json) is not Dictionary<string, object> values)
{
throw new InvalidOperationException($"JSON root must be an object for {target.GetType().Name}.");
}
PopulateFromDictionary(values, target);
}
public static IReadOnlyList<T> DeserializeArray<T>(string json) where T : class
{
if (SimpleJson.Parse(json) is not List<object> rows)
{
throw new InvalidOperationException("JSON root must be an array.");
}
var results = new List<T>(rows.Count);
foreach (var row in rows)
{
if (row is not Dictionary<string, object> rowValues)
{
throw new InvalidOperationException($"JSON row must be an object for {typeof(T).Name}.");
}
var instance = (T)Activator.CreateInstance(typeof(T), true);
PopulateFromDictionary(rowValues, instance);
results.Add(instance);
}
return results;
}
internal static object ToJsonValue(object value)
{
if (value == null)
{
return null;
}
var type = value.GetType();
if (IsSimple(type))
{
return value;
}
if (TryGetReactivePropertyValue(value, out var reactiveValue))
{
return ToJsonValue(reactiveValue);
}
if (IsReactiveCollection(type) || value is IEnumerable && value is not string)
{
var array = new List<object>();
foreach (var item in (IEnumerable)value)
{
array.Add(ToJsonValue(item));
}
return array;
}
var result = new Dictionary<string, object>();
foreach (var property in GetSerializableProperties(type))
{
result[property.Name] = ToJsonValue(property.GetValue(value));
}
foreach (var field in GetSerializableFields(type))
{
result[field.Name] = ToJsonValue(field.GetValue(value));
}
return result;
}
internal static object ConvertValue(object value, Type targetType, string fieldName)
{
if (value == null)
{
if (targetType.IsValueType && Nullable.GetUnderlyingType(targetType) == null)
{
throw new InvalidOperationException($"Field '{fieldName}' cannot be null.");
}
return null;
}
var nullableType = Nullable.GetUnderlyingType(targetType);
if (nullableType != null)
{
return ConvertValue(value, nullableType, fieldName);
}
if (targetType == typeof(string))
{
if (value is string text)
{
return text;
}
throw TypeMismatch(fieldName, targetType, value);
}
if (targetType == typeof(bool))
{
if (value is bool boolValue)
{
return boolValue;
}
throw TypeMismatch(fieldName, targetType, value);
}
if (targetType.IsEnum)
{
if (value is string enumName)
{
return Enum.Parse(targetType, enumName);
}
return Enum.ToObject(targetType, Convert.ToInt32(value, CultureInfo.InvariantCulture));
}
if (IsNumeric(targetType))
{
if (value is string || value is bool)
{
throw TypeMismatch(fieldName, targetType, value);
}
return Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture);
}
if (value is Dictionary<string, object> nested)
{
var instance = Activator.CreateInstance(targetType, true);
PopulateFromDictionary(nested, instance);
return instance;
}
throw TypeMismatch(fieldName, targetType, value);
}
private static void PopulateFromDictionary(IReadOnlyDictionary<string, object> values, object target)
{
var members = GetWritableMembers(target.GetType());
foreach (var pair in values)
{
if (!members.TryGetValue(pair.Key, out var member))
{
continue;
}
PopulateMember(target, member, pair.Value, pair.Key);
}
}
private static void PopulateMember(object target, MemberInfo member, object value, string fieldName)
{
var memberType = GetMemberType(member);
var currentValue = GetMemberValue(member, target);
if (IsReactiveProperty(memberType))
{
var valueProperty = memberType.GetProperty("Value", BindingFlags.Instance | BindingFlags.Public);
var converted = ConvertValue(value, valueProperty.PropertyType, fieldName);
valueProperty.SetValue(currentValue, converted);
return;
}
if (IsReactiveCollection(memberType) || IsMutableCollection(memberType, currentValue))
{
PopulateCollection(currentValue, memberType, value, fieldName);
return;
}
if (CanSet(member))
{
SetMemberValue(member, target, ConvertValue(value, memberType, fieldName));
}
}
private static void PopulateCollection(object collection, Type collectionType, object value, string fieldName)
{
if (value is not List<object> rows)
{
throw TypeMismatch(fieldName, collectionType, value);
}
var itemType = GetCollectionItemType(collectionType);
collectionType.GetMethod("Clear", Type.EmptyTypes)?.Invoke(collection, Array.Empty<object>());
var addMethod = collectionType.GetMethod("Add", new[] { itemType });
if (addMethod == null)
{
throw new InvalidOperationException($"Field '{fieldName}' collection does not expose Add.");
}
foreach (var row in rows)
{
addMethod.Invoke(collection, new[] { ConvertValue(row, itemType, fieldName) });
}
}
private static Dictionary<string, MemberInfo> GetWritableMembers(Type type)
{
var members = new Dictionary<string, MemberInfo>(StringComparer.OrdinalIgnoreCase);
foreach (var property in type.GetProperties(BindingFlags.Instance | BindingFlags.Public))
{
if (property.GetIndexParameters().Length == 0)
{
members[property.Name] = property;
}
}
foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public))
{
members[field.Name] = field;
}
return members;
}
private static IEnumerable<PropertyInfo> GetSerializableProperties(Type type)
{
foreach (var property in type.GetProperties(BindingFlags.Instance | BindingFlags.Public))
{
if (property.GetIndexParameters().Length == 0 && property.CanRead)
{
yield return property;
}
}
}
private static IEnumerable<FieldInfo> GetSerializableFields(Type type)
{
return type.GetFields(BindingFlags.Instance | BindingFlags.Public);
}
private static Type GetMemberType(MemberInfo member)
{
return member switch
{
PropertyInfo property => property.PropertyType,
FieldInfo field => field.FieldType,
_ => throw new NotSupportedException(member.MemberType.ToString())
};
}
private static object GetMemberValue(MemberInfo member, object target)
{
return member switch
{
PropertyInfo property => property.GetValue(target),
FieldInfo field => field.GetValue(target),
_ => null
};
}
private static void SetMemberValue(MemberInfo member, object target, object value)
{
switch (member)
{
case PropertyInfo property:
property.SetValue(target, value);
break;
case FieldInfo field:
field.SetValue(target, value);
break;
}
}
private static bool CanSet(MemberInfo member)
{
return member switch
{
PropertyInfo property => property.CanWrite,
FieldInfo field => !field.IsInitOnly,
_ => false
};
}
private static bool TryGetReactivePropertyValue(object value, out object reactiveValue)
{
reactiveValue = null;
if (!IsReactiveProperty(value.GetType()))
{
return false;
}
reactiveValue = value.GetType().GetProperty("Value", BindingFlags.Instance | BindingFlags.Public)?.GetValue(value);
return true;
}
private static bool IsReactiveProperty(Type type)
{
return type.IsGenericType && type.GetGenericTypeDefinition().FullName == "R3.ReactiveProperty`1";
}
private static bool IsReactiveCollection(Type type)
{
return type.IsGenericType && type.GetGenericTypeDefinition().FullName == "R3.ReactiveCollection`1";
}
private static bool IsMutableCollection(Type type, object value)
{
return value != null &&
type != typeof(string) &&
GetCollectionItemType(type) != null &&
type.GetMethod("Clear", Type.EmptyTypes) != null;
}
private static Type GetCollectionItemType(Type type)
{
if (type.IsArray)
{
return type.GetElementType();
}
if (type.IsGenericType && type.GetGenericArguments().Length == 1)
{
return type.GetGenericArguments()[0];
}
foreach (var interfaceType in type.GetInterfaces())
{
if (interfaceType.IsGenericType &&
interfaceType.GetGenericTypeDefinition() == typeof(ICollection<>))
{
return interfaceType.GetGenericArguments()[0];
}
}
return null;
}
private static bool IsSimple(Type type)
{
type = Nullable.GetUnderlyingType(type) ?? type;
return type.IsPrimitive || type.IsEnum || type == typeof(string) || type == typeof(decimal);
}
private static bool IsNumeric(Type type)
{
type = Nullable.GetUnderlyingType(type) ?? type;
return type == typeof(byte) || type == typeof(sbyte) ||
type == typeof(short) || type == typeof(ushort) ||
type == typeof(int) || type == typeof(uint) ||
type == typeof(long) || type == typeof(ulong) ||
type == typeof(float) || type == typeof(double) ||
type == typeof(decimal);
}
private static InvalidOperationException TypeMismatch(string fieldName, Type targetType, object value)
{
return new InvalidOperationException(
$"Field '{fieldName}' expected {targetType.Name}, but JSON value was {value.GetType().Name}.");
}
internal static class SimpleJson
{
public static object Parse(string json)
{
var parser = new Parser(json);
var value = parser.ParseValue();
parser.SkipWhitespace();
if (!parser.IsEnd)
{
throw new FormatException("Unexpected trailing JSON content.");
}
return value;
}
public static string Write(object value)
{
var builder = new StringBuilder();
WriteValue(builder, value);
return builder.ToString();
}
private static void WriteValue(StringBuilder builder, object value)
{
switch (value)
{
case null:
builder.Append("null");
break;
case string text:
WriteString(builder, text);
break;
case bool boolValue:
builder.Append(boolValue ? "true" : "false");
break;
case IDictionary<string, object> map:
WriteObject(builder, map);
break;
case IEnumerable<object> array:
WriteArray(builder, array);
break;
case float or double or decimal:
builder.Append(Convert.ToString(value, CultureInfo.InvariantCulture));
break;
default:
if (value.GetType().IsEnum)
{
WriteString(builder, value.ToString());
}
else
{
builder.Append(Convert.ToString(value, CultureInfo.InvariantCulture));
}
break;
}
}
private static void WriteObject(StringBuilder builder, IDictionary<string, object> map)
{
builder.Append('{');
var first = true;
foreach (var pair in map)
{
if (!first)
{
builder.Append(',');
}
first = false;
WriteString(builder, pair.Key);
builder.Append(':');
WriteValue(builder, pair.Value);
}
builder.Append('}');
}
private static void WriteArray(StringBuilder builder, IEnumerable<object> values)
{
builder.Append('[');
var first = true;
foreach (var value in values)
{
if (!first)
{
builder.Append(',');
}
first = false;
WriteValue(builder, value);
}
builder.Append(']');
}
private static void WriteString(StringBuilder builder, string value)
{
builder.Append('"');
foreach (var character in value)
{
switch (character)
{
case '"':
builder.Append("\\\"");
break;
case '\\':
builder.Append("\\\\");
break;
case '\n':
builder.Append("\\n");
break;
case '\r':
builder.Append("\\r");
break;
case '\t':
builder.Append("\\t");
break;
default:
builder.Append(character);
break;
}
}
builder.Append('"');
}
private sealed class Parser
{
private readonly string _json;
private int _index;
public Parser(string json)
{
_json = json ?? throw new ArgumentNullException(nameof(json));
}
public bool IsEnd => _index >= _json.Length;
public object ParseValue()
{
SkipWhitespace();
if (IsEnd)
{
throw new FormatException("Unexpected end of JSON.");
}
return _json[_index] switch
{
'{' => ParseObject(),
'[' => ParseArray(),
'"' => ParseString(),
't' => ParseLiteral("true", true),
'f' => ParseLiteral("false", false),
'n' => ParseLiteral("null", null),
_ => ParseNumber()
};
}
public void SkipWhitespace()
{
while (!IsEnd && char.IsWhiteSpace(_json[_index]))
{
_index++;
}
}
private Dictionary<string, object> ParseObject()
{
Expect('{');
var map = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
SkipWhitespace();
if (TryConsume('}'))
{
return map;
}
while (true)
{
SkipWhitespace();
var key = ParseString();
SkipWhitespace();
Expect(':');
map[key] = ParseValue();
SkipWhitespace();
if (TryConsume('}'))
{
return map;
}
Expect(',');
}
}
private List<object> ParseArray()
{
Expect('[');
var array = new List<object>();
SkipWhitespace();
if (TryConsume(']'))
{
return array;
}
while (true)
{
array.Add(ParseValue());
SkipWhitespace();
if (TryConsume(']'))
{
return array;
}
Expect(',');
}
}
private string ParseString()
{
Expect('"');
var builder = new StringBuilder();
while (!IsEnd)
{
var character = _json[_index++];
if (character == '"')
{
return builder.ToString();
}
if (character != '\\')
{
builder.Append(character);
continue;
}
if (IsEnd)
{
throw new FormatException("Unexpected end of JSON escape sequence.");
}
var escaped = _json[_index++];
builder.Append(escaped switch
{
'"' => '"',
'\\' => '\\',
'/' => '/',
'b' => '\b',
'f' => '\f',
'n' => '\n',
'r' => '\r',
't' => '\t',
_ => throw new FormatException($"Unsupported JSON escape '\\{escaped}'.")
});
}
throw new FormatException("Unterminated JSON string.");
}
private object ParseNumber()
{
var start = _index;
if (!IsEnd && _json[_index] == '-')
{
_index++;
}
while (!IsEnd && char.IsDigit(_json[_index]))
{
_index++;
}
var isFloatingPoint = false;
if (!IsEnd && _json[_index] == '.')
{
isFloatingPoint = true;
_index++;
while (!IsEnd && char.IsDigit(_json[_index]))
{
_index++;
}
}
if (!IsEnd && (_json[_index] == 'e' || _json[_index] == 'E'))
{
isFloatingPoint = true;
_index++;
if (!IsEnd && (_json[_index] == '+' || _json[_index] == '-'))
{
_index++;
}
while (!IsEnd && char.IsDigit(_json[_index]))
{
_index++;
}
}
var text = _json.Substring(start, _index - start);
if (string.IsNullOrEmpty(text) || text == "-")
{
throw new FormatException("Invalid JSON number.");
}
return isFloatingPoint
? double.Parse(text, CultureInfo.InvariantCulture)
: long.Parse(text, CultureInfo.InvariantCulture);
}
private object ParseLiteral(string literal, object value)
{
if (_index + literal.Length > _json.Length ||
string.CompareOrdinal(_json, _index, literal, 0, literal.Length) != 0)
{
throw new FormatException($"Invalid JSON literal near index {_index}.");
}
_index += literal.Length;
return value;
}
private bool TryConsume(char expected)
{
if (!IsEnd && _json[_index] == expected)
{
_index++;
return true;
}
return false;
}
private void Expect(char expected)
{
if (IsEnd || _json[_index] != expected)
{
throw new FormatException($"Expected '{expected}' near index {_index}.");
}
_index++;
}
}
}
}
}

View File

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

View File

@@ -1,31 +0,0 @@
using System.Threading.Tasks;
namespace FlowScope.Flow
{
public abstract class FeatureBase : IFeature
{
protected FeatureContext Context { get; private set; }
public virtual Task LoadAsync(FeatureContext context)
{
Context = context;
return Task.CompletedTask;
}
public virtual Task EnterAsync(FeatureContext context)
{
return Task.CompletedTask;
}
public virtual Task ExitAsync(FeatureContext context)
{
context?.Disposables.Clear();
return Task.CompletedTask;
}
public virtual void Dispose()
{
Context = null;
}
}
}

View File

@@ -1,27 +0,0 @@
using System.Threading;
using FlowScope.Resources;
using R3;
using ContainerType = FlowScope.Container.Container;
namespace FlowScope.Flow
{
public sealed class FeatureContext
{
public FeatureContext(
ContainerType scope,
IResourceGroup resources,
CompositeDisposable disposables,
CancellationToken cancellationToken)
{
Scope = scope;
Resources = resources;
Disposables = disposables;
CancellationToken = cancellationToken;
}
public ContainerType Scope { get; }
public IResourceGroup Resources { get; }
public CompositeDisposable Disposables { get; }
public CancellationToken CancellationToken { get; }
}
}

View File

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

View File

@@ -1,285 +0,0 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using FlowScope.Config;
using FlowScope.Resources;
using R3;
using UnityEngine;
using ContainerType = FlowScope.Container.Container;
namespace FlowScope.Flow
{
public sealed class GameFlow
{
private ContainerType _root;
private IFeature _activeFeature;
private FeatureContext _activeContext;
public GameFlowState State { get; private set; } = GameFlowState.Idle;
public async Task StartupAsync<TInitialFeature>(
ContainerType root,
CancellationToken cancellationToken)
where TInitialFeature : IFeature
{
EnsureState(nameof(StartupAsync), GameFlowState.Idle);
_root = root ?? throw new ArgumentNullException(nameof(root));
State = GameFlowState.Starting;
IFeature feature = null;
FeatureContext context = null;
try
{
var config = _root.Resolve<IConfigProvider>();
await config.LoadAllAsync(cancellationToken);
(feature, context) = CreateFeature<TInitialFeature>(cancellationToken);
await feature.LoadAsync(context);
await feature.EnterAsync(context);
_activeFeature = feature;
_activeContext = context;
State = GameFlowState.Running;
}
catch
{
BestEffortDisposeFeature(feature);
BestEffortDisposeContext(context);
_activeFeature = null;
_activeContext = null;
State = GameFlowState.Idle;
throw;
}
}
public async Task SwitchToAsync<TFeature>(CancellationToken cancellationToken)
where TFeature : IFeature
{
EnsureState(
nameof(SwitchToAsync),
GameFlowState.Running,
GameFlowState.NoActiveFeature);
State = GameFlowState.Switching;
await CleanupActiveFeatureAsync();
IFeature nextFeature = null;
FeatureContext nextContext = null;
try
{
(nextFeature, nextContext) = CreateFeature<TFeature>(cancellationToken);
await nextFeature.LoadAsync(nextContext);
await nextFeature.EnterAsync(nextContext);
_activeFeature = nextFeature;
_activeContext = nextContext;
State = GameFlowState.Running;
}
catch
{
BestEffortDisposeFeature(nextFeature);
BestEffortDisposeContext(nextContext);
_activeFeature = null;
_activeContext = null;
State = GameFlowState.NoActiveFeature;
throw;
}
}
public async Task ShutdownAsync(CancellationToken cancellationToken)
{
EnsureState(
nameof(ShutdownAsync),
GameFlowState.Running,
GameFlowState.NoActiveFeature);
State = GameFlowState.ShuttingDown;
Exception cancellationException = null;
try
{
cancellationToken.ThrowIfCancellationRequested();
}
catch (OperationCanceledException exception)
{
cancellationException = exception;
}
await CleanupActiveFeatureAsync();
BestEffortDisposeRoot();
State = GameFlowState.Disposed;
if (cancellationException != null)
{
throw cancellationException;
}
}
private (IFeature Feature, FeatureContext Context) CreateFeature<TFeature>(
CancellationToken cancellationToken)
where TFeature : IFeature
{
ContainerType scope = null;
IResourceGroup resources = null;
FeatureContext context = null;
try
{
scope = _root.CreateScope();
var resourceService = _root.Resolve<IResourceService>();
resources = resourceService.CreateGroup();
context = new FeatureContext(
scope,
resources,
new CompositeDisposable(),
cancellationToken);
var feature = scope.Resolve<TFeature>();
return (feature, context);
}
catch
{
if (context != null)
{
BestEffortDisposeContext(context);
}
else
{
BestEffortDisposePartialContext(resources, scope);
}
throw;
}
}
private async Task CleanupActiveFeatureAsync()
{
var feature = _activeFeature;
var context = _activeContext;
_activeFeature = null;
_activeContext = null;
if (feature != null)
{
try
{
await feature.ExitAsync(context);
}
catch (Exception exception)
{
Debug.LogError(exception);
}
BestEffortDisposeFeature(feature);
}
BestEffortDisposeContext(context);
}
private void BestEffortDisposeFeature(IFeature feature)
{
if (feature == null)
{
return;
}
try
{
feature.Dispose();
}
catch (Exception exception)
{
Debug.LogError(exception);
}
}
private void BestEffortDisposeContext(FeatureContext context)
{
if (context == null)
{
return;
}
try
{
context.Disposables.Clear();
}
catch (Exception exception)
{
Debug.LogError(exception);
}
try
{
context.Resources?.Dispose();
}
catch (Exception exception)
{
Debug.LogError(exception);
}
try
{
context.Scope?.Dispose();
}
catch (Exception exception)
{
Debug.LogError(exception);
}
}
private void BestEffortDisposePartialContext(
IResourceGroup resources,
ContainerType scope)
{
try
{
resources?.Dispose();
}
catch (Exception exception)
{
Debug.LogError(exception);
}
try
{
scope?.Dispose();
}
catch (Exception exception)
{
Debug.LogError(exception);
}
}
private void BestEffortDisposeRoot()
{
try
{
_root?.Dispose();
}
catch (Exception exception)
{
Debug.LogError(exception);
}
finally
{
_root = null;
}
}
private void EnsureState(string operation, params GameFlowState[] allowedStates)
{
foreach (var allowedState in allowedStates)
{
if (State == allowedState)
{
return;
}
}
throw new InvalidOperationException(
$"{operation} cannot run while GameFlow state is {State}.");
}
}
}

View File

@@ -1,13 +0,0 @@
namespace FlowScope.Flow
{
public enum GameFlowState
{
Idle,
Starting,
Running,
Switching,
NoActiveFeature,
ShuttingDown,
Disposed
}
}

View File

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

View File

@@ -1,12 +0,0 @@
using System;
using System.Threading.Tasks;
namespace FlowScope.Flow
{
public interface IFeature : IDisposable
{
Task LoadAsync(FeatureContext context);
Task EnterAsync(FeatureContext context);
Task ExitAsync(FeatureContext context);
}
}

View File

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

View File

@@ -1,9 +1,7 @@
{ {
"name": "FlowScope.Runtime", "name": "FlowScope.Runtime",
"rootNamespace": "FlowScope", "rootNamespace": "FlowScope",
"references": [ "references": [],
"R3.Unity"
],
"includePlatforms": [], "includePlatforms": [],
"excludePlatforms": [], "excludePlatforms": [],
"allowUnsafeCode": false, "allowUnsafeCode": false,

View File

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

View File

@@ -1,18 +0,0 @@
using System.Threading;
using System.Threading.Tasks;
namespace FlowScope.Resources
{
public interface IResourceBackend
{
string Name { get; }
Task<T> LoadAsync<T>(
string key,
CancellationToken cancellationToken)
where T : class;
void Release<T>(string key, T asset)
where T : class;
}
}

View File

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

View File

@@ -1,10 +0,0 @@
using System;
namespace FlowScope.Resources
{
public interface IResourceGroup : IDisposable
{
void Add<T>(IResourceHandle<T> handle) where T : class;
int Count { get; }
}
}

View File

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

View File

@@ -1,11 +0,0 @@
using System;
namespace FlowScope.Resources
{
public interface IResourceHandle<T> : IDisposable where T : class
{
string Key { get; }
T Asset { get; }
bool IsDisposed { get; }
}
}

View File

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

View File

@@ -1,21 +0,0 @@
using System.Threading;
using System.Threading.Tasks;
namespace FlowScope.Resources
{
public interface IResourceService
{
Task<IResourceHandle<T>> LoadAsync<T>(
string key,
CancellationToken cancellationToken)
where T : class;
IResourceGroup CreateGroup();
}
public interface ICompletedResourceService
{
bool TryLoadCompleted<T>(string key, out IResourceHandle<T> handle)
where T : class;
}
}

View File

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

View File

@@ -1,74 +0,0 @@
using System;
namespace FlowScope.Resources
{
internal sealed class ResourceEntry
{
private readonly Action<string, Type, object> _release;
private int _referenceCount;
private bool _released;
public ResourceEntry(string key, object asset, Action<string, object> release)
: this(key, asset?.GetType(), asset, (_, _, releasedAsset) => release(key, releasedAsset))
{
}
public ResourceEntry(string key, Type assetType, object asset, Action<string, Type, object> release)
{
Key = string.IsNullOrWhiteSpace(key)
? throw new ArgumentException("Resource key is required.", nameof(key))
: key;
AssetType = assetType ?? throw new ArgumentNullException(nameof(assetType));
Asset = asset ?? throw new ArgumentNullException(nameof(asset));
_release = release ?? throw new ArgumentNullException(nameof(release));
}
public string Key { get; }
public Type AssetType { get; }
public object Asset { get; private set; }
public int ReferenceCount => _referenceCount;
public ResourceHandle<T> CreateHandle<T>() where T : class
{
if (_released)
{
throw new ObjectDisposedException(nameof(ResourceEntry));
}
_referenceCount++;
return new ResourceHandle<T>(this);
}
public void ReleaseHandle()
{
if (_released)
{
return;
}
_referenceCount--;
if (_referenceCount > 0)
{
return;
}
_released = true;
var asset = Asset;
Asset = null;
_release(Key, AssetType, asset);
}
public void ReleaseWithoutHandle()
{
if (_released)
{
return;
}
_released = true;
var asset = Asset;
Asset = null;
_release(Key, AssetType, asset);
}
}
}

View File

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

View File

@@ -1,44 +0,0 @@
using System;
using System.Collections.Generic;
namespace FlowScope.Resources
{
public sealed class ResourceGroup : IResourceGroup
{
private readonly HashSet<IDisposable> _handles = new();
private bool _disposed;
public int Count => _handles.Count;
public void Add<T>(IResourceHandle<T> handle) where T : class
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(ResourceGroup));
}
if (handle == null)
{
throw new ArgumentNullException(nameof(handle));
}
_handles.Add(handle);
}
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
foreach (var handle in _handles)
{
handle.Dispose();
}
_handles.Clear();
}
}
}

View File

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

View File

@@ -1,51 +0,0 @@
using System;
namespace FlowScope.Resources
{
public sealed class ResourceHandle<T> : IResourceHandle<T> where T : class
{
private readonly ResourceEntry _entry;
private readonly Action<string, object> _release;
private T _asset;
internal ResourceHandle(ResourceEntry entry)
{
_entry = entry ?? throw new ArgumentNullException(nameof(entry));
Key = entry.Key;
_asset = (T)entry.Asset;
}
public ResourceHandle(string key, T asset, Action<string, object> release)
{
Key = string.IsNullOrWhiteSpace(key)
? throw new ArgumentException("Resource key is required.", nameof(key))
: key;
_asset = asset ?? throw new ArgumentNullException(nameof(asset));
_release = release ?? throw new ArgumentNullException(nameof(release));
}
public string Key { get; }
public T Asset => IsDisposed ? null : _asset;
public bool IsDisposed { get; private set; }
public void Dispose()
{
if (IsDisposed)
{
return;
}
IsDisposed = true;
var asset = _asset;
_asset = null;
if (_entry != null)
{
_entry.ReleaseHandle();
return;
}
_release(Key, asset);
}
}
}

View File

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

View File

@@ -1,239 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace FlowScope.Resources
{
public sealed class ResourceService : IResourceService, ICompletedResourceService
{
private readonly object _gate = new();
private readonly IResourceBackend _backend;
private readonly Dictionary<ResourceKey, ResourceEntry> _entries = new();
private readonly Dictionary<ResourceKey, SharedLoad> _loads = new();
public ResourceService(IResourceBackend backend)
{
_backend = backend ?? throw new ArgumentNullException(nameof(backend));
}
public async Task<IResourceHandle<T>> LoadAsync<T>(
string key,
CancellationToken cancellationToken)
where T : class
{
if (string.IsNullOrWhiteSpace(key))
{
throw new ArgumentException("Resource key is required.", nameof(key));
}
cancellationToken.ThrowIfCancellationRequested();
var resourceKey = new ResourceKey(key, typeof(T));
SharedLoad sharedLoad;
lock (_gate)
{
if (_entries.TryGetValue(resourceKey, out var entry))
{
return entry.CreateHandle<T>();
}
if (!_loads.TryGetValue(resourceKey, out sharedLoad))
{
sharedLoad = new SharedLoad();
_loads.Add(resourceKey, sharedLoad);
}
sharedLoad.WaiterCount++;
if (sharedLoad.LoadTask == null)
{
sharedLoad.LoadTask = LoadAndOwnEntryAsync<T>(resourceKey, key, sharedLoad);
}
}
try
{
var loaded = await WaitForSharedLoad(sharedLoad.LoadTask, cancellationToken);
lock (_gate)
{
return loaded.CreateHandle<T>();
}
}
catch (OperationCanceledException)
{
lock (_gate)
{
if (sharedLoad.WaiterCount > 0)
{
sharedLoad.WaiterCount--;
}
}
throw;
}
}
public bool TryLoadCompleted<T>(string key, out IResourceHandle<T> handle)
where T : class
{
if (string.IsNullOrWhiteSpace(key))
{
throw new ArgumentException("Resource key is required.", nameof(key));
}
lock (_gate)
{
if (_entries.TryGetValue(new ResourceKey(key, typeof(T)), out var entry))
{
handle = entry.CreateHandle<T>();
return true;
}
}
handle = null;
return false;
}
public IResourceGroup CreateGroup()
{
return new ResourceGroup();
}
private async Task<ResourceEntry> LoadEntryAsync<T>(string key)
where T : class
{
T asset;
try
{
asset = await _backend.LoadAsync<T>(key, CancellationToken.None);
}
catch (Exception exception) when (exception is not OperationCanceledException)
{
throw new InvalidOperationException(
$"Failed to load resource '{key}' as {typeof(T).Name} from backend '{_backend.Name}'.",
exception);
}
if (asset == null)
{
throw new InvalidOperationException(
$"Resource '{key}' returned null for {typeof(T).Name} from backend '{_backend.Name}'.");
}
return new ResourceEntry(
key,
typeof(T),
asset,
(entryKey, assetType, releasedAsset) =>
ReleaseBackendEntry(entryKey, assetType, (T)releasedAsset));
}
private async Task<ResourceEntry> LoadAndOwnEntryAsync<T>(
ResourceKey resourceKey,
string key,
SharedLoad sharedLoad)
where T : class
{
ResourceEntry entry = null;
try
{
entry = await LoadEntryAsync<T>(key);
lock (_gate)
{
_loads.Remove(resourceKey);
if (sharedLoad.WaiterCount > 0)
{
_entries.Add(resourceKey, entry);
return entry;
}
}
ReleaseEntry(entry);
return entry;
}
catch
{
lock (_gate)
{
_loads.Remove(resourceKey);
}
throw;
}
}
private static async Task<ResourceEntry> WaitForSharedLoad(
Task<ResourceEntry> loadTask,
CancellationToken cancellationToken)
{
if (!cancellationToken.CanBeCanceled)
{
return await loadTask;
}
var cancellation = new TaskCompletionSource<bool>();
using (cancellationToken.Register(() => cancellation.TrySetResult(true)))
{
if (await Task.WhenAny(loadTask, cancellation.Task) == cancellation.Task)
{
throw new OperationCanceledException(cancellationToken);
}
}
return await loadTask;
}
private void ReleaseEntry(ResourceEntry entry)
{
entry.ReleaseWithoutHandle();
}
private void ReleaseBackendEntry<T>(string key, Type assetType, T asset)
where T : class
{
lock (_gate)
{
_entries.Remove(new ResourceKey(key, assetType));
}
_backend.Release(key, asset);
}
private sealed class SharedLoad
{
public Task<ResourceEntry> LoadTask;
public int WaiterCount;
}
private readonly struct ResourceKey : IEquatable<ResourceKey>
{
private readonly string _key;
private readonly Type _type;
public ResourceKey(string key, Type type)
{
_key = key;
_type = type;
}
public bool Equals(ResourceKey other)
{
return _key == other._key && _type == other._type;
}
public override bool Equals(object obj)
{
return obj is ResourceKey other && Equals(other);
}
public override int GetHashCode()
{
unchecked
{
return ((_key != null ? _key.GetHashCode() : 0) * 397) ^
(_type != null ? _type.GetHashCode() : 0);
}
}
}
}
}

View File

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

View File

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

View File

@@ -1,93 +0,0 @@
using System;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
namespace FlowScope.Save
{
public interface ISaveStorage
{
Task WriteAsync(string key, byte[] data, CancellationToken cancellationToken);
Task<byte[]> ReadAsync(string key, CancellationToken cancellationToken);
void Delete(string key);
bool Exists(string key);
}
public sealed class FileSaveStorage : ISaveStorage
{
private readonly string _rootDirectory;
public FileSaveStorage(string rootDirectory = null)
{
_rootDirectory = string.IsNullOrWhiteSpace(rootDirectory)
? Path.Combine(Application.persistentDataPath, "FlowScopeSaves")
: rootDirectory;
}
public async Task WriteAsync(string key, byte[] data, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (data == null)
{
throw new ArgumentNullException(nameof(data));
}
Directory.CreateDirectory(_rootDirectory);
var path = GetPath(key);
using var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, 4096, true);
await stream.WriteAsync(data, 0, data.Length, cancellationToken);
}
public async Task<byte[]> ReadAsync(string key, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var path = GetPath(key);
if (!File.Exists(path))
{
return null;
}
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true);
var data = new byte[stream.Length];
var read = 0;
while (read < data.Length)
{
var count = await stream.ReadAsync(data, read, data.Length - read, cancellationToken);
if (count == 0)
{
break;
}
read += count;
}
return data;
}
public void Delete(string key)
{
var path = GetPath(key);
if (File.Exists(path))
{
File.Delete(path);
}
}
public bool Exists(string key)
{
return File.Exists(GetPath(key));
}
private string GetPath(string key)
{
if (string.IsNullOrWhiteSpace(key))
{
throw new ArgumentException("Save key is required.", nameof(key));
}
return Path.Combine(_rootDirectory, Convert.ToBase64String(Encoding.UTF8.GetBytes(key)) + ".json");
}
}
}

View File

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

View File

@@ -1,11 +0,0 @@
namespace FlowScope.Save
{
public interface ISaveMigration
{
string Key { get; }
int FromVersion { get; }
int ToVersion { get; }
string Migrate(string json);
}
}

View File

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

View File

@@ -1,13 +0,0 @@
using System.Threading;
using System.Threading.Tasks;
namespace FlowScope.Save
{
public interface ISaveService
{
Task SaveAsync<T>(string key, T data, CancellationToken cancellationToken);
Task<T> LoadAsync<T>(string key, T defaultValue, CancellationToken cancellationToken);
void Delete(string key);
bool Exists(string key);
}
}

View File

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

Some files were not shown because too many files have changed in this diff Show More