Ratatoskr

Reliable event-driven messaging for .NET — transport-agnostic publishing, durable Outbox & Inbox patterns, and native CloudEvents support.

Ratatoskr — the cyber squirrel messenger
Install via NuGet: dotnet add package Ratatoskr

Why Ratatoskr?

Everything you need for reliable, observable messaging in .NET applications.

📡

Channel-First Design

Declare channels with intent — event publish, command consume — and attach message types, handlers, and transports in one place.

🔒

Transactional Outbox

Messages are persisted in the same database transaction as your business data. Nothing is lost, even if the broker goes down.

📥

Durable Inbox

Per-handler deduplication and retry with persistent state via EF Core. Poisoned messages are quarantined, not dropped.

CloudEvents Native

Messages follow the CloudEvents specification with W3C trace context propagation out of the box.

🔀

Multiple Transports

RabbitMQ for production messaging, EF Core for broker-free development — swap transports without changing your handlers.

📊

Observable

Built-in OpenTelemetry tracing and metrics. See every message flow through your system end to end.

Horizontally Scalable

Distributed locks and optimistic concurrency let you run multiple instances safely without double-processing.

🧪

Testable

Ratatoskr.Testing provides trace-isolated parallel test sessions — integration tests that run fast and don't interfere.

Packages

Ratatoskr

Core abstractions, message routing, CloudEvents support, and serialization.

Ratatoskr.EfCore

Outbox and Inbox durability patterns with EF Core transport.

Ratatoskr.RabbitMq

RabbitMQ transport with topology management, retry, and DLQ support.

Ratatoskr.Testing

Test utilities with W3C trace-isolated sessions and assertion helpers.

Quick Example

Define a message, handle it, wire it up — in minutes.

Define a message:

[RatatoskrMessage("order.placed")]
public record OrderPlaced(Guid OrderId, string CustomerEmail, decimal Total);

Handle it:

public class OrderPlacedHandler(ILogger<OrderPlacedHandler> logger) : IMessageHandler<OrderPlaced>
{
    public Task HandleAsync(OrderPlaced message, MessageProperties properties, CancellationToken cancellationToken)
    {
        logger.LogInformation("Order {OrderId} placed for {Email}, total: {Total}",
            message.OrderId, message.CustomerEmail, message.Total);
        return Task.CompletedTask;
    }
}

Configure channels and register handlers:

builder.Services.AddRatatoskr(bus =>
{
    bus.UseRabbitMq(c =>
    {
        c.ConnectionString = new Uri(builder.Configuration.GetConnectionString("RabbitMq")!);
    });

    bus.AddEfCoreDurability<OrderDbContext>(d =>
    {
        d.UseOutbox();
        d.UseInbox();
    });

    bus.AddEventPublishChannel("orders.events", c => c
        .WithRabbitMq(r => r.WithTopicExchange())
        .Produces<OrderPlaced>()
        .Produces<PaymentCompleted>()
        .Produces<OrderShipped>());

    bus.AddCommandPublishChannel("orders.commands", c => c
        .WithRabbitMq(r => r.WithDirectExchange())
        .Produces<ProcessPayment>()
        .Produces<ShipOrder>()
        .Produces<SendOrderConfirmation>());

    bus.AddEventConsumeChannel("orders.events", c => c
        .WithRabbitMq(r => r
            .WithQueueName("orders.events.subscriptions")
            .WithRetry(maxRetries: 3, delay: TimeSpan.FromSeconds(30)))
        .Consumes<OrderPlaced>(m => m.WithHandler<OrderPlacedHandler>("order-placed"))
        .UseInbox<OrderDbContext>());

    bus.AddCommandConsumeChannel("orders.commands", c => c
        .WithRabbitMq(r => r
            .WithDirectExchange()
            .WithQueueName("orders.commands.queue"))
        .Consumes<ProcessPayment>(m => m
            .WithHandler<ProcessPaymentHandler>("process-payment"))
        .Consumes<ShipOrder>(m => m
            .WithHandler<ShipOrderHandler>("ship-order"))
        .Consumes<SendOrderConfirmation>(m => m
            .WithHandler<SendOrderConfirmationHandler>("send-confirmation"))
        .UseInbox<OrderDbContext>());

    bus.ConfigureAsyncApi(api =>
    {
        api.WithTitle("Order Processing API");
        api.WithVersion("1.0.0");
        api.WithDescription("Ratatoskr-powered order processing messaging API");
    });

    bus.ConfigureCloudEvents(ce =>
    {
        ce.DefaultSource = "/order-service";
    });
});

Publish directly or through the outbox:

app.MapPost("/orders/direct", async (OrderPlaced order, IRatatoskr bus) =>
{
    await bus.PublishDirectAsync(order);
    return TypedResults.Ok(order);
});
app.MapPost("/orders", async (OrderPlaced order, OrderDbContext db) =>
{
    db.OutboxMessages.Add(order);
    await db.SaveChangesAsync();
    return TypedResults.Ok(order);
});

When to Use Ratatoskr

Great fit

  • Reliable message delivery with transactional outbox guarantees
  • Per-handler deduplication and retry via the inbox pattern
  • CloudEvents-based messaging with standard metadata
  • A channel-first API that enforces ownership and topology conventions

Not designed for

  • Request/reply or RPC patterns
  • In-memory pub/sub without persistence requirements
  • Saga or process manager orchestration (use MassTransit or Wolverine)
  • Stream processing (use Kafka, EventStoreDB, or similar)

Ready to get started?

Build your first Ratatoskr application in under 10 minutes.