Feat/readmodel graph relations#14
Feat/readmodel graph relations#14loning merged 2 commits intofeat/generic-event-sourcing-elasticsearch-readmodelfrom
Conversation
…ions - Introduced a comprehensive blueprint for refactoring ReadModel graph relations, detailing the current capabilities, gaps, and restructuring goals. - Added new abstractions for managing projection relation stores, including interfaces and classes for nodes, edges, and queries. - Implemented support for relation capabilities in existing providers, enhancing the architecture to accommodate graph relations. - Updated dependency injection configurations for InMemory and Neo4j providers to register relation stores, ensuring compatibility with the new abstractions. - Enhanced validation mechanisms to include relation requirements and capabilities, improving overall robustness in provider selection and usage.
- Completed Phase 2 of the ReadModel graph relations refactor, implementing breaking changes to improve the architecture. - Introduced `IProjectionStoreStartupValidator` for enhanced validation of read model and relation providers during startup. - Split `IWorkflowExecutionProjectionPort` into `IWorkflowExecutionProjectionLifecyclePort` and `IWorkflowExecutionProjectionQueryPort`, clarifying responsibilities and improving maintainability. - Updated dependency injection to register new lifecycle and query services, ensuring proper integration with the refactored architecture. - Enhanced documentation to reflect changes in the projection architecture and the new validation mechanisms, providing clearer guidance for future development.
ba6da81
into
feat/generic-event-sourcing-elasticsearch-readmodel
There was a problem hiding this comment.
Pull request overview
This PR completes the Phase 2 refactor of the CQRS Projection subsystem by introducing first-class graph relation support (node/edge stores + traversal), splitting the workflow projection port into lifecycle/query dual ports, and wiring provider selection/validation end-to-end (runtime → providers → workflow API).
Changes:
- Added relation-store abstractions + runtime selection/validation, plus InMemory/Neo4j implementations and an explicit “unsupported” Elasticsearch relation store.
- Refactored workflow projection ports into
IWorkflowExecutionProjectionLifecyclePortandIWorkflowExecutionProjectionQueryPort, updated DI, orchestration, and startup validation accordingly. - Extended workflow querying and HTTP endpoints to support actor relation listing and subgraph retrieval, with new tests and migration/blueprint docs.
Reviewed changes
Copilot reviewed 86 out of 86 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tools/ci/architecture_guards.sh | Updates CI architecture guardrails for the split lifecycle/query projection ports. |
| test/Aevatar.Workflow.Host.Api.Tests/WorkflowReadModelSelectionPlannerTests.cs | Updates selection-plan tests for separate read-model vs relation provider selection. |
| test/Aevatar.Workflow.Host.Api.Tests/WorkflowProjectionOrchestrationComponentTests.cs | Updates query reader construction to include relation store dependency. |
| test/Aevatar.Workflow.Host.Api.Tests/WorkflowHostingExtensionsCoverageTests.cs | Extends DI coverage to assert relation store registrations are present. |
| test/Aevatar.Workflow.Host.Api.Tests/WorkflowExecutionRelationProjectorTests.cs | Adds unit tests covering relation projection behavior (edges/nodes/subgraph). |
| test/Aevatar.Workflow.Host.Api.Tests/WorkflowExecutionProjectionServiceTests.cs | Refactors tests to use split lifecycle/query services via a harness. |
| test/Aevatar.Workflow.Host.Api.Tests/WorkflowExecutionProjectionRegistrationTests.cs | Adds tests for split provider resolution and fail-fast relation validation. |
| test/Aevatar.Workflow.Host.Api.Tests/WorkflowCapabilityEndpointsCoverageTests.cs | Updates fake query service to cover new relations/subgraph API surface. |
| test/Aevatar.Workflow.Host.Api.Tests/ChatWebSocketCoordinatorAndProtocolTests.cs | Updates fake query service for new relations/subgraph methods. |
| test/Aevatar.Workflow.Host.Api.Tests/ChatEndpointsInternalTests.cs | Adds route and endpoint tests for relations and relation-subgraph queries. |
| test/Aevatar.Workflow.Application.Tests/WorkflowRunOrchestrationComponentTests.cs | Updates projection port dependency to lifecycle-only and adds relation query stubs. |
| test/Aevatar.Workflow.Application.Tests/WorkflowApplicationLayerTests.cs | Adds application-layer tests for relations and relation-subgraph queries. |
| src/workflow/extensions/Aevatar.Workflow.Extensions.Hosting/WorkflowProjectionProviderServiceCollectionExtensions.cs | Registers relation store providers (InMemory/ES/Neo4j) alongside read-model stores. |
| src/workflow/README.md | Updates diagrams/text to reflect port split and relation capabilities. |
| src/workflow/Aevatar.Workflow.Projection/ReadModels/WorkflowExecutionRelationConstants.cs | Introduces workflow relation scope, node types, and relation types constants. |
| src/workflow/Aevatar.Workflow.Projection/ReadModels/WorkflowExecutionReadModelMapper.cs | Adds mapping from relation nodes/edges/subgraphs to workflow query models. |
| src/workflow/Aevatar.Workflow.Projection/README.md | Documents new lifecycle/query ports and relation-enabled selection planning. |
| src/workflow/Aevatar.Workflow.Projection/Projectors/WorkflowExecutionRelationProjector.cs | Adds projector that writes workflow run/step/topology relations to relation store. |
| src/workflow/Aevatar.Workflow.Projection/Orchestration/WorkflowReadModelStartupValidationHostedService.cs | Refactors startup validation to validate both read-model and relation providers. |
| src/workflow/Aevatar.Workflow.Projection/Orchestration/WorkflowReadModelSelectionPlanner.cs | Splits selection into read-model vs relation requirements/options (with fallback rules). |
| src/workflow/Aevatar.Workflow.Projection/Orchestration/WorkflowProjectionQueryReader.cs | Extends query reader to serve relation neighbors and bounded-depth subgraphs. |
| src/workflow/Aevatar.Workflow.Projection/Orchestration/WorkflowExecutionProjectionService.cs | Removes legacy unified projection facade (breaking change). |
| src/workflow/Aevatar.Workflow.Projection/Orchestration/WorkflowExecutionProjectionQueryService.cs | Adds query-port implementation using generic query-port base. |
| src/workflow/Aevatar.Workflow.Projection/Orchestration/WorkflowExecutionProjectionLifecycleService.cs | Adds lifecycle-port implementation using generic lifecycle-port base. |
| src/workflow/Aevatar.Workflow.Projection/Orchestration/IWorkflowReadModelSelectionPlanner.cs | Updates selection plan contract to include separate read-model/relation plans. |
| src/workflow/Aevatar.Workflow.Projection/Orchestration/IWorkflowProjectionSinkSubscriptionManager.cs | Adopts generic projection sink subscription manager contract. |
| src/workflow/Aevatar.Workflow.Projection/Orchestration/IWorkflowProjectionReleaseService.cs | Adopts generic projection release service contract. |
| src/workflow/Aevatar.Workflow.Projection/Orchestration/IWorkflowProjectionQueryReader.cs | Extends query reader contract with relations and subgraph queries. |
| src/workflow/Aevatar.Workflow.Projection/Orchestration/IWorkflowProjectionLiveSinkForwarder.cs | Adopts generic projection live-sink forwarder contract. |
| src/workflow/Aevatar.Workflow.Projection/Orchestration/IWorkflowProjectionActivationService.cs | Adopts generic projection activation service contract. |
| src/workflow/Aevatar.Workflow.Projection/DependencyInjection/ServiceCollectionExtensions.cs | Registers split ports and relation store selector in workflow projection DI. |
| src/workflow/Aevatar.Workflow.Projection/Configuration/WorkflowExecutionProjectionOptions.cs | Adds relation provider config + startup validation toggle. |
| src/workflow/Aevatar.Workflow.Infrastructure/DependencyInjection/WorkflowCapabilityServiceCollectionExtensions.cs | Applies global relation provider option into workflow projection options. |
| src/workflow/Aevatar.Workflow.Infrastructure/CapabilityApi/ChatQueryEndpoints.cs | Adds HTTP endpoints for relations list and relation subgraph retrieval. |
| src/workflow/Aevatar.Workflow.Application/Runs/WorkflowRunResourceFinalizer.cs | Switches dependency to lifecycle port after port split. |
| src/workflow/Aevatar.Workflow.Application/Runs/WorkflowRunContextFactory.cs | Switches dependency to lifecycle port after port split. |
| src/workflow/Aevatar.Workflow.Application/README.md | Updates docs to reference query port for read-side queries. |
| src/workflow/Aevatar.Workflow.Application/Queries/WorkflowExecutionQueryApplicationService.cs | Routes new relation/subgraph queries through the projection query port. |
| src/workflow/Aevatar.Workflow.Application.Abstractions/Queries/WorkflowExecutionQueryModels.cs | Adds relation node/edge/subgraph query DTOs. |
| src/workflow/Aevatar.Workflow.Application.Abstractions/Queries/IWorkflowExecutionQueryApplicationService.cs | Extends application query contract with relations/subgraph methods. |
| src/workflow/Aevatar.Workflow.Application.Abstractions/Projections/IWorkflowExecutionProjectionQueryPort.cs | Introduces query-only projection port contract. |
| src/workflow/Aevatar.Workflow.Application.Abstractions/Projections/IWorkflowExecutionProjectionLifecyclePort.cs | Renames/splits lifecycle-only projection port contract. |
| src/Aevatar.CQRS.Projection.Runtime/Runtime/ProjectionStoreStartupValidator.cs | Adds runtime-level startup validator that can validate read-model and relation providers. |
| src/Aevatar.CQRS.Projection.Runtime/Runtime/ProjectionRelationStoreProviderSelector.cs | Implements relation provider selection + capability validation. |
| src/Aevatar.CQRS.Projection.Runtime/Runtime/ProjectionRelationStoreProviderRegistry.cs | Adds registry to collect relation store registrations from DI. |
| src/Aevatar.CQRS.Projection.Runtime/Runtime/ProjectionRelationStoreFactory.cs | Adds factory to create relation store based on selection plan. |
| src/Aevatar.CQRS.Projection.Runtime/Runtime/ProjectionReadModelProviderSelector.cs | Extends selection logging to include relation-related requirements/capabilities. |
| src/Aevatar.CQRS.Projection.Runtime/Runtime/ProjectionReadModelBindingResolver.cs | Makes Graph bindings imply relation storage + traversal requirements. |
| src/Aevatar.CQRS.Projection.Runtime/DependencyInjection/ServiceCollectionExtensions.cs | Registers relation runtime components and startup validator. |
| src/Aevatar.CQRS.Projection.Providers.Neo4j/Stores/Neo4jProjectionRelationStore.cs | Adds Neo4j-backed relation store with traversal and optional schema init. |
| src/Aevatar.CQRS.Projection.Providers.Neo4j/Stores/Neo4jProjectionReadModelStore.cs | Declares Neo4j read-model provider supports relations/traversal. |
| src/Aevatar.CQRS.Projection.Providers.Neo4j/DependencyInjection/ServiceCollectionExtensions.cs | Adds DI registration for Neo4j relation store provider. |
| src/Aevatar.CQRS.Projection.Providers.Neo4j/Configuration/Neo4jProjectionRelationStoreOptions.cs | Introduces configuration options for Neo4j relation store. |
| src/Aevatar.CQRS.Projection.Providers.InMemory/Stores/InMemoryProjectionRelationStore.cs | Adds in-memory relation store implementation (neighbors + bounded subgraph). |
| src/Aevatar.CQRS.Projection.Providers.InMemory/Stores/InMemoryProjectionReadModelStore.cs | Declares InMemory provider supports relations/traversal capability flags. |
| src/Aevatar.CQRS.Projection.Providers.InMemory/DependencyInjection/ServiceCollectionExtensions.cs | Adds DI registration for InMemory relation store provider. |
| src/Aevatar.CQRS.Projection.Providers.Elasticsearch/Stores/ElasticsearchProjectionRelationStore.cs | Adds explicit no-op relation store declaring no relation support. |
| src/Aevatar.CQRS.Projection.Providers.Elasticsearch/DependencyInjection/ServiceCollectionExtensions.cs | Adds DI registration for Elasticsearch relation store provider. |
| src/Aevatar.CQRS.Projection.Core/README.md | Documents new generic lifecycle/query port base services. |
| src/Aevatar.CQRS.Projection.Core/Orchestration/ProjectionQueryPortServiceBase.cs | Adds generic base class centralizing query enable-gates (snapshot/timeline/relations/subgraph). |
| src/Aevatar.CQRS.Projection.Core/Orchestration/ProjectionLifecyclePortServiceBase.cs | Adds generic base class centralizing lifecycle orchestration and sink attach/detach/release. |
| src/Aevatar.CQRS.Projection.Abstractions/README.md | Documents new projection port abstractions. |
| src/Aevatar.CQRS.Projection.Abstractions/Abstractions/ProjectionRelationSubgraph.cs | Introduces relation subgraph model. |
| src/Aevatar.CQRS.Projection.Abstractions/Abstractions/ProjectionRelationQuery.cs | Introduces relation query model (scope/root/direction/types/depth/take). |
| src/Aevatar.CQRS.Projection.Abstractions/Abstractions/ProjectionRelationNode.cs | Introduces relation node model. |
| src/Aevatar.CQRS.Projection.Abstractions/Abstractions/ProjectionRelationEdge.cs | Introduces relation edge model. |
| src/Aevatar.CQRS.Projection.Abstractions/Abstractions/ProjectionRelationDirection.cs | Introduces relation traversal direction enum. |
| src/Aevatar.CQRS.Projection.Abstractions/Abstractions/ProjectionReadModelRuntimeOptions.cs | Adds relation provider option to runtime options. |
| src/Aevatar.CQRS.Projection.Abstractions/Abstractions/ProjectionReadModelRequirements.cs | Extends requirements with relation storage + traversal flags. |
| src/Aevatar.CQRS.Projection.Abstractions/Abstractions/ProjectionReadModelProviderCapabilities.cs | Extends provider capabilities with relation storage + traversal flags. |
| src/Aevatar.CQRS.Projection.Abstractions/Abstractions/ProjectionReadModelCapabilityValidator.cs | Extends capability validation rules for relation requirements. |
| src/Aevatar.CQRS.Projection.Abstractions/Abstractions/IProjectionStoreStartupValidator.cs | Adds abstraction for startup validation of read-model and relation providers. |
| src/Aevatar.CQRS.Projection.Abstractions/Abstractions/IProjectionRelationStoreRegistration.cs | Adds relation provider registration abstraction. |
| src/Aevatar.CQRS.Projection.Abstractions/Abstractions/IProjectionRelationStoreProviderSelector.cs | Adds relation provider selector abstraction. |
| src/Aevatar.CQRS.Projection.Abstractions/Abstractions/IProjectionRelationStoreProviderRegistry.cs | Adds relation provider registry abstraction. |
| src/Aevatar.CQRS.Projection.Abstractions/Abstractions/IProjectionRelationStoreProviderMetadata.cs | Adds relation provider metadata interface. |
| src/Aevatar.CQRS.Projection.Abstractions/Abstractions/IProjectionRelationStoreFactory.cs | Adds relation store factory abstraction. |
| src/Aevatar.CQRS.Projection.Abstractions/Abstractions/IProjectionRelationStore.cs | Adds relation store contract (upsert/delete/query/subgraph). |
| src/Aevatar.CQRS.Projection.Abstractions/Abstractions/IProjectionPortSinkSubscriptionManager.cs | Adds generic sink subscription manager abstraction. |
| src/Aevatar.CQRS.Projection.Abstractions/Abstractions/IProjectionPortReleaseService.cs | Adds generic lease release abstraction. |
| src/Aevatar.CQRS.Projection.Abstractions/Abstractions/IProjectionPortLiveSinkForwarder.cs | Adds generic live sink forwarder abstraction. |
| src/Aevatar.CQRS.Projection.Abstractions/Abstractions/IProjectionPortActivationService.cs | Adds generic activation abstraction. |
| src/Aevatar.CQRS.Projection.Abstractions/Abstractions/DelegateProjectionRelationStoreRegistration.cs | Adds delegate-based relation provider registration helper. |
| docs/architecture/readmodel-graph-relations-refactor-blueprint.md | Adds blueprint/migration guide for the breaking refactor and relation capability rollout. |
| docs/FOUNDATION.md | Updates foundation docs to reflect lifecycle/query port split and shared base classes. |
| docs/CQRS_ARCHITECTURE.md | Updates CQRS architecture docs to reflect the new projection port layering. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| private static string BuildEdgeId(string relationType, string fromNodeId, string toNodeId) | ||
| { | ||
| var payload = $"{relationType}|{fromNodeId}|{toNodeId}"; | ||
| var hash = SHA256.HashData(Encoding.UTF8.GetBytes(payload)); | ||
| return $"{relationType}:{Convert.ToHexString(hash.AsSpan(0, 8))}"; | ||
| } |
There was a problem hiding this comment.
BuildEdgeId truncates the SHA-256 hash to 8 bytes (64 bits). This increases the probability of EdgeId collisions, which would cause distinct relations to overwrite each other in stores keyed by EdgeId. Consider using the full hash (or a longer prefix) and/or including from/to in the EdgeId without truncation.
| for (var currentDepth = 0; currentDepth < depth; currentDepth++) | ||
| { | ||
| if (frontier.Count == 0 || collectedEdges.Count >= take) | ||
| break; | ||
|
|
||
| var nextFrontier = new HashSet<string>(StringComparer.Ordinal); | ||
| foreach (var nodeId in frontier) | ||
| { | ||
| ct.ThrowIfCancellationRequested(); | ||
| var neighbors = await GetNeighborsAsync( | ||
| new ProjectionRelationQuery | ||
| { | ||
| Scope = scope, | ||
| RootNodeId = nodeId, | ||
| Direction = query.Direction, | ||
| RelationTypes = query.RelationTypes, | ||
| Depth = 1, | ||
| Take = take - collectedEdges.Count, | ||
| }, | ||
| ct); |
There was a problem hiding this comment.
GetSubgraphAsync issues one Neo4j query per frontier node per depth step by calling GetNeighborsAsync inside nested loops. This N+1 query pattern will scale poorly on larger graphs. Consider replacing the BFS loop with a single Cypher variable-length path query (bounded by depth/take) that returns distinct nodes and relationships in one round trip.
| private ProjectionRelationNode CloneNode(ProjectionRelationNode source, string scope, string nodeId) | ||
| { | ||
| var payload = JsonSerializer.Serialize(source, _jsonOptions); | ||
| var clone = JsonSerializer.Deserialize<ProjectionRelationNode>(payload, _jsonOptions) | ||
| ?? throw new InvalidOperationException("Failed to clone relation node."); |
There was a problem hiding this comment.
InMemoryProjectionRelationStore deep-clones nodes/edges via JsonSerializer serialize+deserialize on every upsert/query. This is significantly more CPU/alloc heavy than necessary and can become a bottleneck even in test/dev usage. Consider cloning by constructing new objects and copying dictionaries (or returning immutable snapshots) instead of JSON round-trips.
| int take, | ||
| CancellationToken ct); | ||
|
|
||
| protected virtual TRelationSubgraph CreateEmptyRelationSubgraph(string entityId) => default!; |
There was a problem hiding this comment.
CreateEmptyRelationSubgraph defaults to default!. If a derived query port forgets to override this, disabled/invalid relation-subgraph queries can return null (for reference types) despite a non-null return contract. Consider making this method abstract (forcing an override) or adding a new() constraint and returning a concrete empty instance by default.
| protected virtual TRelationSubgraph CreateEmptyRelationSubgraph(string entityId) => default!; | |
| protected abstract TRelationSubgraph CreateEmptyRelationSubgraph(string entityId); |
This pull request introduces a major refactor to the CQRS Projection subsystem, primarily to add first-class graph relation capabilities to the ReadModel layer and to decouple and enhance the provider, runtime, and workflow layers. The changes include new abstractions for relation stores and providers, expanded capability and requirements models, runtime and provider implementations for graph relations, and full support for relation queries in the Workflow API. This is a breaking change that finalizes the Phase 2 refactor, requiring migration to new dual-port interfaces.
Key changes by theme:
1. ReadModel Graph Relation Capability & Abstractions
IProjectionRelationStore,IProjectionRelationStoreFactory,IProjectionRelationStoreProviderRegistry,IProjectionRelationStoreProviderSelector, and registration/metadata abstractions. These enable pluggable, capability-aware graph relation providers. [1] [2] [3] [4] [5] [6] [7]ProjectionReadModelProviderCapabilitiesandProjectionReadModelRequirementsto include relation storage and traversal support, with corresponding validation logic inProjectionReadModelCapabilityValidator. [1] [2] [3]2. Runtime and Provider Layer Refactor
3. Workflow Layer and API Enhancements
WorkflowExecutionProjectionLifecycleServiceandWorkflowExecutionProjectionQueryService, both inheriting from new generic base classes for lifecycle and query ports. [1] [2] [3]WorkflowExecutionRelationProjectorfor writing graph relations, and extended the query chain and API endpoints to support relation and subgraph queries.4. New Generic Projection Port Contracts
5. Documentation and Migration Guidance
docs/architecture/readmodel-graph-relations-refactor-blueprint.md) detailing the new architecture, provider capability matrix, migration steps, and best practices for rollout and validation.This refactor is breaking and requires migration to the new dual-port projection interfaces. The old
IWorkflowExecutionProjectionPortinterface has been removed. Please refer to the included blueprint for migration steps and compatibility notes.