diff --git a/readme.md b/readme.md
index 9180b9b..81b7f8f 100644
--- a/readme.md
+++ b/readme.md
@@ -1,179 +1,42 @@
# Shiny Mediator (Preview)
-## PLEASE NOTE - THIS IS AN ALPHA. THINGS ARE LIKELY TO CHANGE
-
-A mediator pattern, but for apps. Apps have pages with lifecycles that don't necessarily participate in the standard
-dependency injection lifecycle. .NET MAUI generally tends to favor the Messenger pattern. We hate this pattern for many reasons
+Mediator is a behavioral design pattern that lets you reduce chaotic dependencies between objects. The pattern restricts direct communications between the objects and forces them to collaborate only via a mediator object.
+
+Shiny Mediator is a mediator pattern implementation, but for apps. Apps have pages with lifecycles that don't necessarily participate in the standard
+dependency injection lifecycle. .NET MAUI generally tends to favor the Messenger pattern. We hate this pattern for many reasons
which we won't get into. That being said, we do offer a messenger subscription in our Mediator for where interfaces
and dependency injection can't reach.
This project is heavily inspired by [MediatR](https://github.com/jbogard/mediatr) with some lesser features that we feel
were aimed more at server scenarios, while also adding some features we feel benefit apps
+## Links
+- Docs
+ - [Main](https://shinylib.net/client/mediator/)
+ - [Quick Start](https://shinylib.net/client/mediator/quick-start/)
+ - [Middleware](http://localhost:4321/client/mediator/middleware)
+ - [Advanced](http://localhost:4321/client/mediator/advanced/)
+- [Sample](https://github.com/shinyorg/mediator/tree/main/Sample)
+
## Features
-* A Mediator for your .NET Apps (MAUI & Blazor are the main targets for us)
-* Request & event middleware with some great "out of the box" scenarios for your app
-* Think of "weak" message subscriptions without the fuss or mess to cleanup
-* Our MAUI & Blazor integrations allow your viewmodels or pages to implement an IEventHandler interface(s) without them having to participate in the dependency injection provider
-* We still have a "messagingcenter" type subscribe off IMediator for cases where you can't have your current type implement an interface
-* Instead of Assembly Scanning, we have source generators to automatically wireup the necessary registrations for you! (WIP)
-* Lightweight, No external dependencies, tiny bit of reflection
-* Help remove service overrun and reduce your constructor fat
-* Easy to Unit Test
+- A Mediator for your .NET Apps (MAUI & Blazor are the main targets for us)
+- Request/Response "Command" Handling
+- Event Publication
+- Request & event middleware with some great "out of the box" scenarios for your app
+- Think of "weak" message subscriptions without the fuss or mess to cleanup
+- Our MAUI & Blazor integrations allow your viewmodels or pages to implement an IEventHandler interface(s) without them having to participate in the dependency injection provider
+- We still have a "messagingcenter" type subscribe off IMediator for cases where you can't have your current type implement an interface
+- Instead of Assembly Scanning, we have source generators to automatically wireup the necessary registrations for you! (WIP)
+- Lightweight, No external dependencies, tiny bit of reflection
+- Help remove service overrun and reduce your constructor fat
+- Easy to Unit Test
## Works With
-* .NET MAUI - all platforms
-* MVVM Frameworks like Prism, ReactiveUI, & .NET MAUI Shell
-* Blazor - Work In Progress
-* Any other .NET platform - but you'll have to come up with your own "event collector" for the out-of-state stuff
-
-## Getting Started
-
-Install [Shiny.Mediator](https://www.nuget.org/packages/Shiny.Mediator) from NuGet
-
-First, let's create our request & event handlers
-
-```
-using Shiny.Mediator;
-
-public record TestRequest(string AnyArgs, int YouWant) : IRequest;
-public record TestEvent(MyObject AnyArgs) : IEvent;
-
-// and for request/response requests - we'll come back to this
-public record TestResponseRequest : IRequest {}
-public record TestResponse {}
-```
-
-Next - let's wire up a RequestHandler. You can have ONLY 1 request handler per request type.
-This is where you would do the main business logic or data requests.
-
-Let's create our RequestHandler
-
-```csharp
-using Shiny.Mediator;
-
-// NOTE: Request handlers are registered as singletons
-public class TestRequestHandler : IRequestHandler
-{
- // you can do all dependency injection here
- public async Task Handle(TestRequest request, CancellationToken ct)
- {
- // do something async here
- }
-}
-
-public class TestResponseRequestHandler : IRequestHandler
-{
- public async Task Handle(TestResponseRequest request, CancellationToken ct)
- {
- var response = await GetResponseThing(ct);
- return response;
- }
-}
-
-public class TestEventHandler : IEventHandler
-{
- // Dependency injection works here
- public async Task Handle(TestEvent @event, CancellationToken ct)
- {
- // Do something async here
- }
-}
-```
-
-Now, let's register all of our stuff with our .NET MAUI MauiProgram.cs
-
-```csharp
-public static class MauiProgram
-{
- public static MauiApp CreateMauiApp()
- {
- var builder = MauiApp
- .CreateBuilder()
- .UseMauiApp();
-
- builder.Services.AddShinyMediator();
- builder.Services.AddSingletonAsImplementedInterfaces();
- builder.Services.AddSingletonAsImplementedInterfaces();
- builder.Services.AddSingltonAsImplementedInterfaces();
- // OR if you're using our attribute for source generation
- }
-}
-```
-
-Lastly, any model model/viewmodel/etc participating in dependency injection can now inject the mediator
-
-```
-public class MyViewModel(Shiny.Mediator.IMediator mediator)
-{
- public async Task Execute()
- {
- await mediator.Send(new TestRequest()); // this will execute TestRequestHandler
- var response = await mediator.Request(new TestResponseRequest()); // this will execute TestResponseRequestHandler and return a value
-
- // this will publish to any service registered that implement IEventHandler
- // there are additional args here to allow you to execute values in sequentially or wait for all events to complete
- await mediator.Publish(new TestEvent());
- }
-}
-```
-
-### What about my ViewModels?
-
-For .NET MAUI, your viewmodels have the ability to participate in the event publishing chain without being part of dependency injection
-
-With this setup, you don't need to worry about deallocating any events, unsubscribing from some service, or hooking to anything.
-
-Lastly, if your page/viewmodel is navigated away from (popped), it will no longer participate in the event broadcast
-
-First, let's install [Shiny.Mediator.Maui](https://www.nuget.org/packages/Shiny.Mediator.Maui)
-Now...let's go back to our MauiProgram.cs and alter the AddShinyMediator
-
-```csharp
-builder.Services.AddShinyMediator(cfg => cfg.UseMaui());
-```
-
-Now your viewmodel (or page) can simply implement the IEventHandler interface to participate
-
-NOTE: Further example to below - you can implement multiple event handlers (or request handlers)
-
-```csharp
-public class MyViewModel : BaseViewModel,
- Shiny.Mediator.IEventHandler,
- Shiny.Mediator.IEventHandler
-{
- public async Task Handle(TestEvent @event, CancellationToken ct)
- {
- }
-
- public async Task Handle(TestEvent2 @event, CancellationToken ct)
- {
- }
-}
-```
-
-## Sample
-There is a sample in this repo. You do not need any other part of Shiny, Prism, ReactiveUI, etc - those are included as I write things faster with it.
-Focus on the interfaces from the mediator & the mediator calls itself
-
-## Ideas for Workflows Request & Events
-* Use Prism with Modules - want strongly typed navigation parameters, navigations, and have them available to other modules - we're the guy to help!
- * Example TBD
-* Using a Shiny Foreground Job - want to push data an event that new data came in to anyone listening?
-* Have a Shiny Push Delegate that is executing on the delegate but want to push it to the UI, Mediator has a plan!
-
-
-## TODO
-* Document
- * Event Collectors
- * Request Middleware
- * Covariance Event Handlers & Request Middleware - Dependency Injection Support - nothing to do with the library
- * OOBE Middleware - easy, not feature rich - use as a template once you outgrow them
-* Event Collectors for MAUI execute on main thread?
-* Streams - IAsyncEnumerable or IObservable
-* Source Generator Registration
- * Need to use a different method or not use extension methods - maybe AddHandlersFromAssemblyName or allow it to be custom named
+- .NET MAUI - all platforms
+- MVVM Frameworks like Prism, ReactiveUI, & .NET MAUI Shell
+- Blazor - Work In Progress
+- Any other .NET platform - but you'll have to come up with your own "event collector" for the out-of-state stuff
\ No newline at end of file
diff --git a/src/Shiny.Mediator.Maui/Attributes.cs b/src/Shiny.Mediator.Maui/Attributes.cs
new file mode 100644
index 0000000..19a5d01
--- /dev/null
+++ b/src/Shiny.Mediator.Maui/Attributes.cs
@@ -0,0 +1,48 @@
+namespace Shiny.Mediator;
+
+public enum StoreType
+{
+ File,
+ Memory
+}
+
+[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
+public class CacheAttribute : Attribute
+{
+ ///
+ /// Setting this to true will tell the middleware to always passthrough to the server if the app detects online connectivity
+ /// False will follow the standard expiration
+ ///
+ public bool OnlyForOffline { get; set; }
+
+ ///
+ /// Max age of a cache item
+ ///
+ public int MaxAgeSeconds { get; set; }
+
+ ///
+ /// You can store in-memory which means the data is not available in subsequent starts OR file where it available across
+ /// application sessions
+ ///
+ public StoreType Storage { get; set; }
+}
+
+
+[AttributeUsage(AttributeTargets.Method, Inherited = false)]
+public class MainThreadAttribute : Attribute {}
+
+
+[AttributeUsage(AttributeTargets.Class, Inherited = false)]
+public class TimedLoggingAttribute : Attribute
+{
+ public double ErrorThresholdMillis { get; set; } = 0;
+}
+
+
+public class UserExceptionRequestMiddlewareConfig
+{
+ public bool ShowFullException { get; set; }
+ public string ErrorMessage { get; set; } = "We're sorry. An error has occurred";
+ public string ErrorTitle { get; set; } = "Error";
+ public string ErrorConfirm { get; set; } = "OK";
+}
\ No newline at end of file
diff --git a/src/Shiny.Mediator.Maui/Middleware/CacheRequestMiddleware.cs b/src/Shiny.Mediator.Maui/Middleware/CacheRequestMiddleware.cs
index 92a8dd5..a0d1e51 100644
--- a/src/Shiny.Mediator.Maui/Middleware/CacheRequestMiddleware.cs
+++ b/src/Shiny.Mediator.Maui/Middleware/CacheRequestMiddleware.cs
@@ -1,25 +1,26 @@
+using System.Reflection;
using System.Text.Json;
-using Microsoft.Extensions.Configuration;
namespace Shiny.Mediator.Middleware;
public class CacheRequestMiddleware(
- IConfiguration configuration,
IConnectivity connectivity,
IFileSystem fileSystem
) : IRequestMiddleware where TRequest : IRequest
// IRequestHandler,
// IRequestHandler
{
+ readonly Dictionary> memCache = new();
+
+
public async Task Process(TRequest request, RequestHandlerDelegate next, IRequestHandler requestHandler, CancellationToken cancellationToken)
{
// no caching for void requests
if (typeof(TResult) == typeof(Unit))
return await next().ConfigureAwait(false);
- // no config no cache - TODO: could consider ICacheItem taking default values
- var cfg = GetConfiguration(request);
+ var cfg = requestHandler.GetType().GetCustomAttribute();
if (cfg == null)
return await next().ConfigureAwait(false);
@@ -50,18 +51,18 @@ public async Task Process(TRequest request, RequestHandlerDelegate item, CacheConfiguration cfg)
+ protected virtual bool IsExpired(CachedItem item, CacheAttribute cfg)
{
- if (cfg.MaxAge == null)
+ if (cfg.MaxAgeSeconds <= 0)
return false;
- var expiry = item.CreatedOn.Add(cfg.MaxAge.Value);
+ var expiry = item.CreatedOn.Add(TimeSpan.FromSeconds(cfg.MaxAgeSeconds));
var expired = expiry < DateTimeOffset.UtcNow;
return expired;
}
- protected virtual async Task GetAndStore(TRequest request, RequestHandlerDelegate next, CacheConfiguration cfg)
+ protected virtual async Task GetAndStore(TRequest request, RequestHandlerDelegate next, CacheAttribute cfg)
{
var result = await next().ConfigureAwait(false);
this.Store(request, result, cfg);
@@ -86,7 +87,7 @@ protected virtual string GetCacheFilePath(TRequest request)
}
- protected virtual void Store(TRequest request, TResult result, CacheConfiguration cfg)
+ protected virtual void Store(TRequest request, TResult result, CacheAttribute cfg)
{
switch (cfg.Storage)
{
@@ -110,7 +111,7 @@ protected virtual void Store(TRequest request, TResult result, CacheConfiguratio
}
- protected virtual CachedItem? GetFromStore(TRequest request, CacheConfiguration cfg)
+ protected virtual CachedItem? GetFromStore(TRequest request, CacheAttribute cfg)
{
CachedItem? returnValue = null;
@@ -143,44 +144,15 @@ protected virtual void Store(TRequest request, TResult result, CacheConfiguratio
return returnValue;
}
-
-
- readonly Dictionary> memCache = new();
- readonly object syncLock = new();
- CacheConfiguration[]? configurations;
-
-
- protected virtual CacheConfiguration? GetConfiguration(TRequest request)
- {
- var type = request.GetType();
- var key = $"{type.Namespace}.{type.Name}";
-
- if (this.configurations == null)
- {
- lock (this.syncLock)
- {
- this.configurations = configuration
- .GetSection("Cache")
- .Get();
- }
- }
-
- var cfg = this.configurations?
- .FirstOrDefault(x => x
- .RequestType
- .Equals(
- key,
- StringComparison.InvariantCultureIgnoreCase
- )
- );
-
- return cfg;
- }
}
// public record FlushCacheItemRequest(Type RequestType) : IRequest;
// public record FlushAllCacheRequest : IRequest;
+///
+/// Implementing this interface will allow you to create your own cache key, otherwise the cache key is based on the name
+/// of the request model
+///
public interface ICacheItem
{
string CacheKey { get; }
@@ -191,16 +163,3 @@ public record CachedItem(
T Value
);
-public enum StoreType
-{
- File,
- Memory
-}
-
-public class CacheConfiguration
-{
- public string RequestType { get; set; }
- public bool OnlyForOffline { get; set; }
- public TimeSpan? MaxAge { get; set; }
- public StoreType Storage { get; set; }
-}
\ No newline at end of file
diff --git a/src/Shiny.Mediator.Maui/Middleware/MainTheadEventMiddleware.cs b/src/Shiny.Mediator.Maui/Middleware/MainTheadEventMiddleware.cs
index d0c1697..4374a62 100644
--- a/src/Shiny.Mediator.Maui/Middleware/MainTheadEventMiddleware.cs
+++ b/src/Shiny.Mediator.Maui/Middleware/MainTheadEventMiddleware.cs
@@ -4,9 +4,6 @@
namespace Shiny.Mediator.Middleware;
-[AttributeUsage(AttributeTargets.Method, Inherited = false)]
-public class MainThreadAttribute : Attribute {}
-
public class MainTheadEventMiddleware : IEventMiddleware where TEvent : IEvent
{
public async Task Process(IEvent @event, EventHandlerDelegate next, IEventHandler eventHandler, CancellationToken cancellationToken)
diff --git a/src/Shiny.Mediator.Maui/Middleware/TimedLoggingRequestMiddleware.cs b/src/Shiny.Mediator.Maui/Middleware/TimedLoggingRequestMiddleware.cs
index 98b963c..8ce678f 100644
--- a/src/Shiny.Mediator.Maui/Middleware/TimedLoggingRequestMiddleware.cs
+++ b/src/Shiny.Mediator.Maui/Middleware/TimedLoggingRequestMiddleware.cs
@@ -5,13 +5,6 @@
namespace Shiny.Mediator.Middleware;
-[AttributeUsage(AttributeTargets.Class, Inherited = false)]
-public class TimedLoggingAttribute : Attribute
-{
- public double ErrorThresholdMillis { get; set; } = 0;
-}
-
-
public class TimedLoggingRequestMiddleware(ILogger logger) : IRequestMiddleware where TRequest : IRequest
{
public async Task Process(TRequest request, RequestHandlerDelegate next, IRequestHandler requestHandler, CancellationToken cancellationToken)
diff --git a/src/Shiny.Mediator.Maui/Middleware/UserNotificationExceptionRequestMiddleware.cs b/src/Shiny.Mediator.Maui/Middleware/UserNotificationExceptionRequestMiddleware.cs
index 80e4e8e..09db4ad 100644
--- a/src/Shiny.Mediator.Maui/Middleware/UserNotificationExceptionRequestMiddleware.cs
+++ b/src/Shiny.Mediator.Maui/Middleware/UserNotificationExceptionRequestMiddleware.cs
@@ -3,14 +3,6 @@
namespace Shiny.Mediator.Middleware;
-public class UserExceptionRequestMiddlewareConfig
-{
- public bool ShowFullException { get; set; }
- public string ErrorMessage { get; set; } = "We're sorry. An error has occurred";
- public string ErrorTitle { get; set; } = "Error";
- public string ErrorConfirm { get; set; } = "OK";
-}
-
public class UserExceptionRequestMiddleware(ILogger logger, UserExceptionRequestMiddlewareConfig config) : IRequestMiddleware where TRequest : IRequest
{
public async Task Process(TRequest request, RequestHandlerDelegate next, IRequestHandler requestHandler, CancellationToken cancellationToken)
diff --git a/src/Shiny.Mediator.Maui/Shiny.Mediator.Maui.csproj b/src/Shiny.Mediator.Maui/Shiny.Mediator.Maui.csproj
index c30d876..215dbce 100644
--- a/src/Shiny.Mediator.Maui/Shiny.Mediator.Maui.csproj
+++ b/src/Shiny.Mediator.Maui/Shiny.Mediator.Maui.csproj
@@ -7,7 +7,6 @@
-
diff --git a/src/Shiny.Mediator/Shiny.Mediator.csproj b/src/Shiny.Mediator/Shiny.Mediator.csproj
index 9ac4e88..7037b1d 100644
--- a/src/Shiny.Mediator/Shiny.Mediator.csproj
+++ b/src/Shiny.Mediator/Shiny.Mediator.csproj
@@ -8,9 +8,10 @@
-
-
-
+
+
@@ -23,4 +24,8 @@
PackagePath="analyzers/dotnet/cs"
Visible="false" />
+
+
+
+
diff --git a/src/Shiny.Mediator/build/Package.targets b/src/Shiny.Mediator/Shiny.Mediator.targets
similarity index 93%
rename from src/Shiny.Mediator/build/Package.targets
rename to src/Shiny.Mediator/Shiny.Mediator.targets
index 0378f00..a8c55c2 100644
--- a/src/Shiny.Mediator/build/Package.targets
+++ b/src/Shiny.Mediator/Shiny.Mediator.targets
@@ -2,4 +2,4 @@
-
\ No newline at end of file
+
diff --git a/tests/Shiny.Mediator.Tests/CacheRequestMiddlewareTests.cs b/tests/Shiny.Mediator.Tests/CacheRequestMiddlewareTests.cs
index 0266991..0b8f689 100644
--- a/tests/Shiny.Mediator.Tests/CacheRequestMiddlewareTests.cs
+++ b/tests/Shiny.Mediator.Tests/CacheRequestMiddlewareTests.cs
@@ -1,4 +1,3 @@
-using Microsoft.Extensions.Configuration;
using Shiny.Mediator.Middleware;
namespace Shiny.Mediator.Tests;
@@ -10,15 +9,9 @@ public async Task EndToEnd()
{
var conn = new MockConnectivity();
var fs = new MockFileSystem();
- var configuration = new ConfigurationBuilder()
- .AddInMemoryCollection(new Dictionary
- {
- ["Cache:Shiny.Mediator.Tests.CacheRequest"] = ""
- })
- .Build();
var handler = new CacheRequestHandler();
- var middleware = new CacheRequestMiddleware(configuration, conn, fs);
+ var middleware = new CacheRequestMiddleware(conn, fs);
// TODO: test with ICacheItem
var request = new CacheRequest();
@@ -50,6 +43,7 @@ public async Task EndToEnd()
public record CacheRequest : IRequest;
public record CacheResult(long Ticks);
+[Cache(MaxAgeSeconds = 5, Storage = StoreType.Memory, OnlyForOffline = false)]
public class CacheRequestHandler : IRequestHandler
{
public Task Handle(CacheRequest request, CancellationToken cancellationToken)