BrighterCommand / BrighterCommand/Brighter
V11: Replace `OnMissingChannel` with `IAmAProvisioner<TConnection>` — nullable, null defaults to Assume
- Dominant language
- C#
- Stars
- 2.5k
- Forks
- 296
- Avg merge
- 1d 11h
- Merged PRs (30d)
- 21
Description
### **Motivation**
`Publication` and `Subscription` currently expose `MakeChannels` as an `OnMissingChannel` enum (`Create`, `Validate`, `Assume`). This scatters infrastructure logic inside transport-specific consumer/producer factories and helper classes, making it:
- **Inconsistent across transports** — each gateway handles `OnMissingChannel` differently
---
### **Proposed Core Abstraction**
```csharp
namespace Paramore.Brighter;
///
/// Ensures messaging infrastructure exists and is correctly configured.
/// The implementing class holds all channel-specific configuration via required properties.
/// The connection is supplied at execution time.
///
public interface IAmAProvisioner
{
///
/// Validates or creates infrastructure using the supplied connection.
///
Task ProvisionAsync(TConnection connection, CancellationToken cancellationToken = default);
}
```
---
### **Transport Implementations — `required` Properties**
Each implementation uses **required properties** for configuration. No constructor needed. `ProvisionAsync` receives **only** the connection.
#### **RabbitMQ**
```csharp
namespace Paramore.Brighter.MessagingGateway.RMQ;
public class RmqProvisionerCreate : IAmAProvisioner
{
public required ExchangeName ExchangeName { get; init; }
public required RoutingKey RoutingKey { get; init; }
public required ChannelName QueueName { get; init; }
public bool Durable { get; init; } = true;
public bool AutoDelete { get; init; } = false;
public bool Exclusive { get; init; } = false;
public Task ProvisionAsync(
RmqMessagingGatewayConnection connection,
CancellationToken cancellationToken = default)
{
using var channel = connection.CreateModel();
channel.ExchangeDeclare(
exchange: ExchangeName,
type: "topic",
durable: Durable,
autoDelete: AutoDelete);
channel.QueueDeclare(
queue: QueueName,
durable: Durable,
exclusive: Exclusive,
autoDelete: AutoDelete,
arguments: null);
channel.QueueBind(
queue: QueueName,
exchange: ExchangeName,
routingKey: RoutingKey);
return Task.CompletedTask;
}
}
public class RmqProvisionerValidator : IAmAProvisioner
{
public required ExchangeName ExchangeName { get; init; }
public required RoutingKey RoutingKey { get; init; }
public required ChannelName QueueName { get; init; }
public Task ProvisionAsync(
RmqMessagingGatewayConnection connection,
CancellationToken cancellationToken = default)
{
using var channel = connection.CreateModel();
channel.ExchangeDeclarePassive(ExchangeName);
channel.QueueDeclarePassive(QueueName);
return Task.CompletedTask;
}
}
```
#### **Kafka**
```csharp
namespace Paramore.Brighter.MessagingGateway.Kafka;
public class KafkaProvisionerCreate : IAmAProvisioner
{
public required string Topic { get; init; }
public int NumPartitions { get; init; } = 1;
public short ReplicationFactor { get; init; } = 1;
public async Task ProvisionAsync(
KafkaMessagingGatewayConfiguration configuration,
CancellationToken cancellationToken = default)
{
using var adminClient = new AdminClientBuilder(configuration).Build();
await adminClient.CreateTopicsAsync(new[]
{
new TopicSpecification
{
Name = Topic,
NumPartitions = NumPartitions,
ReplicationFactor = ReplicationFactor
}
});
}
}
```
---
### **Integration with Publication / Subscription**
Remove `OnMissingChannel MakeChannels` entirely from `Publication` and `Subscription`. Replace with an **optional** `IAmAProvisioner? Provisioner` property.
- **`null` (default)** → Assume behavior (no provisioning, no validation)
- **`RmqProvisionerCreate`** → Create infrastructure
- **`RmqProvisionerValidator`** → Validate infrastructure exists
```csharp
// BEFORE (V10)
new RmqPublication
{
MakeChannels = OnMissingChannel.Create,
Topic = new RoutingKey("greeting.topic"),
ChannelName = new ChannelName("greeting.queue"),
}
// AFTER (V11) — null = Assume (default)
new RmqPublication
{
Topic = new RoutingKey("greeting.topic"),
ChannelName = new ChannelName("greeting.queue"),
// Provisioner = null // implicit: no provisioning
}
// AFTER (V11) — explicit Create
new RmqPublication
{
Topic = new RoutingKey("greeting.topic"),
ChannelName = new ChannelName("greeting.queue"),
Provisioner = new RmqProvisionerCreate
{
ExchangeName = new ExchangeName("greeting.exchange"),
RoutingKey = new RoutingKey("greeting.topic"),
QueueName = new ChannelName("greeting.queue"),
Durable = true
}
}
// Subscription with Validator
new RmqSubscription(
new SubscriptionName("greeting-sub"),
new ChannelName("greeting.queue"),
new RoutingKey("greeting.topic"),
provisioner: new RmqProvisionerValidator
{
ExchangeName = new ExchangeName("greeting.exchange"),
RoutingKey = new RoutingKey("greeting.topic"),
QueueName = new ChannelName("greeting.queue")
}
);
```
---
### **Key Design Points**
| Aspect | Rationale |
|--------|-----------|
| **No backward compatibility** | V11 is a major version; `OnMissingChannel` is removed entirely |
| **Nullable provisioner** | `IAmAProvisioner? Provisioner` — optional, null = Assume |
| **`required` properties** | No constructor boilerplate; clean object initializer syntax |
| **Connection passed at runtime** | `ProvisionAsync(TConnection)` receives only the gateway/connection |
| **Generic interface** | `IAmAProvisioner` is type-safe per transport; no common `IAmAGateway` needed |
| **Two implementations per transport** | `*ProvisionerCreate`, `*ProvisionerValidator` — maps 1:1 to `OnMissingChannel.Create` and `OnMissingChannel.Validate` |
---
### **Benefits**
- **Testability** — Provisioners can be unit-tested in isolation by mocking `TConnection`
- **Extensibility** — Users can write custom provisioners (e.g., `TaggedRmqProvisionerCreate` for AWS cost tagging)
- **Separation of concerns** — `Publication`/`Subscription` describe *what*; provisioners handle *how*
- **Clean V11 break** — Removes `OnMissingChannel` enum entirely; no adapter/legacy code
Contributor guide
Research direction
Start by locating Publication and Subscription, then trace their existing OnMissingChannel handling through the transport-specific factories and helpers. Define the generic provisioner abstraction and transport implementations described in the issue, then replace MakeChannels while preserving null-as-Assume behavior. Done means the old enum is removed and create, validate, and default-assume flows work for the affected transports.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp, kafka, rabbitmq
- Domain
- backend-api-design, distributed-systems
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100