Skip to content

Add Claude Code skills for Eventuous library users#495

Merged
alexeyzimarev merged 3 commits intodevfrom
alexey/claude
Feb 27, 2026
Merged

Add Claude Code skills for Eventuous library users#495
alexeyzimarev merged 3 commits intodevfrom
alexey/claude

Conversation

@alexeyzimarev
Copy link
Contributor

Summary

  • Add a core skill file (skills/eventuous.md) covering Eventuous domain model, command services (aggregate-based and functional), event serialization, stream naming, HTTP API patterns, subscriptions, producers, and DI registration
  • Add 9 infrastructure-specific skill files with progressive disclosure so users only load what's relevant to their stack:
    • eventuous-kurrentdb.md - KurrentDB/EventStoreDB event store, subscriptions, producer
    • eventuous-postgres.md - PostgreSQL event store, subscriptions, projections
    • eventuous-mongodb.md - MongoDB projections and checkpoint store
    • eventuous-sqlserver.md - SQL Server event store, subscriptions
    • eventuous-rabbitmq.md - RabbitMQ producer and subscription
    • eventuous-kafka.md - Kafka producer and subscription
    • eventuous-google-pubsub.md - Google Pub/Sub producer and subscription
    • eventuous-azure-servicebus.md - Azure Service Bus producer and subscription
    • eventuous-gateway.md - Event gateway for cross-context routing
  • Add CLAUDE.md with repository-level instructions for contributors

Test plan

  • Verify skill files render correctly as markdown
  • Spot-check code examples against actual API signatures
  • Test with Claude Code on a sample Eventuous project to confirm skills provide accurate guidance

🤖 Generated with Claude Code

Add a core skill file covering domain model, command services, persistence,
subscriptions, and HTTP API patterns, plus infrastructure-specific skill files
for KurrentDB, PostgreSQL, MongoDB, SQL Server, RabbitMQ, Kafka, Google Pub/Sub,
Azure Service Bus, and Gateway. Uses progressive disclosure so users only load
the infrastructure guides relevant to their stack.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@qodo-free-for-open-source-projects
Copy link
Contributor

Review Summary by Qodo

Add Claude Code skill files and CLAUDE.md for Eventuous library users

Grey Divider

Walkthroughs

Description
• Add CLAUDE.md with repository-level guidance for Claude Code contributors
• Add core skills/eventuous.md covering domain model, command services, serialization, HTTP API,
  subscriptions, producers, and DI patterns
• Add 9 infrastructure-specific skill files for KurrentDB, PostgreSQL, MongoDB, SQL Server,
  RabbitMQ, Kafka, Google Pub/Sub, Azure Service Bus, and Gateway
• Skill files use progressive disclosure so users load only infrastructure-relevant guides
Diagram
flowchart LR
  CLAUDE["CLAUDE.md\n(repo guidance)"]
  core["skills/eventuous.md\n(core skill)"]
  kurrentdb["eventuous-kurrentdb.md"]
  postgres["eventuous-postgres.md"]
  mongodb["eventuous-mongodb.md"]
  sqlserver["eventuous-sqlserver.md"]
  rabbitmq["eventuous-rabbitmq.md"]
  kafka["eventuous-kafka.md"]
  pubsub["eventuous-google-pubsub.md"]
  azure["eventuous-azure-servicebus.md"]
  gateway["eventuous-gateway.md"]
  core -- "references" --> kurrentdb
  core -- "references" --> postgres
  core -- "references" --> mongodb
  core -- "references" --> sqlserver
  core -- "references" --> rabbitmq
  core -- "references" --> kafka
  core -- "references" --> pubsub
  core -- "references" --> azure
  core -- "references" --> gateway
Loading

Grey Divider

File Changes

1. CLAUDE.md 📝 Documentation +132/-0

Repository-level Claude Code guidance for contributors

CLAUDE.md


2. skills/eventuous.md 📝 Documentation +427/-0

Core Eventuous skill file covering all major concepts

skills/eventuous.md


3. skills/eventuous-kurrentdb.md 📝 Documentation +269/-0

KurrentDB event store, subscriptions, and producer skill

skills/eventuous-kurrentdb.md


View more (8)
4. skills/eventuous-postgres.md 📝 Documentation +250/-0

PostgreSQL event store, subscriptions, and projections skill

skills/eventuous-postgres.md


5. skills/eventuous-mongodb.md 📝 Documentation +357/-0

MongoDB projections and checkpoint store skill file

skills/eventuous-mongodb.md


6. skills/eventuous-sqlserver.md 📝 Documentation +162/-0

