Skip to content

Commit

Permalink
Merge branch 'dev'
Browse files Browse the repository at this point in the history
  • Loading branch information
aritchie committed Jun 29, 2024
2 parents 50ded6b + 6d94a04 commit e93e84a
Show file tree
Hide file tree
Showing 18 changed files with 343 additions and 149 deletions.
2 changes: 1 addition & 1 deletion Sample/Handlers/CachedRequestHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Sample.Handlers;
[RegisterHandler]
public class CachedRequestHandler : IRequestHandler<CacheRequest, string>
{
[Cache]
[Cache(AbsoluteExpirationSeconds = 20)]
public Task<string> Handle(CacheRequest request, CancellationToken cancellationToken)
{
var r = DateTimeOffset.Now.ToString("h:mm:ss tt");
Expand Down
4 changes: 2 additions & 2 deletions src/Shiny.Mediator.Caching/CacheAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ namespace Shiny.Mediator;
public class CacheAttribute : Attribute
{
public CacheItemPriority Priority { get; set; } = CacheItemPriority.Normal;
public int? AbsoluteExpirationSeconds { get; set; }
public int? SlidingExpirationSeconds { get; set; }
public int AbsoluteExpirationSeconds { get; set; }
public int SlidingExpirationSeconds { get; set; }
}
4 changes: 3 additions & 1 deletion src/Shiny.Mediator.Caching/CacheExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Shiny.Mediator.Caching;
using Shiny.Mediator.Caching.Infrastructure;
using Shiny.Mediator.Middleware;

namespace Shiny.Mediator;

Expand All @@ -10,6 +11,7 @@ public static class CacheExtensions
public static ShinyConfigurator AddMemoryCaching(this ShinyConfigurator cfg, Action<MemoryCacheOptions> configureCache)
{
cfg.Services.AddMemoryCache(configureCache);
cfg.Services.AddSingleton<IEventHandler<FlushAllStoresEvent>, FlushCacheEventHandler>();
cfg.AddOpenRequestMiddleware(typeof(CachingRequestMiddleware<,>));
return cfg;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using Microsoft.Extensions.Caching.Memory;
using Shiny.Mediator.Infrastructure;

namespace Shiny.Mediator.Caching;
namespace Shiny.Mediator.Caching.Infrastructure;


public class CachingRequestMiddleware<TRequest, TResult>(IMemoryCache cache) : IRequestMiddleware<TRequest, TResult>
Expand All @@ -21,23 +20,33 @@ CancellationToken cancellationToken
return await next().ConfigureAwait(false);

var cacheKey = this.GetCacheKey(request!, requestHandler);
var e = cache.CreateEntry(cacheKey);
e.Priority = cfg.Priority;
var result = await cache.GetOrCreateAsync<TResult>(
cacheKey,
entry =>
{
entry.Priority = cfg.Priority;

if (cfg.AbsoluteExpirationSeconds != null)
e.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(cfg.AbsoluteExpirationSeconds.Value);
if (cfg.AbsoluteExpirationSeconds > 0)
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(cfg.AbsoluteExpirationSeconds);

if (cfg.SlidingExpirationSeconds != null)
e.SlidingExpiration = TimeSpan.FromSeconds(cfg.SlidingExpirationSeconds.Value);

var result = await cache.GetOrCreateAsync<TResult>(
e,
_ => next()
if (cfg.SlidingExpirationSeconds > 0)
entry.SlidingExpiration = TimeSpan.FromSeconds(cfg.SlidingExpirationSeconds);

return next();
}
);

return result!;
}


protected virtual string GetCacheKey(object request, IRequestHandler handler)
=> Utils.GetRequestKey(request);
{
if (request is IRequestKey keyProvider)
return keyProvider.GetKey();

var t = request.GetType();
var key = $"{t.Namespace}_{t.Name}";
return key;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Microsoft.Extensions.Caching.Memory;
using Shiny.Mediator.Middleware;

namespace Shiny.Mediator.Caching.Infrastructure;


public class FlushCacheEventHandler(IMemoryCache cache) : IEventHandler<FlushAllStoresEvent>
{
public Task Handle(FlushAllStoresEvent @event, CancellationToken cancellationToken)
{
cache.Clear();
return Task.CompletedTask;
}
}
11 changes: 11 additions & 0 deletions src/Shiny.Mediator.Maui/Infrastructure/IStorageManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Shiny.Mediator.Infrastructure;

public interface IStorageManager
{
void Store(object request, object result, bool isPeristent);

TResult? Get<TResult>(object request, bool isPeristent);

void ClearAll();
// TODO: remove(request) & clearall, clearbyrequesttype
}
143 changes: 143 additions & 0 deletions src/Shiny.Mediator.Maui/Infrastructure/Impl/StorageManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
using System.Text.Json;

namespace Shiny.Mediator.Infrastructure.Impl;


// TODO: can the request keys get "dirty" - chances are yes
public class StorageManager(IFileSystem fileSystem) : IStorageManager
{
readonly Dictionary<string, object> memCache = new();
Dictionary<string, string> keys = null!;


public virtual void Store(object request, object result, bool isPersistent)
{
if (isPersistent)
{
var path = this.GetFilePath(request, true);
var json = JsonSerializer.Serialize(result);
File.WriteAllText(path, json);
}
else
{
var key = this.GetStoreKeyFromRequest(request);
lock (this.memCache)
this.memCache[key] = result!;
}
}


public virtual TResult? Get<TResult>(object request, bool isPersistent)
{
TResult? returnValue = default;

if (isPersistent)
{
var path = this.GetFilePath(request, false);
if (File.Exists(path))
{
var json = File.ReadAllText(path);
var obj = JsonSerializer.Deserialize<TResult>(json)!;
returnValue = obj;
}
}
else
{
var key = this.GetStoreKeyFromRequest(request);
lock (this.memCache)
{
if (this.memCache.ContainsKey(key))
{
var item = this.memCache[key];
returnValue = (TResult)item;
}
}
}
return returnValue;
}


// TODO: clear by category (cache/replay/offline)
public virtual void ClearAll()
{
lock (this.memCache)
this.memCache.Clear();

lock (this.keys)
this.keys.Clear();

Directory.GetFiles(fileSystem.CacheDirectory, "*.mediator").ToList().ForEach(File.Delete);
Directory.GetFiles(fileSystem.AppDataDirectory, "*.mediator").ToList().ForEach(File.Delete);
}


protected virtual string GetStoreKeyFromRequest(object request)
{
if (request is IRequestKey keyProvider)
return keyProvider.GetKey();

var t = request.GetType();
var key = $"{t.Namespace}_{t.Name}";

return key;
}


protected virtual string GetPersistentStoreKey(object request, bool createIfNotExists)
{
var key = this.GetStoreKeyFromRequest(request);
this.EnsureKeyLoad();
if (this.keys.ContainsKey(key))
{
key = this.keys[key];
}
else if (createIfNotExists)
{
var newKey = Guid.NewGuid().ToString();
this.keys.Add(key, newKey);
key = newKey;

this.PersistKeyStore();
}

return key;
}


protected virtual string GetFilePath(object request, bool createIfNotExists)
{
var key = this.GetPersistentStoreKey(request, createIfNotExists);
var path = Path.Combine(fileSystem.CacheDirectory, $"{key}.mediator");
return path;
}


bool initialized = false;
protected void EnsureKeyLoad()
{
if (this.initialized)
return;

var storePath = this.KeyStorePath;
if (File.Exists(storePath))
{
var json = File.ReadAllText(storePath);
this.keys = JsonSerializer.Deserialize<Dictionary<string, string>>(json)!;
}
else
{
this.keys = new();
}
this.initialized = true;
}


protected void PersistKeyStore()
{
var json = JsonSerializer.Serialize(this.keys);
File.WriteAllText(this.KeyStorePath, json);
}


protected string KeyStorePath => Path.Combine(fileSystem.AppDataDirectory, "keys.mediator");
}
57 changes: 44 additions & 13 deletions src/Shiny.Mediator.Maui/MauiMediatorExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Shiny.Mediator.Infrastructure;
using Shiny.Mediator.Infrastructure.Impl;
using Shiny.Mediator.Maui;
using Shiny.Mediator.Middleware;

Expand All @@ -7,6 +10,28 @@ namespace Shiny.Mediator;

public static class MauiMediatorExtensions
{
/// <summary>
/// Fire & Forget task pattern that logs errors
/// </summary>
/// <param name="task"></param>
/// <param name="errorLogger"></param>
public static void RunInBackground(this Task task, ILogger errorLogger)
=> task.ContinueWith(x =>
{
if (x.Exception != null)
errorLogger.LogError(x.Exception, "Fire & Forget trapped error");
}, TaskContinuationOptions.OnlyOnFaulted);


internal static ShinyConfigurator EnsureInfrastructure(this ShinyConfigurator cfg)
{
cfg.Services.TryAddSingleton<IEventHandler<FlushAllStoresEvent>, FlushAllCacheEventHandler>();
cfg.Services.TryAddSingleton<IStorageManager, StorageManager>();
cfg.Services.TryAddSingleton(Connectivity.Current);
cfg.Services.TryAddSingleton(FileSystem.Current);
return cfg;
}

/// <summary>
/// Adds Maui Event Collector to mediator
/// </summary>
Expand All @@ -20,11 +45,13 @@ public static ShinyConfigurator UseMaui(this ShinyConfigurator cfg, bool include
if (includeStandardMiddleware)
{
cfg.AddEventExceptionHandlingMiddleware();
cfg.AddMainThreadEventMiddleware();
cfg.AddMainThreadMiddleware();

cfg.AddUserNotificationExceptionMiddleware();
cfg.AddTimedMiddleware();
cfg.AddOfflineAvailabilityMiddleware();
// cfg.AddUserNotificationExceptionMiddleware();

cfg.AddReplayStreamMiddleware();
}
return cfg;
}
Expand All @@ -36,34 +63,38 @@ public static ShinyConfigurator AddTimedMiddleware(this ShinyConfigurator cfg)

public static ShinyConfigurator AddEventExceptionHandlingMiddleware(this ShinyConfigurator cfg)
=> cfg.AddOpenEventMiddleware(typeof(ExceptionHandlerEventMiddleware<>));


public static ShinyConfigurator AddMainThreadEventMiddleware(this ShinyConfigurator cfg)
=> cfg.AddOpenEventMiddleware(typeof(MainTheadEventMiddleware<>));



public static ShinyConfigurator AddMainThreadMiddleware(this ShinyConfigurator cfg)
{
cfg.AddOpenEventMiddleware(typeof(MainTheadEventMiddleware<>));
cfg.AddOpenRequestMiddleware(typeof(MainThreadRequestHandler<,>));

return cfg;
}


public static ShinyConfigurator AddOfflineAvailabilityMiddleware(this ShinyConfigurator cfg)
{
cfg.Services.TryAddSingleton(Connectivity.Current);
cfg.Services.TryAddSingleton(FileSystem.Current);
cfg.EnsureInfrastructure();
cfg.Services.AddSingletonAsImplementedInterfaces<OfflineAvailableFlushRequestHandler>();
cfg.AddOpenRequestMiddleware(typeof(OfflineAvailableRequestMiddleware<,>));

return cfg;
}


public static ShinyConfigurator AddReplayStreamMiddleware(this ShinyConfigurator cfg)
{
cfg.Services.TryAddSingleton(Connectivity.Current);
cfg.Services.TryAddSingleton(FileSystem.Current);
cfg.EnsureInfrastructure();
cfg.AddOpenStreamMiddleware(typeof(ReplayStreamMiddleware<,>));
return cfg;
}


public static ShinyConfigurator AddUserNotificationExceptionMiddleware(this ShinyConfigurator cfg, UserExceptionRequestMiddlewareConfig config)
public static ShinyConfigurator AddUserNotificationExceptionMiddleware(this ShinyConfigurator cfg, UserExceptionRequestMiddlewareConfig? config = null)
{
cfg.Services.AddSingleton(config);
cfg.Services.AddSingleton(config ?? new());
cfg.AddOpenRequestMiddleware(typeof(UserExceptionRequestMiddleware<,>));
return cfg;
}
Expand Down
13 changes: 13 additions & 0 deletions src/Shiny.Mediator.Maui/Middleware/FlushAllCacheEventHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Shiny.Mediator.Infrastructure;

namespace Shiny.Mediator.Middleware;


public class FlushAllCacheEventHandler(IStorageManager storage) : IEventHandler<FlushAllStoresEvent>
{
public Task Handle(FlushAllStoresEvent @event, CancellationToken cancellationToken)
{
storage.ClearAll();
return Task.CompletedTask;
}
}
Loading

0 comments on commit e93e84a

Please sign in to comment.