Skip to content
Introducing AI Conversations: Natural Language Interaction for Your Apps! Learn More

Introduction

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 NuGet package 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.

GitHubGitHub stars for shinyorg/mediator
CoreNuGet downloads for Shiny.Mediator
MAUINuGet downloads for Shiny.Mediator.Maui
BlazorNuGet downloads for Shiny.Mediator.Blazor
ASP.NETNuGet downloads for Shiny.Mediator.AspNet
PrismNuGet downloads for Shiny.Mediator.Prism
Uno PlatformNuGet downloads for Shiny.Mediator.Uno
DapperNuGet downloads for Shiny.Mediator.DapperRequests
ResilienceNuGet downloads for Shiny.Mediator.Resilience
FluentValidationNuGet downloads for Shiny.Mediator.FluentValidation
CachingNuGet downloads for Shiny.Mediator.Caching.MicrosoftMemoryCache
Frameworks
.NET
.NET MAUI
Blazor
ASP.NET
Operating Systems
Android
iOS
Windows
  • Built for all .NET apps — not just ASP.NET
  • AOT & trimming ready — ahead-of-time compilation and trimming support
  • Source generated — registrations wired up automatically, no assembly scanning
  • Lightweight — no external dependencies
  • Telemetry & observability — built-in via Microsoft.Extensions.Diagnostics
  • Easy to unit test
  • MAUI, Blazor, & Uno Platform — ViewModels and pages implement IEventHandler without 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
  • AI Tools — Expose contracts as AI-callable tools via Microsoft.Extensions.AI
  • Offline Data — Cache and replay when connectivity drops
  • Caching — Automatic response caching
  • Resiliency — Retry, circuit breaker, and timeout policies
  • Validation — Contract validation via FluentValidation
  • 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
  • Event Throttle — Discard rapid duplicate events within a cooldown window
  • Command Scheduling — Schedule commands for later execution

Shiny Mediator offers 4 types of contracts that flow through the pipeline:

ContractDescriptionHandler TypeMiddleware Type
Commands (ICommand)Fire-and-forget messages sent to a single handlerICommandHandler<TRequest, TResult>ICommandMiddleware<TRequest, TResult>
Requests (IRequest<Result>)Messages sent to a single handler that return a responseIRequestHandler<TRequest, TResult>IRequestMiddleware<TRequest, TResult>
Streams (IStreamRequest<TResult>)Messages that return an async enumerable from a single handlerIStreamRequestHandler<TRequest, TResult>IStreamRequestMiddleware<TRequest, TResult>
Events (IEvent)Messages broadcast to multiple handlersIEventHandler<TEvent>IEventMiddleware<TEvent>

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.