SQL Server event store and subscriptions skill file

skills/eventuous-sqlserver.md


7. skills/eventuous-rabbitmq.md 📝 Documentation +282/-0

RabbitMQ producer and subscription skill file

skills/eventuous-rabbitmq.md


8. skills/eventuous-kafka.md 📝 Documentation +85/-0

Kafka producer and subscription skill file

skills/eventuous-kafka.md


9. skills/eventuous-google-pubsub.md 📝 Documentation +171/-0

Google Pub/Sub producer and subscription skill file

skills/eventuous-google-pubsub.md


10. skills/eventuous-azure-servicebus.md 📝 Documentation +180/-0

Azure Service Bus producer and subscription skill file

skills/eventuous-azure-servicebus.md


11. skills/eventuous-gateway.md 📝 Documentation +213/-0

Event gateway cross-context routing skill file

skills/eventuous-gateway.md


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Contributor

qodo-free-for-open-source-projects bot commented Feb 23, 2026

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. RabbitMQ queue override misbind🐞 Bug ✓ Correctness
Description
The new RabbitMQ skill file documents overriding the queue name via RabbitMqQueueOptions.Queue,
but the implementation binds the exchange to Options.SubscriptionId instead of the resolved queue
name, so overriding the queue will break consumption.
Code

skills/eventuous-rabbitmq.md[R123-129]

+| Property | Type | Default | Description |
+|----------|------|---------|-------------|
+| `Queue` | `string?` | `null` | Queue name; defaults to `SubscriptionId` if null |
+| `Durable` | `bool` | `true` | Survive broker restart |
+| `Exclusive` | `bool` | `false` | Exclusive to this connection |
+| `AutoDelete` | `bool` | `false` | Delete when last consumer disconnects |
+| `Arguments` | `IDictionary<string, object>?` | `null` | Additional queue arguments (e.g., dead-letter exchange) |
Evidence
The docs claim the queue name can be overridden, but RabbitMqSubscription declares the queue using
the override and then binds a different queue name (SubscriptionId), so messages won’t be routed to
the declared queue when QueueOptions.Queue is set.

skills/eventuous-rabbitmq.md[123-129]
src/RabbitMq/src/Eventuous.RabbitMq/Subscriptions/RabbitMqSubscription.cs[115-133]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
RabbitMQ subscription declares a queue using `QueueOptions.Queue ?? SubscriptionId`, but binds the exchange to `Options.SubscriptionId` instead of the declared queue. This breaks message delivery when users override the queue name (as suggested by the new skill docs).
## Issue Context
The PR adds docs that encourage using `RabbitMqQueueOptions.Queue` to override the queue name.
## Fix Focus Areas
- src/RabbitMq/src/Eventuous.RabbitMq/Subscriptions/RabbitMqSubscription.cs[115-133]
- skills/eventuous-rabbitmq.md[123-129]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Gateway GetOriginalStream broken🐞 Bug ✓ Correctness
Description
The gateway skill file recommends ProducedMessageExtensions.GetOriginalStream(), but the
implementation retrieves the header as System.IO.Stream while the gateway stores StreamName, so
this method will always return null.
Code

skills/eventuous-gateway.md[R199-207]

