Skip to content

Advanced

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!

First off, let’s define some template rules/knowledge that we can apply

Contract Library Layout Template

  • Install Shiny.Mediator.Contracts nuget
  • Contains simple POCOs (plain old C# classes) or records that implements (logic does not belong here)
    • Shiny.Mediator.IEvent
    • Shiny.Mediator.IRequest (or with result)
  • Should not cross reference any other contract libraries

Module Layout Template

  • Install Shiny.Mediator to be able to use the IMediator (Shiny.Mediator.IMediator) service where all things begin!
  • Create Any

    • Request Handlers (Shiny.Mediator.IRequestHandler)
    • Event Handlers (Shiny.Mediator.IEventHandler)
    • Your extension method to install these services into the dependency injection container (or use the source generator) - Refer to registration documentation

Now, lets take our template from approve and apply to an overall solution

  • Main App
    • Setup
      • Install Shiny.Mediator
        • If using .NET MAUI - Install Shiny.Mediator.Maui
        • If using Blazor - Install Shiny.Mediator.Blazor
        • If using Blazor Hybrid - Install Both
      • Reference all modules & contracts defined below
    • Responsibilities
      • Registers the Mediator services using builder.Services.AddShinyMediator(cfg => ...)
      • References all Module and Contract Libraries we define below
      • Registers all source generated or manually created extension methods to register your handlers & middleware from your Modules
  • Module 1 Contracts
    • Follow ‘Contract Library Layout Template’
  • Module 1
    • Follow ‘Module Layout Template’
    • References
      • Module 1 Contracts
      • Module 2 Contracts (Any other module CONTRACTs - not the module libraries as this will lead to circulator references)
  • Module 2
    • Follow ‘Module Layout Template’
    • References
      • Module 2 Contracts
      • Module 1 Contracts (Any other module CONTRACTs - not the module libraries as this will lead to circulator references)

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