Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What Is Eventuous

Eventuous is a production-grade Event Sourcing library for .NET implementing DDD tactical patterns. It provides abstractions and implementations for aggregates, command services, event stores, subscriptions, producers, and projections.

## Build & Test Commands

```bash
# Build the entire solution
dotnet build Eventuous.slnx

# Run all tests (all target frameworks: net8.0, net9.0, net10.0)
dotnet test Eventuous.slnx

# Run tests for a specific framework
dotnet test Eventuous.slnx -f net10.0

# Run a single test project
dotnet test src/Core/test/Eventuous.Tests/Eventuous.Tests.csproj

# Run a single test by name filter
dotnet test src/Core/test/Eventuous.Tests/Eventuous.Tests.csproj --filter "FullyQualifiedName~TestClassName"

# CI configuration (used in pull requests)
dotnet test -c "Debug CI" -f net10.0
```

The solution file is `Eventuous.slnx` (new .slnx format, not .sln). The test runner is Microsoft.Testing.Platform with TUnit as the test framework (not xUnit or NUnit). Test results output as TRX to `test-results/`.

Integration tests require infrastructure services. Start them with:
```bash
docker compose up -d
```
Services: EventStoreDB (:2113), PostgreSQL (:5432), MongoDB (:27017), RabbitMQ (:5672), Kafka (:9092), SQL Server (:1433).

## Architecture

### Core Domain Model

**Aggregates** (`src/Core/src/Eventuous.Domain/`): `Aggregate<T> where T : State<T>, new()` — tracks pending `Changes` and `Original` events, enforces business invariants via `Apply<TEvent>()`, uses optimistic concurrency through version tracking.

**State** (`src/Core/src/Eventuous.Domain/`): `State<T>` is an abstract record reconstructed from events using the `When(object @event)` fold pattern. States are immutable.

**Id** (`src/Core/src/Eventuous.Domain/`): `Id` is an abstract record for string-based identity values with validation.

### Command Services (Two Approaches)

**Aggregate-based** (`src/Core/src/Eventuous.Application/AggregateService/`): `CommandService<TAggregate, TState, TId>` — loads aggregate, executes domain logic, persists events. Handlers registered via `On<TCommand>().InState(...).GetId(...).Act(...)`.

**Functional** (`src/Core/src/Eventuous.Application/FunctionalService/`): `CommandService<TState>` — no aggregate instances, pure functions that take state + command and return events. Uses `On<TCommand>().InState(...).GetStream(...).Act(...)`.

### Event Store Layer

`IEventStore` (combined), `IEventReader`, `IEventWriter` in `src/Core/src/Eventuous.Persistence/`. Implementations: EventStoreDB (`src/EventStore/`), PostgreSQL (`src/Postgres/`), SQL Server (`src/SqlServer/`).

`IAggregateStore` is **deprecated** — use `IEventReader.LoadAggregate<>()` and `IEventWriter.StoreAggregate<>()` extension methods instead.

### Subscriptions & Producers

**Subscriptions** (`src/Core/src/Eventuous.Subscriptions/`): `IEventHandler` processes events, with consume filters/pipes, checkpoint management, and partitioning support.

**Producers** (`src/Core/src/Eventuous.Producers/`): `IProducer`/`BaseProducer` for publishing to RabbitMQ, Kafka, Google Pub/Sub, Azure Service Bus.

**Gateway** (`src/Gateway/`): Connects subscriptions to producers for cross-context event routing.

### Key Conventions

- **Stream naming**: Default pattern is `{AggregateType}-{AggregateId}` via `StreamNameMap`.
- **Type mapping**: Events must be registered in `TypeMap` for serialization.
- **Async everywhere**: All I/O is async; use `.NoContext()` for `ConfigureAwait(false)`.
- **Diagnostics**: Built-in OpenTelemetry tracing/metrics in `src/Diagnostics/`.

## Project Layout

