This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
OpenSleigh is a distributed saga management library for .NET that enables reliable, event-driven orchestration across microservices. It implements the saga pattern with choreography using an outbox pattern for guaranteed message delivery.
Key Facts:
- Language: C# 12 with nullable reference types enabled
- Target Frameworks: net8.0 and net9.0 (multi-targeting)
- Main Solution:
src/OpenSleigh.sln - Primary Branch:
develop(use this for PRs, not main/master) - License: Apache 2.0
- Current Version: 3.0.6 (defined in
Versions.props)
CRITICAL: Always work from the src/ directory for build operations.
cd src/
# 1. Restore dependencies (ALWAYS run first to avoid package resolution issues)
dotnet restore
# 2. Build the solution
dotnet build -c Release # Expect ~174 nullable warnings (acceptable), 0 errors required
# 3. Run unit tests (~105 tests, no infrastructure needed, ~5-10 seconds)
dotnet test --framework net9.0 --filter "Category!=E2E&Category!=Integration"
# 4. Run a specific test class or method
dotnet test --framework net9.0 --filter "FullyQualifiedName~SagaRunnerTests"
# 5. Verify code formatting
dotnet format --verify-no-changes
# 6. Create NuGet packages (outputs to repo root packages/ directory)
dotnet pack -c ReleaseIntegration tests require Docker infrastructure (MongoDB, RabbitMQ, Kafka, SQL Server, PostgreSQL):
# From repo root, start infrastructure
cd tests/infra
docker-compose up -d # Wait 10-15 seconds for services to initialize
# Return to src/ and run integration tests (~20-30 seconds)
cd ../../src
dotnet test --framework net9.0 --filter "FullyQualifiedName!~Cosmos&Category=Integration"
# If tests fail, clean up and restart infrastructure
cd ../tests/infra
docker-compose down && docker-compose up -dDocker credentials (for debugging connection issues):
- SQL Server:
SA_PASSWORD=Sup3r_p4ssword123 - PostgreSQL:
POSTGRES_PASSWORD=Sup3r_p4ssword123 - RabbitMQ vhost:
/opensleigh-tests
Note: Cosmos tests are excluded (not fully implemented). E2E tests are disabled in CI.
Saga: A long-running, distributed transaction coordinated through messages. Each saga instance maintains state and reacts to messages by executing handlers and publishing new messages.
Outbox Pattern: All messages published by sagas are stored in an outbox table/collection and processed asynchronously by a background service. This ensures at-least-once delivery with exactly-once processing guarantees.
Correlation: Each saga flow is identified by a CorrelationId that flows through all related messages, enabling distributed tracing and state management.
┌─────────────────────────────────────────────────────────────┐
│ Application Layer │
│ - Saga implementations (ISaga<TState>) │
│ - Message handlers (IHandleMessage<TMessage>) │
│ - Message definitions (IMessage) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ OpenSleigh Core │
│ - Saga orchestration (SagaRunner, SagaExecutionService) │
│ - Message processing pipeline (MessageProcessor) │
│ - Outbox management (OutboxBackgroundService) │
│ - Instance lifecycle (SagaInstance, SagaInstanceFactory) │
└─────────────────────────────────────────────────────────────┘
↓
┌────────────────────────────┬────────────────────────────────┐
│ Persistence Layer │ Transport Layer │
│ - ISagaStateRepository │ - IMessageSubscriber │
│ - IOutboxRepository │ - Message routing │
│ - Implementations: │ - Implementations: │
│ • Mongo │ • RabbitMQ │
│ • PostgreSQL │ • Kafka │
│ • SQL Server │ │
│ • InMemory │ │
└────────────────────────────┴────────────────────────────────┘
Saga Definition:
ISaga/ISaga<TState>: Marker interface for sagas (src/OpenSleigh/ISaga.cs)Saga<TState>: Base class providingPublish<TMessage>()method (src/OpenSleigh/Saga.cs)IStartedBy<TMessage>: Marks the message that initiates a saga (src/OpenSleigh/Transport/IStartedBy.cs)IHandleMessage<TMessage>: Interface for handling messages (src/OpenSleigh/Transport/IHandleMessage.cs)
Saga Lifecycle:
ISagaInstance/SagaInstance<TState>: Execution context with state, outbox, correlation tracking, and lock management (src/OpenSleigh/ISagaInstance.cs, SagaInstance.cs)ISagaInstanceFactory: Creates new saga instances with Guid v7 IDs (src/OpenSleigh/SagaInstanceFactory.cs)SagaDescriptor: Metadata describing saga type, initiator message, and state type (src/OpenSleigh/SagaDescriptor.cs)
Message Processing Pipeline:
- MessageProcessor: Entry point that routes messages from outbox to applicable sagas (src/OpenSleigh/Transport/MessageProcessor.cs)
- SagaRunner: Orchestrates processing for a single saga/message pair (src/OpenSleigh/SagaRunner.cs)
- SagaExecutionService: Two-phase execution: BeginProcessingAsync (fetch/lock) + CommitAsync (persist/unlock) (src/OpenSleigh/SagaExecutionService.cs)
- MessageHandlerManager: Invokes the actual handler with error handling and rollback (src/OpenSleigh/MessageHandlerManager.cs)
Outbox Pattern:
MessageEnvelope: Wraps messages with metadata (correlation ID, message ID, sender) (src/OpenSleigh/Outbox/MessageEnvelope.cs)IOutboxRepository: Persistence for pending messages with AppendAsync/ReadPendingAsync/DeleteAsync (src/OpenSleigh/Outbox/IOutboxRepository.cs)OutboxBackgroundService: Polls outbox repository at configured intervals (src/OpenSleigh/Outbox/OutboxBackgroundService.cs)
Configuration:
ServiceCollectionExtensions.AddOpenSleigh(): Single entry point for DI registration (src/OpenSleigh/DependencyInjection/ServiceCollectionExtensions.cs)BusConfigurator: Fluent builder for saga registration with.AddSaga<TSaga>()or.AddSaga<TSaga, TState>()(src/OpenSleigh/DependencyInjection/BusConfigurator.cs)SetPublishOnly(): Configures publish-only mode (no message processing, useful for API endpoints that only publish messages)
1. Idempotency
- Sagas track processed messages in
SagaInstance.ProcessedMessagesdictionary IIdempotentMessageinterface allows custom idempotency keys via SHA256 hashNoOpSagaInstancereturned for duplicate messages (src/OpenSleigh/NoOpSagaInstance.cs)- Prevents double-processing even with at-least-once delivery
2. Pessimistic Locking
ISagaStateRepository.LockAsync()acquires lock, returns lock ID stored in saga instance- Lock held during entire message processing
- Released via
ReleaseAsync()in commit phase - Prevents concurrent processing of same saga instance
3. Message Correlation
CorrelationIdflows through all related messages in a saga flow- First message (IStartedBy) establishes correlation ID
- All published messages inherit correlation ID from saga instance
- Enables distributed tracing and saga instance routing
4. Two-Phase Processing
- Phase 1:
BeginProcessingAsync()- fetch/lock saga, check idempotency, execute handler - Phase 2:
CommitAsync()- persist state and outbox messages, release lock - Allows rollback without persisting partial state
5. Discovery via Reflection
TypeExtensions.GetHandledMessageTypes()discovers allIHandleMessage<>implementationsTypeExtensions.GetInitiatorMessageType()finds theIStartedBy<>messageSagaDescriptorsResolverbuilds message-to-saga mappings at startup
Understanding how messages flow through the system:
1. Application publishes message via IMessageBus.PublishAsync()
↓
2. Message wrapped in MessageEnvelope and stored in IOutboxRepository
↓
3. OutboxBackgroundService polls outbox at configured interval
↓
4. MessageProcessor routes message to applicable sagas (multiple sagas can handle same message)
↓
5. For each saga: SagaRunner → SagaExecutionService.BeginProcessingAsync()
↓
6. BeginProcessingAsync: fetch/create saga instance → check idempotency → acquire lock
↓
7. SagaInstance.ProcessAsync() → MessageHandlerManager → IHandleMessage.HandleAsync()
↓
8. Handler publishes new messages via Saga.Publish() (queued in instance outbox)
↓
9. SagaExecutionService.CommitAsync() → persist state + outbox messages → release lock
↓
10. Outbox messages picked up in next poll cycle (back to step 3)
For Persistence: Implement ISagaStateRepository and IOutboxRepository, create IBusConfigurator extension method (see OpenSleigh.Persistence.Mongo for example)
For Transport: Implement IMessageSubscriber (start/stop subscription), create message parser to MessageEnvelope, route via IMessageProcessor (see OpenSleigh.Transport.RabbitMQ for example)
- OpenSleigh/ - Core library with saga orchestration, message processing, DI setup
- OpenSleigh.InMemory/ - In-memory implementations for development/testing
Persistence Implementations:
- OpenSleigh.Persistence.SQL/ - Base SQL abstractions (Entity Framework Core)
- OpenSleigh.Persistence.SQLServer/ - SQL Server provider
- OpenSleigh.Persistence.PostgreSQL/ - PostgreSQL provider
- OpenSleigh.Persistence.Mongo/ - MongoDB provider (no EF Core, uses MongoDB.Driver)
Transport Implementations:
- OpenSleigh.Transport.RabbitMQ/ - RabbitMQ transport with persistent connections and channel pooling
- OpenSleigh.Transport.Kafka/ - Kafka transport with consumer groups
- OpenSleigh.Tests/ - Unit tests (no external dependencies)
- OpenSleigh.E2ETests/ - End-to-end tests (currently disabled in CI)
- OpenSleigh.Persistence.*.Tests/ - Persistence layer integration tests (require Docker)
- OpenSleigh.Transport.*.Tests/ - Transport layer integration tests (require Docker)
- tests/infra/docker-compose.yml - Infrastructure services for integration tests
- OpenSleigh.Samples.Sample1/ - Basic single-service saga example
- OpenSleigh.Samples.Sample2/ - Multi-service example with API and Worker
- OpenSleigh.Samples.ECommerce/ - Complex distributed saga example (Orchestrator, Payment, Shipping, Inventory, Notifications)
- OpenSleigh.Samples.Blazor/ - Web UI example with Blazor
Samples demonstrate various persistence and transport configurations. Use as reference for integration patterns.
- Check logs -
SagaRunnerandMessageProcessorhave extensive logging for message routing and handler invocation - Verify saga registration - Ensure saga appears in
SagaDescriptorsResolvermessage-to-saga mappings - Trace correlation IDs - Follow
CorrelationIdthrough message flow to identify where saga instance diverges - Check locks - Verify
ISagaStateRepository.LockAsync()/ReleaseAsync()calls; stuck locks prevent processing - Inspect outbox - Query
IOutboxRepositoryfor pending messages; background service polls at configured interval - Verify idempotency - Check if message already in
ProcessedMessagesdictionary (would returnNoOpSagaInstance)
Tests use xUnit with NSubstitute for mocking. Categorize with traits:
[Trait("Category", "Integration")]- Requires Docker infrastructure[Trait("Category", "E2E")]- End-to-end scenarios (currently disabled in CI)
Enforced via codecov.yaml: 70% minimum for both project and patch coverage, with 1% threshold.
- CircleCI (primary): Runs build + unit tests + integration tests on every push; SonarCloud quality scan
- GitHub Actions: CodeQL security scanning (develop/releases branches); NuGet publishing (manual/releases)
- Multi-targeting requires
--framework net9.0when running tests/commands to avoid ambiguity - Persistence implementations differ - SQL providers use EF Core with transactions; Mongo uses native driver without EF
- Saga state serialization - State classes must be serializable (used by persistence layer); avoid circular references
- Lock lifetime - Locks are held for entire message processing; long-running handlers can create bottlenecks
- Outbox polling interval - Default configured via
OutboxProcessorOptions.Interval; balance responsiveness vs. database load - Message routing is many-to-many - One message type can trigger multiple sagas; one saga can handle multiple message types
- Correlation ID immutability - Once set on saga instance, correlation ID never changes; all published messages inherit it
- Do not modify
Versions.props,Directory.Build.props, ortest.runsettingsunless the task specifically requires it
Discuss changes via issue before implementing. Fork from develop branch (not main). Add tests for new code. Ensure dotnet format passes. Submit PR to develop.
- Every new implementation MUST happen in a feature branch
- Never commit new features directly to main
- If already on a feature branch, ask the user whether a sub-feature branch is needed before starting new work
- NEVER commit changes - the user will commit manually after code review
- MANDATORY: Always create a Pull Request to merge into main - never merge directly to main, even locally
- After completing any implementation, run the
superpowers:requesting-code-reviewskill to review the changes - When writing implementation plans, include a final task for code review using the
superpowers:code-reviewersubagent - Code review should check: plan compliance, code quality, architecture, and codebase conventions
- Address Critical and Important issues before considering work complete
- write comments only when strictly necessary (eg. the implementation is not obvious and the code is not self-explanatory)
- prefer clear and descriptive names for classes, methods, variables
- organize code into small, single-responsibility methods
- use consistent formatting and indentation
- follow SOLID principles and best practices
- classes should be small and focused on a single responsibility
- avoid magic strings; use
const string,nameof(), or similar approaches instead - favor interfaces over concrete implementations (e.g.,
IEnumerable<T>overList<T>,IReadOnlyList<T>over arrays)