diff --git a/Elsa.sln b/Elsa.sln index 73b357117..864a2eb11 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -250,6 +250,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Resilience.Core", "src EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Resilience", "src\modules\Elsa.Resilience\Elsa.Resilience.csproj", "{E7137FB0-1988-4562-AD8D-D0D9D2EE85F6}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "caching", "caching", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Caching", "src\modules\Elsa.Caching\Elsa.Caching.csproj", "{6F14B066-DF7B-2409-59D8-CCCA90A4974C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -418,6 +422,10 @@ Global {E7137FB0-1988-4562-AD8D-D0D9D2EE85F6}.Debug|Any CPU.Build.0 = Debug|Any CPU {E7137FB0-1988-4562-AD8D-D0D9D2EE85F6}.Release|Any CPU.ActiveCfg = Release|Any CPU {E7137FB0-1988-4562-AD8D-D0D9D2EE85F6}.Release|Any CPU.Build.0 = Release|Any CPU + {6F14B066-DF7B-2409-59D8-CCCA90A4974C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6F14B066-DF7B-2409-59D8-CCCA90A4974C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6F14B066-DF7B-2409-59D8-CCCA90A4974C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6F14B066-DF7B-2409-59D8-CCCA90A4974C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -489,6 +497,8 @@ Global {CD7DC0D1-FFDC-417A-89BE-7F32408F583E} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} {70593549-8B26-4D63-9857-6BA8BB3E31DB} = {CD7DC0D1-FFDC-417A-89BE-7F32408F583E} {E7137FB0-1988-4562-AD8D-D0D9D2EE85F6} = {CD7DC0D1-FFDC-417A-89BE-7F32408F583E} + {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} + {6F14B066-DF7B-2409-59D8-CCCA90A4974C} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E} diff --git a/src/modules/Elsa.Caching/Contracts/ICacheManager.cs b/src/modules/Elsa.Caching/Contracts/ICacheManager.cs new file mode 100644 index 000000000..6e93fe8c8 --- /dev/null +++ b/src/modules/Elsa.Caching/Contracts/ICacheManager.cs @@ -0,0 +1,32 @@ +using Elsa.Caching.Options; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; + +namespace Elsa.Caching; + +/// +/// A thin wrapper around , allowing for centralized handling of cache entries. +/// +public interface ICacheManager +{ + /// + /// Provides options for configuring caching. + /// + IOptions CachingOptions { get; } + + /// + /// Gets a change token for the specified key. + /// + IChangeToken GetToken(string key); + + /// + /// Triggers the change token for the specified key. + /// + ValueTask TriggerTokenAsync(string key, CancellationToken cancellationToken = default); + + /// + /// Gets an item from the cache, or creates it if it doesn't exist. + /// + Task GetOrCreateAsync(object key, Func> factory); +} \ No newline at end of file diff --git a/src/modules/Elsa.Caching/Contracts/IChangeTokenSignalInvoker.cs b/src/modules/Elsa.Caching/Contracts/IChangeTokenSignalInvoker.cs new file mode 100644 index 000000000..700da3d9d --- /dev/null +++ b/src/modules/Elsa.Caching/Contracts/IChangeTokenSignalInvoker.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.Primitives; + +namespace Elsa.Caching; + +/// +/// Triggers the change token associated with the specified key. +/// +public interface IChangeTokenSignalInvoker +{ + /// + /// Gets a change token for the specified key. + /// + IChangeToken GetToken(string key); + + /// + /// Triggers the change token for the specified key. + /// + ValueTask TriggerTokenAsync(string key, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/modules/Elsa.Caching/Contracts/IChangeTokenSignaler.cs b/src/modules/Elsa.Caching/Contracts/IChangeTokenSignaler.cs new file mode 100644 index 000000000..f937e2b63 --- /dev/null +++ b/src/modules/Elsa.Caching/Contracts/IChangeTokenSignaler.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.Primitives; + +namespace Elsa.Caching; + +/// +/// Provides change tokens for memory caches, allowing code to evict cache entries by triggering a signal. +/// +public interface IChangeTokenSignaler +{ + /// + /// Gets a change token for the specified key. + /// + IChangeToken GetToken(string key); + + /// + /// Triggers the change token for the specified key. + /// + ValueTask TriggerTokenAsync(string key, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/modules/Elsa.Caching/Elsa.Caching.csproj b/src/modules/Elsa.Caching/Elsa.Caching.csproj new file mode 100644 index 000000000..ab4269db6 --- /dev/null +++ b/src/modules/Elsa.Caching/Elsa.Caching.csproj @@ -0,0 +1,19 @@ + + + + + Provides caching services and utility functions common to Elsa modules. + + elsa module caching + + + + + + + + + + + + diff --git a/src/modules/Elsa.Caching/Elsa.Caching.csproj.DotSettings b/src/modules/Elsa.Caching/Elsa.Caching.csproj.DotSettings new file mode 100644 index 000000000..127787e89 --- /dev/null +++ b/src/modules/Elsa.Caching/Elsa.Caching.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/src/modules/Elsa.Caching/Extensions/ModuleExtensions.cs b/src/modules/Elsa.Caching/Extensions/ModuleExtensions.cs new file mode 100644 index 000000000..879f9c9b7 --- /dev/null +++ b/src/modules/Elsa.Caching/Extensions/ModuleExtensions.cs @@ -0,0 +1,19 @@ +using Elsa.Caching.Features; +using Elsa.Features.Services; + +// ReSharper disable once CheckNamespace +namespace Elsa.Extensions; + +/// +/// Provides methods to install and configure the distributed caching feature. +/// +public static class ModuleExtensions +{ + /// + /// Adds the feature to the system. + /// + public static MemoryCacheFeature UseMemoryCache(this IModule module, Action? configure = default) + { + return module.Configure(configure); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Caching/Features/MemoryCacheFeature.cs b/src/modules/Elsa.Caching/Features/MemoryCacheFeature.cs new file mode 100644 index 000000000..1a3ad07cc --- /dev/null +++ b/src/modules/Elsa.Caching/Features/MemoryCacheFeature.cs @@ -0,0 +1,31 @@ +using Elsa.Caching.Options; +using Elsa.Caching.Services; +using Elsa.Features.Abstractions; +using Elsa.Features.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Caching.Features; + +/// +/// Configures the MemoryCache. +/// +public class MemoryCacheFeature(IModule module) : FeatureBase(module) +{ + /// + /// A delegate to configure the . + /// + public Action CachingOptions { get; set; } = _ => { }; + + /// + public override void Apply() + { + Services.Configure(CachingOptions); + + Services + .AddMemoryCache() + .AddSingleton() + .AddSingleton() + .AddSingleton() + ; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Caching/FodyWeavers.xml b/src/modules/Elsa.Caching/FodyWeavers.xml new file mode 100644 index 000000000..00e1d9a1c --- /dev/null +++ b/src/modules/Elsa.Caching/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/modules/Elsa.Caching/Options/CachingOptions.cs b/src/modules/Elsa.Caching/Options/CachingOptions.cs new file mode 100644 index 000000000..4b9b5dd02 --- /dev/null +++ b/src/modules/Elsa.Caching/Options/CachingOptions.cs @@ -0,0 +1,12 @@ +namespace Elsa.Caching.Options; + +/// +/// Provides options for configuring caching. +/// +public class CachingOptions +{ + /// + /// Gets or sets the duration for which cache entries are stored. + /// + public TimeSpan CacheDuration { get; set; } = TimeSpan.FromMinutes(1); +} \ No newline at end of file diff --git a/src/modules/Elsa.Caching/Services/CacheManager.cs b/src/modules/Elsa.Caching/Services/CacheManager.cs new file mode 100644 index 000000000..6db501327 --- /dev/null +++ b/src/modules/Elsa.Caching/Services/CacheManager.cs @@ -0,0 +1,31 @@ +using Elsa.Caching.Options; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; + +namespace Elsa.Caching.Services; + +/// +public class CacheManager(IMemoryCache memoryCache, IChangeTokenSignaler changeTokenSignaler, IOptions options) : ICacheManager +{ + /// + public IOptions CachingOptions => options; + + /// + public IChangeToken GetToken(string key) + { + return changeTokenSignaler.GetToken(key); + } + + /// + public ValueTask TriggerTokenAsync(string key, CancellationToken cancellationToken = default) + { + return changeTokenSignaler.TriggerTokenAsync(key, cancellationToken); + } + + /// + public async Task GetOrCreateAsync(object key, Func> factory) + { + return await memoryCache.GetOrCreateAsync(key, async entry => await factory(entry)); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Caching/Services/ChangeTokenSignalInvoker.cs b/src/modules/Elsa.Caching/Services/ChangeTokenSignalInvoker.cs new file mode 100644 index 000000000..1bffad120 --- /dev/null +++ b/src/modules/Elsa.Caching/Services/ChangeTokenSignalInvoker.cs @@ -0,0 +1,38 @@ +using System.Collections.Concurrent; +using Microsoft.Extensions.Primitives; + +namespace Elsa.Caching.Services; + +/// +public class ChangeTokenSignalInvoker : IChangeTokenSignalInvoker +{ + private readonly ConcurrentDictionary _changeTokens = new(); + + /// + public IChangeToken GetToken(string key) + { + return _changeTokens.GetOrAdd( + key, + _ => + { + var cancellationTokenSource = new CancellationTokenSource(); + var changeToken = new CancellationChangeToken(cancellationTokenSource.Token); + return new ChangeTokenInfo(changeToken, cancellationTokenSource); + }).ChangeToken; + } + + /// + public ValueTask TriggerTokenAsync(string key, CancellationToken cancellationToken = default) + { + if (_changeTokens.TryRemove(key, out var changeTokenInfo)) + changeTokenInfo.TokenSource.Cancel(); + + return default; + } + + private readonly struct ChangeTokenInfo(IChangeToken changeToken, CancellationTokenSource tokenSource) + { + public IChangeToken ChangeToken { get; } = changeToken; + public CancellationTokenSource TokenSource { get; } = tokenSource; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Caching/Services/ChangeTokenSignaler.cs b/src/modules/Elsa.Caching/Services/ChangeTokenSignaler.cs new file mode 100644 index 000000000..69d9f2fdd --- /dev/null +++ b/src/modules/Elsa.Caching/Services/ChangeTokenSignaler.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.Primitives; + +namespace Elsa.Caching.Services; + +/// +public class ChangeTokenSignaler(IChangeTokenSignalInvoker invoker) : IChangeTokenSignaler +{ + /// + public IChangeToken GetToken(string key) + { + return invoker.GetToken(key); + } + + /// + public ValueTask TriggerTokenAsync(string key, CancellationToken cancellationToken = default) + { + return invoker.TriggerTokenAsync(key, cancellationToken); + } +} \ No newline at end of file