Building a Live Event Bus for .NET MAUI
π‘ Building a Live Event Bus for .NET MAUI
Designing Decoupled, Asynchronous, Observable, and Memory-Safe Communication Between Application Components
π§ Introduction
Modern .NET MAUI applications rarely consist of a single page, a single ViewModel, or a small collection of isolated services. As applications grow, different components need to communicate continuously:
- A synchronization service downloads new information.
- A ViewModel needs to refresh its data.
- A background operation reports progress.
- An authentication service announces that the session expired.
- A connectivity monitor reports that the device went offline.
- A SignalR client receives a real-time notification.
- A platform-specific service publishes a native lifecycle event.
- Multiple pages need to respond to the same state change.
The simplest solution is often to inject one component directly into another and call a method:
_dashboardViewModel.Refresh();
_notificationsViewModel.Add(notification);
_headerViewModel.UpdateBadge();
This works initially, but it creates tight coupling. The publisher must know:
- Which components are interested.
- How those components are created.
- Which methods they expose.
- Whether they are still alive.
- Which thread they expect.
- How failures should be handled.
As the number of subscribers increases, the architecture gradually becomes harder to maintain.
SynchronizationService
βββ DashboardViewModel
βββ OrdersViewModel
βββ NotificationsViewModel
βββ HeaderViewModel
βββ AnalyticsService
A better approach is to introduce an intermediary responsible for delivering messages between components without forcing them to reference each other directly.
Publisher
β
βΌ
Event Bus
β
βββ Subscriber A
βββ Subscriber B
βββ Subscriber C
βββ Subscriber D
The publisher only knows that an event occurred. Subscribers decide whether they are interested. This pattern is known as an Event Bus.
In this guide, we will design a production-oriented Live Event Bus for .NET MAUI featuring: β
Strongly typed events
β
Asynchronous publishing
β
Weak and strong subscriptions
β
UI-thread dispatching
β
Subscriber priorities
β
Event filtering
β
Sticky events
β
Cancellation support
β
Error isolation
β
Runtime diagnostics
β
Event history and replay
β
Dependency Injection integration
β
Memory-safe subscription lifecycles
The goal is not simply to recreate a basic messenger. It is to design a complete event communication layer suitable for large, modular, and real-time .NET MAUI applications.
π§ What Is an Event Bus?
An Event Bus is an intermediary that enables one component to publish an event without knowing which components will receive it. A traditional direct call looks like this:
OrderService
β
βΌ
OrdersViewModel.Refresh()
With an Event Bus, the flow becomes:
OrderService
β
βΌ
OrderCreatedEvent
β
βΌ
Event Bus
β
βββ OrdersViewModel
βββ DashboardViewModel
βββ AnalyticsService
βββ NotificationService
The OrderService does not know about any subscriber. It only publishes an immutable message:
public sealed record OrderCreatedEvent(
Guid OrderId,
string CustomerName,
decimal Total,
DateTimeOffset CreatedAt);
Any component interested in that event can subscribe independently.
π Direct Communication vs Event-Driven Communication
| Characteristic | Direct Calls | Event Bus | | --- | --- | --- | | Coupling | High | Low | | Publisher knows subscribers | Yes | No | | Multiple subscribers | Manual coordination | Native | | Extensibility | Limited | High | | Testing | Increasingly difficult | Easier with abstractions | | Cross-module communication | Fragile | Centralized | | Runtime diagnostics | Usually absent | Can be built in | | Subscription lifecycle | Ad hoc | Standardized | | Event replay | Not available | Possible | | Error isolation | Manual | Centralized | An Event Bus is especially valuable when a single domain event affects several independent areas of the application.
π± Why .NET MAUI Applications Benefit from an Event Bus
Cross-platform applications contain communication boundaries that are not always present in simpler backend systems. A MAUI application may include:
ββββββββββββββββββββββββββββββββββββββββββββ
β .NET MAUI App β
ββββββββββββββββββββββββββββββββββββββββββββ€
β Pages and Views β
β ViewModels β
β Domain Services β
β REST and SignalR Clients β
β SQLite Repositories β
β Background Synchronization β
β Device and Connectivity Services β
β Android / iOS / Windows Implementations β
ββββββββββββββββββββββββββββββββββββββββββββ
These layers frequently need to communicate without introducing circular dependencies. Typical examples include:
- πΆ Connectivity changes
- π Authentication expiration
- π Synchronization completion
- π₯ Push notification arrival
- π‘ SignalR messages
- π Shopping cart changes
- π Culture or language changes
- π¨ Theme changes
- π Location updates
- πΎ Local database mutations
- β οΈ Application-wide errors
An Event Bus provides a common communication contract across all these layers.
ποΈ High-Level Architecture
A production-ready Event Bus should do more than store delegates in a dictionary. A scalable architecture can be organized like this:
ββββββββββββββββββββββββββββββββββββββββββββ
β .NET MAUI App β
ββββββββββββββββββββββββββββββββββββββββββββ€
β Pages and Views β
β ViewModels β
β Domain Services β
β REST and SignalR Clients β
β SQLite Repositories β
β Background Synchronization β
β Device and Connectivity Services β
β Android / iOS / Windows Implementations β
ββββββββββββββββββββββββββββββββββββββββββββ
This architecture separates four responsibilities:
- Registration β determine who receives each event.
- Dispatching β determine how and where handlers execute.
- Lifecycle β prevent stale subscriptions and memory leaks.
- Diagnostics β observe event delivery, failures, and performance.
π¦ Defining Strongly Typed Events
Events should be represented as immutable types. A marker interface can make event intent explicit:
public interface IAppEvent
{
}
Then define individual event records:
public sealed record ConnectivityChangedEvent(
bool IsConnected,
string? ConnectionProfile,
DateTimeOffset OccurredAt) : IAppEvent;
public sealed record UserSessionExpiredEvent(
string Reason,
DateTimeOffset ExpiredAt) : IAppEvent;
public sealed record SynchronizationCompletedEvent(
int UploadedItems,
int DownloadedItems,
TimeSpan Duration) : IAppEvent;
Using strongly typed event records provides several advantages: β
Compile-time validation
β
IntelliSense support
β
Refactoring safety
β
Immutable payloads
β
Clear domain language
β
No magic strings
β
Easier unit testing
Avoid string-based APIs such as:
eventBus.Publish("SyncCompleted", payload);
A typo would remain invisible until runtime:
eventBus.Publish("SynCompleted", payload);
Strong typing eliminates that entire class of errors.
π§ Defining the Core Contract
The public abstraction should remain small and predictable.
public interface IEventBus
{
ValueTask PublishAsync<TEvent>(
TEvent eventData,
PublishOptions? options = null,
CancellationToken cancellationToken = default)
where TEvent : IAppEvent;
IDisposable Subscribe<TEvent>(
Func<TEvent, CancellationToken, ValueTask> handler,
SubscriptionOptions? options = null)
where TEvent : IAppEvent;
bool HasSubscribers<TEvent>()
where TEvent : IAppEvent;
int GetSubscriberCount<TEvent>()
where TEvent : IAppEvent;
}
This contract supports:
- Asynchronous publishing
- Asynchronous handlers
- Cancellation
- Configurable subscriptions
- Explicit subscription disposal
- Runtime inspection
The publisher only needs PublishAsync. The subscriber receives an IDisposable token that controls its subscription lifetime.
βοΈ Subscription Options
Different subscribers may have different execution requirements.
public sealed class SubscriptionOptions
{
public string? Name { get; init; }
public int Priority { get; init; }
public bool UseWeakReference { get; init; } = true;
public EventDispatchThread DispatchThread { get; init; }
= EventDispatchThread.Current;
public Predicate<object>? Filter { get; init; }
public bool ReceiveStickyEvent { get; init; }
}
The dispatch target can be represented with an enum:
public enum EventDispatchThread
{
Current,
MainThread,
Background
}
This avoids forcing every event handler onto the same thread.
π€ Publishing Events
Publishing should remain simple.
await _eventBus.PublishAsync(
new SynchronizationCompletedEvent(
UploadedItems: 12,
DownloadedItems: 36,
Duration: elapsed),
cancellationToken: cancellationToken);
The publisher does not need to know:
- How many subscribers exist.
- Whether they are ViewModels or services.
- Whether they need the UI thread.
- Whether they execute sequentially or concurrently.
- Whether one subscriber fails.
- Whether the event is recorded for diagnostics.
Those concerns belong to the Event Bus.
π₯ Subscribing to Events
A ViewModel can subscribe through constructor injection:
public sealed class DashboardViewModel : IDisposable
{
private readonly IDisposable _syncSubscription;
public DashboardViewModel(IEventBus eventBus)
{
_syncSubscription =
eventBus.Subscribe<SynchronizationCompletedEvent>(
HandleSynchronizationCompletedAsync,
new SubscriptionOptions
{
Name = nameof(DashboardViewModel),
DispatchThread = EventDispatchThread.MainThread
});
}
private ValueTask HandleSynchronizationCompletedAsync(
SynchronizationCompletedEvent message,
CancellationToken cancellationToken)
{
LastSyncSummary =
$"{message.DownloadedItems} items downloaded";
return ValueTask.CompletedTask;
}
public string? LastSyncSummary { get; private set; }
public void Dispose()
{
_syncSubscription.Dispose();
}
}
The ViewModel never references the synchronization service directly.
The synchronization service never references the ViewModel. Both depend only on the event contract.
π§Ή Managing Subscription Lifecycles
Event subscriptions are a common source of memory leaks. Consider this sequence:
Page Created
β
ViewModel Subscribes
β
Page Removed
β
Event Bus Still Holds Delegate
β
ViewModel Cannot Be Collected
If the Event Bus stores strong references indefinitely, a page or ViewModel may remain alive after navigation. There are two common strategies:
Strong subscriptions
The Event Bus holds the subscriber until it explicitly unsubscribes.
Advantages:
- Predictable lifetime
- High dispatch performance
- Simple implementation
Risks:
- The subscriber must dispose correctly.
- Forgotten subscriptions may cause leaks.
Weak subscriptions
The Event Bus holds a WeakReference to the recipient. Advantages:
- Abandoned subscribers can be garbage-collected.
- Safer for transient pages and ViewModels.
Trade-offs:
- More complex implementation
- Additional runtime checks
- Dead references must be cleaned periodically
A robust implementation can support both.
| Subscriber Type | Recommended Strategy |
|---|---|
| Singleton service | Strong subscription |
| Application-wide manager | Strong subscription |
| Transient ViewModel | Weak or disposable subscription |
| Page | Weak or lifecycle-controlled subscription |
| Background worker | Strong with explicit shutdown |
| Temporary dialog | Weak subscription |
πͺͺ Subscription Tokens
A subscription token provides explicit ownership.
internal sealed class EventSubscriptionToken : IDisposable
{
private Action? _unsubscribe;
public EventSubscriptionToken(Action unsubscribe)
{
_unsubscribe = unsubscribe;
}
public void Dispose()
{
Interlocked.Exchange(ref _unsubscribe, null)?.Invoke();
}
}
When Dispose is called, the subscription is removed exactly once. This pattern works naturally with ViewModel and page lifecycles.
private IDisposable? _subscription;
public void Activate()
{
_subscription = _eventBus.Subscribe<OrderCreatedEvent>(
HandleOrderCreatedAsync);
}
public void Deactivate()
{
_subscription?.Dispose();
_subscription = null;
}
π§΅ UI Thread Dispatching
Event publishers may run on background threads. Examples include:
- HTTP responses
- SignalR callbacks
- Database workers
- Connectivity monitors
- Native platform callbacks
- Background synchronization
A ViewModel that updates UI-bound properties may need execution on the MAUI main thread. Instead of requiring every subscriber to write:
await MainThread.InvokeOnMainThreadAsync(...);
the Event Bus can centralize thread dispatching.
private static async ValueTask DispatchAsync<TEvent>(
EventSubscription<TEvent> subscription,
TEvent eventData,
CancellationToken cancellationToken)
where TEvent : IAppEvent
{
switch (subscription.DispatchThread)
{
case EventDispatchThread.MainThread:
await MainThread.InvokeOnMainThreadAsync(
async () =>
{
await subscription.Handler(
eventData,
cancellationToken);
});
break;
case EventDispatchThread.Background:
await Task.Run(
async () =>
{
await subscription.Handler(
eventData,
cancellationToken);
},
cancellationToken);
break;
default:
await subscription.Handler(
eventData,
cancellationToken);
break;
}
}
This keeps thread requirements declarative and consistent.
β‘ Sequential vs Parallel Dispatch
Suppose an event has five subscribers. Should they execute sequentially?
Subscriber A
β
Subscriber B
β
Subscriber C
Or concurrently?
βββ Subscriber A
Event βββΌββ Subscriber B
βββ Subscriber C
Both models are valid.
Sequential dispatch
Benefits:
- Predictable ordering
- Lower concurrency complexity
- Easier debugging
Trade-offs:
- One slow handler delays all others.
Parallel dispatch
Benefits:
- Lower total latency
- Better for independent subscribers
Trade-offs:
- Ordering is not guaranteed.
- Concurrency bugs become possible.
- Error aggregation is required.
Publishing options can expose this decision:
public sealed class PublishOptions
{
public EventDispatchMode DispatchMode { get; init; }
= EventDispatchMode.Sequential;
public bool ContinueOnSubscriberError { get; init; } = true;
public bool StoreAsSticky { get; init; }
public TimeSpan? Timeout { get; init; }
}
public enum EventDispatchMode
{
Sequential,
Parallel
}
Then publish in parallel when handlers are independent:
await _eventBus.PublishAsync(
new ApplicationThemeChangedEvent("Dark"),
new PublishOptions
{
DispatchMode = EventDispatchMode.Parallel
});
π― Subscriber Priorities
Some handlers must execute before others. For example:
1. Persist the event
2. Update the application state
3. Refresh the UI
4. Emit analytics
A priority value can control ordering:
eventBus.Subscribe<OrderCreatedEvent>(
PersistOrderAsync,
new SubscriptionOptions
{
Priority = 100
});
eventBus.Subscribe<OrderCreatedEvent>(
RefreshDashboardAsync,
new SubscriptionOptions
{
Priority = 50
});
eventBus.Subscribe<OrderCreatedEvent>(
TrackAnalyticsAsync,
new SubscriptionOptions
{
Priority = 10
});
Higher-priority subscribers execute first during sequential dispatch. Priority should be used carefully. Excessive reliance on ordering may indicate hidden coupling between subscribers.
π Event Filtering
Not every subscriber needs every instance of an event. A warehouse page may only care about inventory changes for one location.
_eventBus.Subscribe<InventoryChangedEvent>(
HandleInventoryChangedAsync,
new SubscriptionOptions
{
Filter = message =>
message is InventoryChangedEvent inventory &&
inventory.WarehouseId == _warehouseId
});
Without filtering, every subscriber would need to receive the event and discard irrelevant messages manually. Centralized filters reduce unnecessary work and make subscription intent clearer.
π Sticky Events
Some events represent the latest application state rather than a momentary action. Examples include:
- Connectivity state
- Current authenticated user
- Selected tenant
- Active language
- Current theme
- Synchronization status
Suppose connectivity changed before a page was created.
App Goes Offline
β
Connectivity Event Published
β
Page Created Later
A normal event is already gone. A sticky event stores the latest value and delivers it immediately to new subscribers.
await _eventBus.PublishAsync(
new ConnectivityChangedEvent(
IsConnected: false,
ConnectionProfile: null,
OccurredAt: DateTimeOffset.UtcNow),
new PublishOptions
{
StoreAsSticky = true
});
A later subscriber requests the latest value:
_eventBus.Subscribe<ConnectivityChangedEvent>(
HandleConnectivityChangedAsync,
new SubscriptionOptions
{
ReceiveStickyEvent = true
});
Sticky events are useful for state announcements, but they should not be used indiscriminately. A transient event such as ButtonClickedEvent should never be sticky.
π‘οΈ Error Isolation
One failing subscriber should not necessarily prevent other subscribers from receiving an event. Without isolation:
Subscriber A succeeds
β
Subscriber B throws
β
Subscriber C never executes
With isolation:
Subscriber A succeeds
Subscriber B fails β recorded
Subscriber C succeeds
Subscriber D succeeds
A dispatch result can capture individual failures:
public sealed record EventHandlerFailure(
string SubscriberName,
Exception Exception,
TimeSpan Duration);
public sealed record EventPublishResult(
Type EventType,
int SubscriberCount,
int SuccessfulHandlers,
IReadOnlyList<EventHandlerFailure> Failures,
TimeSpan Duration);
This gives the caller and diagnostics layer complete visibility without automatically crashing the event pipeline.
β±οΈ Handler Timeouts
A subscriber may become blocked indefinitely.
eventBus.Subscribe<DataUpdatedEvent>(
async (message, cancellationToken) =>
{
await SomeOperationThatNeverCompletes();
});
The Event Bus can apply a timeout per publication or subscription.
Handler Starts
β
Timeout Reached
β
Cancellation Requested
β
Failure Recorded
β
Remaining Subscribers Continue
Timeouts protect the pipeline from misbehaving handlers, especially during sequential dispatch.
π Live Diagnostics
A Live Event Bus should make communication observable. Useful diagnostics include:
- Total events published
- Publications per event type
- Active subscriptions
- Average dispatch duration
- Slowest handlers
- Failed handlers
- Main-thread dispatch count
- Sticky event count
- Dead weak references removed
- Current queue depth
- Event throughput per second
A diagnostic snapshot might look like this:
public sealed record EventBusSnapshot(
long PublishedEvents,
long DeliveredEvents,
long FailedDeliveries,
int ActiveSubscriptions,
int StickyEvents,
double AverageDispatchMilliseconds,
DateTimeOffset CapturedAt);
This information can power an internal development dashboard.
βββββββββββββββββββββββββββββββββββββββββ
β Event Bus Monitor β
βββββββββββββββββββββββββββββββββββββββββ€
β Published Events: 4,231 β
β Active Subscriptions: 38 β
β Failed Handlers: 4 β
β Avg. Dispatch: 2.4 ms β
β Slowest Event: DataSyncedEvent β
βββββββββββββββββββββββββββββββββββββββββ
π Event History and Replay
Diagnostics become significantly more useful when the Event Bus records recent publications.
public sealed record EventHistoryEntry(
Guid EventId,
string EventType,
DateTimeOffset PublishedAt,
int SubscriberCount,
TimeSpan Duration,
bool Succeeded);
A bounded history prevents unlimited memory growth:
Newest Event
β
[500-entry circular buffer]
β
Oldest Event Removed
During development, event history can answer questions such as:
- Which event triggered this screen update?
- Was the event published more than once?
- Which subscriber failed?
- Did the event execute on the main thread?
- How long did delivery take?
- Was the event published before the page subscribed?
Replay can also be useful for debugging:
await diagnostics.ReplayAsync(eventId);
Replay should generally remain a development-only feature because re-executing domain events in production may produce unintended side effects.
π Dependency Injection Integration
Register the Event Bus as a singleton:
builder.Services.AddSingleton<IEventBus, LiveEventBus>();
A singleton is appropriate because the bus coordinates communication across the entire application. ViewModels and services can then receive it through constructor injection:
public sealed class CheckoutViewModel
{
private readonly IEventBus _eventBus;
public CheckoutViewModel(IEventBus eventBus)
{
_eventBus = eventBus;
}
}
Avoid using a static global Event Bus:
GlobalEventBus.Instance.Publish(...);
Static access introduces hidden dependencies and makes unit testing more difficult. Dependency Injection keeps dependencies explicit and replaceable.
π§ͺ Testing Publishers
A publisher can be tested using a fake bus:
public sealed class RecordingEventBus : IEventBus
{
public List<IAppEvent> PublishedEvents { get; } = [];
public ValueTask PublishAsync<TEvent>(
TEvent eventData,
PublishOptions? options = null,
CancellationToken cancellationToken = default)
where TEvent : IAppEvent
{
PublishedEvents.Add(eventData);
return ValueTask.CompletedTask;
}
public IDisposable Subscribe<TEvent>(
Func<TEvent, CancellationToken, ValueTask> handler,
SubscriptionOptions? options = null)
where TEvent : IAppEvent
{
throw new NotSupportedException();
}
public bool HasSubscribers<TEvent>()
where TEvent : IAppEvent => false;
public int GetSubscriberCount<TEvent>()
where TEvent : IAppEvent => 0;
}
Then verify that the correct event was published:
[Fact]
public async Task CheckoutPublishesOrderCompletedEvent()
{
var bus = new RecordingEventBus();
var service = new CheckoutService(bus);
await service.CompleteAsync();
Assert.Contains(
bus.PublishedEvents,
message => message is OrderCompletedEvent);
}
π§ͺ Testing Subscribers
Subscriber tests can invoke the handler through a real in-memory implementation:
[Fact]
public async Task DashboardRefreshesAfterSynchronization()
{
var eventBus = new LiveEventBus();
using var viewModel = new DashboardViewModel(eventBus);
await eventBus.PublishAsync(
new SynchronizationCompletedEvent(
UploadedItems: 2,
DownloadedItems: 10,
Duration: TimeSpan.FromSeconds(1)));
Assert.Equal(
"10 items downloaded",
viewModel.LastSyncSummary);
}
Because event contracts are strongly typed, test setup remains straightforward.
π’ Real-World Scenarios
πΆ Connectivity Monitoring
Connectivity Service
β
ConnectivityChangedEvent
βββ Offline Banner
βββ Sync Engine
βββ REST Layer
βββ Analytics
π Session Expiration
Authentication Service
β
UserSessionExpiredEvent
βββ Clear Secure Data
βββ Stop SignalR
βββ Reset Navigation
βββ Show Login
π Data Synchronization
Sync Engine
β
SynchronizationCompletedEvent
βββ Refresh Dashboard
βββ Refresh Orders
βββ Update Badge
βββ Record Metrics
π‘ Real-Time Messaging
SignalR Client
β
NotificationReceivedEvent
βββ Notification Center
βββ Badge Counter
βββ Current Page
βββ Local Storage
π¨ Runtime Theme Changes
Settings Page
β
ApplicationThemeChangedEvent
βββ App Resources
βββ Custom Controls
βββ Charts
βββ Native Platform UI
π Event Bus vs Common Communication Approaches
| Capability | Direct Events | Static Messenger | Weak Messenger | Live Event Bus |
|---|---|---|---|---|
| Strongly typed messages | β οΈ | β οΈ | β | β |
| Multiple subscribers | β | β | β | β |
| Weak subscriptions | β | Varies | β | β |
| Async handlers | Manual | Varies | Limited | β |
| UI-thread dispatch | Manual | Manual | Manual | β |
| Subscriber priority | β | β | β | β |
| Filters | Manual | Limited | Tokens | β |
| Sticky events | β | Varies | β | β |
| Parallel dispatch | Manual | β | β | β |
| Error isolation | Manual | Limited | Limited | β |
| Cancellation | Manual | β | β | β |
| Event history | β | β | β | β |
| Runtime diagnostics | β | β | β | β |
| Replay | β | β | β | β |
A complete Live Event Bus is not always necessary. For simple ViewModel communication, a lightweight weak-reference messenger may be sufficient.
The more advanced architecture becomes valuable when the application needs centralized policies, asynchronous delivery, diagnostics, replay, filtering, or complex lifecycle management.
β οΈ Common Pitfalls
β Turning Every Method Call into an Event
Events should represent meaningful occurrences:
OrderCompletedEvent
not arbitrary commands:
PleaseCallThisMethodEvent
Commands express intent. Events describe something that has already happened.
β Publishing Mutable Objects
A subscriber should not be able to modify a message before another subscriber receives it. Prefer immutable records:
public sealed record CartUpdatedEvent(
int ItemCount,
decimal Total);
β Hiding Critical Business Workflows
If one subscriber must always run before another for correctness, the workflow may be too important to model as loosely coupled events. Use direct orchestration for mandatory business steps. Use events for independent reactions.
β Ignoring Subscription Lifetimes
Even a sophisticated Event Bus can leak memory if strong subscriptions are never disposed. Choose weak references or explicit tokens according to subscriber lifetime.
β Running Heavy Work on the Main Thread
Only UI-related handlers should request main-thread dispatch. Database operations, telemetry, and network work should normally remain in the background.
β Keeping Unlimited Event History
Diagnostics history must always be bounded. Otherwise, a long-running application may accumulate thousands of event payloads and retain sensitive data.
β Best Practices
π Recommended practices for a production Event Bus
- π¦ Use immutable event records.
- π§ Name events in past tense when they represent completed actions.
- π Avoid including secrets or complete authentication tokens.
- π§Ή Dispose strong subscriptions explicitly.
- πͺΆ Prefer weak subscriptions for transient ViewModels.
- π§΅ Dispatch to the main thread only when necessary.
- β‘ Use parallel execution only for independent handlers.
- π― Avoid excessive priority-based coupling.
- π‘οΈ Isolate subscriber failures.
- β±οΈ Support timeouts and cancellation.
- π Instrument event latency and failure rates.
- π Keep diagnostic history bounded.
- π§ͺ Test publishers and subscribers independently.
- π Resolve the bus through Dependency Injection.
- π« Avoid using the Event Bus as a global service locator.
π Key Benefits
| Benefit | Description |
|---|---|
| Loose coupling | Publishers and subscribers remain independent |
| Scalability | New subscribers can be added without modifying publishers |
| Testability | Event contracts and handlers can be tested independently |
| Thread safety | Dispatch policies centralize UI and background execution |
| Memory safety | Weak references and subscription tokens control lifetimes |
| Observability | Metrics and history reveal application communication |
| Extensibility | Filters, priorities, sticky events, and middleware can evolve |
| Cross-platform consistency | Native events enter one shared communication model |
π Final Thoughts
A Live Event Bus can become one of the most valuable infrastructure components in a large .NET MAUI application.
It provides a clear communication boundary between ViewModels, services, native platform implementations, real-time clients, repositories, and background workers without requiring those components to reference one another directly.
However, the real value comes from going beyond basic publish-and-subscribe behavior.
A production-ready Event Bus should address: π‘ Message delivery
π§΅ Thread dispatching
π§Ή Subscription lifecycles
π‘οΈ Failure isolation
β±οΈ Cancellation and timeouts
π Runtime observability
π Event history
π§ͺ Testability
Used carefully, this architecture reduces coupling, improves maintainability, and makes complex application behavior easier to understand.
Used indiscriminately, it can hide control flow and make the application harder to reason about. The key is balance.
Use direct method calls for explicit, required workflows.
Use events when independent components need to react to meaningful state changes. With that distinction in place, a Live Event Bus becomes much more than a messengerβit becomes an observable, memory-safe, and extensible communication backbone for modern .NET MAUI applications. ππ‘
