dotnet-outbox-pattern
Implements the Outbox pattern for reliable domain event processing. Ensures events are persisted in the same transaction as the aggregate changes and processed asynchronously with guaranteed delivery.
git clone --depth 1 https://github.com/ronnythedev/dotnet-clean-architecture-skills /tmp/dotnet-outbox-pattern && cp -r /tmp/dotnet-outbox-pattern/skills/14-dotnet-outbox-pattern ~/.claude/skills/dotnet-outbox-patternSKILL.md
# Outbox Pattern Implementation
## Overview
The Outbox pattern ensures reliable event processing:
- **Atomic persistence** - Events saved in same transaction as aggregate
- **Guaranteed delivery** - Events processed even if app crashes
- **Eventual consistency** - Async processing with retry
- **Idempotency** - Handle duplicate processing gracefully
## Quick Reference
| Component | Purpose |
|-----------|---------|
| `OutboxMessage` | Persisted event entity |
| `OutboxMessageConfiguration` | EF Core mapping |
| `ProcessOutboxMessagesJob` | Background processor (Quartz) |
| `IdempotentDomainEventHandler` | Deduplicated handler wrapper |
| `OutboxConsumer` | Alternative direct DB poller |
---
## Outbox Structure
```
/Infrastructure/
├── Outbox/
│ ├── OutboxMessage.cs
│ ├── OutboxMessageConfiguration.cs
│ ├── ProcessOutboxMessagesJob.cs
│ ├── ProcessOutboxMessagesJobSetup.cs
│ └── IdempotentDomainEventHandler.cs
└── ApplicationDbContext.cs
```
---
## Template: Outbox Message Entity
```csharp
// src/{name}.infrastructure/Outbox/OutboxMessage.cs
namespace {name}.infrastructure.outbox;
/// <summary>
/// Represents a domain event stored for reliable delivery
/// </summary>
public sealed class OutboxMessage
{
public OutboxMessage()
{
}
public OutboxMessage(Guid id, string type, string content, DateTime occurredOnUtc)
{
Id = id;
Type = type;
Content = content;
OccurredOnUtc = occurredOnUtc;
}
/// <summary>
/// Unique identifier for this message
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// Assembly-qualified type name of the domain event
/// </summary>
public string Type { get; set; } = string.Empty;
/// <summary>
/// JSON-serialized event content
/// </summary>
public string Content { get; set; } = string.Empty;
/// <summary>
/// When the event originally occurred
/// </summary>
public DateTime OccurredOnUtc { get; set; }
/// <summary>
/// When the message was successfully processed (null if not yet processed)
/// </summary>
public DateTime? ProcessedOnUtc { get; set; }
/// <summary>
/// Error message if processing failed
/// </summary>
public string? Error { get; set; }
/// <summary>
/// Number of processing attempts
/// </summary>
public int RetryCount { get; set; }
}
```
---
## Template: EF Core Configuration
```csharp
// src/{name}.infrastructure/Outbox/OutboxMessageConfiguration.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace {name}.infrastructure.outbox;
internal sealed class OutboxMessageConfiguration
: IEntityTypeConfiguration<OutboxMessage>
{
public void Configure(EntityTypeBuilder<OutboxMessage> builder)
{
builder.ToTable("outbox_message");
builder.HasKey(o => o.Id);
builder.Property(o => o.Id)
.ValueGeneratedNever();
builder.Property(o => o.Type)
.HasMaxLength(500)
.IsRequired();
builder.Property(o => o.Content)
.HasColumnType("jsonb") // PostgreSQL JSONB
.IsRequired();
builder.Property(o => o.OccurredOnUtc)
.IsRequired();
builder.Property(o => o.ProcessedOnUtc);
builder.Property(o => o.Error)
.HasColumnType("text");
builder.Property(o => o.RetryCount)
.HasDefaultValue(0);
// Index for efficient polling of unprocessed messages
builder.HasIndex(o => o.ProcessedOnUtc)
.HasFilter("processed_on_utc IS NULL")
.HasDatabaseName("ix_outbox_message_unprocessed");
// Index for cleanup of old processed messages
builder.HasIndex(o => o.ProcessedOnUtc)
.HasFilter("processed_on_utc IS NOT NULL")
.HasDatabaseName("ix_outbox_message_processed");
}
}
```
---
## Template: DbContext Integration
```csharp
// src/{name}.infrastructure/ApplicationDbContext.cs
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using {name}.domain.abstractions;
using {name}.infrastructure.outbox;
namespace {name}.infrastructure;
public sealed class ApplicationDbContext : DbContext, IUnitOfWork
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false
};
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public DbSet<OutboxMessage> OutboxMessages => Set<OutboxMessage>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
base.OnModelCreating(modelBuilder);
}
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
// ═══════════════════════════════════════════════════════════════
// CRITICAL: Add domain events to outbox BEFORE SaveChanges
// This ensures atomic persistence - events saved in same transaction
// ═══════════════════════════════════════════════════════════════
ConvertDomainEventsToOutboxMessages();
return await base.SaveChangesAsync(cancellationToken);
}
private void ConvertDomainEventsToOutboxMessages()
{
// Get all entities with domain events
var entitiesWithEvents = ChangeTracker
.Entries<Entity>()
.Where(e => e.Entity.GetDomainEvents().Any())
.Select(e => e.Entity)
.ToList();
// Extract all domain events
var domainEvents = entitiesWithEvents
.SelectMany(e => e.GetDomainEvents())
.ToList();
// Clear events from entities (they're now in outbox)
foreach (var entity in entitiesWithEvents)
{
entityScaffolds a complete .NET solution following Clean Architecture principles with proper layer separation (API, Application, Domain, Infrastructure). Creates project structure, dependency injection setup, and cross-cutting concerns configuration.
Generates CQRS Commands with Handlers, Validators, and Request DTOs following Clean Architecture patterns. Commands represent actions that modify state and return Result types for proper error handling.
Generates CQRS Queries with Handlers and Response DTOs for read operations. Uses Dapper for optimized read queries, bypassing the domain model for better performance.
Generates Domain Entities following DDD principles with factory methods, private setters, domain events, and proper encapsulation. Supports aggregate roots, child entities, and value objects.
Generates Repository interfaces and implementations following the Repository pattern. Provides data access abstraction for aggregate roots with EF Core implementations.
Generates Entity Framework Core configurations using Fluent API. Maps domain entities to database tables with proper relationships, constraints, and conventions.
Generates RESTful API Controllers with proper routing, versioning, authorization, and MediatR integration. Follows REST conventions and Clean Architecture patterns.
Generates Minimal API endpoints following Microsoft's recommended approach. Creates fast, testable HTTP APIs with minimal code using MapGet/MapPost/MapPut/MapDelete. Preferred over controller-based APIs for new projects.