From 2cd381933d32a75f6716025f29bb438aceb58229 Mon Sep 17 00:00:00 2001 From: iancooper Date: Sun, 6 Sep 2026 23:55:14 +0100 Subject: [PATCH] =?UTF-8?q?docs:=20013=20phase=201=20=E2=80=94=20repair=20?= =?UTF-8?q?an=20API=20surface=20the=20corpus=20never=20had?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P0-2 and P0-4. Eleven pages, ten code blocks, and every gate where design §9 said it would be: link 160 unmoved, pagelint pages 158 unmoved, shape, redirects, versioncheck and optioncheck all unmoved, and the using-directive warning count 779 -> 773. Fifteen dead call sites, three further defects, ten dead type names. 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. Every repaired block was compiled before it was written to a page: one project per block, disable, optioncheck's pinned packages, 0 errors and 0 warnings each. Seven findings the approved task list did not predict — full ledger in spec/013-howto_guides/tasks.md § Phase 1 as executed: A. TWO OF THE SEVENTEEN SITES ARE CORRECT AND DESIGN §5 WOULD HAVE FALSIFIED THEM. MigratingToPollyV8.md:99 and :101 sit inside a block the page labels V9. At tag 9.33 git grep -c declares With() 1 and Policies( 2; both are 0 at 10.7.0. They are genuine V9 API shown as history on a migration page, and rewriting them would have told readers V9 had a method it never had — a repair inventing a history. The block now carries CLAUDE.md's ❌/✅ markers so the next sweep meets a label rather than a bare .With(). B. EVERY FLUENT EXAMPLE WAS MISSING THREE MANDATORY CHAIN STEPS. The builder's interfaces force ExternalBus|NoExternalBus, ConfigureInstrumentation| NoInstrumentation and RequestSchedulerFactory between Resilience and Build. Repairing only the names design §5 lists would have produced blocks that still do not compile. No grep could see it; the compiler reads a chain, a sweep reads a token. C. AddBrighterDefault lives in Paramore.Brighter.Extensions — a namespace on 0 of 158 pages. The method the spec set out to add is one the corpus cannot import. D. UseResiliencePipelineAsync is in Paramore.Brighter.Policies.Attributes, and the two pages already using it carry no using lines at all. E. TWO OF P0-4's TEN SITES ARE INSIDE FENCES, NOT PROSE, so ten blocks turned strict where eight were budgeted. --changed said so immediately, which is the argument for reading the scope line rather than the verdict. F. Renaming a type inside a block is not finished until its namespace is imported: BrighterBasicConfiguration.md had Paramore.Brighter.MySql but not ...MySql.EntityFrameworkCore. It passed rule 6 either way — directives, but not the right ones. G. BrighterOptions.PolicyRegistry raises CS0618 and is NOT a defect: under AC10's three states it is live, merely deprecated. Recorded, not swept. Checked and left alone: five Darker calls on CQRSWithBrighterAndDarker.md, verified real in ../Darker against a control of 0 — only the Brighter half of that block was touched. DynamoDbUnitOfWork and MongoDbUnitOfWork survive on five pages and are the control proving the P0-4 sweep discriminated rather than pattern-matching on UnitOfWork. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0146UueHL6H3zGBGTYwz7GtL --- contents/BrighterBasicConfiguration.md | 3 +- contents/CQRSWithBrighterAndDarker.md | 14 +- .../CommandProcessorConfigurationReference.md | 32 ++++- .../HowConfiguringTheCommandProcessorWorks.md | 21 ++- contents/HowConfiguringTheDispatcherWorks.md | 63 ++++++--- contents/MSSQLOutbox.md | 4 +- contents/MigratingToPollyV8.md | 36 ++++- contents/MySQLOutbox.md | 4 +- contents/PolicyRetryAndCircuitBreaker.md | 98 +++++++++++--- contents/PostgresOutbox.md | 4 +- contents/SqliteOutbox.md | 4 +- spec/013-howto_guides/tasks.md | 123 ++++++++++++++++-- 12 files changed, 331 insertions(+), 75 deletions(-) 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