> For the complete documentation index, see [llms.txt](https://docs.radicalframework.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.radicalframework.com/upgrade-guides/async-message-broker.md).

# Radical 2.x to Radical 3.0: the async-only message broker

Starting with Radical 3.0.0, the message broker is async-only: subscribers, standalone handlers and publishers all use `Task`-based APIs. This guide applies to every application using the Radical message broker. WPF and Avalonia applications also need the changes in [the async-only message broker in WPF and Avalonia applications](/upgrade-guides/async-message-broker/async-message-broker-ui.md).

## API changes

| Radical 2.x                                                                                  | Radical 3.0                                                                                      |
| -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `Subscribe<T>(subscriber, Action<object, T> callback)`                                       | `Subscribe<T>(subscriber, Func<object, T, Task> callback)`                                       |
| `Subscribe<T>(subscriber, Func<object, T, bool> callbackFilter, Action<object, T> callback)` | `Subscribe<T>(subscriber, Func<object, T, bool> callbackFilter, Func<object, T, Task> callback)` |
| `Dispatch(sender, message)`                                                                  | `Task DispatchAsync(sender, message)`                                                            |
| `Broadcast(sender, message)`                                                                 | `Task BroadcastAsync(sender, message)`, which already existed                                    |
| `IHandleMessage<T>.Handle(object sender, T message)`                                         | `Task HandleAsync(object sender, T message)`                                                     |
| `AbstractMessageHandler<T>.Handle(object sender, T message)`                                 | `Task HandleAsync(object sender, T message)`                                                     |

Everything else is unchanged:

* the overloads with a sender filter, an `InvocationModel` or a message type
* `callbackFilter`, which remains synchronous: filters should be cheap and side-effect free
* `Unsubscribe`
* the [inheritance support](/concepts/message-broker.md#inheritance-support)

## Subscribers

Subscription callbacks return a `Task`. Callbacks that do async work become `async` lambdas:

```csharp
// before
broker.Subscribe<CustomerSelected>(this, (sender, message) =>
{
    Customer = repository.Load(message.CustomerId);
});

// after
broker.Subscribe<CustomerSelected>(this, async (sender, message) =>
{
    Customer = await repository.LoadAsync(message.CustomerId);
});
```

Callbacks with nothing to await return a completed task:

```csharp
broker.Subscribe<CustomerSelected>(this, (sender, message) =>
{
    SelectedCustomerId = message.CustomerId;
    return Task.CompletedTask;
});
```

Don't keep a callback synchronous by making it `async void`: the broker can't observe its exceptions or wait for it to complete.

## Standalone message handlers

[Standalone message handlers](/concepts/message-broker/abstract-message-handler.md) implement `HandleAsync` instead of `Handle`:

```csharp
// before
class MyMessageHandler : AbstractMessageHandler<MyMessage>
{
    public override void Handle(object sender, MyMessage message)
    {
        //handle my message here.
    }
}

// after
class MyMessageHandler : AbstractMessageHandler<MyMessage>
{
    public override async Task HandleAsync(object sender, MyMessage message)
    {
        //handle my message here, awaiting what needs to be awaited.
    }
}
```

Handlers implementing `IHandleMessage<T>` directly change in the same way. `ShouldHandle` remains synchronous.

## Publishers

### Dispatch

`Dispatch` invoked the subscribers on the publisher's thread, one at a time, and returned once all of them completed. `DispatchAsync` keeps those semantics, awaiting each subscriber in turn, so the publisher awaits it:

```csharp
// before
public void Save()
{
    broker.Dispatch(this, new CustomerSaving(customer));
    repository.Save(customer);
}

// after
public async Task SaveAsync()
{
    await broker.DispatchAsync(this, new CustomerSaving(customer));
    await repository.SaveAsync(customer);
}
```

The method calling `DispatchAsync` becomes `async`, and so do its callers. In a UI application this usually stops at a command handler; see [commands](/upgrade-guides/async-message-broker/async-message-broker-ui.md#commands).

Code that reads the message after dispatching it, for example a `Cancel` flag set by the subscribers, keeps working, as long as it awaits `DispatchAsync` before reading.

### Broadcast

`Broadcast` was already asynchronous: it invoked the subscribers in parallel, on thread pool threads, without waiting for them. `BroadcastAsync` does the same, and its task completes once all the subscribers completed:

```csharp
// before
broker.Broadcast(this, new CustomerSaved(customer));

// after: wait for the subscribers, observing their exceptions
await broker.BroadcastAsync(this, new CustomerSaved(customer));

// after: fire-and-forget, as Broadcast did
_ = broker.BroadcastAsync(this, new CustomerSaved(customer));
```

With fire-and-forget, nothing observes the subscribers' exceptions: subscribers must handle their own errors.

Keep using `DispatchAsync` where the code used `Dispatch`, and `BroadcastAsync` where it used `Broadcast`: the two differ in more than waiting, as described in [dispatching or broadcasting](/concepts/message-broker.md#dispatching-or-broadcasting).

## Never block on the broker

Don't wait for the broker's tasks synchronously:

```csharp
// don't
broker.DispatchAsync(this, message).Wait();
var result = broker.DispatchAsync(this, message).Result;
broker.DispatchAsync(this, message).GetAwaiter().GetResult();
```

On a UI thread, with subscribers marshalled to the UI thread (`InvocationModel.Safe`), this deadlocks: the UI thread waits for a subscriber that needs the UI thread to run. That's the sync-over-async design flaw the async-only broker removes. Always `await`, all the way up to the command or event handler that started the operation.

## How the old API fails to compile

The synchronous API isn't removed right away: it's obsolete with an error in Radical 3.0.0, and removed in 4.0.0. The compiler therefore finds every place that needs to change.

Code that calls the old API gets error CS0619 with a message pointing to this guide:

* `Dispatch` and `Broadcast` are obsolete with an error.
* The `Subscribe` overloads taking an `Action<object, T>` callback are obsolete with an error. The compiler picks the overload from the shape of the lambda: a callback that returns nothing binds to the obsolete overload and fails, while an `async` lambda, or one returning a `Task`, binds to the new overload and compiles.

If code compiled against the old API still reaches the obsolete members at runtime, through reflection for example, they throw a `NotSupportedException`. `Dispatch` can't keep working, because it would have to block on asynchronous subscribers.

Code that implements or overrides the old API doesn't get obsolete errors: the compiler reports them only where a member is used. Instead, the types change so that the old code fails with standard compiler errors, without a custom message:

| Code                                                                  | Change                                                              | Compiler errors                                                                             |
| --------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| Classes inheriting from `AbstractMessageHandler<T>`                   | `HandleAsync` is abstract, and `Handle` is no longer virtual        | CS0534, the class doesn't implement `HandleAsync`, and CS0506, `Handle` can't be overridden |
| Classes implementing `IHandleMessage<T>` or `IHandleMessage` directly | `Handle` is replaced by `HandleAsync`                               | CS0535, the class doesn't implement `HandleAsync`                                           |
| Classes implementing `IMessageBroker`, such as test fakes             | `DispatchAsync` is added, and the obsolete members stay until 4.0.0 | CS0535, the class doesn't implement `DispatchAsync`                                         |

Fix those errors as described in [standalone message handlers](#standalone-message-handlers).

## Upgrade checklist

* [ ] Every `Subscribe` callback returns a `Task`, and none is `async void`.
* [ ] Every `AbstractMessageHandler<T>` and `IHandleMessage<T>` implements `HandleAsync`.
* [ ] Every `Dispatch` becomes `await DispatchAsync`, and its callers are `async`.
* [ ] Every `Broadcast` becomes `await BroadcastAsync`, or a deliberate fire-and-forget `_ = BroadcastAsync`.
* [ ] No `.Wait()`, `.Result` or `.GetAwaiter().GetResult()` on broker tasks.
* [ ] WPF and Avalonia applications: [the UI-specific changes](/upgrade-guides/async-message-broker/async-message-broker-ui.md).