```
src/Core/src/ Core packages (Domain, Persistence, Application, Subscriptions, Producers, Serialization, Shared)
src/Core/gen/ Source generators
src/Core/test/ Core test projects
src/EventStore/ EventStoreDB integration (src/ + test/)
src/Postgres/ PostgreSQL integration (src/ + test/)
src/SqlServer/ SQL Server integration (src/ + test/)
src/Mongo/ MongoDB projections
src/RabbitMq/ RabbitMQ integration
src/Kafka/ Kafka integration
src/GooglePubSub/ Google Pub/Sub integration
src/Azure/ Azure Service Bus integration
src/Extensions/ ASP.NET Core, DI extensions
src/Diagnostics/ OpenTelemetry, Logging
src/Gateway/ Event gateway
src/Testing/ Test utilities
test/ Shared test helpers (Eventuous.Sut.App, Eventuous.Sut.Domain, Eventuous.TestHelpers, Eventuous.TestHelpers.TUnit)
samples/ Sample apps (esdb, postgres, kurrentdb, banking)
```

## Documentation Site

The `docs/` directory is a Docusaurus v3 site (https://eventuous.dev). Requires Node >=18.19.0 and pnpm.

```bash
cd docs

# Install dependencies
pnpm install

# Local dev server with hot reload
pnpm start

# Production build (output to docs/build/)
pnpm build

# Serve the production build locally
pnpm serve

# TypeScript validation
pnpm typecheck
```

Docs content lives in `docs/docs/` as `.md` and `.mdx` files organized by topic: `domain/`, `persistence/`, `application/`, `subscriptions/`, `read-models/`, `producers/`, `gateway/`, `diagnostics/`, and `infra/` (per-provider: esdb, postgres, mongodb, mssql, kafka, rabbitmq, pubsub, elastic). MDX files can embed React components. Mermaid diagrams are supported in markdown code blocks. Versioned docs are in `versioned_docs/` (current version: 0.15). The build enforces no broken links.

## Code Style

- Targets .NET 10/9/8 (`TargetFrameworks: net10.0;net9.0;net8.0`)
- C# preview language features (`LangVersion: preview`)
- Nullable reference types enabled
- Implicit usings enabled
- Follow `.editorconfig` formatting rules
- Root namespace is `Eventuous` for most projects; integration-specific projects use `Eventuous.PostgreSQL`, `Eventuous.EventStore`, etc.
- Centralized package versions in `Directory.Packages.props`
- Versioning via MinVer from Git tags
180 changes: 180 additions & 0 deletions skills/eventuous-azure-servicebus.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# Eventuous Azure Service Bus Integration

NuGet package: `Eventuous.Azure.ServiceBus`
Namespace: `Eventuous.Azure.ServiceBus.Producers`, `Eventuous.Azure.ServiceBus.Subscriptions`
Source: `src/Azure/src/Eventuous.Azure.ServiceBus/`

## Producer

`ServiceBusProducer` extends `BaseProducer<ServiceBusProduceOptions>` and implements `IHostedProducer`, `IAsyncDisposable`.

### Constructor

```csharp
public ServiceBusProducer(
ServiceBusClient client, // Azure SDK ServiceBusClient
ServiceBusProducerOptions options,
IEventSerializer? serializer = null,
ILogger<ServiceBusProducer>? log = null
)
```

The producer creates a `ServiceBusSender` from the client for the configured queue/topic.

### ServiceBusProducerOptions

```csharp
public class ServiceBusProducerOptions {
public required string QueueOrTopicName { get; init; }
public ServiceBusSenderOptions? SenderOptions { get; init; }
public ServiceBusMessageAttributeNames AttributeNames { get; init; } = new();
}
```

### ServiceBusProduceOptions (per-message)

```csharp
public class ServiceBusProduceOptions {
public string? Subject { get; set; }
public string? To { get; set; }
public string? ReplyTo { get; set; }
public string? SessionId { get; init; } // for session-enabled entities
public string? ReplyToSessionId { get; init; }
public TimeSpan TimeToLive { get; set; } = TimeSpan.MaxValue;
}
```

### How it works

- Single messages sent via `SendMessageAsync`; multiple messages batched automatically via `ServiceBusMessageBatch`
- Event type stored in `ApplicationProperties["MessageType"]`; content type in `ServiceBusMessage.ContentType`
- Metadata and additional headers added as application properties (filtered by Service Bus-compatible types)
- Session ID on produce options enables ordered message processing
- Supports delivery acknowledgement callbacks

### DI Registration

```csharp
services.AddSingleton(new ServiceBusClient("your-connection-string"));
services.AddProducer<ServiceBusProducer>(sp =>
new ServiceBusProducer(
sp.GetRequiredService<ServiceBusClient>(),
new ServiceBusProducerOptions { QueueOrTopicName = "my-topic" }
)
);
```

## Subscription

`ServiceBusSubscription` extends `EventSubscription<ServiceBusSubscriptionOptions>`.

### Constructor

```csharp
public ServiceBusSubscription(
ServiceBusClient client,
ServiceBusSubscriptionOptions options,
ConsumePipe consumePipe,
ILoggerFactory? loggerFactory,
IEventSerializer? eventSerializer
)
```

### ServiceBusSubscriptionOptions

```csharp
public record ServiceBusSubscriptionOptions : SubscriptionOptions {
public required IQueueOrTopic QueueOrTopic { get; set; }
public ServiceBusProcessorOptions ProcessorOptions { get; set; } = new();
public ServiceBusSessionProcessorOptions? SessionProcessorOptions { get; set; } // enables session mode
public ServiceBusMessageAttributeNames AttributeNames { get; init; } = new();
public Func<ProcessErrorEventArgs, Task>? ErrorHandler { get; init; }
}
```

### Queue or Topic targets

Three implementations of `IQueueOrTopic`:

```csharp
// Subscribe to a queue
new Queue("my-queue")

// Subscribe to a topic (uses SubscriptionId from options as the subscription name)
new Topic("my-topic")

// Subscribe to a topic with an explicit subscription name
new TopicAndSubscription("my-topic", "my-subscription")
```

### Session support

When `SessionProcessorOptions` is set, the subscription uses `ServiceBusSessionProcessor` instead of `ServiceBusProcessor`, enabling ordered processing per session:

```csharp
services.AddSubscription<ServiceBusSubscription, ServiceBusSubscriptionOptions>(
"SessionSub",
builder => builder
.Configure(o => {
o.QueueOrTopic = new Queue("session-queue");
o.SessionProcessorOptions = new ServiceBusSessionProcessorOptions();
})
.AddEventHandler<MyHandler>()
);
```

### Message attribute names

`ServiceBusMessageAttributeNames` controls how metadata maps to Service Bus properties:

```csharp
public class ServiceBusMessageAttributeNames {
public string MessageType { get; set; } = "MessageType";
public string StreamName { get; set; } = "StreamName";
public string CorrelationId { get; set; } = "correlation-id";
public string CausationId { get; set; } = "causation-id";
public string ReplyTo { get; set; } = "ReplyTo";
public string Subject { get; set; } = "Subject";
public string To { get; set; } = "To";
public string MessageId { get; set; } = "message-id";
}
```

## Complete Example

```csharp
// Register the Azure SDK client
services.AddSingleton(new ServiceBusClient("Endpoint=sb://..."));

// Register producer
services.AddProducer<ServiceBusProducer>(sp =>
new ServiceBusProducer(
sp.GetRequiredService<ServiceBusClient>(),
new ServiceBusProducerOptions { QueueOrTopicName = "events-topic" }
)
);

// Register subscription from a queue
services.AddSubscription<ServiceBusSubscription, ServiceBusSubscriptionOptions>(
"MyQueueSubscription",
builder => builder
.Configure(o => {
o.QueueOrTopic = new Queue("events-queue");
})
.AddEventHandler<MyEventHandler>()
);

// Register subscription from a topic with explicit subscription
services.AddSubscription<ServiceBusSubscription, ServiceBusSubscriptionOptions>(
"MyTopicSubscription",
builder => builder
.Configure(o => {
o.QueueOrTopic = new TopicAndSubscription("events-topic", "my-sub");
})
.AddEventHandler<MyEventHandler>()
);
```

## Tracing

Built-in OpenTelemetry tracing with `MessagingSystem = "azure-service-bus"`.
Loading
Loading