Prism
async programming
event aggregator
C#
software development

How to call async methods from within a Prism event aggregator subscriber?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Prism's EventAggregator uses synchronous Action<T> delegates for subscriptions, which means you cannot directly pass an async method as a subscriber. Calling async void handlers works but swallows exceptions silently and creates fire-and-forget behavior. The proper approach is to subscribe with a synchronous handler that safely invokes your async logic, using patterns like async void with explicit error handling, or bridging to a Task that you track and await elsewhere.

The Problem

Prism's PubSubEvent<T> expects an Action<T> subscriber, not a Func<T, Task>:

csharp
1// Prism event definition
2public class OrderPlacedEvent : PubSubEvent<Order> { }
3
4// This compiles but is async void — dangerous
5eventAggregator.GetEvent<OrderPlacedEvent>()
6    .Subscribe(async order => await ProcessOrderAsync(order));
7// The lambda is implicitly async void — exceptions are unobserved

The Subscribe method signature is Subscribe(Action<T> action). When you pass an async lambda, the compiler converts it to async void, which means:

  • Exceptions thrown inside are unobserved and crash the application
  • The caller has no way to know when the async work completes
  • No backpressure — events fire faster than handlers can process

Solution 1: Async Void with Explicit Error Handling

The simplest approach — use async void but wrap the entire body in try/catch:

csharp
1eventAggregator.GetEvent<OrderPlacedEvent>()
2    .Subscribe(async order =>
3    {
4        try
5        {
6            await ProcessOrderAsync(order);
7        }
8        catch (Exception ex)
9        {
10            logger.LogError(ex, "Failed to process order {OrderId}", order.Id);
11        }
12    });

This is the most pragmatic approach for event handlers where you do not need to await the result. The try/catch prevents unobserved exceptions from crashing the app.

Solution 2: Helper Extension Method

Create a reusable extension to subscribe with async handlers safely:

csharp
1public static class PubSubEventExtensions
2{
3    public static SubscriptionToken SubscribeAsync<T>(
4        this PubSubEvent<T> pubSubEvent,
5        Func<T, Task> asyncAction,
6        ThreadOption threadOption = ThreadOption.PublisherThread,
7        bool keepSubscriberReferenceAlive = false,
8        Predicate<T>? filter = null)
9    {
10        return pubSubEvent.Subscribe(
11            payload =>
12            {
13                _ = Task.Run(async () =>
14                {
15                    try
16                    {
17                        await asyncAction(payload);
18                    }
19                    catch (Exception ex)
20                    {
21                        Debug.WriteLine($"Async subscriber error: {ex}");
22                        // Log or handle as needed
23                    }
24                });
25            },
26            threadOption,
27            keepSubscriberReferenceAlive,
28            filter);
29    }
30}

Usage:

csharp
1eventAggregator.GetEvent<OrderPlacedEvent>()
2    .SubscribeAsync(async order =>
3    {
4        await ProcessOrderAsync(order);
5        await NotifyCustomerAsync(order);
6    });

Solution 3: UI Thread with Dispatcher

When the async work needs to update the UI (WPF/Xamarin), subscribe on the UI thread:

csharp
1eventAggregator.GetEvent<DataLoadedEvent>()
2    .Subscribe(async data =>
3    {
4        try
5        {
6            // This runs on the UI thread
7            IsLoading = true;
8
9            var result = await LoadDetailsAsync(data);
10
11            // Back on UI thread after await (thanks to SynchronizationContext)
12            Items = new ObservableCollection<Item>(result);
13            IsLoading = false;
14        }
15        catch (Exception ex)
16        {
17            ErrorMessage = ex.Message;
18            IsLoading = false;
19        }
20    }, ThreadOption.UIThread);

ThreadOption.UIThread ensures the subscriber runs on the UI thread, and await returns to the UI thread because the SynchronizationContext is captured.

Solution 4: Command-Based Approach

For ViewModels, use Prism's DelegateCommand which has built-in async support:

csharp
1public class OrderViewModel : BindableBase
2{
3    private readonly IEventAggregator _eventAggregator;
4
5    public OrderViewModel(IEventAggregator eventAggregator)
6    {
7        _eventAggregator = eventAggregator;
8
9        _eventAggregator.GetEvent<OrderPlacedEvent>()
10            .Subscribe(OnOrderPlaced);
11    }
12
13    private void OnOrderPlaced(Order order)
14    {
15        // Trigger async work through a command or service
16        ProcessOrderCommand.Execute(order);
17    }
18
19    public DelegateCommand<Order> ProcessOrderCommand => new DelegateCommand<Order>(
20        async (order) =>
21        {
22            await _orderService.ProcessAsync(order);
23            await _notificationService.SendAsync(order);
24        });
25}

Thread Safety Considerations

csharp
1// BAD: multiple events can fire while async work is in progress
2eventAggregator.GetEvent<DataChangedEvent>()
3    .Subscribe(async data =>
4    {
5        await SaveDataAsync(data);  // Takes 2 seconds
6        // Another event fires at 1 second — two saves run concurrently
7    });
8
9// BETTER: use SemaphoreSlim for sequential processing
10private readonly SemaphoreSlim _semaphore = new(1, 1);
11
12eventAggregator.GetEvent<DataChangedEvent>()
13    .Subscribe(async data =>
14    {
15        await _semaphore.WaitAsync();
16        try
17        {
18            await SaveDataAsync(data);
19        }
20        finally
21        {
22            _semaphore.Release();
23        }
24    });

Common Pitfalls

  • Unobserved exceptions in async void: async void methods do not propagate exceptions to the caller. An unhandled exception in an async void subscriber crashes the application. Always wrap the body in try/catch.
  • Deadlocks from .Result or .Wait(): Calling task.Result or task.Wait() inside a subscriber that runs on the UI thread causes a deadlock because the UI thread is blocked waiting for a task that needs the UI thread to complete. Use await instead.
  • Forgetting to unsubscribe: Async subscribers hold references that prevent garbage collection. Store the SubscriptionToken and call Unsubscribe(token) or use keepSubscriberReferenceAlive: false to allow GC cleanup.
  • Concurrent event handling: Events can fire faster than async handlers complete. Without synchronization (like SemaphoreSlim), multiple handlers run concurrently on the same shared state, causing race conditions.
  • Subscribing on the wrong thread: ThreadOption.PublisherThread runs the handler on whatever thread published the event. If your handler updates UI elements, use ThreadOption.UIThread to avoid cross-thread access exceptions.

Summary

  • Prism's Subscribe takes Action<T>, so async handlers become async void implicitly
  • Always wrap async void handlers in try/catch to prevent unobserved exceptions
  • Create a SubscribeAsync extension method for reusable, safe async subscriptions
  • Use ThreadOption.UIThread when the handler updates UI-bound properties
  • Use SemaphoreSlim to serialize concurrent async event handlers
  • Store SubscriptionToken and unsubscribe when the subscriber is no longer needed

Course illustration
Course illustration

All Rights Reserved.