Skip to content

Blog

What's New in Shiny Mediator 6

Shiny Mediator v6 has landed and it’s packed with some exciting new features and improvements. This release is a big step forward for the library, focusing on better source generation, AOT readiness, ASP.NET interop, and some slick new middleware. Let’s dive in.

The biggest (and breaking) change is that Shiny Mediator v6 now targets .NET 8 and .NET 10 as minimum versions. This allows us to take advantage of the latest runtime features for performance, trimming, and AOT compilation. If you’re still on older versions, you’ll need to stay on v5.

The HTTP client source generation has been completely overhauled. In v5, the OpenAPI generation would produce contracts, but you’d still need to wire up handlers and serialization yourself. In v6, the source generator now produces everything — handlers, contracts, JSON converters, and dependency injection registration.

This is a breaking change because you’ll need to call a new registration method, but the setup through MediatorHttpItem in your csproj remains the same:

<ItemGroup>
<MediatorHttp Include="OpenApiRemote"
Uri="https://yourapi.com/openapi.json"
Namespace="My.Namespace"
ContractPostfix="HttpRequest"
GenerateJsonConverters="true"
Visible="false" />
</ItemGroup>

And then in your startup:

builder.Services.AddShinyMediator(x => x.AddGeneratedOpenApiClient());

That’s it. Full HTTP client with AOT-safe serialization, middleware support, and zero boilerplate.

Another nice improvement: if your OpenAPI spec doesn’t have an OperationId, the source generator now infers a name using the HTTP verb and path. For example, GET /user/list becomes GetUserList. No more missing contracts because someone forgot to set an operation ID.

The DirectHttpRequest type has been removed. You can still create your own HTTP objects with the [Http] attribute which will source generate a handler for you. This was cleaned up to reduce confusion between the direct approach and the more powerful generated approach.

One of the more subtle but impactful additions is PublishToBackground on IMediator. If you’ve ever tried to fire-and-forget an event in ASP.NET, you’ve probably run into scoping issues where the HttpContext or scoped services get disposed before your event handlers finish.

PublishToBackground solves this by spawning a new child scope, so you can fire events without worrying about disposal:

public class MyController(IMediator mediator) : ControllerBase
{
[HttpPost]
public async Task<IActionResult> DoSomething()
{
// this returns immediately and the event handlers
// run in a separate scope
await mediator.PublishToBackground(new SomethingHappenedEvent());
return Ok();
}
}

You can still use the standard Publish to await all event handlers in the same scope.

Async Enumerable Responses & Server-Sent Events

Section titled “Async Enumerable Responses & Server-Sent Events”

Stream requests now support IAsyncEnumerable<T> responses that can be wired directly to Server-Sent Events endpoints. Just mark your contract with IServerSentEventsStream:

[Get("/api/sse")]
public class ServerSentEventsRequest : IStreamRequest<EventItem>, IServerSentEventsStream
{
}

If your endpoint does not support server-sent events, you can still use IStreamRequest<T> — just drop the IServerSentEventsStream interface and you get a standard async enumerable response.

The new [Throttle] attribute implements a debounce pattern for event handlers. This is huge for scenarios like search-as-you-type, sensor data processing, or any rapid-fire event where you only care about the latest value:

services.AddShinyMediator(cfg => cfg.AddMediatorRegistry().AddThrottleEventMiddleware());
[MediatorSingleton]
public partial class SearchHandler : IEventHandler<SearchChangedEvent>
{
[Throttle(300)] // wait 300ms after last event
public async Task Handle(
SearchChangedEvent @event,
IMediatorContext context,
CancellationToken ct)
{
// only fires once the user stops typing for 300ms
var results = await searchService.Search(@event.Query, ct);
}
}

How it works:

  1. When an event is published, a timer starts
  2. If the same event fires again before the timer expires, the timer resets and the previous event is discarded
  3. Only after the full delay with no new events does the handler execute with the latest data

Your handler class must be partial for source generation to work.

