GraphFoundation动态更新图实现了一个基于ECS(实体组件系统)的泛型图数据结构库,支持事务操作、事件驱动、索引管理和链式结构。核心功能如下:
- 图核心模型
节点(Node):由键(TKey)、数据(TNodeData)和可选符号(Symbol)组成,存储为ECS组件。
边(Edge):由唯一ID、源节点、目标节点、方向、权重和数据(TEdgeData)组成,同样存储为ECS组件。
关系管理:通过ECS的RelationManager维护节点与边之间的连接关系(出边/入边)。
索引:利用IndexManager为节点键和边ID建立快速查找索引。
- 事务支持
提供ITransaction接口,支持命令模式:所有修改操作(增删改)都封装为命令(如AddNodeCommand),在事务中执行。
事务使用读写锁(ReaderWriterLockSlim)保证线程安全,支持原子提交和回滚(通过命令的Undo方法)。
事务提交时发布对应的图事件(如NodeAddedEvent),供外部订阅。
- 事件系统
所有图变更都会发布事件(继承自GraphEvent),包括节点/边的添加、移除、更新。
提供扩展方法筛选特定事件类型(如NodeAdded、EdgeRemoved)。
- 链式结构(Chain)
Chain<TKey, TNodeData, TEdgeData> 表示一个有序节点序列,内部自动维护连接相邻节点的有向边。
支持动态插入、移除、移动节点,并自动更新边(保留原有边的数据和权重)。
支持延迟节点创建:当链中插入不存在的节点时,可通过回调创建节点。
实现了IReadOnlyList,可像列表一样访问,并实现了IDisposable,销毁时自动清理所有边。
- 符号索引投影器(SymbolIndexProjector)
维护节点符号(Symbol)到节点键的映射,便于通过符号快速查找节点。
自动订阅图事件,实时更新映射。
- 错误处理
定义IErrorHandler接口,提供默认的ErrorHandlerService(组合多个处理器)和FallbackErrorHandler(回退策略)。
ErrorHandlingCommand包装原始命令,在执行或回滚时捕获异常并交由错误处理器处理。
通过UseErrorHandling扩展方法将错误处理集成到上下文中。
- 线程安全与并发控制
所有读操作使用读锁,事务操作使用写锁,保证多线程环境下的数据一致性。
使用ECS框架(ECSEnhanced)的EcsContext和EntityManager管理实体生命周期。
- 扩展与集成
实现了ICoreGraph接口,可注入到IContext(上下文管理)中作为服务使用。
提供扩展方法CreateChain快速从节点序列构建链。
典型应用场景
工作流/流程图:节点为任务或状态,边为流转关系,链表示顺序执行路径。
数据流图:节点为算子,边为数据依赖,支持事务性修改。
游戏开发:节点为游戏对象,边为关系,需要高性能和事务回滚。
该库通过ECS优化性能,通过事务和命令保证操作原子性,通过事件实现响应式更新,是一个功能完备的图数据结构基础设施。
csharp
#region GraphFoundation
#nullable enable
namespace GraphFoundation.Core
{
using ContextManagement;
using ECSEnhanced;
using ECSEnhanced.Locking;
using System;
using System.Collections.Generic;
using System.Reactive.Subjects;
public abstract record GraphEvent();
public record NodeAddedEvent<TKey>(TKey Key, object? Data, string? Symbol) : GraphEvent;
public record NodeRemovedEvent<TKey>(TKey Key, object? OldData, string? OldSymbol) : GraphEvent;
public record NodeUpdatedEvent<TKey>(TKey Key, object? OldData, object? NewData, string? OldSymbol, string? NewSymbol) : GraphEvent;
public record EdgeAddedEvent<TKey>(string EdgeId, TKey Source, TKey Target, bool IsDirected, double Weight, object? Data) : GraphEvent;
public record EdgeRemovedEvent<TKey>(string EdgeId, TKey Source, TKey Target) : GraphEvent;
public record EdgeUpdatedEvent<TKey>(string EdgeId, TKey Source, TKey Target, bool IsDirected, double Weight, object? OldData, object? NewData) : GraphEvent;
public interface ICommand
{
void Execute(TransactionContextBase context);
void Undo(TransactionContextBase context);
}
public abstract class TransactionContextBase
{
internal List<GraphEvent> PendingEvents { get; } = new();
}
public interface ITransaction : IDisposable
{
void Enqueue(ICommand command);
void Commit();
void Rollback();
bool IsActive { get; }
void OnCommit(Action callback);
IContext Context { get; }
}
public interface IReadOnlyGraph<TKey, TNodeData, TEdgeData>
where TKey : notnull
where TNodeData : class
{
TNodeData? GetNodeData(TKey key);
string? GetNodeSymbol(TKey key);
TEdgeData? GetEdgeData(string edgeId);
double GetEdgeWeight(string edgeId);
TKey GetEdgeSource(string edgeId);
TKey GetEdgeTarget(string edgeId);
IReadOnlyList<TKey> GetAllNodeKeys();
IReadOnlyList<string> GetAllEdgeIds();
IReadOnlyList<(TKey Source, TKey Target, string EdgeId, bool IsDirected, double Weight, TEdgeData Data)> GetOutgoingEdges(TKey source);
IReadOnlyList<(TKey Source, TKey Target, string EdgeId, bool IsDirected, double Weight, TEdgeData Data)> GetIncomingEdges(TKey target);
bool ContainsNode(TKey key);
bool ContainsEdge(string edgeId);
IObservable<GraphEvent> Events { get; }
}
public interface ICoreGraph<TKey, TNodeData, TEdgeData> : IReadOnlyGraph<TKey, TNodeData, TEdgeData>, IDisposable
where TKey : notnull
where TNodeData : class
{
ITransaction BeginTransaction();
void AddNode(TKey key, TNodeData data, string? symbol = null);
void RemoveNode(TKey key);
void AddEdge(string edgeId, TKey source, TKey target, bool isDirected, TEdgeData data, double weight = 1.0);
void RemoveEdge(string edgeId);
void UpdateNode(TKey key, TNodeData newData, string? newSymbol = null);
IContext Context { get; }
}
internal static class Constants
{
public const string ChainEdgeIdFormat = "Chain_{0}_{1}";
public const double DefaultEdgeWeight = 1.0;
public const int MinChainLengthForEdges = 2;
}
}
namespace GraphFoundation.Core.Implementation
{
using ECSEnhanced;
using GraphFoundation.Core;
using GraphFoundation.ErrorHandling;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Subjects;
using System.Threading;
public struct NodeComponent<TKey, TNodeData> : IComponent where TKey : notnull
{
public TKey Key;
public TNodeData Data;
public string? Symbol;
public NodeComponent(TKey key, TNodeData data, string? symbol = null) => (Key, Data, Symbol) = (key, data, symbol);
}
public struct EdgeComponent<TKey, TEdgeData> : IComponent where TKey : notnull
{
public string Id;
public TKey Source;
public TKey Target;
public bool IsDirected;
public double Weight;
public TEdgeData Data;
public EdgeComponent(string id, TKey source, TKey target, bool directed, TEdgeData data, double weight = 1.0)
=> (Id, Source, Target, IsDirected, Weight, Data) = (id, source, target, directed, weight, data);
}
public enum EdgeRelationType { From, To }
public class TransactionContext<TKey, TNodeData, TEdgeData> : TransactionContextBase
where TKey : notnull
where TNodeData : class
{
internal CoreGraph<TKey, TNodeData, TEdgeData> Core { get; }
internal IContext Context { get; }
public TransactionContext(CoreGraph<TKey, TNodeData, TEdgeData> core) => (Core, Context) = (core, core.Context);
}
public abstract class NodeCommandBase<TKey, TNodeData, TEdgeData> : ICommand
where TKey : notnull
where TNodeData : class
{
protected abstract void ExecuteCore(TransactionContext<TKey, TNodeData, TEdgeData> ctx);
protected abstract void UndoCore(TransactionContext<TKey, TNodeData, TEdgeData> ctx);
public void Execute(TransactionContextBase context)
{
var typedCtx = (TransactionContext<TKey, TNodeData, TEdgeData>)context;
ExecuteCore(typedCtx);
}
public void Undo(TransactionContextBase context)
{
var typedCtx = (TransactionContext<TKey, TNodeData, TEdgeData>)context;
UndoCore(typedCtx);
}
}
public abstract class EdgeCommandBase<TKey, TNodeData, TEdgeData> : ICommand
where TKey : notnull
where TNodeData : class
{
protected abstract void ExecuteCore(TransactionContext<TKey, TNodeData, TEdgeData> ctx);
protected abstract void UndoCore(TransactionContext<TKey, TNodeData, TEdgeData> ctx);
public void Execute(TransactionContextBase context)
{
var typedCtx = (TransactionContext<TKey, TNodeData, TEdgeData>)context;
ExecuteCore(typedCtx);
}
public void Undo(TransactionContextBase context)
{
var typedCtx = (TransactionContext<TKey, TNodeData, TEdgeData>)context;
UndoCore(typedCtx);
}
}
public class CoreGraph<TKey, TNodeData, TEdgeData> : ICoreGraph<TKey, TNodeData, TEdgeData>
where TKey : notnull
where TNodeData : class
{
private readonly EcsContext _context;
private readonly EntityManager _entityManager;
private readonly IndexManager _indexManager;
private readonly RelationManager _relationManager;
public readonly ReaderWriterLockSlim _transactionLock = new(LockRecursionPolicy.NoRecursion);
private readonly Subject<GraphEvent> _eventSubject = new();
private bool _disposed;
private readonly CancellationTokenRegistration _cancellationRegistration;
private bool _eventCompleted;
private readonly IErrorHandler? _errorHandler;
public IContext Context { get; }
public IObservable<GraphEvent> Events => _eventSubject;
internal EntityManager EntityManager => _entityManager;
internal IndexManager IndexManager => _indexManager;
internal RelationManager RelationManager => _relationManager;
internal void PublishEvent(GraphEvent evt)
{
lock (_eventSubject)
{
if (_eventCompleted) return;
_eventSubject.OnNext(evt);
}
}
internal void PublishEvents(IEnumerable<GraphEvent> events)
{
foreach (var evt in events)
PublishEvent(evt);
}
internal void ReleaseWriteLock() => _transactionLock.ExitWriteLock();
public CoreGraph(IContext context, EcsContext? ecsContext = null)
{
Context = context ?? throw new ArgumentNullException(nameof(context));
_context = ecsContext ?? new EcsContext(enableBgUpdate: false);
_entityManager = _context.World.EntityManager;
_indexManager = _context.Indexes;
_relationManager = _context.Relations;
_indexManager.RegisterIndex<NodeComponent<TKey, TNodeData>>(comp => comp.Key, unique: true);
_indexManager.RegisterIndex<EdgeComponent<TKey, TEdgeData>>(comp => comp.Id, unique: true);
context.GetOrAddService<ICoreGraph<TKey, TNodeData, TEdgeData>>(() => this);
_cancellationRegistration = context.CancellationToken.Register(() =>
{
if (!_disposed && !_eventCompleted)
{
lock (_eventSubject)
{
if (!_eventCompleted)
{
_eventSubject.OnCompleted();
_eventCompleted = true;
}
}
}
});
_errorHandler = context.GetErrorHandler();
}
#region Internal helpers (no locks, assume caller holds write lock or is internal)
internal Entity GetNodeEntityInternal(TKey key)
=> _indexManager.FindEntity<NodeComponent<TKey, TNodeData>>(key);
internal Entity GetEdgeEntityInternal(string edgeId)
=> _indexManager.FindEntity<EdgeComponent<TKey, TEdgeData>>(edgeId);
internal bool ContainsNodeInternal(TKey key)
{
var nodeComp = GetNodeComponentInternal(key);
return nodeComp.HasValue && EqualityComparer<TKey>.Default.Equals(nodeComp.Value.Key, key);
}
internal bool ContainsEdgeInternal(string edgeId)
{
var edgeComp = GetEdgeComponentInternal(edgeId);
return edgeComp.HasValue && edgeComp.Value.Id == edgeId;
}
internal NodeComponent<TKey, TNodeData>? GetNodeComponentInternal(TKey key)
{
var entity = GetNodeEntityInternal(key);
if (!entity.IsValid)
return null;
if (_entityManager.TryGetComponent<NodeComponent<TKey, TNodeData>>(entity, out var comp))
return comp;
return null;
}
internal EdgeComponent<TKey, TEdgeData>? GetEdgeComponentInternal(string edgeId)
{
var entity = GetEdgeEntityInternal(edgeId);
if (!entity.IsValid)
return null;
if (_entityManager.TryGetComponent<EdgeComponent<TKey, TEdgeData>>(entity, out var comp))
return comp;
return null;
}
internal TNodeData? GetNodeDataInternal(TKey key)
=> GetNodeComponentInternal(key)?.Data;
internal string? GetNodeSymbolInternal(TKey key)
=> GetNodeComponentInternal(key)?.Symbol;
internal TEdgeData? GetEdgeDataInternal(string edgeId)
{
var comp = GetEdgeComponentInternal(edgeId);
return comp.HasValue ? comp.Value.Data : default;
}
internal double GetEdgeWeightInternal(string edgeId)
{
var comp = GetEdgeComponentInternal(edgeId);
if (comp == null) throw new KeyNotFoundException($"Edge with id '{edgeId}' not found.");
return comp.Value.Weight;
}
internal IReadOnlyList<(TKey Source, TKey Target, string EdgeId, bool IsDirected, double Weight, TEdgeData Data)>
GetOutgoingEdgesInternal(TKey source)
{
var nodeEntity = GetNodeEntityInternal(source);
if (!nodeEntity.IsValid)
return Array.Empty<(TKey, TKey, string, bool, double, TEdgeData)>();
var result = new List<(TKey, TKey, string, bool, double, TEdgeData)>();
foreach (var edgeEntity in _relationManager.GetRelatedTargets<EdgeRelationType>(nodeEntity, EdgeRelationType.From))
{
if (!edgeEntity.IsValid) continue;
if (!_entityManager.TryGetComponent<EdgeComponent<TKey, TEdgeData>>(edgeEntity, out var comp))
continue;
result.Add((comp.Source, comp.Target, comp.Id, comp.IsDirected, comp.Weight, comp.Data));
}
return result;
}
internal IReadOnlyList<(TKey Source, TKey Target, string EdgeId, bool IsDirected, double Weight, TEdgeData Data)>
GetIncomingEdgesInternal(TKey target)
{
var nodeEntity = GetNodeEntityInternal(target);
if (!nodeEntity.IsValid)
return Array.Empty<(TKey, TKey, string, bool, double, TEdgeData)>();
var result = new List<(TKey, TKey, string, bool, double, TEdgeData)>();
foreach (var edgeEntity in _relationManager.GetRelatedTargets<EdgeRelationType>(nodeEntity, EdgeRelationType.To))
{
if (!edgeEntity.IsValid) continue;
if (!_entityManager.TryGetComponent<EdgeComponent<TKey, TEdgeData>>(edgeEntity, out var comp))
continue;
result.Add((comp.Source, comp.Target, comp.Id, comp.IsDirected, comp.Weight, comp.Data));
}
return result;
}
public (TKey Source, TKey Target, TEdgeData Data, double Weight)? TryGetEdgeDetails(string edgeId)
{
using (Lock.Read(_transactionLock))
{
var comp = GetEdgeComponentInternal(edgeId);
if (comp == null) return null;
return (comp.Value.Source, comp.Value.Target, comp.Value.Data, comp.Value.Weight);
}
}
#endregion
#region Public read methods (with read lock)
public TNodeData? GetNodeData(TKey key) { using (Lock.Read(_transactionLock)) return GetNodeDataInternal(key); }
public string? GetNodeSymbol(TKey key) { using (Lock.Read(_transactionLock)) return GetNodeSymbolInternal(key); }
public TEdgeData? GetEdgeData(string edgeId) { using (Lock.Read(_transactionLock)) return GetEdgeDataInternal(edgeId); }
public double GetEdgeWeight(string edgeId) { using (Lock.Read(_transactionLock)) return GetEdgeWeightInternal(edgeId); }
public TKey GetEdgeSource(string edgeId)
{
using (Lock.Read(_transactionLock))
{
var comp = GetEdgeComponentInternal(edgeId);
if (comp == null) throw new KeyNotFoundException($"Edge with id '{edgeId}' not found.");
return comp.Value.Source;
}
}
public TKey GetEdgeTarget(string edgeId)
{
using (Lock.Read(_transactionLock))
{
var comp = GetEdgeComponentInternal(edgeId);
if (comp == null) throw new KeyNotFoundException($"Edge with id '{edgeId}' not found.");
return comp.Value.Target;
}
}
public IReadOnlyList<TKey> GetAllNodeKeys()
{
using (Lock.Read(_transactionLock))
{
var keys = new List<TKey>();
foreach (var entity in _entityManager.GetEntities<NodeComponent<TKey, TNodeData>>())
keys.Add(_entityManager.GetComponent<NodeComponent<TKey, TNodeData>>(entity).Key);
return keys;
}
}
public IReadOnlyList<string> GetAllEdgeIds()
{
using (Lock.Read(_transactionLock))
{
var ids = new List<string>();
foreach (var entity in _entityManager.GetEntities<EdgeComponent<TKey, TEdgeData>>())
ids.Add(_entityManager.GetComponent<EdgeComponent<TKey, TEdgeData>>(entity).Id);
return ids;
}
}
public IReadOnlyList<(TKey Source, TKey Target, string EdgeId, bool IsDirected, double Weight, TEdgeData Data)> GetOutgoingEdges(TKey source)
{ using (Lock.Read(_transactionLock)) return GetOutgoingEdgesInternal(source); }
public IReadOnlyList<(TKey Source, TKey Target, string EdgeId, bool IsDirected, double Weight, TEdgeData Data)> GetIncomingEdges(TKey target)
{ using (Lock.Read(_transactionLock)) return GetIncomingEdgesInternal(target); }
public bool ContainsNode(TKey key) { using (Lock.Read(_transactionLock)) return ContainsNodeInternal(key); }
public bool ContainsEdge(string edgeId) { using (Lock.Read(_transactionLock)) return ContainsEdgeInternal(edgeId); }
#endregion
#region Transactions and high-level API
public ITransaction BeginTransaction() { _transactionLock.EnterWriteLock(); return new LockedTransaction(this, _errorHandler); }
public void AddNode(TKey key, TNodeData data, string? symbol = null)
{
using var tx = BeginTransaction();
tx.Enqueue(new AddNodeCommand<TKey, TNodeData, TEdgeData>(key, data, symbol));
tx.Commit();
}
public void RemoveNode(TKey key)
{
using var tx = BeginTransaction();
tx.Enqueue(new RemoveNodeCommand<TKey, TNodeData, TEdgeData>(key));
tx.Commit();
}
public void AddEdge(string edgeId, TKey source, TKey target, bool isDirected, TEdgeData data, double weight = 1.0)
{
using var tx = BeginTransaction();
tx.Enqueue(new AddEdgeCommand<TKey, TNodeData, TEdgeData>(edgeId, source, target, isDirected, data, weight));
tx.Commit();
}
public void RemoveEdge(string edgeId)
{
using var tx = BeginTransaction();
tx.Enqueue(new RemoveEdgeCommand<TKey, TNodeData, TEdgeData>(edgeId));
tx.Commit();
}
public void UpdateNode(TKey key, TNodeData newData, string? newSymbol = null)
{
using var tx = BeginTransaction();
tx.Enqueue(new UpdateNodeCommand<TKey, TNodeData, TEdgeData>(key, newData, newSymbol));
tx.Commit();
}
public void Dispose()
{
if (_disposed) return;
_cancellationRegistration.Dispose();
lock (_eventSubject)
{
if (!_eventCompleted)
{
_eventSubject.OnCompleted();
_eventCompleted = true;
}
}
_context.Dispose();
_transactionLock.Dispose();
_eventSubject.Dispose();
_disposed = true;
}
#endregion
private class LockedTransaction : ITransaction
{
private readonly CoreGraph<TKey, TNodeData, TEdgeData> _parent;
private readonly IErrorHandler? _errorHandler;
private readonly List<ICommand> _commands = new();
private readonly List<Action> _commitCallbacks = new();
private bool _committed, _disposed;
public LockedTransaction(CoreGraph<TKey, TNodeData, TEdgeData> parent, IErrorHandler? errorHandler)
{
_parent = parent;
_errorHandler = errorHandler;
}
public IContext Context => _parent.Context;
public void Enqueue(ICommand command) => _commands.Add(command);
public void OnCommit(Action callback) => _commitCallbacks.Add(callback);
public bool IsActive => !_committed && !_disposed;
public void Commit()
{
if (_committed || _disposed) return;
var context = new TransactionContext<TKey, TNodeData, TEdgeData>(_parent);
bool commitSuccess = false;
try
{
foreach (var cmd in _commands)
{
var executableCmd = _errorHandler != null
? new ErrorHandlingCommand(cmd, _errorHandler)
: cmd;
executableCmd.Execute(context);
}
commitSuccess = true;
}
catch (Exception ex)
{
try
{
RollbackCommands(context);
}
catch (Exception rollbackEx)
{
throw new AggregateException("Transaction commit failed and rollback encountered errors.", ex, rollbackEx);
}
throw;
}
finally
{
_parent.ReleaseWriteLock();
}
if (commitSuccess)
{
foreach (var evt in context.PendingEvents)
{
try
{
_parent.PublishEvent(evt);
}
catch (Exception ex)
{
_errorHandler?.HandleError(new GraphError(GraphOperationErrorType.Unknown, ex, "Event publishing failed after transaction commit"));
}
}
foreach (var cb in _commitCallbacks)
{
try { cb(); }
catch (Exception ex)
{
_errorHandler?.HandleError(new GraphError(GraphOperationErrorType.Unknown, ex, "Commit callback failed"));
}
}
}
_committed = true;
_disposed = true;
}
private void RollbackCommands(TransactionContext<TKey, TNodeData, TEdgeData> context)
{
var undoExceptions = new List<Exception>();
for (int i = _commands.Count - 1; i >= 0; i--)
{
try { _commands[i].Undo(context); }
catch (Exception undoEx) { undoExceptions.Add(undoEx); }
}
if (undoExceptions.Count > 0)
throw new AggregateException("Transaction commit failed and undo encountered errors.", undoExceptions);
}
public void Rollback()
{
if (_committed || _disposed) return;
var context = new TransactionContext<TKey, TNodeData, TEdgeData>(_parent);
var undoExceptions = new List<Exception>();
try
{
for (int i = _commands.Count - 1; i >= 0; i--)
{
try { _commands[i].Undo(context); }
catch (Exception ex) { undoExceptions.Add(ex); }
}
if (undoExceptions.Count > 0)
throw new AggregateException("Rollback encountered errors.", undoExceptions);
}
finally
{
_parent.ReleaseWriteLock();
}
_disposed = true;
}
public void Dispose()
{
if (_disposed) return;
if (!_committed) try { Rollback(); } catch { }
_disposed = true;
}
}
}
public class AddNodeCommand<TKey, TNodeData, TEdgeData> : NodeCommandBase<TKey, TNodeData, TEdgeData>
where TKey : notnull
where TNodeData : class
{
private readonly TKey _key;
private readonly TNodeData _data;
private readonly string? _symbol;
private Entity _createdEntity;
public AddNodeCommand(TKey key, TNodeData data, string? symbol = null) => (_key, _data, _symbol) = (key, data, symbol);
protected override void ExecuteCore(TransactionContext<TKey, TNodeData, TEdgeData> ctx)
{
var core = ctx.Core;
if (core.ContainsNodeInternal(_key))
throw new InvalidOperationException($"Node with key '{_key}' already exists.");
_createdEntity = core.EntityManager.CreateEntity();
core.EntityManager.AddComponent(_createdEntity, new NodeComponent<TKey, TNodeData>(_key, _data, _symbol));
ctx.PendingEvents.Add(new NodeAddedEvent<TKey>(_key, _data, _symbol));
}
protected override void UndoCore(TransactionContext<TKey, TNodeData, TEdgeData> ctx)
{
if (!_createdEntity.IsValid) return;
ctx.Core.EntityManager.DestroyEntity(_createdEntity);
}
}
public class RemoveNodeCommand<TKey, TNodeData, TEdgeData> : NodeCommandBase<TKey, TNodeData, TEdgeData>
where TKey : notnull
where TNodeData : class
{
private readonly TKey _key;
private Entity _removedEntity;
private NodeComponent<TKey, TNodeData> _oldComponent;
private List<EdgeComponent<TKey, TEdgeData>> _removedEdges = new();
public RemoveNodeCommand(TKey key) => _key = key;
protected override void ExecuteCore(TransactionContext<TKey, TNodeData, TEdgeData> ctx)
{
var core = ctx.Core;
var entity = core.GetNodeEntityInternal(_key);
if (!entity.IsValid)
throw new InvalidOperationException($"Node with key '{_key}' not found.");
_removedEntity = entity;
_oldComponent = core.EntityManager.GetComponent<NodeComponent<TKey, TNodeData>>(_removedEntity);
var outgoing = core.RelationManager.GetRelatedTargets<EdgeRelationType>(_removedEntity, EdgeRelationType.From);
var incoming = core.RelationManager.GetRelatedTargets<EdgeRelationType>(_removedEntity, EdgeRelationType.To);
var allEdgeEntities = new HashSet<Entity>(outgoing);
allEdgeEntities.UnionWith(incoming);
foreach (var edgeEntity in allEdgeEntities)
{
var edgeComp = core.EntityManager.GetComponent<EdgeComponent<TKey, TEdgeData>>(edgeEntity);
_removedEdges.Add(edgeComp);
var srcEntity = core.GetNodeEntityInternal(edgeComp.Source);
var tgtEntity = core.GetNodeEntityInternal(edgeComp.Target);
core.RelationManager.RemoveRelation(srcEntity, edgeEntity, EdgeRelationType.From);
core.RelationManager.RemoveRelation(tgtEntity, edgeEntity, EdgeRelationType.To);
core.EntityManager.DestroyEntity(edgeEntity);
}
core.EntityManager.DestroyEntity(_removedEntity);
ctx.PendingEvents.Add(new NodeRemovedEvent<TKey>(_key, _oldComponent.Data, _oldComponent.Symbol));
}
protected override void UndoCore(TransactionContext<TKey, TNodeData, TEdgeData> ctx)
{
var core = ctx.Core;
if (!core.ContainsNodeInternal(_key))
{
var newEntity = core.EntityManager.CreateEntity();
core.EntityManager.AddComponent(newEntity, _oldComponent);
}
foreach (var edgeComp in _removedEdges)
{
if (!core.ContainsEdgeInternal(edgeComp.Id))
{
var newEntity = core.EntityManager.CreateEntity();
core.EntityManager.AddComponent(newEntity, edgeComp);
var srcEntity = core.GetNodeEntityInternal(edgeComp.Source);
var tgtEntity = core.GetNodeEntityInternal(edgeComp.Target);
core.RelationManager.AddRelation(srcEntity, newEntity, EdgeRelationType.From);
core.RelationManager.AddRelation(tgtEntity, newEntity, EdgeRelationType.To);
}
}
_removedEdges.Clear();
}
}
public class AddEdgeCommand<TKey, TNodeData, TEdgeData> : EdgeCommandBase<TKey, TNodeData, TEdgeData>
where TKey : notnull
where TNodeData : class
{
private readonly string _edgeId;
private readonly TKey _source;
private readonly TKey _target;
private readonly bool _isDirected;
private readonly double _weight;
private readonly TEdgeData _data;
private Entity _createdEntity;
public AddEdgeCommand(string edgeId, TKey source, TKey target, bool isDirected, TEdgeData data, double weight = Constants.DefaultEdgeWeight)
=> (_edgeId, _source, _target, _isDirected, _weight, _data) = (edgeId, source, target, isDirected, weight, data);
protected override void ExecuteCore(TransactionContext<TKey, TNodeData, TEdgeData> ctx)
{
var core = ctx.Core;
if (!core.ContainsNodeInternal(_source))
throw new InvalidOperationException($"Source node '{_source}' not found.");
if (!core.ContainsNodeInternal(_target))
throw new InvalidOperationException($"Target node '{_target}' not found.");
if (core.ContainsEdgeInternal(_edgeId))
throw new InvalidOperationException($"Edge with id '{_edgeId}' already exists.");
_createdEntity = core.EntityManager.CreateEntity();
if (!_createdEntity.IsValid)
throw new InvalidOperationException($"Failed to create valid edge entity.");
var edgeComp = new EdgeComponent<TKey, TEdgeData>(_edgeId, _source, _target, _isDirected, _data, _weight);
core.EntityManager.AddComponent(_createdEntity, edgeComp);
var srcEntity = core.GetNodeEntityInternal(_source);
var tgtEntity = core.GetNodeEntityInternal(_target);
if (!srcEntity.IsValid || !tgtEntity.IsValid)
throw new InvalidOperationException($"Source or target node entity invalid.");
core.RelationManager.AddRelation(srcEntity, _createdEntity, EdgeRelationType.From);
core.RelationManager.AddRelation(tgtEntity, _createdEntity, EdgeRelationType.To);
ctx.PendingEvents.Add(new EdgeAddedEvent<TKey>(_edgeId, _source, _target, _isDirected, _weight, _data));
}
protected override void UndoCore(TransactionContext<TKey, TNodeData, TEdgeData> ctx)
{
if (!_createdEntity.IsValid) return;
var core = ctx.Core;
var srcEntity = core.GetNodeEntityInternal(_source);
var tgtEntity = core.GetNodeEntityInternal(_target);
core.RelationManager.RemoveRelation(srcEntity, _createdEntity, EdgeRelationType.From);
core.RelationManager.RemoveRelation(tgtEntity, _createdEntity, EdgeRelationType.To);
core.EntityManager.DestroyEntity(_createdEntity);
}
}
public class RemoveEdgeCommand<TKey, TNodeData, TEdgeData> : EdgeCommandBase<TKey, TNodeData, TEdgeData>
where TKey : notnull
where TNodeData : class
{
private readonly string _edgeId;
private Entity _removedEntity;
private EdgeComponent<TKey, TEdgeData> _oldComponent;
public RemoveEdgeCommand(string edgeId) => _edgeId = edgeId;
protected override void ExecuteCore(TransactionContext<TKey, TNodeData, TEdgeData> ctx)
{
var core = ctx.Core;
var entity = core.GetEdgeEntityInternal(_edgeId);
if (!entity.IsValid)
{
throw new InvalidOperationException($"Edge with id '{_edgeId}' not found.");
}
_removedEntity = entity;
_oldComponent = core.EntityManager.GetComponent<EdgeComponent<TKey, TEdgeData>>(_removedEntity);
var srcEntity = core.GetNodeEntityInternal(_oldComponent.Source);
var tgtEntity = core.GetNodeEntityInternal(_oldComponent.Target);
core.RelationManager.RemoveRelation(srcEntity, _removedEntity, EdgeRelationType.From);
core.RelationManager.RemoveRelation(tgtEntity, _removedEntity, EdgeRelationType.To);
core.EntityManager.DestroyEntity(_removedEntity);
ctx.PendingEvents.Add(new EdgeRemovedEvent<TKey>(_edgeId, _oldComponent.Source, _oldComponent.Target));
}
protected override void UndoCore(TransactionContext<TKey, TNodeData, TEdgeData> ctx)
{
var core = ctx.Core;
if (core.ContainsEdgeInternal(_edgeId)) return;
var newEntity = core.EntityManager.CreateEntity();
core.EntityManager.AddComponent(newEntity, _oldComponent);
var srcEntity = core.GetNodeEntityInternal(_oldComponent.Source);
var tgtEntity = core.GetNodeEntityInternal(_oldComponent.Target);
core.RelationManager.AddRelation(srcEntity, newEntity, EdgeRelationType.From);
core.RelationManager.AddRelation(tgtEntity, newEntity, EdgeRelationType.To);
}
}
public class UpdateNodeCommand<TKey, TNodeData, TEdgeData> : NodeCommandBase<TKey, TNodeData, TEdgeData>
where TKey : notnull
where TNodeData : class
{
private readonly TKey _key;
private readonly TNodeData _newData;
private readonly string? _newSymbol;
private TNodeData? _oldData;
private string? _oldSymbol;
private bool _executed;
public UpdateNodeCommand(TKey key, TNodeData newData, string? newSymbol = null) => (_key, _newData, _newSymbol) = (key, newData, newSymbol);
protected override void ExecuteCore(TransactionContext<TKey, TNodeData, TEdgeData> ctx)
{
var core = ctx.Core;
var entity = core.GetNodeEntityInternal(_key);
if (!entity.IsValid)
throw new InvalidOperationException($"Node with key '{_key}' not found.");
var oldComp = core.EntityManager.GetComponent<NodeComponent<TKey, TNodeData>>(entity);
_oldData = oldComp.Data;
_oldSymbol = oldComp.Symbol;
core.EntityManager.SetComponent(entity, new NodeComponent<TKey, TNodeData>(_key, _newData, _newSymbol));
_executed = true;
ctx.PendingEvents.Add(new NodeUpdatedEvent<TKey>(_key, _oldData, _newData, _oldSymbol, _newSymbol));
}
protected override void UndoCore(TransactionContext<TKey, TNodeData, TEdgeData> ctx)
{
if (!_executed) return;
var core = ctx.Core;
var entity = core.GetNodeEntityInternal(_key);
if (entity.IsValid)
core.EntityManager.SetComponent(entity, new NodeComponent<TKey, TNodeData>(_key, _oldData!, _oldSymbol));
}
}
}
namespace GraphFoundation
{
using GraphFoundation.Core;
using GraphFoundation.Core.Implementation;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
public class SymbolIndexProjector<TKey, TNodeData, TEdgeData> : IDisposable
where TKey : notnull
where TNodeData : class
{
private readonly Dictionary<string, TKey> _symbolToKey = new();
private readonly IDisposable _subscription;
private readonly object _lock = new();
private bool _disposed;
public SymbolIndexProjector(ICoreGraph<TKey, TNodeData, TEdgeData> graph)
{
lock (_lock)
{
foreach (var key in graph.GetAllNodeKeys())
{
var symbol = graph.GetNodeSymbol(key);
if (symbol != null) _symbolToKey[symbol] = key;
}
}
_subscription = graph.Events.Subscribe(OnEvent);
}
private void OnEvent(GraphEvent evt)
{
lock (_lock)
{
if (_disposed) return;
switch (evt)
{
case NodeAddedEvent<TKey> added when added.Symbol != null:
_symbolToKey[added.Symbol] = added.Key;
break;
case NodeRemovedEvent<TKey> removed when removed.OldSymbol != null:
_symbolToKey.Remove(removed.OldSymbol);
break;
case NodeUpdatedEvent<TKey> updated:
if (updated.OldSymbol != null) _symbolToKey.Remove(updated.OldSymbol);
if (updated.NewSymbol != null) _symbolToKey[updated.NewSymbol] = updated.Key;
break;
}
}
}
public TKey? GetKeyBySymbol(string symbol)
{
lock (_lock)
return _symbolToKey.TryGetValue(symbol, out var key) ? key : default;
}
public void Dispose()
{
if (!_disposed)
{
_subscription.Dispose();
_disposed = true;
}
}
}
public class Chain<TKey, TNodeData, TEdgeData> : IReadOnlyList<TKey>, IDisposable
where TKey : notnull
where TNodeData : class
{
private readonly CoreGraph<TKey, TNodeData, TEdgeData> _graph;
private readonly string _chainId;
private List<TKey> _keys;
private List<string> _edgeIds;
private bool _disposed;
private readonly object _lock = new();
public IReadOnlyList<string> EdgeIds => _edgeIds.AsReadOnly();
public bool IsValid => !_disposed;
public Chain(IContext context, IEnumerable<TKey> keys, IEnumerable<string> edgeIds)
{
if (context == null) throw new ArgumentNullException(nameof(context));
if (!context.TryGetService<ICoreGraph<TKey, TNodeData, TEdgeData>>(out var graph) || graph is not CoreGraph<TKey, TNodeData, TEdgeData> coreGraph)
throw new InvalidOperationException("No CoreGraph service found in context or invalid type.");
_graph = coreGraph;
_chainId = Guid.NewGuid().ToString("N");
_keys = keys.ToList();
_edgeIds = edgeIds.ToList();
foreach (var key in _keys)
if (!_graph.ContainsNode(key))
throw new ArgumentException($"Node {key} does not exist.");
foreach (var eid in _edgeIds)
if (!_graph.ContainsEdge(eid))
throw new ArgumentException($"Edge {eid} does not exist.");
}
private string GetEdgeId(int index) => string.Format(Constants.ChainEdgeIdFormat, _chainId, index);
private void ThrowIfDisposed()
{
if (_disposed) throw new ObjectDisposedException(nameof(Chain<TKey, TNodeData, TEdgeData>));
}
public TKey this[int index]
{
get
{
ThrowIfDisposed();
lock (_lock) return _keys[index];
}
}
public int Count
{
get
{
ThrowIfDisposed();
lock (_lock) return _keys.Count;
}
}
public IEnumerator<TKey> GetEnumerator()
{
ThrowIfDisposed();
lock (_lock) return _keys.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
private void UpdateChain(Func<List<TKey>, List<TKey>> keyUpdate, Func<TKey, (TNodeData data, string? symbol)?>? nodeCreator = null)
{
ThrowIfDisposed();
lock (_lock)
{
var newKeys = keyUpdate(_keys.ToList());
var oldEdgesMap = new Dictionary<(TKey, TKey), (TEdgeData Data, double Weight)>();
foreach (var eid in _edgeIds)
{
var details = _graph.TryGetEdgeDetails(eid);
if (details.HasValue)
oldEdgesMap[(details.Value.Source, details.Value.Target)] = (details.Value.Data, details.Value.Weight);
}
using (var tx = _graph.BeginTransaction())
{
// Add missing nodes (using internal method to avoid read lock recursion)
foreach (var key in newKeys)
{
if (!_graph.ContainsNodeInternal(key))
{
if (nodeCreator == null)
throw new InvalidOperationException($"Node '{key}' does not exist and no nodeCreator provided.");
var creation = nodeCreator(key) ?? throw new InvalidOperationException($"Node '{key}' does not exist and nodeCreator returned null.");
tx.Enqueue(new AddNodeCommand<TKey, TNodeData, TEdgeData>(key, creation.data, creation.symbol));
}
}
// Remove old edges (using internal method)
foreach (var eid in _edgeIds)
if (_graph.ContainsEdgeInternal(eid))
tx.Enqueue(new RemoveEdgeCommand<TKey, TNodeData, TEdgeData>(eid));
var newEdgeInfo = new List<(string EdgeId, TKey Source, TKey Target, TEdgeData Data, double Weight)>();
if (newKeys.Count >= Constants.MinChainLengthForEdges)
{
for (int i = 0; i < newKeys.Count - 1; i++)
{
var edgeId = GetEdgeId(i);
var source = newKeys[i];
var target = newKeys[i + 1];
TEdgeData dataToUse = default(TEdgeData)!;
double weight = Constants.DefaultEdgeWeight;
if (oldEdgesMap.TryGetValue((source, target), out var oldInfo))
{
dataToUse = oldInfo.Data;
weight = oldInfo.Weight;
}
newEdgeInfo.Add((edgeId, source, target, dataToUse, weight));
tx.Enqueue(new AddEdgeCommand<TKey, TNodeData, TEdgeData>(edgeId, source, target, true, dataToUse, weight));
}
}
tx.OnCommit(() =>
{
lock (_lock)
{
_keys = newKeys;
_edgeIds = newEdgeInfo.Select(e => e.EdgeId).ToList();
}
});
tx.Commit();
}
}
}
public void InsertAt(int index, TKey key, TNodeData? data = default, string? symbol = null) =>
UpdateChain(keys => { if (index < 0 || index > keys.Count) throw new ArgumentOutOfRangeException(nameof(index)); keys.Insert(index, key); return keys; },
keyNode => keyNode.Equals(key) ? (data ?? default(TNodeData)!, symbol) : null);
public void RemoveAt(int index) =>
UpdateChain(keys => { if (index < 0 || index >= keys.Count) throw new ArgumentOutOfRangeException(nameof(index)); keys.RemoveAt(index); return keys; });
public void Move(int oldIndex, int newIndex) =>
UpdateChain(keys =>
{
if (oldIndex == newIndex) return keys;
if (oldIndex < 0 || oldIndex >= keys.Count || newIndex < 0 || newIndex >= keys.Count)
throw new ArgumentOutOfRangeException();
var key = keys[oldIndex];
keys.RemoveAt(oldIndex);
keys.Insert(newIndex, key);
return keys;
});
public void Dispose()
{
if (_disposed) return;
try
{
using (var tx = _graph.BeginTransaction())
{
foreach (var eid in _edgeIds)
if (_graph.ContainsEdgeInternal(eid))
tx.Enqueue(new RemoveEdgeCommand<TKey, TNodeData, TEdgeData>(eid));
tx.OnCommit(() => { lock (_lock) _edgeIds.Clear(); });
tx.Commit();
}
}
catch
{
// Ignore exceptions during disposal
}
_disposed = true;
}
}
public static class GraphExtensions
{
public static Chain<TKey, TNodeData, TEdgeData> CreateChain<TKey, TNodeData, TEdgeData>(this IContext context, IEnumerable<TKey> keys)
where TKey : notnull
where TNodeData : class
{
if (!context.TryGetService<ICoreGraph<TKey, TNodeData, TEdgeData>>(out var graph) || graph is not CoreGraph<TKey, TNodeData, TEdgeData> coreGraph)
throw new InvalidOperationException("No CoreGraph service found in context or invalid type.");
var keysList = keys.ToList();
var chainId = Guid.NewGuid().ToString("N");
var edgeIds = new List<string>();
using (var tx = coreGraph.BeginTransaction())
{
for (int i = 0; i < keysList.Count - 1; i++)
{
var edgeId = string.Format(Constants.ChainEdgeIdFormat, chainId, i);
tx.Enqueue(new AddEdgeCommand<TKey, TNodeData, TEdgeData>(edgeId, keysList[i], keysList[i + 1], true, default(TEdgeData)!, Constants.DefaultEdgeWeight));
edgeIds.Add(edgeId);
}
tx.Commit();
}
return new Chain<TKey, TNodeData, TEdgeData>(context, keysList, edgeIds);
}
public static IObservable<NodeAddedEvent<TKey>> NodeAdded<TKey, TNodeData, TEdgeData>(this IReadOnlyGraph<TKey, TNodeData, TEdgeData> graph)
where TKey : notnull where TNodeData : class => graph.Events.OfType<NodeAddedEvent<TKey>>();
public static IObservable<NodeRemovedEvent<TKey>> NodeRemoved<TKey, TNodeData, TEdgeData>(this IReadOnlyGraph<TKey, TNodeData, TEdgeData> graph)
where TKey : notnull where TNodeData : class => graph.Events.OfType<NodeRemovedEvent<TKey>>();
public static IObservable<NodeUpdatedEvent<TKey>> NodeUpdated<TKey, TNodeData, TEdgeData>(this IReadOnlyGraph<TKey, TNodeData, TEdgeData> graph)
where TKey : notnull where TNodeData : class => graph.Events.OfType<NodeUpdatedEvent<TKey>>();
public static IObservable<EdgeAddedEvent<TKey>> EdgeAdded<TKey, TNodeData, TEdgeData>(this IReadOnlyGraph<TKey, TNodeData, TEdgeData> graph)
where TKey : notnull where TNodeData : class => graph.Events.OfType<EdgeAddedEvent<TKey>>();
public static IObservable<EdgeRemovedEvent<TKey>> EdgeRemoved<TKey, TNodeData, TEdgeData>(this IReadOnlyGraph<TKey, TNodeData, TEdgeData> graph)
where TKey : notnull where TNodeData : class => graph.Events.OfType<EdgeRemovedEvent<TKey>>();
public static IObservable<EdgeUpdatedEvent<TKey>> EdgeUpdated<TKey, TNodeData, TEdgeData>(this IReadOnlyGraph<TKey, TNodeData, TEdgeData> graph)
where TKey : notnull where TNodeData : class => graph.Events.OfType<EdgeUpdatedEvent<TKey>>();
}
}
namespace GraphFoundation.ErrorHandling
{
using ContextManagement;
using GraphFoundation.Core;
using System;
using System.Collections.Generic;
public enum GraphOperationErrorType
{
None,
NodeNotFound,
EdgeNotFound,
DuplicateNode,
DuplicateEdge,
InvalidOperation,
TransactionFailure,
CommandExecutionFailed,
UndoFailed,
RollbackFailed,
ConcurrencyConflict,
Timeout,
Cancellation,
Unknown
}
public class GraphError
{
public string Id { get; } = Guid.NewGuid().ToString();
public DateTime Timestamp { get; } = DateTime.UtcNow;
public GraphOperationErrorType Type { get; }
public Exception Exception { get; }
public string Message { get; }
public object? Context { get; }
public GraphError(GraphOperationErrorType type, Exception exception, string? message = null, object? context = null)
{
Type = type;
Exception = exception;
Message = message ?? exception.Message;
Context = context;
}
}
public interface IErrorHandler
{
bool HandleError(GraphError error);
}
public class ErrorHandlerService : IErrorHandler, IDisposable
{
private readonly List<IErrorHandler> _handlers = new();
private bool _disposed;
public void AddHandler(IErrorHandler handler)
{
if (handler == null) throw new ArgumentNullException(nameof(handler));
_handlers.Add(handler);
}
public bool HandleError(GraphError error)
{
if (_disposed) return false;
bool handled = false;
foreach (var handler in _handlers)
{
try
{
if (handler.HandleError(error))
handled = true;
}
catch { }
}
return handled;
}
public void Dispose()
{
if (_disposed) return;
_handlers.Clear();
_disposed = true;
}
}
public class FallbackErrorHandler : IErrorHandler
{
private readonly Func<GraphError, bool> _fallbackAction;
public FallbackErrorHandler(Func<GraphError, bool> fallbackAction) => _fallbackAction = fallbackAction ?? throw new ArgumentNullException(nameof(fallbackAction));
public bool HandleError(GraphError error) => _fallbackAction(error);
}
public static class ErrorHandlingExtensions
{
public static IContext UseErrorHandling(this IContext context, Action<ErrorHandlerService> configure)
{
var service = new ErrorHandlerService();
configure(service);
context.GetOrAddService<IErrorHandler>(() => service);
context.GetOrAddService<ErrorHandlerService>(() => service);
return context;
}
public static IErrorHandler? GetErrorHandler(this IContext context)
{
return context.TryGetService<IErrorHandler>(out var handler) ? handler : null;
}
}
public class ErrorHandlingCommand : ICommand
{
private readonly ICommand _inner;
private readonly IErrorHandler _errorHandler;
public ErrorHandlingCommand(ICommand inner, IErrorHandler errorHandler)
{
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
_errorHandler = errorHandler ?? throw new ArgumentNullException(nameof(errorHandler));
}
public void Execute(TransactionContextBase context)
{
try
{
_inner.Execute(context);
}
catch (Exception ex)
{
var error = CreateError(ex, context);
_errorHandler.HandleError(error);
throw;
}
}
public void Undo(TransactionContextBase context)
{
try
{
_inner.Undo(context);
}
catch (Exception ex)
{
var error = CreateError(ex, context);
_errorHandler.HandleError(error);
}
}
private GraphError CreateError(Exception ex, TransactionContextBase context)
{
var errorType = DetermineErrorType(ex);
return new GraphError(errorType, ex, context: context);
}
private GraphOperationErrorType DetermineErrorType(Exception ex) => ex switch
{
KeyNotFoundException => GraphOperationErrorType.NodeNotFound,
InvalidOperationException => GraphOperationErrorType.InvalidOperation,
OperationCanceledException => GraphOperationErrorType.Cancellation,
TimeoutException => GraphOperationErrorType.Timeout,
_ => GraphOperationErrorType.Unknown
};
}
}
#endregion