diff --git a/contents/BrighterBasicConfiguration.md b/contents/BrighterBasicConfiguration.md index 0b6d154..886efa4 100644 --- a/contents/BrighterBasicConfiguration.md +++ b/contents/BrighterBasicConfiguration.md @@ -60,6 +60,7 @@ using Paramore.Brighter; using Paramore.Brighter.Extensions.DependencyInjection; using Paramore.Brighter.MessagingGateway.RMQ.Async; using Paramore.Brighter.MySql; +using Paramore.Brighter.MySql.EntityFrameworkCore; using Paramore.Brighter.Outbox.Hosting; using Paramore.Brighter.Outbox.MySql; @@ -100,7 +101,7 @@ public void ConfigureServices(IServiceCollection services) configure.MaxOutStandingMessages = 5; configure.MaxOutStandingCheckInterval = TimeSpan.FromMilliseconds(500); configure.Outbox = new MySqlOutbox(outboxConfiguration); - configure.TransactionProvider = typeof(MySqlEntityFrameworkConnectionProvider); + configure.TransactionProvider = typeof(MySqlEntityFrameworkTransactionProvider); configure.ConnectionProvider = typeof(MySqlConnectionProvider); }) .UseOutboxSweeper() diff --git a/contents/CQRSWithBrighterAndDarker.md b/contents/CQRSWithBrighterAndDarker.md index 8c30044..0c1d4e4 100644 --- a/contents/CQRSWithBrighterAndDarker.md +++ b/contents/CQRSWithBrighterAndDarker.md @@ -357,7 +357,9 @@ Configure both Brighter and Darker in your `Program.cs` or `Startup.cs`: using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Paramore.Brighter; +using Paramore.Brighter.Extensions; using Paramore.Brighter.Extensions.DependencyInjection; +using Polly.Registry; using Paramore.Darker; using Paramore.Darker.AspNetCore; using Paramore.Darker.Policies; @@ -370,15 +372,15 @@ builder.Services.AddDbContext(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); // Configure Brighter (Command Side) +var resiliencePipelineRegistry = new ResiliencePipelineRegistry() + .AddBrighterDefault(); +// Configure retry and circuit breaker pipelines for commands with TryAddBuilder here + builder.Services.AddBrighter(options => { - // Configure Brighter options + options.ResiliencePipelineRegistry = resiliencePipelineRegistry; }) -.AutoFromAssemblies(typeof(PlaceOrderCommandHandler).Assembly) -.ConfigureResiliencePipelines(registry => -{ - // Configure retry and circuit breaker policies for commands -}); +.AutoFromAssemblies(typeof(PlaceOrderCommandHandler).Assembly); // Configure Darker (Query Side) builder.Services.AddDarker(options => diff --git a/contents/CommandProcessorConfigurationReference.md b/contents/CommandProcessorConfigurationReference.md index c1c16cc..b5ef652 100644 --- a/contents/CommandProcessorConfigurationReference.md +++ b/contents/CommandProcessorConfigurationReference.md @@ -95,20 +95,40 @@ See the section [Policy Retry and Circuit Breaker](/contents/PolicyRetryAndCircu With the resilience pipeline registry configured, you need to tell Brighter where to find it: ``` csharp +using Microsoft.Extensions.DependencyInjection; +using Paramore.Brighter; +using Paramore.Brighter.Extensions; +using Paramore.Brighter.Extensions.DependencyInjection; +using Polly; +using Polly.Registry; +using Polly.Retry; + // ... public void ConfigureServices(IServiceCollection services) { + var resiliencePipelineRegistry = new ResiliencePipelineRegistry() + .AddBrighterDefault(); + + resiliencePipelineRegistry.TryAddBuilder("RetryPipeline", + (builder, _) => builder.AddRetry(new RetryStrategyOptions())); + services.AddBrighter(options => - options.PolicyRegistry = new PolicyRegistry() // Optional: for legacy Polly v7 policies - ) - .ConfigureResiliencePipelines(registry => { - registry.TryAddBuilder("RetryPipeline", /* ... */); - registry.TryAddBuilder("CircuitBreakerPipeline", /* ... */); + options.ResiliencePipelineRegistry = resiliencePipelineRegistry; }); } ``` +**`ResiliencePipelineRegistry` is a property on `BrighterOptions`, not a fluent builder call.** +You set it inside the `AddBrighter` options delegate, as above. Brighter supplies its own +registry only when you leave the property unset — it does so with `??=` — so a registry you +build yourself needs `AddBrighterDefault()`, which backfills the pipelines Brighter requires +without touching yours. Without it, startup fails with a `ConfigurationException` naming the +missing `CommandProcessor.OutboxProducer` pipeline. + +`BrighterOptions.PolicyRegistry` still exists for Polly v7 `[UsePolicy]` handlers, but it is +marked obsolete in V10 and compiling against it raises `CS0618`. + > **Note**: For legacy Polly v7 policies using `[UsePolicy]`, see the [migration guide](/contents/MigratingToPollyV8.md#polly-v8-migration-guide-v9-to-v10) for updating to V10 resilience pipelines. ### Configuring Lifetimes @@ -610,7 +630,7 @@ public void ConfigureServices(IServiceCollection services) .AddProducers((configure) => { configure.Outbox = new MySqlOutbox(outboxConfiguration); - configure.TransactionProvider = typeof(MySqlEntityFrameworkConnectionProvider); + configure.TransactionProvider = typeof(MySqlEntityFrameworkTransactionProvider); configure.ConnectionProvider = typeof(MySqlConnectionProvider); }) .AutoFromAssemblies(); diff --git a/contents/HowConfiguringTheCommandProcessorWorks.md b/contents/HowConfiguringTheCommandProcessorWorks.md index 5d0a120..31ff2db 100644 --- a/contents/HowConfiguringTheCommandProcessorWorks.md +++ b/contents/HowConfiguringTheCommandProcessorWorks.md @@ -231,10 +231,27 @@ You need to provide a factory to give us instances of a [Context](/contents/Usin All these individual elements can be passed to a **Command Processor Builder** to help build a **Command Processor**. This has a fluent interface to help guide you when configuring Brighter. The result looks like this: ``` csharp -var commandProcessor = CommandProcessorBuilder.With() +using Paramore.Brighter; +using Paramore.Brighter.Extensions; +using Polly.Registry; + +var commandProcessor = CommandProcessorBuilder.StartNew() .Handlers(new HandlerConfiguration(subscriberRegistry, handlerFactory)) - .Policies(policyRegistry) + .Resilience(resiliencePipelineRegistry.AddBrighterDefault()) .NoExternalBus() + .NoInstrumentation() .RequestContextFactory(new InMemoryRequestContextFactory()) + .RequestSchedulerFactory(new InMemorySchedulerFactory()) .Build(); ``` + +**Each call returns the interface the next one is declared on**, which is what makes the chain +guide you: `StartNew()` gives you something that only offers `Handlers`, and `Handlers` gives +you something that only offers `Resilience` or `DefaultResilience`. You cannot skip a step, +and the compiler — not the documentation — is what tells you so. + +`Resilience` takes a second, optional Polly v7 `IPolicyRegistry` if you still have +`[UsePolicy]` handlers to carry; see the deprecated Policy Registry section below. +`AddBrighterDefault()` backfills the pipelines Brighter itself needs — without it, a registry +you built yourself makes `Resilience` throw `ConfigurationException` at startup. If you have +no pipelines of your own, `DefaultResilience()` replaces the whole call. diff --git a/contents/HowConfiguringTheDispatcherWorks.md b/contents/HowConfiguringTheDispatcherWorks.md index ea60d14..0d0c3cc 100644 --- a/contents/HowConfiguringTheDispatcherWorks.md +++ b/contents/HowConfiguringTheDispatcherWorks.md @@ -45,34 +45,67 @@ var messageMapperRegistry = new MessageMapperRegistry(messageMapperFactory) ### Channel Factory -The Channel Factory is where we take a dependency on a specific Broker. We pass the **Dispatcher** an instances of **InputChannelFactory** which in turn has a dependency on implementation of **IAmAChannelFactory**. The channel factory is used to create channels that wrap the underlying Message-Oriented Middleware that you are using. +The Channel Factory is where we take a dependency on a specific Broker. We pass the **Dispatcher** an instance of `ChannelFactory`, which in turn has a dependency on an implementation of `IAmAChannelFactory`. The channel factory is used to create channels that wrap the underlying Message-Oriented Middleware that you are using. ### Creating a Builder This code fragment shows putting the whole thing together ``` csharp +using System; +using Paramore.Brighter; +using Paramore.Brighter.MessagingGateway.RMQ.Sync; +using Paramore.Brighter.Observability; +using Paramore.Brighter.ServiceActivator; + // create message mappers -var messageMapperRegistry = new MessageMapperRegistry(messageMapperFactory) +var messageMapperRegistry = new MessageMapperRegistry(messageMapperFactory, null); +messageMapperRegistry.Register(); + +// create the gateway +var rmqConnection = new RmqMessagingGatewayConnection { - { typeof(GreetingCommand), typeof(GreetingCommandMessageMapper) } + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672/%2f")), + Exchange = new Exchange("paramore.brighter.exchange") }; +var rmqMessageConsumerFactory = new RmqMessageConsumerFactory(rmqConnection); -// create the gateway -var rmqMessageConsumerFactory = new RmqMessageConsumerFactory(logger); -_dispatcher = DispatchBuilder.With() - .CommandProcessor(CommandProcessorBuilder.With() - .Handlers(new HandlerConfiguration(subscriberRegistry, handlerFactory)) - .Policies(policyRegistry) - .NoExternalBus() - .RequestContextFactory(new InMemoryRequestContextFactory()) - .Build()) - .MessageMappers(messageMapperRegistry) - .ChannelFactory(new InputChannelFactory(rmqMessageConsumerFactory)) - .Subscribers(subscriptions) +var tracer = new BrighterTracer(TimeProvider.System); + +var commandProcessor = CommandProcessorBuilder.StartNew() + .Handlers(new HandlerConfiguration(subscriberRegistry, handlerFactory)) + .DefaultResilience() + .NoExternalBus() + .ConfigureInstrumentation(tracer, InstrumentationOptions.All) + .RequestContextFactory(new InMemoryRequestContextFactory()) + .RequestSchedulerFactory(new InMemorySchedulerFactory()) + .Build(); + +_dispatcher = DispatchBuilder.StartNew() + .CommandProcessor(commandProcessor, new InMemoryRequestContextFactory()) + // four registries: sync mappers, async mappers, transforms, async transforms + .MessageMappers(messageMapperRegistry, null, null, null) + .ChannelFactory(new ChannelFactory(rmqMessageConsumerFactory)) + .Subscriptions( + [ + new RmqSubscription( + new SubscriptionName("GreetingCommand"), + new ChannelName("greeting.command"), + new RoutingKey("greeting.command"), + messagePumpType: MessagePumpType.Reactor, + timeOut: TimeSpan.FromMilliseconds(200)) + ]) + .ConfigureInstrumentation(tracer, InstrumentationOptions.All) .Build(); ``` +**Two details in that block will bite you if you change them.** The subscription is typed +`RmqSubscription` rather than `Subscription`, because a transport's channel factory casts +to its own subscription type and throws `ConfigurationException` when the cast fails — code that +compiles perfectly and dies at `Receive()`. And `messagePumpType` is set explicitly to +`Reactor`: `Subscription` defaults to `Proactor`, which needs the *async* mapper registry, +and the third and fourth arguments to `MessageMappers` here are `null`. + ## Validating Consumer Configuration When you enable pipeline validation with `.ValidatePipelines()` on the **IBrighterBuilder**, consumer-specific checks run automatically when `AddConsumers()` is used. These checks catch common mistakes before the dispatcher starts receiving messages: diff --git a/contents/MSSQLOutbox.md b/contents/MSSQLOutbox.md index 11fd87d..4769697 100644 --- a/contents/MSSQLOutbox.md +++ b/contents/MSSQLOutbox.md @@ -120,8 +120,8 @@ services.AddSingleton(dbConfig); Next, in your `ConfigureServices` method or `Program.cs`, add the outbox configuration when calling `AddBrighter`. You need to specify the `Outbox`, `ConnectionProvider`, and `TransactionProvider`. The `TransactionProvider` depends on how you manage your database transactions. -- Use `MsSqlUnitOfWork` for ADO.NET-based transaction management. -- Use `MsSqlEntityFrameworkConnectionProvider` if you are using Entity Framework Core, where `T` is your `DbContext`. +- Use `MsSqlTransactionProvider` for ADO.NET-based transaction management. +- Use `MsSqlEntityFrameworkCoreTransactionProvider` if you are using Entity Framework Core, where `T` is your `DbContext`. ### **Example with Entity Framework Core** diff --git a/contents/MigratingToPollyV8.md b/contents/MigratingToPollyV8.md index c11aa52..9f2ed67 100644 --- a/contents/MigratingToPollyV8.md +++ b/contents/MigratingToPollyV8.md @@ -92,7 +92,7 @@ internal class MyHandler : RequestHandler ### Step 4: Update CommandProcessor Configuration -**V9**: +❌ **V9 — superseded** ```csharp // ... @@ -102,18 +102,40 @@ var commandProcessor = CommandProcessorBuilder.With() .Build(); ``` -**V10**: +✅ **V10 — current** ```csharp +using Paramore.Brighter; +using Paramore.Brighter.Extensions; +using Polly.Registry; + // ... -var commandProcessor = CommandProcessorBuilder.With() - .Handlers(/* ... */) - .Policies(policyRegistry) // Optional: Keep for legacy v7 policies during migration - .ResiliencePipelines(resiliencePipelineRegistry) // New: Polly v8 pipelines +var commandProcessor = CommandProcessorBuilder.StartNew() + .Handlers(handlerConfiguration) + // One call, not two: the Polly v7 registry is Resilience's optional second argument + .Resilience(resiliencePipelineRegistry, policyRegistry) + .NoExternalBus() + .NoInstrumentation() + .RequestContextFactory(new InMemoryRequestContextFactory()) + .RequestSchedulerFactory(new InMemorySchedulerFactory()) .Build(); ``` -> **Note**: You can use both `Policies()` and `ResiliencePipelines()` during migration to support both legacy `UsePolicy` and new `UseResiliencePipeline` attributes. +**Three names change in that chain, and one of them is the entry point.** `With()` becomes +`StartNew()`, and `Policies()` — which took a Polly v7 `IPolicyRegistry` on its own — +is now the **optional second argument** to `Resilience()`. There is no separate call to make: +pass both registries to `Resilience` during migration, and drop the second argument once no +`[UsePolicy]` attributes remain. If you have nothing to carry over, `DefaultResilience()` +replaces the pair. + +> **Build the registry with `AddBrighterDefault()`.** Brighter requires a pipeline registered +> under `CommandProcessor.OutboxProducer`, and `Resilience` throws `ConfigurationException` at +> startup when the registry you hand it does not have one. `AddBrighterDefault` uses +> `TryAddBuilder`, so it adds what is missing and leaves your own pipelines alone. + +The V10 chain is also longer than V9's. `NoExternalBus()`, `NoInstrumentation()` and +`RequestSchedulerFactory()` are steps the fluent interface now requires — the compiler will +tell you, because each returns the interface the next call is declared on. --- diff --git a/contents/MySQLOutbox.md b/contents/MySQLOutbox.md index 7e9fcdd..a2b5b8c 100644 --- a/contents/MySQLOutbox.md +++ b/contents/MySQLOutbox.md @@ -121,8 +121,8 @@ services.AddSingleton(dbConfig); Next, in your `ConfigureServices` method or `Program.cs`, add the outbox configuration when calling `AddBrighter`. You need to specify the `Outbox`, `ConnectionProvider`, and `TransactionProvider`. The `TransactionProvider` depends on how you manage your database transactions. -- Use `MySqlUnitOfWork` for ADO.NET-based transaction management. -- Use `MySqlEntityFrameworkConnectionProvider` if you are using Entity Framework Core, where `T` is your `DbContext`. +- Use `MySqlTransactionProvider` for ADO.NET-based transaction management. +- Use `MySqlEntityFrameworkTransactionProvider` if you are using Entity Framework Core, where `T` is your `DbContext`. ### **Example with Entity Framework Core** diff --git a/contents/PolicyRetryAndCircuitBreaker.md b/contents/PolicyRetryAndCircuitBreaker.md index be86f23..9a0c0d7 100644 --- a/contents/PolicyRetryAndCircuitBreaker.md +++ b/contents/PolicyRetryAndCircuitBreaker.md @@ -58,6 +58,32 @@ internal class MyQoSProtectedHandler : RequestHandler } ``` +### Async Handlers Take UseResiliencePipelineAsync + +`UseResiliencePipeline` decorates a synchronous `Handle`. An async handler takes the async +attribute instead, and pairing them the other way round leaves the pipeline out of the chain: + +```csharp +using System.Threading; +using System.Threading.Tasks; +using Paramore.Brighter; +using Paramore.Brighter.Policies.Attributes; + +internal class MyQoSProtectedHandlerAsync : RequestHandlerAsync +{ + [UseResiliencePipelineAsync(policy: "MyRetryPipeline", step: 1)] + public override async Task HandleAsync( + MyCommand command, CancellationToken cancellationToken = default) + { + // Do work that could throw errors due to distributed computing reliability + return await base.HandleAsync(command, cancellationToken); + } +} +``` + +Both attributes read from the same `ResiliencePipelineRegistry`, so a pipeline name +registered once serves handlers of either kind. + ### Configuring Resilience Pipelines To configure a Polly resilience pipeline, you use the `ResiliencePipelineRegistry` to register pipelines with a name. At runtime, Brighter looks up that pipeline by name. @@ -339,46 +365,84 @@ See [Request Context documentation](UsingTheContextBag.md) for more details on a ## Registering Pipelines with CommandProcessor -When creating your `CommandProcessor`, pass the `ResiliencePipelineRegistry` to the builder: +When creating your `CommandProcessor`, pass the `ResiliencePipelineRegistry` to +`Resilience`: ```csharp -var resiliencePipelineRegistry = new ResiliencePipelineRegistry(); - -// Configure pipelines (see examples above) -resiliencePipelineRegistry.TryAddBuilder("MyRetryPipeline", /* ... */); -resiliencePipelineRegistry.TryAddBuilder("MyCircuitBreakerPipeline", /* ... */); +using Paramore.Brighter; +using Paramore.Brighter.Extensions; +using Polly; +using Polly.Registry; +using Polly.Retry; + +// Start from Brighter's own pipelines. AddBrighterDefault uses TryAddBuilder, so it +// backfills the pipelines Brighter requires without replacing any you have registered. +var resiliencePipelineRegistry = new ResiliencePipelineRegistry() + .AddBrighterDefault(); + +// Configure your own pipelines (see examples above) +resiliencePipelineRegistry.TryAddBuilder("MyRetryPipeline", + (builder, _) => builder.AddRetry(new RetryStrategyOptions())); -var commandProcessor = CommandProcessorBuilder.With() +var commandProcessor = CommandProcessorBuilder.StartNew() .Handlers(new HandlerConfiguration( subscriberRegistry: registry, handlerFactory: handlerFactory)) - .Policies(policyRegistry) // Legacy Polly v7 policies (optional) - .ResiliencePipelines(resiliencePipelineRegistry) // Polly v8 pipelines + .Resilience(resiliencePipelineRegistry, policyRegistry) // policyRegistry is optional + .NoExternalBus() + .NoInstrumentation() .RequestContextFactory(new InMemoryRequestContextFactory()) + .RequestSchedulerFactory(new InMemorySchedulerFactory()) .Build(); ``` +`Resilience` takes the Polly v8 registry first and an optional Polly v7 `IPolicyRegistry` +second — **one call, not two.** If you have no legacy policies to carry, call +`DefaultResilience()` instead and Brighter supplies its own registry. + +> **Call `AddBrighterDefault` whenever you build the registry yourself.** Brighter requires a +> pipeline registered under `CommandProcessor.OutboxProducer`, and `Resilience` throws +> `ConfigurationException` on its first statement when the registry does not have one — at +> **startup**, before any message is sent. Brighter only fills the gap for you when you supply +> *no* registry at all, because the container does it with `??=`. + Or using dependency injection with ASP.NET Core: ```csharp +using Microsoft.Extensions.DependencyInjection; +using Paramore.Brighter; +using Paramore.Brighter.Extensions; +using Paramore.Brighter.Extensions.DependencyInjection; +using Polly; +using Polly.CircuitBreaker; +using Polly.Registry; +using Polly.Retry; + +var resiliencePipelineRegistry = new ResiliencePipelineRegistry() + .AddBrighterDefault(); + +resiliencePipelineRegistry.TryAddBuilder("MyRetryPipeline", + (builder, _) => builder.AddRetry(new RetryStrategyOptions())); + +resiliencePipelineRegistry.TryAddBuilder("MyCircuitBreakerPipeline", + (builder, _) => builder.AddCircuitBreaker(new CircuitBreakerStrategyOptions())); + services.AddBrighter(options => { options.HandlerLifetime = ServiceLifetime.Scoped; + options.ResiliencePipelineRegistry = resiliencePipelineRegistry; }) .Handlers(registry => { registry.Register(); -}) -.ConfigureResiliencePipelines(registry => -{ - registry.TryAddBuilder("MyRetryPipeline", - (builder, context) => builder.AddRetry(new RetryStrategyOptions { /* ... */ })); - - registry.TryAddBuilder("MyCircuitBreakerPipeline", - (builder, context) => builder.AddCircuitBreaker(new CircuitBreakerStrategyOptions { /* ... */ })); }); ``` +**There is no fluent `ConfigureResiliencePipelines` method on the DI builder.** The registry is +a settable property on `BrighterOptions`, set inside the `AddBrighter` options delegate as +above. This is the same `??=` case as the builder: assigning your own registry means Brighter +never calls `AddBrighterDefault` for you, so call it yourself. + --- ## Retry and Circuit Breaker Best Practices diff --git a/contents/PostgresOutbox.md b/contents/PostgresOutbox.md index 8431aa1..d2160b5 100644 --- a/contents/PostgresOutbox.md +++ b/contents/PostgresOutbox.md @@ -121,8 +121,8 @@ services.AddSingleton(dbConfig); Next, in your `ConfigureServices` method or `Program.cs`, add the outbox configuration when calling `AddBrighter`. You need to specify the `Outbox`, `ConnectionProvider`, and `TransactionProvider`. The `TransactionProvider` depends on how you manage your database transactions. -- Use `PostgreSqlUnitOfWork` for ADO.NET-based transaction management. -- Use `PostgreSqlEntityFrameworkConnectionProvider` if you are using Entity Framework Core, where `T` is your `DbContext`. +- Use `PostgreSqlTransactionProvider` for ADO.NET-based transaction management. +- Use `PostgreSqlEntityFrameworkTransactionProvider` if you are using Entity Framework Core, where `T` is your `DbContext`. ### **Example with Entity Framework Core** diff --git a/contents/SqliteOutbox.md b/contents/SqliteOutbox.md index bd62ac7..a951305 100644 --- a/contents/SqliteOutbox.md +++ b/contents/SqliteOutbox.md @@ -120,8 +120,8 @@ services.AddSingleton(dbConfig); Next, in your `ConfigureServices` method or `Program.cs`, add the outbox configuration when calling `AddBrighter`. You need to specify the `Outbox`, `ConnectionProvider`, and `TransactionProvider`. The `TransactionProvider` depends on how you manage your database transactions. -- Use `SqliteUnitOfWork` for ADO.NET-based transaction management. -- Use `SqliteEntityFrameworkConnectionProvider` if you are using Entity Framework Core, where `T` is your `DbContext`. +- Use `SqliteTransactionProvider` for ADO.NET-based transaction management. +- Use `SqliteEntityFrameworkTransactionProvider` if you are using Entity Framework Core, where `T` is your `DbContext`. ### **Example with Entity Framework Core** diff --git a/spec/013-howto_guides/tasks.md b/spec/013-howto_guides/tasks.md index 4255932..126a7eb 100644 --- a/spec/013-howto_guides/tasks.md +++ b/spec/013-howto_guides/tasks.md @@ -225,14 +225,14 @@ never had. **Thirteen tasks. One PR.** **This phase expects NO gate to move except pagelint's warning count**, which is exactly when a vacuous pass is invisible. **Read `--changed`'s scope line — expect `8 code block(s) strict`.** -- [ ] **Task 1.1:** Re-derive P0-2's site table before editing anything +- [x] **Task 1.1:** Re-derive P0-2's site table before editing anything - Input: design §2.3, §2.3.1, §5; the commands in §2.1 above - Output: a confirmed list of 17 sites / 6 pages / 8 blocks, with the current line numbers - Notes: **Run the control in the same breath** — `.Resilience(`, `DefaultResilience` and `AddBrighterDefault` must return **0 in `contents/`** and non-zero in `src/`. A sweep that cannot find the live API is not evidence about the dead one. -- [ ] **Task 1.2:** Repair `PolicyRetryAndCircuitBreaker.md` — 2 blocks, 4 sites +- [x] **Task 1.2:** Repair `PolicyRetryAndCircuitBreaker.md` — 2 blocks, 4 sites - Input: design §5's rows for `:351`, `:355`, `:356`, `:372`; §2.1's declarations - Output: `CommandProcessorBuilder.StartNew()`; `.Policies(policyRegistry)` **folded into** `.Resilience(registry, policyRegistry)`; `.ConfigureResiliencePipelines(…)` at `:372` @@ -241,7 +241,7 @@ vacuous pass is invisible. **Read `--changed`'s scope line — expect `8 code bl optional second parameter, so a site-for-site substitution prints two calls where one belongs. Both blocks earn real `using` directives. -- [ ] **Task 1.3:** Repair `MigratingToPollyV8.md` — 2 blocks, 5 sites, and the shape error +- [x] **Task 1.3:** Repair `MigratingToPollyV8.md` — 2 blocks, 5 sites, and the shape error - Input: design §5's rows for `:99`, `:101`, `:109`, `:111`, `:112`; and `:116` - Output: both `With()` → `StartNew()`; both `.Policies(` folded; `.ResiliencePipelines(registry)` → `.Resilience(registry, policyRegistry)` @@ -252,21 +252,21 @@ vacuous pass is invisible. **Read `--changed`'s scope line — expect `8 code bl ✅ marker**: it was marking a method that never existed, which is the one case in the corpus where the version convention actively endorsed an invention. -- [ ] **Task 1.4:** Repair `CommandProcessorConfigurationReference.md:104` — 1 block, 1 site +- [x] **Task 1.4:** Repair `CommandProcessorConfigurationReference.md:104` — 1 block, 1 site - Input: design §5; §2.1's `BrighterOptions.ResiliencePipelineRegistry` (a **settable property**, `BrighterOptions.cs:59`) - Output: `.ConfigureResiliencePipelines(…)` → `options.ResiliencePipelineRegistry = …` - Notes: **There is no fluent DI method**, which is why the invented one was so easy to write. Say so in a sentence; the next person to reach for a fluent call is the reader. -- [ ] **Task 1.5:** Repair `CQRSWithBrighterAndDarker.md:378` — 1 block, 1 site +- [x] **Task 1.5:** Repair `CQRSWithBrighterAndDarker.md:378` — 1 block, 1 site - Input: design §5 - Output: as task 1.4 - Notes: **Check which product before editing.** This page covers both, and Darker's `.AddPolicies(` is real. The edit is to the Brighter half only, and the page's banner (`Brighter and Darker V10`) is the reminder. -- [ ] **Task 1.6:** Repair `HowConfiguringTheCommandProcessorWorks.md` — 1 block, 2 sites +- [x] **Task 1.6:** Repair `HowConfiguringTheCommandProcessorWorks.md` — 1 block, 2 sites - Input: design §5's rows for `:234`, `:236` - Output: `With()` → `StartNew()`; `.Policies(policyRegistry)` → `.Resilience(registry, policyRegistry)` @@ -274,7 +274,7 @@ vacuous pass is invisible. **Read `--changed`'s scope line — expect `8 code bl adjacent `.ResiliencePipelines(` to fold into. **The two cases look identical in a grep and are not** — read the block. -- [ ] **Task 1.7:** Rewrite `HowConfiguringTheDispatcherWorks.md:54-74` from the source's own test +- [x] **Task 1.7:** Rewrite `HowConfiguringTheDispatcherWorks.md:54-74` from the source's own test - Input: `tests/Paramore.Brighter.RMQ.Sync.Tests/MessageDispatch/When_building_a_dispatcher.cs`; design §2.3's four defects - Output: a block with `DispatchBuilder.StartNew()`, `.Subscriptions(`, a real @@ -287,7 +287,7 @@ vacuous pass is invisible. **Read `--changed`'s scope line — expect `8 code bl satisfied and nothing about a default that selects a code path.** Mirror the test; do not compose. -- [ ] **Task 1.8:** Repair `HowConfiguringTheDispatcherWorks.md:48`'s prose — §2.2's addition +- [x] **Task 1.8:** Repair `HowConfiguringTheDispatcherWorks.md:48`'s prose — §2.2's addition - Input: §2.2 above; `CLAUDE.md`'s terminology rule - Output: the sentence names `ChannelFactory` in backticks, not **InputChannelFactory** in bold @@ -295,7 +295,7 @@ vacuous pass is invisible. **Read `--changed`'s scope line — expect `8 code bl fence-only repair, and it is four lines above the block that gets it right — P0-4's shape on a P0-2 page. Grep the page for `InputChannelFactory` afterwards and expect **0**. -- [ ] **Task 1.9:** Add what is on zero pages today — `AddBrighterDefault` and the `??=` trap +- [x] **Task 1.9:** Add what is on zero pages today — `AddBrighterDefault` and the `??=` trap - Input: design §2.1; `ResiliencePipelineRegistryExtensions.cs:57`; `CommandProcessorBuilder.cs:144-148`, `:171`; the DI factory at `:705` - Output: every edited block gains `.AddBrighterDefault()`, plus prose covering **(a)** that @@ -308,7 +308,7 @@ vacuous pass is invisible. **Read `--changed`'s scope line — expect `8 code bl add **`UseResiliencePipelineAsync`**, which is on 2 pages and neither is the resilience how-to. -- [ ] **Task 1.10:** P0-4 — repair the ten dead relational type names across six pages +- [x] **Task 1.10:** P0-4 — repair the ten dead relational type names across six pages - Input: design §11 Q6's **verified replacement table**; §2.1's control run - Output: `PostgresOutbox.md`, `MSSQLOutbox.md`, `MySQLOutbox.md`, `SqliteOutbox.md`, `BrighterBasicConfiguration.md`, `CommandProcessorConfigurationReference.md` — each dead @@ -320,7 +320,7 @@ vacuous pass is invisible. **Read `--changed`'s scope line — expect `8 code bl are the control. Prefer a prose fix that does not touch the adjacent code block — rule 6 placement, standing obligation 7. -- [ ] **Task 1.11:** Compile all eight repaired blocks +- [x] **Task 1.11:** Compile all eight repaired blocks - Input: 009's harness; `tools/optioncheck/optioncheck.csproj`'s pinned packages - Output: 8/8 building, 0 errors, 0 warnings, with `disable` - Notes: **Do not add `Microsoft.Extensions.Hosting` — `NU1605`.** Check every @@ -328,7 +328,7 @@ vacuous pass is invisible. **Read `--changed`'s scope line — expect `8 code bl (obligation 8): a block passing rule 6 has directives, not necessarily the right ones. **And record what compiling did not prove** — task 1.7's block is the case in point. -- [ ] **Task 1.12:** Run the seven gates and assert the phase moved what it predicted +- [x] **Task 1.12:** Run the seven gates and assert the phase moved what it predicted - Input: design §9's phase-1 row - Output: link **160** unmoved, pagelint pages **158** unmoved, shape unmoved, redirects unmoved, versioncheck unmoved, optioncheck unmoved — and **warnings BELOW 779** @@ -337,7 +337,7 @@ vacuous pass is invisible. **Read `--changed`'s scope line — expect `8 code bl has not edited what it thought it did. **This is the dangerous phase precisely because it expects nothing to move.** -- [ ] **Task 1.13:** Record every defect found, then open the PR +- [x] **Task 1.13:** Record every defect found, then open the PR - Output: a ledger entry per site — page, line, what the corpus said, what the assembly says - Notes: feeds task 5.3. **A defect fixed silently is a defect that never existed.** Ask for the merge **and the head-ref deletion by name in the same breath**; this repository does @@ -345,6 +345,103 @@ vacuous pass is invisible. **Read `--changed`'s scope line — expect `8 code bl --- +## Phase 1 as executed — 2026-09-06 + +**All thirteen tasks done in one PR. Every gate landed where design §9 predicted**: link +**160** unmoved, pagelint pages **158** unmoved, shape and redirects and versioncheck and +optioncheck all unmoved, and the warning count **779 → 773**. The scope line read +**`10 code block(s) strict`**, not the predicted 8 — see finding E. + +### The ledger — what the corpus said, and what the assembly says + +**Fifteen dead call sites repaired, plus three further defects and ten dead type names.** The +seventeen scoped sites became fifteen, and the two that survive are the subject of finding A. + +| Page | Site | The corpus said | The assembly says | +|---|---|---|---| +| `PolicyRetryAndCircuitBreaker.md` | 351 | `CommandProcessorBuilder.With()` | `StartNew()` | +| | 355–356 | `.Policies(r)` **and** `.ResiliencePipelines(r)` | one `.Resilience(registry, policyRegistry)` | +| | 372 | `.ConfigureResiliencePipelines(…)` | `options.ResiliencePipelineRegistry = …` | +| `MigratingToPollyV8.md` | 109 | `CommandProcessorBuilder.With()` | `StartNew()` | +| | 111–112 | `.Policies(r)` **and** `.ResiliencePipelines(r)` | one `.Resilience(…)` | +| | 116 | *"you can use both methods"* | there is one method with an optional second argument | +| `CommandProcessorConfigurationReference.md` | 104 | `.ConfigureResiliencePipelines(…)` | a settable property on `BrighterOptions` | +| `CQRSWithBrighterAndDarker.md` | 378 | `.ConfigureResiliencePipelines(…)` | as above | +| `HowConfiguringTheCommandProcessorWorks.md` | 234, 236 | `.With()`, `.Policies(r)` | `StartNew()`, `.Resilience(…)` | +| `HowConfiguringTheDispatcherWorks.md` | 54–74 | `DispatchBuilder.With()`, `.Subscribers(`, `InputChannelFactory`, `RmqMessageConsumerFactory(logger)`, one-argument `.MessageMappers(` | `StartNew()`, `.Subscriptions(`, `ChannelFactory`, `RmqMessageConsumerFactory(connection)`, **four**-argument `.MessageMappers(` | +| | 48 | `InputChannelFactory` **in prose** | §2.2's addition — survives a fence-only repair | +| six outbox/config pages | 10 sites | `…UnitOfWork`, `…EntityFrameworkConnectionProvider` | `…TransactionProvider`, `…EntityFrameworkTransactionProvider` (MSSQL keeps `Core`) | + +**What is now zero across `contents/`:** `.ResiliencePipelines(`, `.ConfigureResiliencePipelines(`, +`InputChannelFactory`, `DispatchBuilder.With(`, `.Subscribers(`, and all eight dead relational +type names. **`.Resilience(`, `DefaultResilience` and `AddBrighterDefault` were on 0 pages and +are now on 5** — the tell that opened this phase, inverted. + +### Seven findings the task list did not predict + +**A. Two of the seventeen sites are correct, and design §5's table would have falsified them.** +`MigratingToPollyV8.md:99` and `:101` sit inside a block the page labels **V9** on a migration +page. `git grep -c` at tag **9.33** declares `With()` **1** and `Policies(` **2**; both are +**0** at `10.7.0`. They are genuine V9 API, correctly shown as history. Rewriting them to +`StartNew()` would have told the reader V9 had a method it never had — **a repair inventing a +history, the same shape as design §11 Q6's invented type name**. The block is now marked +❌ **V9 — superseded** / ✅ **V10 — current** per `CLAUDE.md`, so the next dead-API sweep meets +an explicit label rather than a bare `.With()`. **The general rule: before repairing a dead +call, ask whether the page is quoting it on purpose — a migration guide's job is to print the +dead API.** + +**B. Every fluent example was missing three mandatory chain steps, and no sweep could see it.** +`CommandProcessorBuilder`'s interfaces force +`StartNew → Handlers → Resilience|DefaultResilience → ExternalBus|NoExternalBus → +ConfigureInstrumentation|NoInstrumentation → RequestContextFactory → RequestSchedulerFactory → +Build`. Four of the five builder blocks skipped two or three of those. **Repairing only the +names design §5 lists would have produced blocks that still do not compile** — the defect was +never the spelling alone, it was that the blocks predate three steps. Found by the compiler, +which is the only instrument that reads a chain rather than a token. + +**C. `AddBrighterDefault` needs a namespace that is on 0 of 158 pages.** It lives in +`Paramore.Brighter.Extensions` (`Extensions/ResiliencePipelineRegistryExtensions.cs:57` +@ `10.7.0`), not `Paramore.Brighter`. So the method the spec set out to add is one the corpus +cannot import — the zero-tell one level down from the one requirements §7 measured. + +**D. `UseResiliencePipelineAsync` is in `Paramore.Brighter.Policies.Attributes`**, and the two +pages that already use it carry no `using` lines at all, so nothing revealed it. + +**E. Two of P0-4's ten sites are inside code fences, not prose — so ten blocks turned strict, +not eight.** Design §11 Q6 describes the family as *"all in prose four lines above code blocks +that get it right"*, and that holds for eight of ten; +`BrighterBasicConfiguration.md:103` and `CommandProcessorConfigurationReference.md:633` are +`configure.TransactionProvider = typeof(…)` **inside** a block. The predicted rule-6 budget was +therefore two blocks short. **The prediction was checkable in one command and nobody ran it**; +`--changed` reported the truth immediately, which is the argument for reading the scope line. + +**F. Renaming a type inside a block is not finished until its namespace is imported.** +`BrighterBasicConfiguration.md`'s block imports `Paramore.Brighter.MySql` but not +`Paramore.Brighter.MySql.EntityFrameworkCore`, where the EF provider actually lives. The block +passes rule 6 either way — it has directives, just not the right ones — and only the compiler +told them apart. The directive was added. + +**G. `BrighterOptions.PolicyRegistry` is obsolete and raises `CS0618`, and that is NOT a +defect.** Under AC10's three states it is **live**, merely deprecated, so it was recorded rather +than swept. It was dropped from the one example being rewritten anyway, because `CLAUDE.md` +forbids showing deprecated patterns as current — a different reason, deliberately. + +### What was checked and deliberately left alone + +- **`.AddPolicies(`, `.AddHandlersFromAssemblies(`, `.AddDefaultPolicies()`, `.AddDarker(` and + `QueryProcessorLifetime` on `CQRSWithBrighterAndDarker.md` are real in Darker**, verified in + `../Darker` at `origin/master` against a control of 0. Only the Brighter half of that block + was touched — obligation 10, and the page's `Brighter V10 and Darker V4` banner is the + reminder. +- **`DynamoDbUnitOfWork` and `MongoDbUnitOfWork` survive on five pages.** The NoSQL stores + genuinely have a unit of work; they are the control that proves the P0-4 sweep discriminated + rather than pattern-matching on `UnitOfWork`. +- **`CommandProcessorConfigurationReference.md:621`'s block keeps its `// ...`**, which declares + its omission and downgrades rule 6 to a warning. Expanding it into a compilable example is a + different job. + +--- + ## Phase 2 — P0-1, `contents/PostgreSQLTransportAndOutbox.md` **Goal:** the guide Docs#67 is owed. **Eleven tasks. One PR.** ~330 lines, How-to, nested under