Skip to content
Merged
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
46 changes: 24 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,14 @@ services
.CreateTopicIfMissing()))
.UseSubscriptions(sb => sb
.AddSubscription<GreetingEvent>(s => s
.SetSubscriptionName("paramore.example.greeting")
.SetSubscription("paramore.example.greeting")
.SetQueue("greeting.event.queue")
.SetTopic("greeting.event.topic")
.SetTimeout(TimeSpan.FromSeconds(200))
.EnableDurable()
.EnableHighAvailability())
.AddSubscription<FarewellEvent>(s => s
.SetSubscriptionName("paramore.example.farewell")
.SetSubscription("paramore.example.farewell")
.SetQueue("farewell.event.queue")
.SetTopic("farewell.event.topic")))
));
Expand Down Expand Up @@ -111,46 +111,50 @@ services.AddFluentBrighter(brighter => brighter
```csharp
services.AddFluentBrighter(brighter => brighter
.UsingGcp(gcp => gcp
.SetProjectId("my-gcp-project")
.SetConnection(c => c
.SetProjectId("my-gcp-project"))
.UsePubSubPublication(pb => pb
.AddPublication<UserRegisteredEvent>(p => p
.SetTopicAttributes(t => t.SetName("user-registered-topic"))
.SetTopic("user-registered-topic")
.SetSource("https://example.com/users"))
.AddPublication<UserDeletedEvent>(p => p
.SetTopicAttributes(t => t.SetName("user-deleted-topic"))
.SetTopic("user-deleted-topic")
.SetSource("https://example.com/users")))
.UsePubSubSubscription(sb => sb
.AddSubscription<UserRegisteredEvent>(s => s
.SetSubscriptionName("user-registration-handler")
.SetTopicAttributes(t => t.SetName("user-registered-topic"))
.SetSubscription("user-registered-queue")
.SetTopic("user-registered-topic")
.SetNoOfPerformers(5))
.AddSubscription<UserDeletedEvent>(s => s
.SetSubscriptionName("user-deletion-handler")
.SetTopicAttributes(t => t.SetName("user-deleted-topic"))
.SetSubscription("user-deleted-queue")
.SetTopic("user-deleted-topic")
.SetNoOfPerformers(3)))
.SetFirestoreConfiguration("my-firestore-database")
.UseFirestoreOutbox("outbox")
.UseFirestoreInbox("inbox")
.UseFirestoreOutboxArchive(cfg => cfg
.SetMinimumAge(TimeSpan.FromDays(7))
.SetBatchSize(100))));
.SetArchiveBatchSize(100))));
```

### Apache Kafka

```csharp
services.AddFluentBrighter(brighter => brighter
.UsingKafka(kafka => kafka
.SetBootstrapServers("localhost:9092")
.SetConnection(c => c
.SetBootstrapServers("localhost:9092"))
.UsePublications(pb => pb
.AddPublication<PaymentProcessedEvent>(p => p
.SetTopic("payments-processed")
.SetMessageIdHeaderKey("message-id")))
.SetTopic("payments-processed")))
.UseSubscriptions(sb => sb
.AddSubscription<PaymentProcessedEvent>(s => s
.SetSubscriptionName("payment-notification-service")
.SetSubscription("payment-notification-service")
.SetTopic("payments-processed")
.SetGroupId("payment-consumers")
.SetNoOfPerformers(10)))));
.SetConsumerGroupId("payment-consumers")
.SetNumberOfPerformers(10)))));
```

### PostgreSQL with Outbox Pattern
Expand All @@ -161,13 +165,13 @@ services.AddFluentBrighter(brighter => brighter
.Producers(producer => producer
.UsePostgresOutbox(cfg => cfg
.SetConnectionString("Host=localhost;Database=brighter;")
.SetTableName("outbox")))
.SetOutboxTableName("outbox")))
.Subscriptions(sub => sub
.UsePostgresInbox(cfg => cfg
.SetConnectionString("Host=localhost;Database=brighter;")
.SetTableName("inbox")))
.SetInboxTableName("inbox")))
.UseOutboxSweeper(cfg => cfg
.SetTimerInterval(TimeSpan.FromSeconds(30))
.SetTimerInterval(30)
.SetBatchSize(100)));
```

Expand All @@ -177,12 +181,10 @@ services.AddFluentBrighter(brighter => brighter
services.AddFluentBrighter(brighter => brighter
.UsingRabbitMq(rabbitmq => /* ... RabbitMQ config ... */)
.Producers(producer => producer
.UseSqlServerOutbox(cfg => cfg
.SetConnectionString("Server=localhost;Database=Brighter;")
.SetTableName("Outbox"))
.UseSqlServerDistributedLock(cfg => cfg
.UseMicrosoftSqlServerOutbox(cfg => cfg
.SetConnectionString("Server=localhost;Database=Brighter;")
.SetTableName("DistributedLock"))));
.SetOutboxTableName("Outbox"))
.UseMicrosoftSqlServerDistributedLock("Server=localhost;Database=Brighter;")));
```

