Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 63 additions & 39 deletions contents/AgreementDispatcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,17 @@ Brighter supports an Agreement Dispatcher, allowing you to register a lambda fun
### Basic Registration

```csharp
registry.Register<TRequest>(
routingFunc: (request, context) => { /* return handler types */ },
handlerTypes: [typeof(Handler1), typeof(Handler2), ...]
using Paramore.Brighter;

registry.Register<MyCommand>(
router: (request, context) => [typeof(Handler1)], // return the handler types for this request
handlerTypes: [typeof(Handler1), typeof(Handler2)]
);
```

**Parameters:**

- **routingFunc**: Lambda that takes `IRequest` and `IRequestContext`, returns `List<Type>` of handlers
- **router**: Lambda that takes `IRequest` and `IRequestContext`, returns `List<Type>` of handlers
- **handlerTypes**: Array of all possible handler types (for DI registration)

### Accessing Request Content
Expand Down Expand Up @@ -75,7 +77,9 @@ registry.Register<ProcessOrder>((request, context) =>

### Returning Multiple Handlers

Agreement Dispatcher can return multiple handlers, but it must still obey the rule that `Send` expects a single handler and `Publish` can have zero-to-many handlers. If you return multiple handlers in a `Send` request pipeline, Brighter will throw an exception.
Agreement Dispatcher can return multiple handlers, but it must still obey the rule that `Send` expects a single handler and `Publish` can have zero-to-many handlers. If you return multiple handlers in a `Send` request pipeline, Brighter throws an `ArgumentException`, so a route that returns more than one handler belongs to an event.

`Publish` runs the handlers you return concurrently, in no particular order, and one handler failing does not stop the others. Return handlers that are independent of each other; the list is not a sequence, so it cannot express "validate, then finalize". For steps that must run in order, use one handler and its [pipeline](/contents/BuildingAPipeline.md).

## Synchronous and Asynchronous Registration

Expand Down Expand Up @@ -171,6 +175,12 @@ This provides powerful, flexible routing:
Here's a complete example showing Agreement Dispatcher with multiple routing strategies:

```csharp
using System;
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
using Paramore.Brighter;
using Paramore.Brighter.Extensions.DependencyInjection;

public class Startup
{
public void ConfigureServices(IServiceCollection services)
Expand All @@ -194,6 +204,37 @@ public class Startup
},
[typeof(TaxCalculator2024), typeof(TaxCalculator2025)]);

// Content-based routing to several handlers: OrderPlaced is an event, so you Publish it,
// and each handler runs on its own, in no particular order
registry.Register<OrderPlaced>((request, context) =>
{
var order = request as OrderPlaced;
var handlers = new List<Type>
{
typeof(OrderConfirmationHandler) // Every order is confirmed
};

// High-value orders get a fraud check
if (order?.Total > 10000m)
handlers.Add(typeof(FraudCheckHandler));

// International orders need a customs declaration
if (order?.IsInternational == true)
handlers.Add(typeof(CustomsDeclarationHandler));

return handlers;
},
[
typeof(OrderConfirmationHandler),
typeof(FraudCheckHandler),
typeof(CustomsDeclarationHandler)
]);

// Standard routing for simple commands
registry.Register<SimpleCommand, SimpleCommandHandler>();
})
.AsyncHandlers(registry =>
{
// Country-based routing for payments
registry.RegisterAsync<ProcessPayment>((request, context) =>
{
Expand All @@ -213,37 +254,6 @@ public class Startup
typeof(LocalPaymentHandlerAsync),
typeof(InternationalPaymentHandlerAsync)
]);

// Content-based routing with multiple handlers
registry.Register<ProcessOrder>((request, context) =>
{
var order = request as ProcessOrder;
var handlers = new List<Type>
{
typeof(ValidateOrderHandler) // Always validate
};

// High-value orders get fraud check
if (order?.Total > 10000m)
handlers.Add(typeof(FraudCheckHandler));

// International orders need approval
if (order?.IsInternational == true)
handlers.Add(typeof(ApprovalHandler));

handlers.Add(typeof(FinalizeOrderHandler)); // Always finalize

return handlers;
},
[
typeof(ValidateOrderHandler),
typeof(FraudCheckHandler),
typeof(ApprovalHandler),
typeof(FinalizeOrderHandler)
]);

// Standard routing for simple commands
registry.Register<SimpleCommand, SimpleCommandHandler>();
});
}
}
Expand Down Expand Up @@ -283,6 +293,8 @@ registry.Register<MyCommand>((request, context) =>
Handle unexpected cases gracefully:

```csharp
using System;

registry.Register<ProcessOrder>((request, context) =>
{
var order = request as ProcessOrder;
Expand Down Expand Up @@ -317,8 +329,14 @@ registry.RegisterAsync<ProcessPayment>((request, context) =>
{
var payment = request as ProcessPayment;
// ...
return [typeof(InternationalPaymentHandlerAsync)];
},
[/* handlers */]);
[
typeof(StripePaymentHandlerAsync),
typeof(PayPalPaymentHandlerAsync),
typeof(LocalPaymentHandlerAsync),
typeof(InternationalPaymentHandlerAsync)
]);
```

### 4. Use Standard Routing When Possible
Expand All @@ -333,8 +351,10 @@ registry.Register<SimpleCommand, SimpleCommandHandler>();
registry.Register<ComplexCommand>((request, context) =>
{
// Dynamic routing based on content
// ...
return [typeof(ComplexCommandHandler)];
},
[/* handlers */]);
[typeof(ComplexCommandHandler)]);
```

### 5. List All Possible Handlers
Expand All @@ -343,7 +363,11 @@ Always provide the complete list of handler types:

```csharp
// Good - Complete list
registry.Register<MyCommand>((request, context) => { /* ... */ },
registry.Register<MyCommand>((request, context) =>
{
// ...
return [typeof(Handler1)];
},
[
typeof(Handler1),
typeof(Handler2),
Expand Down
2 changes: 1 addition & 1 deletion contents/AgreementDispatcherRouting.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ services.AddBrighter(options => { })

- Flexible routing based on content
- Can change behavior over time
- Supports multiple handlers
- Chooses among several handlers: for a `Send` the route returns exactly one, or `Send` throws; for a `Publish` it may return several, and each runs
- Access to request context
- Registered explicitly, with `.Handlers()`
- `AutoFromAssemblies()` must be told to skip the route's handlers
Expand Down
64 changes: 41 additions & 23 deletions contents/AsyncDispatchARequest.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,33 @@ Once you have [implemented your async Request Handler](/contents/ImplementingAsy
In the following example code we register a handler, create a command processor, and then use that command processor to send a command to the handler asynchronously.


``` csharp
public class Program
```csharp
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Paramore.Brighter;
using Paramore.Brighter.Extensions.DependencyInjection;

public class Program
{
private static async Task Main(string[] args)
{
private static async Task Main(string[] args)
{
var host = Host.CreateDefaultBuilder()
.ConfigureServices((hostContext, services) =>

{
services.AddBrighter()
.AutoFromAssemblies();
}
)
.UseConsoleLifetime()
.Build();

var commandProcessor = host.Services.GetService<IAmACommandProcessor>();

await commandProcessor.SendAsync(new GreetingCommand("Ian"));

await host.RunAsync();
}
var host = Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
services.AddBrighter()
.AutoFromAssemblies();
})
.UseConsoleLifetime()
.Build();

var commandProcessor = host.Services.GetRequiredService<IAmACommandProcessor>();

await commandProcessor.SendAsync(new GreetingCommand("Ian"));

await host.RunAsync();
}
}
```

## Registering an Async Handler
Expand Down Expand Up @@ -98,8 +103,21 @@ A handler exposes the parameter you supply via the property **ContinueOnCapture

You should pass this value via **ConfigureAwait** if you need to be able to support making this choice at the call site. For example, when you call the base handler in your return statement, to ensure that the decision as to whether to use the scheduler or the context flows down the pipeline.

``` csharp
return await base.HandleAsync(command, ct).ConfigureAwait(ContinueOnCapturedContext);
```csharp
using System;
using System.Threading;
using System.Threading.Tasks;
using Paramore.Brighter;

public class GreetingCommandHandlerAsync : RequestHandlerAsync<GreetingCommand>
{
public override async Task<GreetingCommand> HandleAsync(GreetingCommand command, CancellationToken ct = default)
{
Console.WriteLine("Hello from the handler");

return await base.HandleAsync(command, ct).ConfigureAwait(ContinueOnCapturedContext);
}
}
```

You can ignore this, if you want to default to using the Task Scheduler.
Expand Down
4 changes: 2 additions & 2 deletions contents/BrighterInboxSupport.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,9 @@ Your inbox is configured as part of the Brighter extensions to `IServiceCollecti

### Global Inbox Configuration in a Consumer-Only Application

In Brighter 10.7.0, the `InboxConfiguration` you set in `AddConsumers` reaches your handlers' pipelines only when the application also calls `AddProducers`. In an application that only consumes, Brighter adds no Inbox to any handler, so a duplicate is handled again, with no error. This is [BrighterCommand/Brighter#4335](https://github.com/BrighterCommand/Brighter/issues/4335); the fix is on Brighter's development branch and in no release yet.
From Brighter 10.8.0, the `InboxConfiguration` you set in `AddConsumers` reaches your handlers' pipelines whether or not the application also calls `AddProducers`. At 10.7.0 it did so only alongside `AddProducers`: in an application that only consumed, Brighter added no Inbox to any handler, so a duplicate was handled again, with no error ([BrighterCommand/Brighter#4335](https://github.com/BrighterCommand/Brighter/issues/4335)).

Until a release carries it, put the attribute on each handler that must not see a duplicate, as in [Adding an Inbox to a Handler](#adding-an-inbox-to-a-handler). The attribute takes effect whether or not the application registers producers. Its own `onceOnlyAction` decides what a duplicate does, whatever `actionOnExists` the configuration sets, so give it the action you want. An application that calls `AddProducers` gets the global Inbox as configured.
On 10.7.0, put the attribute on each handler that must not see a duplicate, as in [Adding an Inbox to a Handler](#adding-an-inbox-to-a-handler). The attribute takes effect whether or not the application registers producers. Its own `onceOnlyAction` decides what a duplicate does, whatever `actionOnExists` the configuration sets, so give it the action you want.

### Provisioning the Inbox Table

Expand Down
4 changes: 2 additions & 2 deletions contents/BrighterOutboxSupport.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ When creating your **CommandProcessor** you can set an outbox bulk chunk size, i

Brighter has the functionality to allow the **Outbox** to participate in the database transactions of your application so that you can ensure that distributed requests will be persisted (or fail to persist) inline with application changes.

To have the Brighter **Outbox** participate in Database transactions your command handler must take a dependency on an **IAmATransactionConnectionProvider**. The provider will be used when **CommandProcessor.DepositPost** is called and if there is an active transaction the **Outbox** will participate in that transaction. You use the **IAmATransactionConnectionProvider** to create the transaction that you use to interact with the Db; you also use it to grab any connection to the Db that you need.
To have the Brighter **Outbox** participate in a relational database's transactions your command handler takes a dependency on an **IAmATransactionConnectionProvider**. (The non-relational stores have their own: see [DynamoDB Outbox](/contents/DynamoOutbox.md) and [MongoDB Outbox](/contents/MongoDBOutbox.md#using-the-outbox-in-handlers).) The provider will be used when **CommandProcessor.DepositPost** is called and if there is an active transaction the **Outbox** will participate in that transaction. You use the **IAmATransactionConnectionProvider** to create the transaction that you use to interact with the Db; you also use it to grab any connection to the Db that you need.

Below is an fragment using Dapper

Expand All @@ -83,7 +83,7 @@ public AddGreetingHandlerAsync(IAmATransactionConnectionProvider transactionProv

public override async Task<AddGreeting> HandleAsync(AddGreeting addGreeting, CancellationToken cancellationToken = default)
{
var posts = new List<Guid>();
var posts = new List<Id>();

// The transaction provider (unit of work) gives us a connection and transaction
// that the Outbox can share 'behind the scenes'.
Expand Down
7 changes: 5 additions & 2 deletions contents/CloudEventsSupport.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,10 +219,13 @@ CloudEvents includes extensions for distributed tracing that integrate with Open
Brighter automatically propagates OpenTelemetry trace context using CloudEvents distributed tracing extensions. See [OpenTelemetry Integration](Telemetry.md) for details.

```csharp
using Paramore.Brighter;

public override MyCommand Handle(MyCommand command)
{
// Access the current span from context
Context.Span.SetAttribute("custom.attribute", "value");
// Access the current span, a System.Diagnostics.Activity, from context;
// OpenTelemetry exports its tags as span attributes
Context?.Span?.SetTag("custom.attribute", "value");

// Trace context is automatically propagated via CloudEvents headers
// when you publish/post messages
Expand Down
2 changes: 1 addition & 1 deletion contents/Compression.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ We provide a matching **UnwrapWithAttribute** of **Decompress** that will use th

In this example, we look for a GZip compressed body and if we find it, decompress the body.

**At 10.7.0, Decompress does not recognise a body compressed on the async path.** `WrapAsync` — which `PostAsync` and `DepositPostAsync` use — sets the *Content Type* to `application/gzip; charset=utf-8`, and **Decompress** compares the whole *Content Type* with `application/gzip`, so the message reaches your mapper still compressed. The same holds for Zlib and Brotli. A body compressed by the synchronous `Post` path round-trips. This is [BrighterCommand/Brighter#4432](https://github.com/BrighterCommand/Brighter/issues/4432).
**Before Brighter 10.8.0, Decompress did not recognise a body compressed on the async path.** `WrapAsync` — which `PostAsync` and `DepositPostAsync` use — sets the *Content Type* to `application/gzip; charset=utf-8`, and at 10.7.0 **Decompress** compared the whole *Content Type* with `application/gzip`, so the message reached your mapper still compressed. The same held for Zlib and Brotli. From 10.8.0 it compares the media type alone, and both paths round-trip ([BrighterCommand/Brighter#4432](https://github.com/BrighterCommand/Brighter/issues/4432)). On 10.7.0, compress on the synchronous `Post` path.

```csharp
using System.Text.Json;
Expand Down
8 changes: 4 additions & 4 deletions contents/ConfiguringOpenTelemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,17 @@ The OpenTelemetry SDK can be configured to listen to Activities emitted by Brigh
Brighter emits traces using the following Activity Source:

- **Source Name**: `Paramore.Brighter`. OpenTelemetry matches source names without regard to case, so `paramore.brighter` also works
- **Version**: The `Paramore.Brighter` assembly's version, which is `10.0.0.0` at package version 10.7.0
- **Version**: The `Paramore.Brighter` assembly's version, which is `10.0.0.0` at package version 10.8.0

### Registering Brighter's Tracer

Brighter writes to that source only through a tracer, an `IAmABrighterTracer`, registered in your
container. `AddBrighter()` does not register one, so listening to the source is not enough:
`AddSource("paramore.brighter")` on its own records no Brighter span.
`AddBrighterInstrumentation()`, from the `Paramore.Brighter.Extensions.Diagnostics` package, does
both: it registers the tracer and adds the source. The Command Processor takes that tracer only when
it has an external bus, configured with `AddProducers`; with no producers, its requests record no
span at 10.7.0. This is reported as [#4510](https://github.com/BrighterCommand/Brighter/issues/4510).
both: it registers the tracer and adds the source. From Brighter 10.8.0 the Command Processor takes
that tracer whether or not it has an external bus; at 10.7.0 it took it only with `AddProducers`, so
with no producers its requests recorded no span ([#4510](https://github.com/BrighterCommand/Brighter/issues/4510)).

Use it on the tracer provider that `AddOpenTelemetry()` builds, which shares your application's
container. A provider built with `Sdk.CreateTracerProviderBuilder()` keeps its own services, so the
Expand Down
2 changes: 1 addition & 1 deletion contents/DispatcherConfigurationReference.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ To configure our *Inbox* we set the **InboxConfiguration** property on the optio

For *Inbox Configuration* you pass the following arguments, each of which is exposed as a property:

* **ActionOnExists**: What do we do if the request has been handled? The default,**OnceOnlyAction.Throw** is to throw a **OnceOnlyException**. If you take no other action this will cause the message to be rejected and sent to a DLQ if one is configured (See [Handler Failure](/contents/HandlerFailure.md)). The alternative is **OnceOnlyAction.Warn** simply logs that the request is a duplicate, but takes no other action. A third option, **OnceOnlyAction.Replay**, is **not in a released package yet**: it ships after Brighter 10.7.0. It also skips the handler but resends the messages that handler produced the first time it ran — it has prerequisites, so see [Replay On Seen](/contents/ReplayOnSeen.md) before you choose it.
* **ActionOnExists**: What do we do if the request has been handled? The default,**OnceOnlyAction.Throw** is to throw a **OnceOnlyException**. If you take no other action this will cause the message to be rejected and sent to a DLQ if one is configured (See [Handler Failure](/contents/HandlerFailure.md)). The alternative is **OnceOnlyAction.Warn** simply logs that the request is a duplicate, but takes no other action. A third option, **OnceOnlyAction.Replay**, requires Brighter 10.8.0 or later. It also skips the handler but resends the messages that handler produced the first time it ran — it has prerequisites, so see [Replay On Seen](/contents/ReplayOnSeen.md) before you choose it.
* **OnceOnly**: This defaults to *true* and will check for a duplicate and take the action indicated by **ActionOnExists**. If *false* the *Inbox* will record the request, but will take no further action. (This tends to be set to *false* if you are using the *Inbox* to record what requests caused current state only and not de-duplicate).
* **Scope**: This indicates the type of request (*Command* or *Event*) to store in the *Inbox*. By default this is set to **InboxScope.All** and captures everything but you can be explicit and just capture **InboxScope.Commands** or **InboxScope.Events**. (This tends to be set to **InboxScope.Commands** when only commands cause changes to state that are not idempotent).
* **Context**: Used to uniquely identify receipt of this request via this handler. If you are recording *Events* and have multiple handlers, then the first event handler to receive the message will block the others from doing so, unless you disambiguate the handler identity by supplying a context method.
Expand Down
Loading
Loading