Previously, middleware executed in DI registration order. That’s fine for simple scenarios, but as your pipeline grows you need more control. The new [MiddlewareOrder] attribute gives you explicit ordering:

[MiddlewareOrder(-100)] // runs first (outermost)
public class ValidationMiddleware<TRequest, TResult>
: IRequestMiddleware<TRequest, TResult> { ... }
[MiddlewareOrder(0)] // default
public class LoggingMiddleware<TRequest, TResult>
: IRequestMiddleware<TRequest, TResult> { ... }
[MiddlewareOrder(100)] // runs last (closest to handler)
public class CachingMiddleware<TRequest, TResult>
: IRequestMiddleware<TRequest, TResult> { ... }

This produces: Validation → Logging → Caching → Handler → Caching → Logging → Validation

Lower values run first. If you don’t use the attribute, everything works exactly as before — it’s fully opt-in.

A new built-in middleware that caches HTTP responses based on CacheControl MaxAge headers. If your API sends proper cache headers, Shiny Mediator will respect them automatically without any extra configuration on your part.

Beyond the headline features, v6 includes a steady stream of refinements:

  • EventStream redesignedIMediator.EventStream was dropping events under load. It’s been completely redesigned for speed and reliability.
  • RuntimeEventRegister is now thread-safe — dynamic event handler registration no longer has race conditions.
  • BaseHttpRequestHandler now handles commands — not just requests.
  • OpenAPI source generation fixes — proper URI path generation for PUT/POST, fixed nullable support, TimeSpan handling, and duplicate type issues.
  • Prism RegionNavigationCommand — contributed by codelisk for region-based navigation in Prism.

If you’re coming from v5, here’s the key things to watch for:

  1. Update your target framework to .NET 8 or .NET 10
  2. HTTP client registration — call the new AddGeneratedOpenApiClient() method
  3. Remove DirectHttpRequest usages — switch to [Http] attribute-based contracts
  4. OpenAPI contracts may have slightly different names due to the automatic HttpRequest postfix

Shiny Mediator v6 continues the push toward zero-boilerplate, AOT-ready mediation for .NET. The revamped HTTP source generation alone is worth the upgrade — and features like event throttling and middleware ordering bring capabilities that would take significant effort to build yourself.

Check out the full documentation and the complete release notes for all the details.

Shiny Mediator - Getting Started

Mediator patterns have been getting a lot of attention lately, and for good reason. They help to decouple components in an application, making it easier to manage complexity and improve maintainability. Mediator patterns also go by the names “Vertical slice architecture” and CQRS (Command Query Responsibility Segregation). The big guy in .NET mediation is obviously MediatR by Jimmy Bogard. It’s an amazing library that has been used by an absolute ton of applications. It recently went to a paid model, which is understandable given the amount of work that goes into maintaining a library of that size.

The reason for building Shiny Mediator was to create a mediation library, but make sure it works with apps built on platforms like .NET MAUI and Blazor WebAssembly while also including some more “batteries included” features. This doesn’t mean I’ve neglected things like ASPNET support, but we’ll get to that in a future article.

Many engineers will call mediators “over engineering” or “an anti-pattern”. I disagree. In fact, I think mediators are one of the best patterns for building complex applications without going full bat stuff crazy with microservices out of the gate. You can run in a monolith while still keeping things decoupled and manageable thereby making it easy to “SLICE” a piece out and move it to a microservice as your application traffic grows. Another advantage of a mediation pattern is can remove the “dependency injection hell” of adding a services for things like logging, caching, and other cross cutting concerns. Instead of having to add these services to every handler, you can just add them to the mediator pipeline, configure them or stick an attribute on a handler.

public class MyViewModel(IDataService data, ICacheManager cache, IConfiguration configuration, ILogger<MyViewModel> logger) {
public async Task LoadData() {
this.Data = cache.TryGet("MyKey", ct => await data.GetData());
}
}

The above code doesn’t look too bad, but

  • Imagine you have 20 view models that all need these services.
  • How do I make sure I’m grabbing from the same cache everytime or clearing it for that matter?
  • Building cache keys can be a pain