+Access original context in a custom producer via `ProducedMessageExtensions`:
+```csharp
+message.GetOriginalStream()
+message.GetOriginalMessage()
+message.GetOriginalMetadata()
+message.GetOriginalStreamPosition()
+message.GetOriginalGlobalPosition()
+message.GetOriginalMessageId()
+message.GetOriginalMessageType()
Evidence
Gateway context metadata stores the original stream as context.Stream (a StreamName). The
extension method reads it back using Get (implicit System.IO.Stream), which can never match a
stored StreamName, so callers following the new docs won’t be able to access the original stream.

skills/eventuous-gateway.md[193-208]
src/Core/src/Eventuous.Subscriptions/Context/IMessageConsumeContext.cs[13-33]
src/Gateway/src/Eventuous.Gateway/GatewayMetaHelper.cs[15-24]
src/Gateway/src/Eventuous.Gateway/GatewayMetaHelper.cs[31-35]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`ProducedMessageExtensions.GetOriginalStream()` is effectively unusable: the gateway stores `OriginalStream` as `StreamName`, but the extension retrieves it as `System.IO.Stream`, so it returns null.
## Issue Context
The new gateway skill file explicitly recommends using `message.GetOriginalStream()`.
## Fix Focus Areas
- src/Gateway/src/Eventuous.Gateway/GatewayMetaHelper.cs[15-35]
- src/Core/src/Eventuous.Subscriptions/Context/IMessageConsumeContext.cs[13-33]
- skills/eventuous-gateway.md[193-208]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@github-actions
Copy link

Test Results

 57 files   57 suites   33m 4s ⏱️
291 tests 291 ✅ 0 💤 0 ❌
882 runs  882 ✅ 0 💤 0 ❌

Results for commit e2959cf.

@alexeyzimarev
Copy link
Contributor Author

/review

@qodo-free-for-open-source-projects
Copy link
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 No relevant tests
🔒 Security concerns

Sensitive information exposure:
Multiple documentation snippets include connection strings and credentials-like placeholders (e.g., Password=secret in skills/eventuous-postgres.md, amqp://guest:guest@localhost:5672 in skills/eventuous-rabbitmq.md, and Azure Service Bus connection string placeholders in skills/eventuous-azure-servicebus.md). Even if illustrative, these patterns can lead contributors/users to copy-paste secrets into code. Consider switching to environment-variable based examples (e.g., configuration["Postgres:ConnectionString"]) and explicitly noting “do not hardcode credentials; use secret storage”.

⚡ Recommended focus areas for review

Docs Accuracy

The guide states that KafkaBasicSubscription is a stub throwing NotImplementedException. Ensure this is still true in the current code (and consider clearly flagging it near the top of the file) so users don’t attempt to build on a non-functional subscription implementation.

## Subscription

`KafkaBasicSubscription` extends `EventSubscription<KafkaSubscriptionOptions>`. Note: the subscription is currently a stub (throws `NotImplementedException`).

```csharp
public record KafkaSubscriptionOptions : SubscriptionOptions {
    public ConsumerConfig ConsumerConfig { get; init; } = null!;
}

</details>

<details><summary><a href='https://github.com/Eventuous/eventuous/pull/495/files#diff-a797ebe461cb6747bacd38eaf7e4c4e995cfd6361a5e41158d6f724319ddc47eR143-R320'><strong>API Mismatch</strong></a>

Several examples reference specific fluent APIs and helper methods (eg `CommandService` builder chains, `GetStream(id)` naming, `TypeMap.RegisterKnownEventTypes`, `MapDiscoveredCommands`, `CommandHttpApiBase`). These should be spot-checked against the current public API surface to avoid guidance drifting from actual signatures/namespaces.
</summary>

```markdown
## Command Services

### Commands

Commands are record types, optionally grouped in a static class:

```csharp
public static class BookingCommands {
    public record BookRoom(string BookingId, string GuestId, string RoomId, DateTime CheckIn, DateTime CheckOut, float Price, string Currency);
    public record CancelBooking(string BookingId, string Reason);
}

Aggregate-Based Command Service

For rich domain models with aggregate classes. Extends CommandService<TAggregate, TState, TId>. Register handlers in the constructor using the fluent builder chain: On<TCommand>().InState(...).GetId(...).Act(...).

public class BookingsCommandService : CommandService<Booking, BookingState, BookingId> {
    public BookingsCommandService(IEventStore store, Services.IsRoomAvailable isRoomAvailable)
        : base(store) {
        On<BookRoom>()
            .InState(ExpectedState.New)
            .GetId(cmd => new BookingId(cmd.BookingId))
            .Act((booking, cmd) => booking.BookRoom(
                cmd.GuestId,
                new RoomId(cmd.RoomId),
                new StayPeriod(cmd.CheckIn, cmd.CheckOut),
                new Money(cmd.Price, cmd.Currency)
            ));

        On<CancelBooking>()
            .InState(ExpectedState.Existing)
            .GetId(cmd => new BookingId(cmd.BookingId))
            .Act((booking, cmd) => booking.Cancel(cmd.Reason));
    }
}

Fluent builder chain (aggregate-based):

  1. On<TCommand>() - register handler for command type
  2. .InState(ExpectedState) - New, Existing, or Any
  3. .GetId(cmd => ...) - extract aggregate ID from command (or .GetIdAsync(...))
  4. Optional: .AmendEvent(...) - modify events before storage
  5. Optional: .ResolveStore(...) / .ResolveReader(...) / .ResolveWriter(...) - per-command store resolution
  6. .Act((aggregate, cmd) => ...) - sync action, or .ActAsync((aggregate, cmd, ct) => ...) for async

Functional Command Service

For pure-function style without aggregate instances. Extends CommandService<TState>. Uses GetStream instead of GetId, and Act returns events instead of calling aggregate methods.

