Return Caching to Core.

This commit is contained in:
Matt 2025-05-20 12:11:27 +01:00
parent c423b324f9
commit 10f92bf16b
13 changed files with 254 additions and 0 deletions

View file

@ -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}

View file

@ -0,0 +1,32 @@
using Elsa.Caching.Options;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
namespace Elsa.Caching;
/// <summary>
/// A thin wrapper around <see cref="IMemoryCache"/>, allowing for centralized handling of cache entries.
/// </summary>
public interface ICacheManager
{
/// <summary>
/// Provides options for configuring caching.
/// </summary>
IOptions<CachingOptions> CachingOptions { get; }
/// <summary>
/// Gets a change token for the specified key.
/// </summary>
IChangeToken GetToken(string key);
/// <summary>
/// Triggers the change token for the specified key.
/// </summary>
ValueTask TriggerTokenAsync(string key, CancellationToken cancellationToken = default);
/// <summary>
/// Gets an item from the cache, or creates it if it doesn't exist.
/// </summary>
Task<TItem?> GetOrCreateAsync<TItem>(object key, Func<ICacheEntry, Task<TItem>> factory);
}

View file

@ -0,0 +1,19 @@
using Microsoft.Extensions.Primitives;
namespace Elsa.Caching;
/// <summary>
/// Triggers the change token associated with the specified key.
/// </summary>
public interface IChangeTokenSignalInvoker
{
/// <summary>
/// Gets a change token for the specified key.
/// </summary>
IChangeToken GetToken(string key);
/// <summary>
/// Triggers the change token for the specified key.
/// </summary>
ValueTask TriggerTokenAsync(string key, CancellationToken cancellationToken = default);
}

View file

@ -0,0 +1,19 @@
using Microsoft.Extensions.Primitives;
namespace Elsa.Caching;
/// <summary>
/// Provides change tokens for memory caches, allowing code to evict cache entries by triggering a signal.
/// </summary>
public interface IChangeTokenSignaler
{
/// <summary>
/// Gets a change token for the specified key.
/// </summary>
IChangeToken GetToken(string key);
/// <summary>
/// Triggers the change token for the specified key.
/// </summary>
ValueTask TriggerTokenAsync(string key, CancellationToken cancellationToken = default);
}

View file

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>
Provides caching services and utility functions common to Elsa modules.
</Description>
<PackageTags>elsa module caching</PackageTags>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" />
<PackageReference Include="Scrutor" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\common\Elsa.Features\Elsa.Features.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=contracts/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View file

@ -0,0 +1,19 @@
using Elsa.Caching.Features;
using Elsa.Features.Services;
// ReSharper disable once CheckNamespace
namespace Elsa.Extensions;
/// <summary>
/// Provides methods to install and configure the distributed caching feature.
/// </summary>
public static class ModuleExtensions
{
/// <summary>
/// Adds the <see cref="MemoryCacheFeature"/> feature to the system.
/// </summary>
public static MemoryCacheFeature UseMemoryCache(this IModule module, Action<MemoryCacheFeature>? configure = default)
{
return module.Configure(configure);
}
}

View file

@ -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;
/// <summary>
/// Configures the MemoryCache.
/// </summary>
public class MemoryCacheFeature(IModule module) : FeatureBase(module)
{
/// <summary>
/// A delegate to configure the <see cref="CachingOptions"/>.
/// </summary>
public Action<CachingOptions> CachingOptions { get; set; } = _ => { };
/// <inheritdoc />
public override void Apply()
{
Services.Configure(CachingOptions);
Services
.AddMemoryCache()
.AddSingleton<ICacheManager, CacheManager>()
.AddSingleton<IChangeTokenSignalInvoker, ChangeTokenSignalInvoker>()
.AddSingleton<IChangeTokenSignaler, ChangeTokenSignaler>()
;
}
}

View file

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait />
</Weavers>

View file

@ -0,0 +1,12 @@
namespace Elsa.Caching.Options;
/// <summary>
/// Provides options for configuring caching.
/// </summary>
public class CachingOptions
{
/// <summary>
/// Gets or sets the duration for which cache entries are stored.
/// </summary>
public TimeSpan CacheDuration { get; set; } = TimeSpan.FromMinutes(1);
}

View file

@ -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;
/// <inheritdoc />
public class CacheManager(IMemoryCache memoryCache, IChangeTokenSignaler changeTokenSignaler, IOptions<CachingOptions> options) : ICacheManager
{
/// <inheritdoc />
public IOptions<CachingOptions> CachingOptions => options;
/// <inheritdoc />
public IChangeToken GetToken(string key)
{
return changeTokenSignaler.GetToken(key);
}
/// <inheritdoc />
public ValueTask TriggerTokenAsync(string key, CancellationToken cancellationToken = default)
{
return changeTokenSignaler.TriggerTokenAsync(key, cancellationToken);
}
/// <inheritdoc />
public async Task<TItem?> GetOrCreateAsync<TItem>(object key, Func<ICacheEntry, Task<TItem>> factory)
{
return await memoryCache.GetOrCreateAsync(key, async entry => await factory(entry));
}
}

View file

@ -0,0 +1,38 @@
using System.Collections.Concurrent;
using Microsoft.Extensions.Primitives;
namespace Elsa.Caching.Services;
/// <inheritdoc />
public class ChangeTokenSignalInvoker : IChangeTokenSignalInvoker
{
private readonly ConcurrentDictionary<string, ChangeTokenInfo> _changeTokens = new();
/// <inheritdoc />
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;
}
/// <inheritdoc />
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;
}
}

View file

@ -0,0 +1,19 @@
using Microsoft.Extensions.Primitives;
namespace Elsa.Caching.Services;
/// <inheritdoc />
public class ChangeTokenSignaler(IChangeTokenSignalInvoker invoker) : IChangeTokenSignaler
{
/// <inheritdoc />
public IChangeToken GetToken(string key)
{
return invoker.GetToken(key);
}
/// <inheritdoc />
public ValueTask TriggerTokenAsync(string key, CancellationToken cancellationToken = default)
{
return invoker.TriggerTokenAsync(key, cancellationToken);
}
}