Introduction
What Is It?
Section titled “What Is It?”Mediator is a behavioral design pattern that reduces chaotic dependencies between objects by restricting direct communication and forcing collaboration through a mediator object.
Shiny Mediator is a mediator pattern implementation built for all .NET apps — mobile, desktop, and server. It ships with a rich set of middleware out-of-the-box so you can add offline support, caching, validation, and more with a single attribute. Check out the Getting Started guide to see how easy it is.
Inspired by MediatR, Shiny Mediator drops server-centric features that don’t translate well to apps and adds capabilities that mobile and desktop developers actually need.
Samples
Section titled “Samples”Features
Section titled “Features”- Built for all .NET apps — not just ASP.NET
- Ahead-of-time (AOT) compilation and trimming ready
- Source generators wire up registrations automatically — no assembly scanning
- Lightweight with no external dependencies
- Built-in telemetry & observability via
Microsoft.Extensions.Diagnostics - Easy to unit test
Contracts
Section titled “Contracts”Shiny Mediator offers 4 types of contracts that flow through the pipeline:
| Contract | Description | Handler Type | Middleware Type |
|---|---|---|---|
Commands (ICommand) | Fire-and-forget messages sent to a single handler | ICommandHandler<TRequest, TResult> | ICommandMiddleware<TRequest, TResult> |
Requests (IRequest<Result>) | Messages sent to a single handler that return a response | IRequestHandler<TRequest, TResult> | IRequestMiddleware<TRequest, TResult> |
Streams (IStreamRequest<TResult>) | Messages that return an async enumerable from a single handler | IStreamRequestHandler<TRequest, TResult> | IStreamRequestMiddleware<TRequest, TResult> |
Events (IEvent) | Messages broadcast to multiple handlers | IEventHandler<TEvent> | IEventMiddleware<TEvent> |
Out-of-the-Box Middleware
Section titled “Out-of-the-Box Middleware”Add powerful behavior to any handler with a single attribute or line of configuration:
- Offline Data — Cache and replay when connectivity drops
- Caching — Automatic response caching
- Resiliency — Retry, circuit breaker, and timeout policies
- Exception Handling — Centralized error handling
- Performance Logging — Automatic slow-request detection
- Main Thread Dispatching — Route handlers to the UI thread
- Replayable Streams — Replay the last emitted stream value
- Refresh Timer — Auto-refresh streams on an interval
- Command Scheduling — Schedule commands for later execution
Extensions
Section titled “Extensions”- MAUI, Blazor, & Uno Platform — ViewModels and pages implement
IEventHandlerwithout DI registration; includes app-specific middleware for caching, offline, and more - ASP.NET Core — Map contracts directly to HTTP endpoints
- HTTP — Easy API handling with OpenAPI contract generation
- Dapper — Streamlined query handling
- Prism — Deep integration with the Prism MVVM framework
Works With
Section titled “Works With”- .NET MAUI (all platforms)
- Uno Platform (all platforms)
- ASP.NET Core, Dapper, and more
- MVVM frameworks: Prism, ReactiveUI, .NET MAUI Shell
- Blazor
- Any .NET platform
What Does It Solve
Section titled “What Does It Solve”Problem #1 - Service & Reference Hell
Section titled “Problem #1 - Service & Reference Hell”Does this look familiar to you? Look at all of those injections! As your app grows, the list will only grow. I feel sorry for the dude that gets to unit test this bad boy.
public class MyViewModel( IConnectivity conn, IDataService data, IAuthService auth, IDialogsService dialogs, ILogger<MyViewModel> logger) { // ... try { if (conn.IsConnected) { var myData = await data.GetDataRequest(); } else { dialogs.Show("No Connection"); // cache? } } catch (Exception ex) { dialogs.Show(ex.Message); logger.LogError(ex); }}With a bit of our middleware and some events, you can get here:
public class MyViewModel(IMediator mediator) : IEventHandler<ConnectivityChangedEvent>, IEventHandler<AuthChangedEvent> { // ... var myData = await mediator.Request(new GetDataRequest());
// logging, exception handling, offline caching can all be bundle into one nice clean call without the need for coupling}
public class GetDataRequestHandler : IRequestHandler<GetDataRequest, MyData> {
[OfflineAvailable] // <= boom done public async Task<MyData> Handle(GetDataRequest request, IMediatorContext context, CancellationToken cancellationToken) { // ... }}Problem #2 - Messages EVERYWHERE (+ Leaks)
Section titled “Problem #2 - Messages EVERYWHERE (+ Leaks)”Do you use the MessagingCenter in Xamarin.Forms? It’s a great tool, but it can lead to some memory leaks if you’re not careful. It also doesn’t have a pipeline, so any errors in any of the responders will crash the entire chain. It doesn’t have a request/response style setup (not that it was meant for it), but this means you still require other services.
public class MyViewModel{ public MyViewModel() { MessagingCenter.Subscribe<SomeEvent1>(this, @event => { // do something }); MessagingCenter.Subscribe<SomeEvent2>(this, @event => { // do something });
MessagingCenter.Send(new SomeEvent1()); MessagingCenter.Send(new SomeEvent2());
// and don't forget to unsubscribe MessagingCenter.Unsubscribe<SomeEvent1>(this); MessagingCenter.Unsubscribe<SomeEvent2>(this); }}Let’s take a look at our mediator in action for this scenarios
public class MyViewModel : IEventHandler<SomeEvent1>, IEventHandler<SomeEvent2>{ public MyViewModel(IMediator mediator) { // no need to unsubscribe mediator.Publish(new SomeEvent1()); mediator.Publish(new SomeEvent2()); }}Problem #3 - Strongly Typed Navigation with Strongly Typed Arguments
Section titled “Problem #3 - Strongly Typed Navigation with Strongly Typed Arguments”Our amazing friends over in Prism offer the “best in class” MVVM framework. We’ll them upsell you beyond that, but one of their amazing features is ‘Modules’. Modules help break up your navigation registration, services, etc.
What they don’t solve is providing a strongly typed nature for this stuff (not their job though). We think we can help addon to their beautiful solution.
A normal call to a navigation service might look like this:
_navigationService.NavigateAsync("MyPage", new NavigationParameters { { "MyArg", "MyValue" } });This is great. It works, but I don’t know the type OR argument requirements of “MyPage” without going to look it up. In a small project with a small dev team, this is fine. In a large project with a large dev team, this can be difficult.
Through our Shiny.Framework library we offer a GlobalNavigationService that can be used to navigate to any page in your app from anywhere, however, for the nature of this example, we’ll pass our navigation service FROM our viewmodel through the mediator request to ensure proper scope.
public record MyPageNavigatonRequest(INavigationService navigator, string MyArg) : IRequest;public class MyPageNavigationHandler : IRequestHandler<MyPageNavigatonRequest>{ public async Task Handle(MyPageNavigatonRequest request, IMediatorContext context, CancellationToken cancellationToken) { await request.navigator.NavigateAsync("MyPage", new NavigationParameters { { "MyArg", request.MyArg } }); }}Now, in your viewmodel, you can do this:
public class MyViewModel{ public MyViewModel(IMediator mediator) { mediator.Request(new MyPageNavigationCommand(_navigationService, "MyValue")); }}Strongly typed. No page required page knowledge from the module upfront. The other dev team of the module can define HOW things work.