public class PaymentsService : CommandService<PaymentState> {
    public PaymentsService(IEventStore store) : base(store) {
        On<RecordPayment>()
            .InState(ExpectedState.New)
            .GetStream(cmd => GetStream(cmd.PaymentId))
            .Act(cmd => [new PaymentRecorded(cmd.BookingId, cmd.Amount, cmd.Currency)]);

        On<RefundPayment>()
            .InState(ExpectedState.Existing)
            .GetStream(cmd => GetStream(cmd.PaymentId))
            .Act((state, events, cmd) => [new PaymentRefunded(cmd.PaymentId, cmd.Reason)]);
    }
}

Fluent builder chain (functional):

  1. On<TCommand>() - register handler
  2. .InState(ExpectedState) - New, Existing, or Any
  3. .GetStream(cmd => ...) - get stream name (use GetStream(id) helper for default naming)
  4. .Act(cmd => events) - for new streams (returns NewEvents / IEnumerable<object>)
  5. .Act((state, originalEvents, cmd) => events) - for existing streams, receives current state

Result Type

Both command services return Result<TState>:

var result = await service.Handle(command, cancellationToken);

// Pattern match
result.Match(
    ok => /* ok.State, ok.Changes, ok.GlobalPosition */,
    error => /* error.Exception, error.ErrorMessage */
);

// Or check directly
if (result.Success) {
    var ok = result.Get();
    // ok.State, ok.Changes
}

Event Serialization & Type Mapping

Events must be registered in TypeMap for serialization. The [EventType] attribute provides automatic registration.

// Option 1: Auto-discover all [EventType]-decorated types in loaded assemblies
TypeMap.RegisterKnownEventTypes();

// Option 2: Auto-discover from specific assemblies
TypeMap.RegisterKnownEventTypes(typeof(BookingEvents).Assembly);

// Option 3: Manual registration
TypeMap.Instance.AddType<V1.RoomBooked>("V1.RoomBooked");

Default serializer uses System.Text.Json. Configure custom options:

DefaultEventSerializer.SetDefaultSerializer(
    new DefaultEventSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web))
);

Stream Naming

Default pattern: {AggregateType}-{AggregateId} (e.g., Booking-booking-123).

For functional services, GetStream(id) uses {StateNameWithoutSuffix}-{id}.

Custom mapping:

var streamNameMap = new StreamNameMap();
streamNameMap.Register<BookingId>(id => new StreamName($"bookings:{id.Value}"));
// Pass to command service or register in DI

Extracting ID from stream name (useful in projections): ctx.Stream.GetId().


HTTP API

Controller-Based

Extend CommandHttpApiBase<TState> and call Handle():

[Route("/booking")]
public class BookingCommandApi(ICommandService<BookingState> service)
    : CommandHttpApiBase<BookingState>(service) {

    [HttpPost("book")]
    public Task<ActionResult<Result<BookingState>.Ok>> BookRoom(
        [FromBody] BookRoom cmd, CancellationToken ct) => Handle(cmd, ct);
}

Minimal API with Auto-Discovery

Annotate commands with [HttpCommand] and map them:

// On command records:
[HttpCommand<BookingState>(Route = "book")]
public record BookRoom(string BookingId, string GuestId, ...);

// Or group commands under a static class:
[HttpCommands<BookingState>]
public static class BookingCommands {
    [HttpCommand(Route = "book")]
    public record BookRoom(...);
}

// In Program.cs:
app.MapDiscoveredCommands<BookingState>();
// Or map individual commands:
app.MapCommand<BookRoom, BookingState>();

</details>

<details><summary><a href='https://github.com/Eventuous/eventuous/pull/495/files#diff-6ebdb617a8104a7756d0cf36578ab01103dc9f07e4dc6feb751296b9c402faf7R9-R31'><strong>Tooling Assumptions</strong></a>

The repository guidance hard-codes solution/test commands (eg `.slnx`, Microsoft.Testing.Platform + TUnit, specific framework filters, `Debug CI` configuration). Validate these commands/config names exist and are stable across contributor environments, otherwise consider adding fallback commands or clarifying prerequisites.
</summary>

```markdown
## 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/.


</details>

</td></tr>
<tr><td>

<details><summary>📄 References</summary><ol><li>No matching references available</li>

</ol></details>

</td></tr>
</table>

…files

Replace connection strings containing passwords and credentials with
configuration-based alternatives and add "do not hardcode credentials"
comments in RabbitMQ, PostgreSQL, MongoDB, and Azure Service Bus examples.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@alexeyzimarev alexeyzimarev merged commit 3c727b3 into dev Feb 27, 2026
4 checks passed
@alexeyzimarev alexeyzimarev deleted the alexey/claude branch February 27, 2026 10:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant