Skip to content

Advanced

Request Sender (IRequestSender)

The request sender is directly responsible for sending requests to the request handlers. Our request handler is pretty straight forward for scenarios it will cover and how it executes the middleware pipeline when compared to event publishing.

namespace Shiny.Mediator.Infrastructure;
public interface IRequestSender
{
Task<TResult> Request<TResult>(
IRequest<TResult> request,
CancellationToken cancellationToken = default
);
Task Send(
IRequest request,
CancellationToken cancellationToken = default
);
}

Once you have implemented the IRequestSender, to register it

services.AddShinyMediator(cfg =>
{
cfg.SetRequestSender<MyRequestSender>();
});

Event Publisher (IEventPublisher)

Our default event publisher pulls in all event handlers that are registered with the dependency injection container including any event collectors that are registered. From there, we wrap each call to each event handler in any registered event middleware.
Lastly, we execute these using Task.WhenAll to ensure all event handlers are executed in parallel.

namespace Shiny.Mediator.Infrastructure;
public interface IEventPublisher
{
Task Publish<TEvent>(
TEvent @event,
CancellationToken cancellationToken = default
) where TEvent : IEvent;
IDisposable Subscribe<TEvent>(
Func<TEvent, CancellationToken, Task> action
) where TEvent : IEvent;
}

Once you have implemented the IEventPublisher, to register it:

services.AddShinyMediator(cfg =>
{
cfg.SetEventPublisher<MyEventPublisher>();
});

What If I can’t add an interface, participate in DI… I’m stuck… how do I listen to an event?

The main Shiny.Mediator.IMediator interface does have a standard MessagingCenter subscription method. It suffers from the same issue as the standard messaging center, you must Dispose of the call when you’re done with it.

// we assume you have access to your DI container statically
var sub = Container.GetService<IMediator>().Subscribe<SomeEvent>(async (@event, ct) => {
// do something
});
// when you're done
sub.Dispose();

Event Collectors

What are they?

Event collectors allow you to bring in event handlers that may not be participating in the dependency injection lifecycle or perhaps they simply have a different scope outside of where you are IN the DI lifecycle.

Traditionally, in classic Xamarin Forms and even some .NET MAUI apps tend not to register their viewmodels or pages with the DI lifecycle, however, all of these apps participate in the navigation stack. Thus, we have an event collector for MAUI that simply traverses to find any pages or viewmodels that implement the IEventHandler at the time of the event publish. What this means is that if you pop a page/viewmodel off the stack before the publish is called, it will no longer participate in the event collector.

Creating an Event Collector

public class MyEventCollector : IEventCollector
{
public IReadOnlyList<IEventHandler<TEvent>> GetHandlers<TEvent>() where TEvent : IEvent
{
// return any handlers in your required scope
}
}

Registering Event Collectors

builder.Services.AddShinyMediator(cfg =>
{
cfg.AddEventCollector<MyEventCollector>();
});

MAUI & Blazor Event Collectors

We offer two event collectors out of the box for MAUI (Shiny.Mediator.Maui) & Blazor (Shiny.Mediator.Blazor).

Our MAUI event collector will traverse the navigation stack to find any pages or viewmodels that implement the IEventHandler interface. This is a great way to participate in the event chain without having to worry about DI.

Our Blazor event collector has a component factory that holds a weak reference to any component created that implements the IEventHandler interface.

Project Structure

Project structure is an important to helping with large applications. This is where ‘vertical slicing’ and cross team implementation really gets good!

Reasons for this Engineering

Allows cross functional teams to work enable cross functional requirements without being tightly coupled Allows each feature/module to register its own services, controls, pages, viewmodels, etc in isolation while still having access to shared functionality like logging, configuration, etc Through use of Shiny.Mediator.Prism, we can achieve strongly typed routing and navigation parameters via a simple Mediator request that implements the IPrismNavigationRequest

Module Layout Template

diagram
ProjectDescription
TheAppThis is the main MAUI executable project
SharedThis is where any shared services, utils, etc go to share amongst ALL modules
PeopleModuleWhere all the handlers, views, viewmodels, and services specific to people live
PeopleModule.ContractsContracts that the PeopleModule owns and manages
VehicleModuleWhere all the handlers, views, viewmodels, and services specific to vehicles live
VehicleModule.ContractsContracts that the VehicleModule owns and manages
OwnerModuleWhere all the handlers, views, viewmodels, and services specific to owners live
OwnerModule.ContractsContracts that the OwnerModule owns and manages

To see this runnable sample code, please go to [https://github.com/shinyorg/mediatorsample/]

Advanced Dependency Injection

Dependency Injection is pluggable as we use the Microsoft.Extension.DependencyInjection setup to provide our servicing. That being said, your mileage will vary depending on what your container will support. We test our apps with

  • DryIoc
  • Microsoft Dependency Injection

If you use DI containers outside of this, you will need to do your own test cases!

One of the big value adds for middleware & events is that they support covariance.

public record ChildEvent : IEvent;
public record ParentEvent : ChildEvent;
public record GrandParentEvent : ParentEvent;
public class AllFamilyEventHandler : IEventHandler<GrandParentEvent>
{
public async Task Handle(GrandParentEvent @event, CancellationToken cancellationToken)
{
// this can actually be
}
}
var services = new ServiceCollection();
services.AddSingleton<IEventHandler<>>

Additional Registration Methods (Out of the Box)

We have several methods to help deliver easier registration experiences

You may have an event handler or request handler that handles multiple commands, but you want this “service” to hold state. Thus, we offer some simple methods to hold that state in scope of a request or publish.

using Shiny.Mediator;
using Microsoft.Extensions.DependencyInjection;
public class YourImplementation : IEventHandler<SomeEvent>, IEventHandler<SomeOtherEvent>, IRequestHandler<MyRequest>, IRequestHandler<AnotherRequest> {
// .. left empty for brevity
}
var services = new ServiceCollection();
services.AddSingletonAsImplementedInterfaces<YourImplementation>();
services.AddScopedAsImplementedInterfaces<YourImplementation>();
services.AddShinyMediator();
var services.BuildServiceProvider();
var e1 = sp.GetRequiredService<IEventHandler<SomeEvent>>();
var e2 = sp.GetRequiredService<IEventHandler<SomeOtherEvent>>();
e1 == e2 // same instance