BrighterCommand / BrighterCommand/Brighter
[Enhancement] Auto-register (or fail fast on) IAmARelationalDatabaseConfiguration for relational transaction providers
- Dominant language
- C#
- Stars
- 2.5k
- Forks
- 296
- Avg merge
- 1d 11h
- Merged PRs (30d)
- 21
Description
### Summary
Setting a relational `TransactionProvider`/`ConnectionProvider` on `AddProducers` also requires a separate `services.AddSingleton(...)` registration. Miss it and the app starts cleanly, provisions its Outbox, runs the Sweeper — and then throws on the first resolve of a command processor, naming a type the user never wrote.
**This is not a bug and it is already answered.** [#3721](https://github.com/BrighterCommand/Brighter/issues/3721) reported it in August 2025 and was closed with the correct explanation — *"You need to register the `IAmARelationalDatabaseConfiguration` that you are using, as it is a dependency of `MsSqlConnectionProvider` from which it obtains the connection string"* — then folded into [#3755](https://github.com/BrighterCommand/Brighter/issues/3755). The documentation is correct too: `PostgresOutbox.md` shows the registration twice, and `BrighterBasicConfiguration.md` shows it as well.
So this issue is purely about ergonomics: the requirement is discoverable only if you already know it, and the failure lands a long way from the omission. Raised after hitting it while writing a tutorial sample, a year after #3721.
### Why it surfaces so late
The descriptor for `IAmABoxTransactionProvider` **is** registered, at [`ServiceCollectionExtensions.cs:289-290`](https://github.com/BrighterCommand/Brighter/blob/master/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs#L289-L290) — the provider is given as a `Type`, so the container owns its activation. Nothing fails at registration time. The first attempt to *construct* it is `AddEventBus` at [`:648`](https://github.com/BrighterCommand/Brighter/blob/master/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs#L648):
```csharp
var transactionProvider = serviceScope.ServiceProvider.GetService();
```
`GetService` rather than `GetRequiredService`, but it still throws — because a descriptor exists and it is *activation* that fails, not lookup. By then the host has started and every log line is green:
```
info: Paramore.Brighter.BoxProvisioning.BoxProvisioningHostedService[0]
Provisioned Outbox 'Outbox' successfully
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Paramore.Brighter.CommandProcessor[1620710603]
Found 0 to clear out of amount 100
Unhandled exception. System.InvalidOperationException: Unable to resolve service for type
'Paramore.Brighter.IAmARelationalDatabaseConfiguration' while attempting to activate
'Paramore.Brighter.PostgreSql.PostgreSqlTransactionProvider'.
```
Provisioning genuinely succeeded, because `AddPostgreSqlOutbox(configuration)` captures the object directly and never asks the container for it. There are two paths to the same configuration and only one of them is wired.
### Blast radius
Every relational backend, both providers each — ten types:
| Backend | Types taking `IAmARelationalDatabaseConfiguration` |
|---|---|
| MsSql | `MsSqlConnectionProvider`, `MsSqlTransactionProvider` |
| MySql | `MySqlConnectionProvider`, `MySqlTransactionProvider` |
| PostgreSql | `PostgreSqlConnectionProvider`, `PostgreSqlTransactionProvider` |
| Spanner | `SpannerConnectionProvider`, `SpannerUnitOfWork` |
| Sqlite | `SqliteConnectionProvider`, `SqliteTransactionProvider` |
### Option A — we already have the value
When `configure.Outbox` is a relational outbox, the configuration is *in Brighter's hands at registration time*. `RelationDatabaseOutbox` holds it:
```csharp
// src/Paramore.Brighter/RelationDatabaseOutbox.cs:27
protected IAmARelationalDatabaseConfiguration DatabaseConfiguration { get; } = configuration;
```
It is `protected`, so `AddProducers` cannot read it — but exposing it (a public getter, or a small `IAmARelationalOutbox` interface) would let `AddProducers` do:
```csharp
if (busConfiguration.Outbox is IAmARelationalOutbox relational)
brighterBuilder.Services.TryAddSingleton(relational.DatabaseConfiguration);
```
`TryAdd` matches what this method already does for the providers themselves at [`:835`](https://github.com/BrighterCommand/Brighter/blob/master/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs#L835) and [`:840`](https://github.com/BrighterCommand/Brighter/blob/master/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs#L840), so an explicit user registration still wins and nobody's existing setup changes.
A variant that avoids touching the outbox types at all is a `ProducersConfiguration.DatabaseConfiguration` slot the user sets in the same delegate they are already in — one place, not two.
### Option B — fail fast, and say what to do
If auto-registration is too magical, the same mistake can at least be caught next to where it is made. `AddProducers` already validates at registration time:
```csharp
// ServiceCollectionExtensions.cs:259
throw new ConfigurationException("An external bus must have an IAmAProducerRegistry");
```
and there is already a resolvability-probe pattern in the same assembly — `ServiceCollectionTransformerResolvabilityProbe` answers "can this type be resolved?" by testing membership against the service types registered in the `IServiceCollection`, without resolving the container or instantiating anything. Pointing the same technique at the transaction/connection provider's constructor parameters would turn the late `InvalidOperationException` into something like:
> `ConfigurationException`: `PostgreSqlTransactionProvider` requires `IAmARelationalDatabaseConfiguration`, which is not registered. Add `services.AddSingleton(configuration)` before `AddProducers`.
One caveat worth stating rather than discovering: such a probe reports on the descriptors present *at the moment `AddProducers` runs*, so a configuration registered afterwards would be a false positive. That argues for the check being ordering-tolerant — deferred to build time, or paired with Option A so the common case never reaches it.
The two compose: A fixes the case where Brighter can answer the question itself, B covers the rest (a hand-rolled provider, or a connection provider used without a relational outbox).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Contributor guide
Assessment
This issue has not been assessed yet.