[ContractKey("{UserId}-{IsActive}")]
public record GetDataRequest(bool IsActive, string UserId) : IRequest<GetDataResponse>;
public class MyViewModel(IMediator mediator) {
[Cache(300)]
public async Task LoadData() {
var response = await mediator.Send(new GetDataRequest());
this.Data = response.Result;
}
}

Homegrown/built in mediation patterns are often added, but require you do the growing. Middleware management is also not a simple process. Creating keys for caching requests, another whamo of complexity. These are all things solved by Shiny Mediator. AI generated code solutions is another one I hear. “Just use AI to generate the code for you”. AI is great at boilerplate or when you give it a very specific input & output, but architecture… it will throw “slop” at you.

As with all patterns, there are some trade offs. Mediators map objects to handlers (methods or controllers). It is currently not tool friendly to find a corresponding handler for a contract, so you want to make sure you structure your solution and projects well to find handlers without too much effort. Another problem that can occur is middleware executes in a pipeline, so if you have a lot of middleware, you can end up with performance issues if you’re not careful about managing the middleware to ensure quick execution.

I’ve seen some large .NET MAUI applications that had so many engineers working on them, there was constant “fire drills” (the running joke that came to be). Team A would change something that would break Team B’s work. Team C would have to make changes to Team A’s code to update navigation or data retrieval.
Quite often, these breaking changes wouldn’t even be picked up until a regression test.

In this post, we’ll explore how to get started with Shiny Mediator, a library that implements the Mediator pattern with a focus on apps written with .NET.

What are some of the “batteries included”?

Section titled “What are some of the “batteries included”?”

Most mediator libraries hand you a pipe and say “good luck”. Shiny Mediator hands you a pipe, a toolbox, a hard hat, and a coffee. Here’s what’s in the box:

Your users don’t always have internet. Shocking, I know. But building offline support from scratch is the kind of soul-crushing work that makes devs question their career choices. With Shiny Mediator, slap an attribute on your handler and you’re done:

public partial class GetOrdersHandler : IRequestHandler<GetOrdersRequest, IReadOnlyList<Order>>
{
[OfflineAvailable]
public async Task<IReadOnlyList<Order>> Handle(
GetOrdersRequest request,
IMediatorContext context,
CancellationToken ct)
{
// When online: calls your API, stores the result
// When offline: returns the last stored result
// You did nothing. You're welcome.
return await api.GetOrders(ct);
}
}

You can also configure it via appsettings.json if attributes aren’t your thing:

{
"Mediator": {
"Offline": {
"MyNamespace.GetOrdersRequest": true
}
}
}

Want to know if the data came from the offline store? The context tells you:

var response = await mediator.Request(new GetOrdersRequest());
var offline = response.Context.Offline();
if (offline != null)
{
// data is from offline store
// offline.Timestamp tells you WHEN it was stored
// maybe show a "stale data" indicator to the user
}

Caching is one of the two hardest problems in computer science (the other being naming things and off-by-one errors). Shiny Mediator makes it embarrassingly easy:

public partial class GetProductsHandler : IRequestHandler<GetProductsRequest, List<Product>>
{
[Cache(AbsoluteExpirationSeconds = 300, SlidingExpirationSeconds = 60)]
public async Task<List<Product>> Handle(
GetProductsRequest request,
IMediatorContext context,
CancellationToken ct)
{
// This only runs when cache misses.
// No cache key management. No IMemoryCache injection. No tears.
return await db.GetProducts(ct);
}
}

Setup is one line:

services.AddShinyMediator(x => x.AddMemoryCaching());

Need a cache that survives app restarts? The MAUI and Uno extensions have a persistent cache that writes to disk. Same attribute, same config — it just doesn’t evaporate when the user kills your app:

services.AddShinyMediator(x => x.AddMauiPersistentCache());

You can also force a cache refresh when you need fresh data (pull-to-refresh, anyone?):

var response = await mediator.Request(
new GetProductsRequest(),
CancellationToken.None,
ctx => ctx.ForceCacheRefresh()
);
// response.Result is guaranteed fresh

Validation: where developers go to argue about whether to throw exceptions or return error objects. We support both. Pick your poison.

Data Annotations (built-in, no extra package):

services.AddShinyMediator(cfg => cfg.AddDataAnnotations());
[Validate]
public class CreateUserCommand : ICommand
{
[Required]
public string Name { get; set; }
[Range(1, 150)] // optimistic about human lifespans
public int Age { get; set; }
}

FluentValidation (for the overachievers):

services.AddShinyMediator(cfg => cfg.AddFluentValidation());
[Validate]
public class CreateUserCommand : ICommand
{
public string? Name { get; set; }
}
public class CreateUserValidator : AbstractValidator<CreateUserCommand>
{
public CreateUserValidator()
{
RuleFor(x => x.Name).NotEmpty().WithMessage("A user needs a name. Even 'Bob' will do.");
}
}

Validation runs before your handler ever sees the request. Invalid data never touches your business logic. It’s like a bouncer for your code.

Want to know which handler is being a lazy bum? Performance logging middleware tracks execution time through built-in diagnostics via Microsoft.Extensions.Diagnostics. No extra setup, no third-party APM required. Your handlers are already emitting telemetry — you just need to listen.

This one deserves its own blog post (coming soon), but the short version: point Shiny Mediator at your OpenAPI spec and it generates contracts, handlers, JSON converters, and DI registration. No HttpClientFactory plumbing. No System.Text.Json serialization contexts. No Polly setup. Just:

<ItemGroup>
<MediatorHttp Include="MyApi"
Uri="https://myapi.com/openapi.json"
Namespace="MyApp.Api"
ContractPostfix="HttpRequest"
GenerateJsonConverters="true"
Visible="false" />
</ItemGroup>
services.AddShinyMediator(x => x.AddGeneratedOpenApiClient());
// Now just use it like any other mediator request
var users = await mediator.Request(new GetUsersHttpRequest());

All your middleware (caching, offline, validation, resilience) works with HTTP requests too. One attribute to cache API calls. One attribute for offline fallback. Your API client just became the most resilient thing in your entire codebase — and you wrote zero infrastructure code.

Enough talk. Let’s build something. I’ll walk you through a .NET MAUI setup since that’s where Shiny Mediator really flexes, but this works just as well with Blazor, ASP.NET, or plain old console apps (we don’t judge).

Terminal window
dotnet add package Shiny.Mediator
dotnet add package Shiny.Mediator.Maui

For Blazor, swap Shiny.Mediator.Maui for Shiny.Mediator.Blazor. For ASP.NET or console apps, just Shiny.Mediator on its own is fine.

Contracts are just plain C# records or classes that implement one of the mediator interfaces. Think of them as the “what” — what data goes in, what data comes out.

using Shiny.Mediator;
// A command - fire and forget, no return value
public record CreateTodoCommand(string Title, string Description) : ICommand;
// A request - send something in, get something back
public record GetTodosRequest(bool IncludeCompleted) : IRequest<List<TodoItem>>;
// An event - broadcast to anyone who's listening
public record TodoCreatedEvent(TodoItem Item) : IEvent;

Handlers are where the actual work happens. One handler per command/request, but you can have multiple event handlers.

using Shiny.Mediator;
[MediatorSingleton] // source generator handles DI registration for you
public class CreateTodoHandler : ICommandHandler<CreateTodoCommand>
{
readonly IMyDatabase db;
readonly IMediator mediator;
public CreateTodoHandler(IMyDatabase db, IMediator mediator)
{
this.db = db;
this.mediator = mediator;
}
public async Task Handle(
CreateTodoCommand command,
IMediatorContext context,
CancellationToken ct)
{
var item = new TodoItem(command.Title, command.Description);
await db.Insert(item, ct);
// broadcast that a todo was created
await mediator.Publish(new TodoCreatedEvent(item));
}
}
[MediatorSingleton]
public partial class GetTodosHandler : IRequestHandler<GetTodosRequest, List<TodoItem>>
{
readonly IMyDatabase db;
public GetTodosHandler(IMyDatabase db) => this.db = db;
[Cache(AbsoluteExpirationSeconds = 120)]
[OfflineAvailable]
public async Task<List<TodoItem>> Handle(
GetTodosRequest request,
IMediatorContext context,
CancellationToken ct)
{
// cached for 2 minutes AND available offline
// two attributes, zero infrastructure code, infinite smugness
return await db.GetTodos(request.IncludeCompleted, ct);
}
}
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp
.CreateBuilder()
.UseMauiApp<App>();
builder.Services.AddShinyMediator(x =>
{
x.UseMaui();
x.AddMemoryCaching();
x.AddDataAnnotations();
});
// let the source generator register everything
builder.Services.AddMediatorRegistry();
return builder.Build();
}
}
public class TodoListViewModel : BaseViewModel, IEventHandler<TodoCreatedEvent>
{
readonly IMediator mediator;
public TodoListViewModel(IMediator mediator)
{
this.mediator = mediator;
}
public async Task LoadTodos()
{
// one line. cached. offline-available. validated.
var response = await mediator.Request(new GetTodosRequest(IncludeCompleted: false));
this.Todos = response.Result;
}
public async Task CreateTodo(string title, string description)
{
await mediator.Send(new CreateTodoCommand(title, description));
// don't reload manually - the event handler below will fire
}
// this fires automatically when TodoCreatedEvent is published
// no subscription. no unsubscription. no memory leaks. no drama.
public async Task Handle(
TodoCreatedEvent @event,
IMediatorContext context,
CancellationToken ct)
{
await LoadTodos();
}
}

Notice that the ViewModel implements IEventHandler<TodoCreatedEvent> directly. With the MAUI extension, your ViewModels and Pages automatically participate in event broadcasting without being registered in DI. When the page is popped from navigation, it stops receiving events. No MessagingCenter.Unsubscribe nightmares. No WeakReferenceMessenger gymnastics. It just works.

Shiny Mediator has come a long way from its v1 days. We’re now on v6 with full AOT and trimming support baked into the source generators. So what’s next?

  • Deeper ASP.NET integration — we’ve already got minimal API endpoint generation from handlers, server-sent events from stream requests, and HTTP response caching middleware. Expect more first-class server scenarios.
  • Event throttling & middleware ordering — both landed in v6. [Throttle(300)] on your event handlers gives you debounce for free (goodbye, search-as-you-type headaches). [MiddlewareOrder] gives you explicit pipeline control.
  • Better tooling — finding the handler for a contract is still a navigation exercise. We’re exploring IDE integrations to make this seamless.
  • More source generation — the goal is zero reflection, zero runtime surprises. If a handler is missing or misconfigured, you should know at compile time, not when your app blows up in production at 2 AM on a Saturday.
  • Community contributions — shout out to codelisk for Prism region navigation and JeremyBP for MAUI modal stack iteration. Keep ‘em coming.

Shiny Mediator isn’t trying to replace MediatR. It’s trying to be the mediator that gives app developers — especially those on MAUI, Blazor, and Uno — a batteries-included experience that “just works” out of the box. Caching, offline, validation, HTTP clients, event broadcasting, source generated registration… all of it designed so you can focus on your actual business logic instead of plumbing.

Give it a spin. Check out the full docs, play with the sample apps, and come yell at me on BlueSky if something doesn’t work.

Happy coding. May your caches always be warm and your pipelines always be clean.

Source generation and full blown AOT. AOT is becoming such an important part of .NET, especially with .NET MAUI and Blazor. We’re working hard on a version 5 release that will be fully AOT compliant out of the box!

Mediator v2.0

Only a little over 3 months, I released v1.0 of Mediator. Since then, I’ve been adding features as fast as they come to mind. The focus on making the Mediator pattern upfront for apps (in my opinion) has been a huge success. I’ve been using it in my MAUI apps for offline, caching, & resiliency middleware. The amount of time it saves me is pretty crazy.

Since version the v1 release, I’ve been adding a ton of features including an ASP.NET extension for HTTP endpoints straight to Mediator request handlers. Quite recently - I found a lot of mediated calls would end up just wrapping HTTP calls done with Refit or Kiota. I decided to add my own source generator to deal with this. Check out the HTTP Extension for more on this.

So what does 2.0 bring that requires a major version bump? The first major feature is that I’ve brought the offline, stream replay, and user error notification middleware to Blazor webassembly. This middleware was already present in MAUI, but Blazor needed a “connectivity” & “storage” service to match MAUI.

The second major feature is that I’ve been moving a lot of the middleware to be configured via Microsoft.Extensions.Configuration. This allows you to configure middleware and handlers in a global way and without polluting your code with attributes everywhere. Below is an example of all configuration we offer now:

{
"Mediator": {
"Http": {
"My.Namespace.Contract": "https://shinylib.net/newbase",
"My.Namespace.*" : "https://shinylib.net/therestofhtenamespace",
"*": "https://shinylib.net/everythingelse"
},
"Performance": {
"*": {
"ErrorThresholdMilliseconds": 5000
}
},
"Offline": {
"Sample.Handlers.OfflineRequestHandler": {
"AvailableAcrossSessions": true
}
},
"ReplayStream": {
"Sample.Handlers.MyRequest": {
"AvailableAcrossSessions": true
}
},
"TimerRefresh": {
"My.Contacts.HttpData": {
"IntervalSeconds": 10
}
},
"Resilience": {
"My.Namespace.ResilientContract": {
"RetryCount": 3,
"RetryDelay": 500
}
},
"Cache": {
"My.Contacts.*": {
"Priority": "High",
"AbsoluteExpirationSeconds": 300,
"SlidingExpirationSeconds": 60
}
},
"UserErrorNotifications": {
"*": {
"*": {
"Title": "ERROR",
"Message" : "Failed to do something"
},
"fr-CA": {
"Title": "ERREUR",
"Message" : "Échec de faire quelque chose"
}
}
}
}
}

So let’s unpack this. The first thing to notice is that we have ”*” to “glob” a namespace or ALL calls. If you want to attack a specific contract, just fully label it. We will find the nearest namespace to your contract before giving up as “not enabled” for a feature to be disabled on a contract.

I’m pretty happy with this release, but I still have a ton of ideas. All the stuff you can do around the handlers with middleware is really exciting. Have an idea for the mediator, head on over to GitHub and add a feature request. I’m always looking for new ideas.

Mediator v1.0

Shiny Mediator is something new I’ve been working on. I love Jimmy Bogard’s MediatR library on the server, but I just couldn’t get it to fit the way I wanted for Apps… especially Blazor & .NET MAUI apps.

What is a mediator? It’s a small in-process version of a message bus. MAUI has MessagingCenter, the Community toolkit has the weak message center, and Prism offers an event aggregator. They’re all great, but they lack in areas that I want “more”.

  • If an event errors, the whole chain dies in the publish
  • Events are fired in a foreach (mostly)
  • You have to tie into them and unsubscribe from them or you can leak memory
  • They don’t provide the concept of command or request/response models (a command can only be responded to by a single handler)
  • They don’t provide any sort of middleware (pre & post handling)

Sure - they aren’t geared for these things, but the question is “what is”?

Some might say “this is overengineering” or “too complex”. I would counter that comment by saying that this can actually simplify your architecture overall by removing a lot of complex plumbing around services, references, and navigation.

Let’s go over some of the problems that we use mediator to solve within our apps

We believe that Shiny Mediator is the answer to these problems. It’s a simple, yet powerful library that allows you to create a mediator in your app

Does this look familiar to you?

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
}

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, 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 MyPageNavigatonRequest(_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.

Lastly, we offer some absolutely epic middleware that you can use to do things like logging, caching, error handling, etc.
Check out our middleware documentation for more information.

Check out our overall documentation for more information on how to use the mediator in your app. We think you’ll love it!

April Mad Releases

This release was mainly a large bugfixing release with some cool additions around Push Notifications

However, we did decide to remove two modules from the Shiny offering

  • Shiny.Logging.AppCenter - this was an easy one. Microsoft is shutting down AppCenter in 2025.
  • Shiny.SpeechRecognition - this was a tough one. The plugin was never really that great and it was a pain to maintain. The MAUI community toolkit recently released a plugin here that you can use.

We’ve improved the push delegate to handle MORE native stuff, more events like UnRegister to centralize your registration & now the unregistration process.

Release Notes

Apple Privacy Info - May 1, 2024 Deadline is Approaching

Section titled “Apple Privacy Info - May 1, 2024 Deadline is Approaching”

Apple has a new privacy requirement that you must disclose what you are doing with the user’s data. This is a requirement for all apps in the App Store. The Shiny templates have been updated to include a new file under the iOS platform that you can fill out to help you comply with this requirement.

We’ve introduced two “guestimate” type helpers to get users rolling. Our Shiny Templates and our App Builder tool will help you generate these files. We generate what most user app requirements will be in terms of user identity info, as well as all of the necessary Shiny & .NET BCL requirements. We also insert any necessary location permissions if you select one of our location based components.

The templates have been great for Shiny discovery. Wiring up all of the permissions with Android & iOS is difficult. I find that I’m constantly using App Builder here on shinylib.net to generate them all for me because I constantly forget them all. Cutting a new proof-of-concept or test with Shiny or any of the whack of 3rd party libraries that are included with the library is a breeze.

This release got the following love:

  • Removal of AppCenter - maybe this is the opposite of love - but don’t use it since it is officially done in 2025
  • Updated to the latest versions of many nuget packages
  • Updated some of the Shiny stuff to 3.3
  • NEW base template for the upcoming Apple Privacy Manifests under Platforms/iOS

The MAUI Project template is so large that it takes 3 screenshots to get it all.

1 2 3

Check It Out

Push Notification End-to-End Tester for Azure Notification Hubs & Firebase

Section titled “Push Notification End-to-End Tester for Azure Notification Hubs & Firebase”

Setting up push notifications with Apple & Google can be a real pain. This tool will help you test your setup end-to-end. It’s a simple mobile app that will send a push notification to your app using Azure Notification Hubs or Firebase Cloud Messaging. It’s a great way to test your setup without having to write a bunch of code.

It isn’t pretty, but it does a good job of helping you test your setup without having to wire up a backend or write a bunch of code.

GitHub

There is work being done to create fresh new slim bindings for Firebase on iOS. I’m working with Jon Dick on some SLIM Bindings over at https://github.com/Redth/DotNet.Platform.SlimBindings

Slim bindings are revolutionary by any means, but they allow you to control the native API surface that you have to bind to. With Swift becoming increasingly popular and not having a true native binding solution with .NET at this time, Slim bindings allow us to control this narrative by building swift code, but still marking the code properly with objective-C headers to be able to bind within .NET

If you’re a user of Shiny.Push.FirebaseMessaging, the good news is that there will be a direct nuget package update in v3 that will allow you to continue using Firebase messaging on iOS without any other code updates. This update for Shiny will be available long before the Firebase June 2024 deadline

We are starting to plan out Shiny 4.0. We are looking for feedback on what you would like to see in the next major version of Shiny.

Currently on the roadmap are:

  • Simplifying the Job framework
  • Allow for more native configuration of Push & Local Notifications
  • More BLE methods to help with some of the fun edge cases that have been rolling in
  • Removing the old Xamarin/Netstandard targets. This will help with new targets moving forward
  • Continue to improve the hosting model to work with other platform vendors like Uno
  • Trying to get RX out of the core for those that don’t want the fat (MAYBE)

So it’s a “smaller” major release.

There are still people using my old dinosaur of a package, so I updated it to the latest AndroidX stuff and .NET 8.
Unfortunately, classic Xamarin and netstandard targets are now gone, but at least it pushes the needle forward if you’re still using this package.

We may introduce Windows support back into the mix in the future, but it’s not a priority right now.