## 🎯 Key Concepts
Expand Down
42 changes: 42 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Fluent Brighter

A fluent, type-safe configuration API for [Paramore.Brighter](https://github.com/BrighterCommand/Brighter) — the command processor and message-oriented middleware framework for .NET.

## Why Fluent Brighter?

Paramore.Brighter is a powerful framework, but configuring its messaging infrastructure can involve significant boilerplate. Fluent Brighter provides an intuitive, strongly-typed builder pattern that makes complex configurations easier to read, write, and maintain.

### Key Features

- **Type-Safe Configuration** — IntelliSense-driven API with compile-time safety
- **Multiple Message Brokers** — RabbitMQ, Kafka, AWS SNS/SQS, GCP Pub/Sub, Redis, RocketMQ
- **Database Transports** — PostgreSQL, SQL Server, MySQL, SQLite, MongoDB
- **Transactional Outbox Pattern** — DynamoDB, Firestore, Spanner, MongoDB, SQL Server, PostgreSQL, MySQL, SQLite
- **Idempotent Inbox Pattern** — Prevent duplicate message processing
- **Distributed Locking** — Coordinate across multiple instances
- **Large Message Storage** — S3, GCS, MongoDB GridFS for oversized payloads
- **Fluent Syntax** — Readable, maintainable configuration code

## Quick Example

```csharp
services.AddFluentBrighter(brighter => brighter
.UsingRabbitMq(rabbitmq => rabbitmq
.SetConnection(conn => conn
.SetAmpq(amqp => amqp.SetUri("amqp://guest:guest@localhost:5672"))
.SetExchange(exchange => exchange.SetName("my.exchange")))
.UsePublications(pb => pb
.AddPublication<GreetingEvent>(p => p
.SetTopic("greeting.topic")
.CreateTopicIfMissing()))
.UseSubscriptions(sb => sb
.AddSubscription<GreetingEvent>(s => s
.SetSubscription("greeting-handler")
.SetQueue("greeting.queue")
.SetTopic("greeting.topic")))));
```

## Supported Platforms

- .NET 8.0
- .NET 9.0
28 changes: 28 additions & 0 deletions docs/SUMMARY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Table of contents

* [Introduction](README.md)
* [Getting Started](getting-started.md)

## Messaging Providers

* [RabbitMQ](providers/rabbitmq.md)
* [Apache Kafka](providers/kafka.md)
* [AWS SNS/SQS](providers/aws.md)
* [Google Cloud Pub/Sub](providers/gcp.md)
* [Redis](providers/redis.md)
* [Apache RocketMQ](providers/rocketmq.md)

## Database Providers

* [PostgreSQL](providers/postgres.md)
* [SQL Server](providers/sqlserver.md)
* [MySQL](providers/mysql.md)
* [SQLite](providers/sqlite.md)
* [MongoDB](providers/mongodb.md)

## Advanced Topics

* [Outbox & Inbox Patterns](advanced/outbox-inbox.md)
* [Distributed Locking](advanced/distributed-locking.md)
* [Large Message Storage](advanced/luggage-store.md)
* [Outbox Sweeper & Archiver](advanced/sweeper-archiver.md)
94 changes: 94 additions & 0 deletions docs/advanced/distributed-locking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Distributed Locking

Distributed locking coordinates message processing across multiple service instances to prevent concurrent execution of the outbox sweeper or other shared operations.

## Supported Providers

| Provider | Method | Package |
|----------|--------|---------|
| PostgreSQL | `UsePostgresDistributedLock(...)` | `Fluent.Brighter.Postgres` |
| SQL Server | `UseMicrosoftSqlServerDistributedLock(...)` | `Fluent.Brighter.SqlServer` |
| MySQL | `UseMySqlDistributedLock(...)` | `Fluent.Brighter.MySql` |
| MongoDB | `UseMongoDbDistributedLock(...)` | `Fluent.Brighter.MongoDb` |
| DynamoDB | `UseDynamoDbDistributedLock(...)` | `Fluent.Brighter.AWS.V4` |
| Firestore | `UseFirestoreLocking(...)` | `Fluent.Brighter.GoogleCloud` |

## Configuration

### Via Provider Configurator (Integrated)

When using a database provider, enable distributed locking directly:

```csharp
services.AddFluentBrighter(brighter => brighter
.UsingPostgres(cfg =>
{
cfg.SetConnection(new RelationalDatabaseConfiguration("Host=localhost;Database=brighter;"));
cfg.UseDistributedLock();
}));
```

### Via Producer Builder (Standalone)

When using a separate messaging transport:

```csharp
services.AddFluentBrighter(brighter => brighter
.UsingRabbitMq(rabbitmq => /* ... */)
.Producers(producer => producer
.UsePostgresOutbox(/* ... */)
.UsePostgresDistributedLock("Host=localhost;Database=brighter;")));
```

### Provider-Specific Examples

#### PostgreSQL

```csharp
// With connection string
.UsePostgresDistributedLock("Host=localhost;Database=brighter;")

// With configuration object
.UsePostgresDistributedLock(new RelationalDatabaseConfiguration("Host=localhost;Database=brighter;"))
```

#### SQL Server

```csharp
.UseMicrosoftSqlServerDistributedLock("Server=localhost;Database=Brighter;")
```

#### MySQL

```csharp
.UseMySqlDistributedLock("Server=localhost;Database=brighter;User=root;Password=password;")
```

#### MongoDB

```csharp
// Via configurator
.UsingMongoDb(mongodb => mongodb
.SetConnection(c => c.SetConnectionString("mongodb://localhost:27017").SetDatabaseName("brighter"))
.UseDistributedLock("locking"))

// Via producer builder
.Producers(producer => producer
.UseMongoDbDistributedLock(cfg => { /* MongoDbLockingBuilder config */ }))
```

#### DynamoDB

```csharp
.UsingAws(aws => aws
// ... connection config ...
.UseDynamoDbDistributedLock(cfg => { /* DynamoDbLockingProviderOptionsBuilder config */ }))
```

#### Firestore

```csharp
.UsingGcp(gcp => gcp
// ... connection config ...
.UseFirestoreLocking(cfg => { /* FirestoreLockingBuilder config */ }))
```
86 changes: 86 additions & 0 deletions docs/advanced/luggage-store.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Large Message Storage (Luggage Store)

When message payloads exceed the size limits of your message broker, Fluent Brighter can automatically store the payload in a cloud storage service and pass a reference through the broker instead.

## Supported Providers

| Provider | Method | Package |
|----------|--------|---------|
| AWS S3 | `UseS3LuggageStore(...)` | `Fluent.Brighter.AWS.V4` |
| Google Cloud Storage | `UseGcsLuggageStore(...)` | `Fluent.Brighter.GoogleCloud` |
| MongoDB GridFS | `UseLuggageStore(...)` | `Fluent.Brighter.MongoDb` |

## Configuration

### AWS S3

```csharp
services.AddFluentBrighter(brighter => brighter
.UsingAws(aws => aws
.SetConnection(/* ... */)
// ... publications and subscriptions ...
.UseS3LuggageStore("my-luggage-bucket")));
```

Or with advanced configuration:

```csharp
.UseS3LuggageStore(cfg => { /* S3LuggageStoreBuilder config */ })
```

### Google Cloud Storage

```csharp
services.AddFluentBrighter(brighter => brighter
.UsingGcp(gcp => gcp
.SetConnection(/* ... */)
// ... publications and subscriptions ...
.UseGcsLuggageStore("my-luggage-bucket")));
```

Or with advanced configuration:

```csharp
.UseGcsLuggageStore(cfg => { /* GcsLuggageStoreBuilder config */ })
```

### MongoDB GridFS

```csharp
services.AddFluentBrighter(brighter => brighter
.UsingMongoDb(mongodb => mongodb
.SetConnection(c => c
.SetConnectionString("mongodb://localhost:27017")
.SetDatabaseName("brighter"))
.UseLuggageStore("my-bucket")));
```

Or with advanced configuration:

```csharp
.UseLuggageStore(cfg => { /* MongoDbLuggageStoreBuilder config */ })
```

### Generic Luggage Store

You can also configure a luggage store using a custom `IAmAStorageProvider` implementation:

```csharp
services.AddFluentBrighter(brighter => brighter
.SetLuggageStore(store => store
.UseLuggageStore<MyCustomStorageProvider>()));
```

Or provide a factory:

```csharp
.SetLuggageStore(store => store
.UseLuggageStore(sp => new MyCustomStorageProvider(sp.GetRequiredService<IConfiguration>())))
```

## How It Works

1. When publishing a message, if the payload exceeds the broker's size limit, the payload is uploaded to the configured storage provider
2. A reference (claim check) is placed in the message instead of the full payload
3. When consuming the message, the reference is used to retrieve the full payload from storage
4. The consumer receives the fully hydrated message transparently
Loading