diff --git a/Directory.Packages.props b/Directory.Packages.props
index ce7bcd4..737af3f 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -151,6 +151,7 @@
+
diff --git a/Elsa.sln b/Elsa.sln
index 5baed45..89d4e38 100644
--- a/Elsa.sln
+++ b/Elsa.sln
@@ -419,6 +419,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Caspian.WorkflowFramework",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Caspian.WorkflowFramework.Sample", "src\apps\Caspian.WorkflowFramework.Sample\Caspian.WorkflowFramework.Sample.csproj", "{C260D75F-19EB-4982-A98B-D8E126776910}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Caching.Redis", "src\modules\Elsa.Caching.Redis\Elsa.Caching.Redis.csproj", "{CEB64AF4-A63C-4204-828A-53E6096095A0}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Caching.UnitTests", "test\unit\Elsa.Caching.UnitTests\Elsa.Caching.UnitTests.csproj", "{86197750-6B7F-4F65-8456-E30E82F53F1C}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Caching.ProcessTests", "test\integration\Elsa.Caching.ProcessTests\Elsa.Caching.ProcessTests.csproj", "{0368A4B4-FA10-489D-B390-F141A6225720}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -875,6 +881,18 @@ Global
{C260D75F-19EB-4982-A98B-D8E126776910}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C260D75F-19EB-4982-A98B-D8E126776910}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C260D75F-19EB-4982-A98B-D8E126776910}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CEB64AF4-A63C-4204-828A-53E6096095A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CEB64AF4-A63C-4204-828A-53E6096095A0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CEB64AF4-A63C-4204-828A-53E6096095A0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CEB64AF4-A63C-4204-828A-53E6096095A0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {86197750-6B7F-4F65-8456-E30E82F53F1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {86197750-6B7F-4F65-8456-E30E82F53F1C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {86197750-6B7F-4F65-8456-E30E82F53F1C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {86197750-6B7F-4F65-8456-E30E82F53F1C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0368A4B4-FA10-489D-B390-F141A6225720}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0368A4B4-FA10-489D-B390-F141A6225720}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0368A4B4-FA10-489D-B390-F141A6225720}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0368A4B4-FA10-489D-B390-F141A6225720}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1030,6 +1048,9 @@ Global
{C583AF05-D517-4B7F-8955-6B61500ED3D8} = {7FD1FD1E-5778-4065-AAA5-1F878129EF77}
{A0F413B7-FA0F-472D-8BC2-6193DA440072} = {D92BEAB2-60D6-4BB4-885A-6BA681C6CCF1}
{C260D75F-19EB-4982-A98B-D8E126776910} = {D92BEAB2-60D6-4BB4-885A-6BA681C6CCF1}
+ {CEB64AF4-A63C-4204-828A-53E6096095A0} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
+ {86197750-6B7F-4F65-8456-E30E82F53F1C} = {18453B51-25EB-4317-A4B3-B10518252E92}
+ {0368A4B4-FA10-489D-B390-F141A6225720} = {1B8D5897-902E-4632-8698-E89CAF3DDF54}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E}
diff --git a/NuGet.rar b/NuGet.rar
deleted file mode 100644
index 228c610..0000000
Binary files a/NuGet.rar and /dev/null differ
diff --git a/scripts/Test-WorkflowCache.ps1 b/scripts/Test-WorkflowCache.ps1
new file mode 100644
index 0000000..43aed32
--- /dev/null
+++ b/scripts/Test-WorkflowCache.ps1
@@ -0,0 +1,31 @@
+param(
+ [Parameter(Mandatory = $true)]
+ [string] $RedisConnection
+)
+
+$cacheRepoRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
+$cachePreviousConnection = $env:REDIS_TEST_CONNECTION
+$cacheValidationRoot = Join-Path $cacheRepoRoot 'artifacts/cache-validation'
+New-Item -ItemType Directory -Path $cacheValidationRoot -Force | Out-Null
+$env:REDIS_TEST_CONNECTION = $RedisConnection
+
+try {
+ Push-Location $cacheRepoRoot
+ dotnet test test/unit/Elsa.Caching.UnitTests/Elsa.Caching.UnitTests.csproj -m:1 -p:BuildInParallel=false -p:GeneratePackageOnBuild=false
+ if ($LASTEXITCODE -ne 0) { throw 'Workflow cache tests failed.' }
+ dotnet build test/integration/Elsa.Caching.ProcessTests/Elsa.Caching.ProcessTests.csproj -m:1 -p:BuildInParallel=false -p:GeneratePackageOnBuild=false
+ if ($LASTEXITCODE -ne 0) { throw 'Workflow cache process test build failed.' }
+
+ $cacheTestRun = [Guid]::NewGuid().ToString('N')
+ $cacheDatabasePath = Join-Path $cacheValidationRoot ($cacheTestRun + '.db')
+ $cacheKeyPrefix = 'workflow-process-tests:' + $cacheTestRun + ':'
+ $cacheProcessProgram = Join-Path $cacheRepoRoot 'test/integration/Elsa.Caching.ProcessTests/bin/Debug/net9.0/Elsa.Caching.ProcessTests.dll'
+ foreach ($cacheProcessMode in @('write', 'read', 'invalidate', 'read-updated', 'pending', 'bypass')) {
+ dotnet $cacheProcessProgram $cacheDatabasePath $cacheKeyPrefix $cacheProcessMode
+ if ($LASTEXITCODE -ne 0) { throw "Independent process test failed: $cacheProcessMode" }
+ }
+}
+finally {
+ Pop-Location
+ $env:REDIS_TEST_CONNECTION = $cachePreviousConnection
+}
diff --git a/src/apps/Elsa.Server.Web/Configuration/RedisSettings.cs b/src/apps/Elsa.Server.Web/Configuration/RedisSettings.cs
new file mode 100644
index 0000000..e9891a5
--- /dev/null
+++ b/src/apps/Elsa.Server.Web/Configuration/RedisSettings.cs
@@ -0,0 +1,22 @@
+namespace Elsa.Server.Web.Configuration;
+
+/// Host connection settings, separate from the workflow cache policy.
+public class RedisSettings
+{
+ public bool AllowAdmin { get; set; }
+ public bool Ssl { get; set; }
+ public int ConnectTimeout { get; set; } = 5000;
+ public int ConnectRetry { get; set; } = 2;
+ public int SyncTimeout { get; set; } = 5000;
+ public int Database { get; set; } = 1;
+ public string? ServiceName { get; set; }
+ public string? User { get; set; }
+ public string? Password { get; set; }
+ public List Hosts { get; set; } = new();
+}
+
+public class RedisHostSettings
+{
+ public string Host { get; set; } = string.Empty;
+ public int Port { get; set; } = 6379;
+}
diff --git a/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj b/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj
index c6414bd..acc6342 100644
--- a/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj
+++ b/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj
@@ -4,6 +4,8 @@
+
+
diff --git a/src/apps/Elsa.Server.Web/Extensions/RedisServiceCollectionExtensions.cs b/src/apps/Elsa.Server.Web/Extensions/RedisServiceCollectionExtensions.cs
new file mode 100644
index 0000000..2f6b9e2
--- /dev/null
+++ b/src/apps/Elsa.Server.Web/Extensions/RedisServiceCollectionExtensions.cs
@@ -0,0 +1,43 @@
+using Elsa.Server.Web.Configuration;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using StackExchange.Redis;
+
+namespace Elsa.Server.Web.Extensions;
+
+public static class RedisServiceCollectionExtensions
+{
+ public static IServiceCollection AddWorkflowRedis(this IServiceCollection services, IConfiguration configuration)
+ {
+ services.TryAddSingleton(_ => ConnectionMultiplexer.Connect(
+ CreateConfiguration(configuration.GetSection("Redis").Get() ?? new())));
+ return services;
+ }
+
+ public static ConfigurationOptions CreateConfiguration(RedisSettings settings)
+ {
+ var options = new ConfigurationOptions
+ {
+ AllowAdmin = settings.AllowAdmin,
+ Ssl = settings.Ssl,
+ AbortOnConnectFail = false,
+ ConnectTimeout = settings.ConnectTimeout,
+ ConnectRetry = settings.ConnectRetry,
+ SyncTimeout = settings.SyncTimeout,
+ DefaultDatabase = settings.Database,
+ User = string.IsNullOrWhiteSpace(settings.User) ? null : settings.User,
+ Password = string.IsNullOrWhiteSpace(settings.Password) ? null : settings.Password
+ };
+ foreach (var host in settings.Hosts.Where(x => !string.IsNullOrWhiteSpace(x.Host)))
+ {
+ if (host.Port is < 1 or > 65535) throw new InvalidOperationException("Redis host port must be between 1 and 65535.");
+ options.EndPoints.Add(host.Host, host.Port);
+ }
+ if (options.EndPoints.Count == 0) throw new InvalidOperationException("Redis:Hosts must contain a Redis or Sentinel endpoint.");
+ if (!string.IsNullOrWhiteSpace(settings.ServiceName))
+ {
+ options.ServiceName = settings.ServiceName;
+ options.TieBreaker = string.Empty;
+ }
+ return options;
+ }
+}
diff --git a/src/apps/Elsa.Server.Web/Extensions/WorkflowRuntimeFeatureExtensions.cs b/src/apps/Elsa.Server.Web/Extensions/WorkflowRuntimeFeatureExtensions.cs
new file mode 100644
index 0000000..3755c64
--- /dev/null
+++ b/src/apps/Elsa.Server.Web/Extensions/WorkflowRuntimeFeatureExtensions.cs
@@ -0,0 +1,62 @@
+using Elsa.Common.DistributedHosting.DistributedLocks;
+using Elsa.Workflows.Runtime.Features;
+using Medallion.Threading;
+using Medallion.Threading.FileSystem;
+using Medallion.Threading.Postgres;
+using Medallion.Threading.Redis;
+using StackExchange.Redis;
+
+namespace Elsa.Server.Web.Extensions;
+
+public static class WorkflowRuntimeFeatureExtensions
+{
+ public static WorkflowRuntimeFeature UseConfiguredDistributedLocking(this WorkflowRuntimeFeature runtime, IConfiguration configuration)
+ {
+ var legacySettings = configuration.GetSection("Runtime:DistributedLocking");
+ var settings = configuration.GetSection("Workflow:Runtime:DistributedLocking");
+
+ runtime.DistributedLockingOptions = options =>
+ {
+ legacySettings.Bind(options);
+ settings.Bind(options);
+ };
+
+ var providerName = settings["Provider"] ?? legacySettings["Provider"];
+ runtime.DistributedLockProvider = sp =>
+ {
+ switch (providerName?.ToUpperInvariant())
+ {
+ case "REDIS":
+ {
+ var database = settings.GetValue("Database")
+ ?? legacySettings.GetValue("Database")
+ ?? configuration.GetValue("Redis:Database") ?? -1;
+ var prefix = settings["KeyPrefix"] ?? legacySettings["KeyPrefix"] ?? "nextbank:workflow:locks:";
+ if (database < -1) throw new InvalidOperationException("Workflow lock database must be -1 or a non-negative number.");
+ if (string.IsNullOrWhiteSpace(prefix)) throw new InvalidOperationException("Workflow lock KeyPrefix must not be empty.");
+ var connection = sp.GetRequiredService();
+ return new PrefixedDistributedLockProvider(new RedisDistributedSynchronizationProvider(connection.GetDatabase(database)), prefix);
+ }
+ case "POSTGRES":
+ return new PostgresDistributedSynchronizationProvider(configuration.GetConnectionString("PostgreSql")!, options =>
+ {
+ options.KeepaliveCadence(TimeSpan.FromMinutes(5));
+ options.UseMultiplexing();
+ });
+ case "FILE":
+ return new FileDistributedSynchronizationProvider(new(Path.Combine(Directory.GetCurrentDirectory(), "App_Data", "locks")));
+ case null:
+ case "NOOP":
+ return new NoopDistributedSynchronizationProvider();
+ default:
+ throw new InvalidOperationException($"Unsupported workflow lock provider '{providerName}'. Configure Redis, File, Postgres or Noop.");
+ }
+ };
+ return runtime;
+ }
+
+ private sealed class PrefixedDistributedLockProvider(IDistributedLockProvider provider, string prefix) : IDistributedLockProvider
+ {
+ public IDistributedLock CreateLock(string name) => provider.CreateLock(prefix + name);
+ }
+}
diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs
index c97a88c..429bc7c 100644
--- a/src/apps/Elsa.Server.Web/Program.cs
+++ b/src/apps/Elsa.Server.Web/Program.cs
@@ -3,7 +3,6 @@ using Elsa.Alterations.Extensions;
using Elsa.Alterations.MassTransit.Extensions;
using Elsa.Caching.Options;
using Elsa.Common.Codecs;
-using Elsa.Common.DistributedHosting.DistributedLocks;
using Elsa.Common.RecurringTasks;
using Elsa.Common.Serialization;
using Elsa.Dapper.Extensions;
@@ -64,9 +63,6 @@ using Hangfire.PostgreSql.Factories;
using Hangfire.SqlServer;
using Hangfire.Storage.SQLite;
using JetBrains.Annotations;
-using Medallion.Threading.FileSystem;
-using Medallion.Threading.Postgres;
-using Medallion.Threading.Redis;
using Microsoft.Extensions.Options;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
@@ -76,7 +72,6 @@ using Proto.Persistence.Sqlite;
using Proto.Persistence.SqlServer;
using Proto.Remote;
using Proto.Remote.GrpcNet;
-using StackExchange.Redis;
// ReSharper disable RedundantAssignment
const PersistenceProvider persistenceProvider = PersistenceProvider.EntityFrameworkCore;
@@ -119,8 +114,6 @@ var cockroachDbConnectionString = configuration.GetConnectionString("CockroachDb
var mongoDbConnectionString = configuration.GetConnectionString("MongoDb")!;
var azureServiceBusConnectionString = configuration.GetConnectionString("AzureServiceBus")!;
var rabbitMqConnectionString = configuration.GetConnectionString("RabbitMq")!;
-var redisConnectionString = configuration.GetConnectionString("Redis")!;
-var distributedLockProviderName = configuration.GetSection("Runtime:DistributedLocking")["Provider"];
var appRole = Enum.Parse(configuration["AppRole"] ?? "Default");
var sqlDatabaseProvider = Enum.Parse(configuration["DatabaseProvider"] ?? "Sqlite");
@@ -164,10 +157,13 @@ if (useManualOtelInstrumentation)
}
// Add Elsa services.
+services.AddWorkflowRedis(configuration);
services.AddScoped();
services
.AddElsa(elsa =>
{
+ elsa.UseCaching(cache => cache.RegisterRedis());
+
if (persistenceProvider == PersistenceProvider.MongoDb)
elsa.UseMongoDb(mongoDbConnectionString);
@@ -324,7 +320,7 @@ services
management.UseMassTransitDispatcher();
if (useCaching)
- management.UseCache();
+ management.UseCache(configuration.GetSection("Workflow:Cache"));
management.SetDefaultLogPersistenceMode(LogPersistenceMode.Inherit);
management.UseReadOnlyMode(useReadOnlyMode);
@@ -394,31 +390,7 @@ services
if (useCaching)
runtime.UseCache();
- runtime.DistributedLockingOptions = options => configuration.GetSection("Runtime:DistributedLocking").Bind(options);
-
- runtime.DistributedLockProvider = _ =>
- {
- switch (distributedLockProviderName)
- {
- case "Postgres":
- return new PostgresDistributedSynchronizationProvider(postgresConnectionString, options =>
- {
- options.KeepaliveCadence(TimeSpan.FromMinutes(5));
- options.UseMultiplexing();
- });
- case "Redis":
- {
- var connectionMultiplexer = ConnectionMultiplexer.Connect(redisConnectionString);
- var database = connectionMultiplexer.GetDatabase();
- return new RedisDistributedSynchronizationProvider(database);
- }
- case "File":
- return new FileDistributedSynchronizationProvider(new(Path.Combine(Directory.GetCurrentDirectory(), "App_Data", "locks")));
- case "Noop":
- default:
- return new NoopDistributedSynchronizationProvider();
- }
- };
+ runtime.UseConfiguredDistributedLocking(configuration);
})
.UseEnvironments(environments => environments.EnvironmentsOptions = options => configuration.GetSection("Environments").Bind(options))
.UseScheduling(scheduling =>
@@ -758,7 +730,6 @@ services.Configure(options =>
services.Configure(options => { options.InactivityThreshold = TimeSpan.FromSeconds(15); });
services.Configure(options => options.Ttl = TimeSpan.FromSeconds(10));
-services.Configure(options => options.CacheDuration = TimeSpan.FromDays(1));
services.Configure(options => options.DefaultIncidentStrategy = typeof(ContinueWithIncidentsStrategy));
services.AddHealthChecks();
services.AddControllers();
@@ -825,4 +796,4 @@ public partial class Program
/// Set by the test runner to configure the module for testing.
///
public static Action? ConfigureForTest { get; set; }
-}
\ No newline at end of file
+}
diff --git a/src/apps/Elsa.Server.Web/Properties/launchSettings.json b/src/apps/Elsa.Server.Web/Properties/launchSettings.json
index 2bff847..65a64df 100644
--- a/src/apps/Elsa.Server.Web/Properties/launchSettings.json
+++ b/src/apps/Elsa.Server.Web/Properties/launchSettings.json
@@ -12,7 +12,7 @@
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
- "applicationUrl": "https://localhost:5002;http://localhost:5072",
+ "applicationUrl": "https://localhost:5001;http://localhost:5072",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
diff --git a/src/apps/Elsa.Server.Web/appsettings.Development.json b/src/apps/Elsa.Server.Web/appsettings.Development.json
new file mode 100644
index 0000000..728f986
--- /dev/null
+++ b/src/apps/Elsa.Server.Web/appsettings.Development.json
@@ -0,0 +1,31 @@
+{
+ "Redis": {
+ "AllowAdmin": true,
+ "Ssl": false,
+ "ConnectTimeout": 5000,
+ "ConnectRetry": 2,
+ "Database": 1,
+ "ServiceName": "",
+ "Hosts": [
+ { "Host": "localhost", "Port": 6379 }
+ ]
+ },
+ "Workflow": {
+ "Cache": {
+ "Enabled": true,
+ "Provider": "Redis",
+ "Duration": "1.00:00:00",
+ "Redis": {
+ "Database": 1,
+ "KeyPrefix": "nextbank:workflow:cache:"
+ }
+ },
+ "Runtime": {
+ "DistributedLocking": {
+ "Provider": "Redis",
+ "Database": 1,
+ "KeyPrefix": "nextbank:workflow:locks:"
+ }
+ }
+ }
+}
diff --git a/src/clients/Elsa.Api.Client/Elsa.Api.Client.csproj b/src/clients/Elsa.Api.Client/Elsa.Api.Client.csproj
index 82d63fa..3332e92 100644
--- a/src/clients/Elsa.Api.Client/Elsa.Api.Client.csproj
+++ b/src/clients/Elsa.Api.Client/Elsa.Api.Client.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Api.Client
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides HTTP clients for the Elsa Workflows REST API.
diff --git a/src/common/Elsa.Api.Common/Elsa.Api.Common.csproj b/src/common/Elsa.Api.Common/Elsa.Api.Common.csproj
index 5f193b5..495975b 100644
--- a/src/common/Elsa.Api.Common/Elsa.Api.Common.csproj
+++ b/src/common/Elsa.Api.Common/Elsa.Api.Common.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Api.Common
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides common features to modules that expose API endpoints.
diff --git a/src/common/Elsa.DropIns.Core/Elsa.DropIns.Core.csproj b/src/common/Elsa.DropIns.Core/Elsa.DropIns.Core.csproj
index b828127..97b1672 100644
--- a/src/common/Elsa.DropIns.Core/Elsa.DropIns.Core.csproj
+++ b/src/common/Elsa.DropIns.Core/Elsa.DropIns.Core.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.DropIns.Core
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Reference this package when developing drop-ins for Elsa Workflows.
diff --git a/src/common/Elsa.DropIns/Elsa.DropIns.csproj b/src/common/Elsa.DropIns/Elsa.DropIns.csproj
index ad755ae..71305e9 100644
--- a/src/common/Elsa.DropIns/Elsa.DropIns.csproj
+++ b/src/common/Elsa.DropIns/Elsa.DropIns.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.DropIns
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides drop-in features for Elsa workflows.
diff --git a/src/common/Elsa.Features/Elsa.Features.csproj b/src/common/Elsa.Features/Elsa.Features.csproj
index 936cad7..acbe4c1 100644
--- a/src/common/Elsa.Features/Elsa.Features.csproj
+++ b/src/common/Elsa.Features/Elsa.Features.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Features
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides utilities to turn your class library into a module that provides features.
diff --git a/src/common/Elsa.Mediator/Elsa.Mediator.csproj b/src/common/Elsa.Mediator/Elsa.Mediator.csproj
index 075676b..5df5b7a 100644
--- a/src/common/Elsa.Mediator/Elsa.Mediator.csproj
+++ b/src/common/Elsa.Mediator/Elsa.Mediator.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Mediator
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides a mediator service inspired by MediatR.
diff --git a/src/common/Elsa.Testing.Shared.Component/Elsa.Testing.Shared.Component.csproj b/src/common/Elsa.Testing.Shared.Component/Elsa.Testing.Shared.Component.csproj
index 3ecfae7..9f7bbf3 100644
--- a/src/common/Elsa.Testing.Shared.Component/Elsa.Testing.Shared.Component.csproj
+++ b/src/common/Elsa.Testing.Shared.Component/Elsa.Testing.Shared.Component.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Testing.Shared.Component
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides common utilities that are helpful when writing component tests against Elsa Workflows functionality.
diff --git a/src/common/Elsa.Testing.Shared.Integration/Elsa.Testing.Shared.Integration.csproj b/src/common/Elsa.Testing.Shared.Integration/Elsa.Testing.Shared.Integration.csproj
index 899d965..f9c65f1 100644
--- a/src/common/Elsa.Testing.Shared.Integration/Elsa.Testing.Shared.Integration.csproj
+++ b/src/common/Elsa.Testing.Shared.Integration/Elsa.Testing.Shared.Integration.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Testing.Shared.Integration
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides common utilities that are helpful when writing integration tests against Elsa Workflows functionality.
diff --git a/src/common/Elsa.Testing.Shared/Elsa.Testing.Shared.csproj b/src/common/Elsa.Testing.Shared/Elsa.Testing.Shared.csproj
index 8371cfa..2f104d5 100644
--- a/src/common/Elsa.Testing.Shared/Elsa.Testing.Shared.csproj
+++ b/src/common/Elsa.Testing.Shared/Elsa.Testing.Shared.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Testing.Shared
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides common utilities that are helpful when writing tests against Elsa Workflows functionality.
diff --git a/src/modules/Elsa.Alterations.Core/Elsa.Alterations.Core.csproj b/src/modules/Elsa.Alterations.Core/Elsa.Alterations.Core.csproj
index ce7b7cd..4787134 100644
--- a/src/modules/Elsa.Alterations.Core/Elsa.Alterations.Core.csproj
+++ b/src/modules/Elsa.Alterations.Core/Elsa.Alterations.Core.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Alterations.Core
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides core interfaces, models and services that support the alteration engine.
diff --git a/src/modules/Elsa.Alterations.MassTransit/Elsa.Alterations.MassTransit.csproj b/src/modules/Elsa.Alterations.MassTransit/Elsa.Alterations.MassTransit.csproj
index 51cfdb0..f563123 100644
--- a/src/modules/Elsa.Alterations.MassTransit/Elsa.Alterations.MassTransit.csproj
+++ b/src/modules/Elsa.Alterations.MassTransit/Elsa.Alterations.MassTransit.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Alterations.MassTransit
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides alteration background job runners using MassTransit.
diff --git a/src/modules/Elsa.Alterations/Elsa.Alterations.csproj b/src/modules/Elsa.Alterations/Elsa.Alterations.csproj
index 9ad1870..6411851 100644
--- a/src/modules/Elsa.Alterations/Elsa.Alterations.csproj
+++ b/src/modules/Elsa.Alterations/Elsa.Alterations.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Alterations
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides alterations for workflow instances.
diff --git a/src/modules/Elsa.CSharp/Elsa.CSharp.csproj b/src/modules/Elsa.CSharp/Elsa.CSharp.csproj
index d7e56a2..9cabcc3 100644
--- a/src/modules/Elsa.CSharp/Elsa.CSharp.csproj
+++ b/src/modules/Elsa.CSharp/Elsa.CSharp.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.CSharp
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides a CSharp expression provider.
diff --git a/src/modules/Elsa.Caching.Distributed.MassTransit/Elsa.Caching.Distributed.MassTransit.csproj b/src/modules/Elsa.Caching.Distributed.MassTransit/Elsa.Caching.Distributed.MassTransit.csproj
index 2aafb37..190440c 100644
--- a/src/modules/Elsa.Caching.Distributed.MassTransit/Elsa.Caching.Distributed.MassTransit.csproj
+++ b/src/modules/Elsa.Caching.Distributed.MassTransit/Elsa.Caching.Distributed.MassTransit.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Caching.Distributed.MassTransit
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides distributed caching services leveraging MassTransit for transport of signals.
diff --git a/src/modules/Elsa.Caching.Distributed.ProtoActor/Elsa.Caching.Distributed.ProtoActor.csproj b/src/modules/Elsa.Caching.Distributed.ProtoActor/Elsa.Caching.Distributed.ProtoActor.csproj
index 81e6fa1..13f1ba9 100644
--- a/src/modules/Elsa.Caching.Distributed.ProtoActor/Elsa.Caching.Distributed.ProtoActor.csproj
+++ b/src/modules/Elsa.Caching.Distributed.ProtoActor/Elsa.Caching.Distributed.ProtoActor.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Caching.Distributed.ProtoActor
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides distributed caching services leveraging Proto.Actor for transport of signals.
diff --git a/src/modules/Elsa.Caching.Distributed/Elsa.Caching.Distributed.csproj b/src/modules/Elsa.Caching.Distributed/Elsa.Caching.Distributed.csproj
index 49303a7..0246b27 100644
--- a/src/modules/Elsa.Caching.Distributed/Elsa.Caching.Distributed.csproj
+++ b/src/modules/Elsa.Caching.Distributed/Elsa.Caching.Distributed.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Caching.Distributed
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides distributed caching services and utility functions common to Elsa modules.
diff --git a/src/modules/Elsa.Caching.Redis/Elsa.Caching.Redis.csproj b/src/modules/Elsa.Caching.Redis/Elsa.Caching.Redis.csproj
new file mode 100644
index 0000000..3a0ba76
--- /dev/null
+++ b/src/modules/Elsa.Caching.Redis/Elsa.Caching.Redis.csproj
@@ -0,0 +1,21 @@
+
+
+ Caspian.Elsa.Caching.Redis
+ Caspian
+ Behzad Alizadeh
+ 1.1.0-preview.11
+ True
+
+ Bundles the most commonly-used packages when building an Elsa workflows application.
+
+ Elsa
+ elsa bundle
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/Elsa.Caching.Redis/Extensions/CacheFeatureExtensions.cs b/src/modules/Elsa.Caching.Redis/Extensions/CacheFeatureExtensions.cs
new file mode 100644
index 0000000..a08f9d1
--- /dev/null
+++ b/src/modules/Elsa.Caching.Redis/Extensions/CacheFeatureExtensions.cs
@@ -0,0 +1,31 @@
+using Elsa.Caching.Features;
+using Elsa.Caching.Redis.Features;
+using Elsa.Caching.Redis.Options;
+using Elsa.Caching.Redis.Services;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Elsa.Extensions;
+
+public static class RedisCacheFeatureExtensions
+{
+ public static CacheFeature RegisterRedis(this CacheFeature cache)
+ => cache.RegisterProvider("Redis", section => cache.UseRedis(options => { if (section != null) section.Bind(options); }));
+
+ public static CacheFeature UseRedis(this CacheFeature cache, Action? configure = null)
+ {
+ cache.Module.Configure(redis =>
+ {
+ // Bind first; explicit property assignments then override individual settings.
+ cache.Configuration?.GetSection("Redis").Bind(redis.Options);
+ configure?.Invoke(redis.Options);
+ });
+ return cache.SelectProvider("Redis", sp => sp.GetRequiredService());
+ }
+
+ public static T UseRedis(this T feature, Action? configure = null) where T : CacheConsumerFeature
+ {
+ feature.Cache.UseRedis(configure);
+ return feature;
+ }
+}
diff --git a/src/modules/Elsa.Caching.Redis/Features/RedisCacheFeature.cs b/src/modules/Elsa.Caching.Redis/Features/RedisCacheFeature.cs
new file mode 100644
index 0000000..58ba566
--- /dev/null
+++ b/src/modules/Elsa.Caching.Redis/Features/RedisCacheFeature.cs
@@ -0,0 +1,33 @@
+using Elsa.Caching.Features;
+using Elsa.Caching.Redis.Options;
+using Elsa.Caching.Redis.Services;
+using Elsa.Features.Abstractions;
+using Elsa.Features.Attributes;
+using Elsa.Features.Services;
+using Microsoft.Extensions.DependencyInjection;
+using StackExchange.Redis;
+
+namespace Elsa.Caching.Redis.Features;
+
+[DependsOn(typeof(CacheFeature))]
+public class RedisCacheFeature(IModule module) : FeatureBase(module)
+{
+ public RedisCacheOptions Options { get; } = new();
+ public override void Apply()
+ {
+ if (Options.Database < -1) throw new InvalidOperationException("Redis cache database must be -1 or a non-negative number.");
+ if (string.IsNullOrWhiteSpace(Options.KeyPrefix)) throw new InvalidOperationException("Redis cache KeyPrefix must not be empty.");
+ Services.AddSingleton(Options);
+ if (!Options.HasExplicitConnectionFactory && !string.IsNullOrWhiteSpace(Options.ConnectionString))
+ {
+ Services.AddSingleton(_ => new(ConnectionMultiplexer.Connect(Options.ConnectionString)));
+ Options.ConnectionFactory = sp => sp.GetRequiredService().Connection;
+ }
+ Services.AddSingleton();
+ }
+ private sealed class OwnedRedisCacheConnection(IConnectionMultiplexer connection) : IDisposable
+ {
+ public IConnectionMultiplexer Connection => connection;
+ public void Dispose() => connection.Dispose();
+ }
+}
diff --git a/src/modules/Elsa.Caching.Redis/FodyWeavers.xml b/src/modules/Elsa.Caching.Redis/FodyWeavers.xml
new file mode 100644
index 0000000..00e1d9a
--- /dev/null
+++ b/src/modules/Elsa.Caching.Redis/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/src/modules/Elsa.Caching.Redis/Options/RedisCacheOptions.cs b/src/modules/Elsa.Caching.Redis/Options/RedisCacheOptions.cs
new file mode 100644
index 0000000..92f9a06
--- /dev/null
+++ b/src/modules/Elsa.Caching.Redis/Options/RedisCacheOptions.cs
@@ -0,0 +1,20 @@
+using StackExchange.Redis;
+
+namespace Elsa.Caching.Redis.Options;
+
+public class RedisCacheOptions
+{
+ /// Uses the host's singleton connection by default. The cache does not dispose host connections.
+ private Func _connectionFactory
+ = sp => (IConnectionMultiplexer)(sp.GetService(typeof(IConnectionMultiplexer))
+ ?? throw new InvalidOperationException("Register IConnectionMultiplexer or configure Redis ConnectionFactory/ConnectionString."));
+ internal bool HasExplicitConnectionFactory { get; private set; }
+ public Func ConnectionFactory
+ {
+ get => _connectionFactory;
+ set { _connectionFactory = value ?? throw new ArgumentNullException(nameof(value)); HasExplicitConnectionFactory = true; }
+ }
+ public string? ConnectionString { get; set; }
+ public int Database { get; set; } = -1;
+ public string KeyPrefix { get; set; } = "nextbank:workflow:cache:";
+}
diff --git a/src/modules/Elsa.Caching.Redis/Services/RedisCacheProvider.cs b/src/modules/Elsa.Caching.Redis/Services/RedisCacheProvider.cs
new file mode 100644
index 0000000..0204c5f
--- /dev/null
+++ b/src/modules/Elsa.Caching.Redis/Services/RedisCacheProvider.cs
@@ -0,0 +1,80 @@
+using System.Text.Json;
+using Elsa.Caching.Models;
+using Elsa.Caching.Redis.Options;
+using Elsa.Workflows;
+using Microsoft.Extensions.Logging;
+using StackExchange.Redis;
+
+namespace Elsa.Caching.Redis.Services;
+
+/// Stores serializable payloads only. Redis failures bypass caching; never fall back to local payloads.
+public class RedisCacheProvider(IServiceProvider services, RedisCacheOptions options, IPayloadSerializer serializer,
+ ILogger logger) : ICacheProvider
+{
+ public bool IsShared => true;
+ private IDatabase Database => options.ConnectionFactory(services).GetDatabase(options.Database);
+ private string Key(string key) => options.KeyPrefix + key;
+
+ private const string ReadScript = """
+ local payload = redis.call('HGET', KEYS[1], 'payload')
+ if not payload then return false end
+ local slide = tonumber(redis.call('HGET', KEYS[1], 'slide')) or 0
+ local deadline = tonumber(redis.call('HGET', KEYS[1], 'deadline')) or 0
+ if slide > 0 then
+ local ttl = slide
+ if deadline > 0 then
+ local t = redis.call('TIME')
+ ttl = math.min(ttl, deadline - (tonumber(t[1])*1000 + math.floor(tonumber(t[2])/1000)))
+ end
+ if ttl <= 0 then redis.call('DEL', KEYS[1]); return false end
+ redis.call('PEXPIRE', KEYS[1], ttl)
+ end
+ return payload
+ """;
+ private const string WriteScript = """
+ local absolute = tonumber(ARGV[2])
+ local slide = tonumber(ARGV[3])
+ local ttl = absolute
+ if ttl <= 0 or (slide > 0 and slide < ttl) then ttl = slide end
+ local deadline = 0
+ if absolute > 0 then
+ local t = redis.call('TIME')
+ deadline = tonumber(t[1])*1000 + math.floor(tonumber(t[2])/1000) + absolute
+ end
+ redis.call('HSET', KEYS[1], 'payload', ARGV[1], 'slide', slide, 'deadline', deadline)
+ redis.call('PEXPIRE', KEYS[1], ttl)
+ return 1
+ """;
+
+ public async Task> GetAsync(string key, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ try
+ {
+ var data = await Database.ScriptEvaluateAsync(ReadScript, new RedisKey[] { Key(key) }).WaitAsync(cancellationToken);
+ if (data.IsNull) return new(false, default);
+ return new(true, JsonSerializer.Deserialize((string)data!, serializer.GetOptions()));
+ }
+ catch (RedisException ex) { logger.LogWarning(ex, "Redis workflow cache read failed; using the persistent store."); return new(false, default); }
+ catch (JsonException ex) { logger.LogWarning(ex, "Invalid Redis workflow cache payload; rebuilding the entry."); return new(false, default); }
+ }
+
+ public async Task SetAsync(string key, T? value, CacheEntryOptions entryOptions, CancellationToken cancellationToken = default)
+ {
+ var absolute = Milliseconds(entryOptions.AbsoluteExpirationRelativeToNow);
+ var sliding = Milliseconds(entryOptions.SlidingExpiration);
+ if (absolute <= 0 && sliding <= 0) throw new ArgumentException("A Redis cache entry must have a positive lifetime.");
+ var data = JsonSerializer.Serialize(value, serializer.GetOptions());
+ try
+ {
+ await Database.ScriptEvaluateAsync(WriteScript, new RedisKey[] { Key(key) }, new RedisValue[] { data, absolute, sliding }).WaitAsync(cancellationToken);
+ }
+ catch (RedisException ex) { logger.LogWarning(ex, "Redis workflow cache write failed; the SQL revision remains authoritative."); }
+ }
+ public async Task RemoveAsync(string key, CancellationToken cancellationToken = default)
+ {
+ try { await Database.KeyDeleteAsync(Key(key)).WaitAsync(cancellationToken); }
+ catch (RedisException ex) { logger.LogWarning(ex, "Redis workflow cache removal failed."); }
+ }
+ private static long Milliseconds(TimeSpan? duration) => duration.HasValue ? Math.Max(1, (long)duration.Value.TotalMilliseconds) : 0;
+}
diff --git a/src/modules/Elsa.Caching/Contracts/ICacheConsistencyStore.cs b/src/modules/Elsa.Caching/Contracts/ICacheConsistencyStore.cs
new file mode 100644
index 0000000..693fa39
--- /dev/null
+++ b/src/modules/Elsa.Caching/Contracts/ICacheConsistencyStore.cs
@@ -0,0 +1,15 @@
+namespace Elsa.Caching;
+
+///
+/// Authoritative cache revision and durable write fences. A pending mutation disables cache reads and writes.
+/// Shared providers require a durable store shared by every node, independently of the cache payload storage.
+///
+public interface ICacheConsistencyStore
+{
+ bool IsDurable { get; }
+ Task ReadAsync(CancellationToken cancellationToken = default);
+ Task BeginMutationAsync(CancellationToken cancellationToken = default, string scope = "workflow");
+ Task CompleteMutationAsync(string mutationId, CancellationToken cancellationToken = default);
+}
+
+public record CacheConsistencyState(string Revision, bool HasPendingMutations, string? RouteRevision = null, bool HasPendingRouteMutations = false);
diff --git a/src/modules/Elsa.Caching/Contracts/ICacheManager.cs b/src/modules/Elsa.Caching/Contracts/ICacheManager.cs
index 29deac5..2139b9e 100644
--- a/src/modules/Elsa.Caching/Contracts/ICacheManager.cs
+++ b/src/modules/Elsa.Caching/Contracts/ICacheManager.cs
@@ -10,6 +10,21 @@ namespace Elsa.Caching;
///
public interface ICacheManager
{
+ bool IsShared => false;
+ bool IsEnabled => true;
+ Task BeginMutationAsync(CancellationToken cancellationToken = default) => Task.FromResult(string.Empty);
+ Task BeginRouteMutationAsync(CancellationToken cancellationToken = default) => BeginMutationAsync(cancellationToken);
+ Task CompleteMutationAsync(string id, CancellationToken cancellationToken = default) => Task.CompletedTask;
+ Task GetConsistencyStateAsync(CancellationToken cancellationToken = default)
+ => Task.FromResult(new CacheConsistencyState("0", false));
+ Task GetOrCreateAsync(string key, Func> factory, Elsa.Caching.Models.CacheEntryOptions options, CancellationToken cancellationToken = default)
+ => GetOrCreateAsync(key, entry =>
+ {
+ entry.AbsoluteExpirationRelativeToNow = options.AbsoluteExpirationRelativeToNow;
+ entry.SlidingExpiration = options.SlidingExpiration;
+ foreach (var tag in options.Tags) entry.ExpirationTokens.Add(GetToken(tag));
+ return factory();
+ });
///
/// Provides options for configuring caching.
///
@@ -35,4 +50,4 @@ public interface ICacheManager
///
///
void Remove(string key);
-}
\ No newline at end of file
+}
diff --git a/src/modules/Elsa.Caching/Contracts/ICacheProvider.cs b/src/modules/Elsa.Caching/Contracts/ICacheProvider.cs
new file mode 100644
index 0000000..2c6959d
--- /dev/null
+++ b/src/modules/Elsa.Caching/Contracts/ICacheProvider.cs
@@ -0,0 +1,14 @@
+using Elsa.Caching.Models;
+
+namespace Elsa.Caching;
+
+/// Storage for cache entries. Implementations must distinguish a miss from a cached null.
+public interface ICacheProvider
+{
+ bool IsShared { get; }
+ Task> GetAsync(string key, CancellationToken cancellationToken = default);
+ Task SetAsync(string key, T? value, CacheEntryOptions options, CancellationToken cancellationToken = default);
+ Task RemoveAsync(string key, CancellationToken cancellationToken = default);
+}
+
+public record CacheResult(bool Found, T? Value);
diff --git a/src/modules/Elsa.Caching/Elsa.Caching.csproj b/src/modules/Elsa.Caching/Elsa.Caching.csproj
index 7271f74..4150689 100644
--- a/src/modules/Elsa.Caching/Elsa.Caching.csproj
+++ b/src/modules/Elsa.Caching/Elsa.Caching.csproj
@@ -1,10 +1,10 @@
-
+
Caspian.Elsa.Caching
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides caching services and utility functions common to Elsa modules.
@@ -19,6 +19,8 @@
+
+
diff --git a/src/modules/Elsa.Caching/Extensions/CacheFeatureExtensions.cs b/src/modules/Elsa.Caching/Extensions/CacheFeatureExtensions.cs
new file mode 100644
index 0000000..af2aeb2
--- /dev/null
+++ b/src/modules/Elsa.Caching/Extensions/CacheFeatureExtensions.cs
@@ -0,0 +1,13 @@
+using Elsa.Caching.Features;
+using Elsa.Features.Services;
+
+namespace Elsa.Extensions;
+
+public static class CacheFeatureExtensions
+{
+ public static IModule UseCaching(this IModule module, Action? configure = null)
+ {
+ module.Configure(configure);
+ return module;
+ }
+}
diff --git a/src/modules/Elsa.Caching/Extensions/CacheMutationExtensions.cs b/src/modules/Elsa.Caching/Extensions/CacheMutationExtensions.cs
new file mode 100644
index 0000000..516941a
--- /dev/null
+++ b/src/modules/Elsa.Caching/Extensions/CacheMutationExtensions.cs
@@ -0,0 +1,34 @@
+using Elsa.Caching;
+
+namespace Elsa.Extensions;
+
+public static class CacheMutationExtensions
+{
+ public static async Task MutateRoutesAsync(this ICacheManager cache, Func operation, CancellationToken cancellationToken = default)
+ {
+ var id = await cache.BeginRouteMutationAsync(cancellationToken);
+ await operation();
+ await cache.CompleteMutationAsync(id, CancellationToken.None);
+ }
+ public static async Task MutateRoutesAsync(this ICacheManager cache, Func> operation, CancellationToken cancellationToken = default)
+ {
+ var id = await cache.BeginRouteMutationAsync(cancellationToken);
+ var value = await operation();
+ await cache.CompleteMutationAsync(id, CancellationToken.None);
+ return value;
+ }
+ public static async Task MutateAsync(this ICacheManager cache, Func operation, CancellationToken cancellationToken = default)
+ {
+ var id = await cache.BeginMutationAsync(cancellationToken);
+ await operation();
+ // Once SQL has committed, request cancellation must not leave a fence unfinished.
+ await cache.CompleteMutationAsync(id, CancellationToken.None);
+ }
+ public static async Task MutateAsync(this ICacheManager cache, Func> operation, CancellationToken cancellationToken = default)
+ {
+ var id = await cache.BeginMutationAsync(cancellationToken);
+ var value = await operation();
+ await cache.CompleteMutationAsync(id, CancellationToken.None);
+ return value;
+ }
+}
diff --git a/src/modules/Elsa.Caching/Features/CacheConsumerFeature.cs b/src/modules/Elsa.Caching/Features/CacheConsumerFeature.cs
new file mode 100644
index 0000000..399333b
--- /dev/null
+++ b/src/modules/Elsa.Caching/Features/CacheConsumerFeature.cs
@@ -0,0 +1,14 @@
+using Elsa.Features.Abstractions;
+using Elsa.Features.Services;
+
+namespace Elsa.Caching.Features;
+
+/// Common configuration entry point for workflow cache consumers.
+public abstract class CacheConsumerFeature(IModule module) : FeatureBase(module)
+{
+ public CacheFeature Cache => Module.Configure();
+ public bool Enabled { get => Cache.Enabled; set => Cache.Enabled = value; }
+ public TimeSpan Duration { get => Cache.Duration; set => Cache.Duration = value; }
+ public void UseMemory() => Cache.UseMemory();
+ public void UseConfiguration(Microsoft.Extensions.Configuration.IConfigurationSection section) => Cache.UseConfiguration(section);
+}
diff --git a/src/modules/Elsa.Caching/Features/CacheFeature.cs b/src/modules/Elsa.Caching/Features/CacheFeature.cs
new file mode 100644
index 0000000..9a2ce3c
--- /dev/null
+++ b/src/modules/Elsa.Caching/Features/CacheFeature.cs
@@ -0,0 +1,80 @@
+using Elsa.Caching.Services;
+using Elsa.Caching.Options;
+using Elsa.Features.Abstractions;
+using Elsa.Features.Attributes;
+using Elsa.Features.Services;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Elsa.Caching.Features;
+
+/// Configures one provider for the entire Elsa module.
+[DependsOn(typeof(MemoryCacheFeature))]
+public class CacheFeature(IModule module) : FeatureBase(module)
+{
+ private readonly Dictionary> _providers = new(StringComparer.OrdinalIgnoreCase);
+ private string? _explicitProvider;
+ private bool _selectingConfiguration;
+ public bool Enabled { get; set; } = true;
+ public TimeSpan Duration { get; set; } = TimeSpan.FromMinutes(1);
+ public Func Provider { get; private set; } = sp => sp.GetRequiredService();
+ public Func ConsistencyStore { get; set; } = sp => sp.GetRequiredService();
+
+ public CacheFeature RegisterProvider(string name, Action configure)
+ {
+ _providers[name] = configure;
+ return this;
+ }
+
+ public CacheFeature SelectProvider(string name, Func factory)
+ {
+ if (_explicitProvider != null && !string.Equals(_explicitProvider, name, StringComparison.OrdinalIgnoreCase))
+ throw new InvalidOperationException($"Conflicting Elsa cache providers: '{_explicitProvider}' and '{name}'. Select one provider per module.");
+ if (!_selectingConfiguration) _explicitProvider = name;
+ Provider = factory;
+ return this;
+ }
+
+ public CacheFeature UseMemory() => SelectProvider("Memory", sp => sp.GetRequiredService());
+
+ public CacheFeature UseConfiguration(IConfigurationSection section)
+ {
+ if (section["Enabled"] is { } enabledValue)
+ Enabled = bool.TryParse(enabledValue, out var enabled) ? enabled
+ : throw new InvalidOperationException("Workflow cache Enabled must be true or false.");
+ if (section["Duration"] is { } durationValue)
+ Duration = TimeSpan.TryParse(durationValue, out var duration) ? duration
+ : throw new InvalidOperationException("Workflow cache Duration must be a valid TimeSpan.");
+ Configuration = section;
+ var name = section["Provider"] ?? "Memory";
+ if (_explicitProvider == null)
+ {
+ _selectingConfiguration = true;
+ try
+ {
+ if (name.Equals("Memory", StringComparison.OrdinalIgnoreCase)) UseMemory();
+ else if (_providers.TryGetValue(name, out var select)) select(section.GetSection(name));
+ else throw new InvalidOperationException($"Elsa cache provider '{name}' is not registered.");
+ }
+ finally { _selectingConfiguration = false; }
+ }
+ Configuration = section;
+ return this;
+ }
+
+ public IConfigurationSection? Configuration { get; private set; }
+ public override void Apply()
+ {
+ if (Duration <= TimeSpan.Zero) throw new InvalidOperationException("Elsa cache duration must be positive.");
+ Services.Configure(options => options.CacheDuration = Duration);
+ Services.AddSingleton(Provider);
+ Services.AddSingleton(ConsistencyStore);
+ Services.AddSingleton(this);
+ Services.AddSingleton(sp =>
+ {
+ if (sp.GetRequiredService().IsShared && !sp.GetRequiredService().IsDurable)
+ throw new InvalidOperationException("Shared Elsa caching requires a durable ICacheConsistencyStore. Configure the EF Core consistency store.");
+ return ActivatorUtilities.CreateInstance(sp);
+ });
+ }
+}
diff --git a/src/modules/Elsa.Caching/Features/MemoryCacheFeature.cs b/src/modules/Elsa.Caching/Features/MemoryCacheFeature.cs
index 1a3ad07..f86c2fa 100644
--- a/src/modules/Elsa.Caching/Features/MemoryCacheFeature.cs
+++ b/src/modules/Elsa.Caching/Features/MemoryCacheFeature.cs
@@ -3,6 +3,7 @@ using Elsa.Caching.Services;
using Elsa.Features.Abstractions;
using Elsa.Features.Services;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Elsa.Caching.Features;
@@ -23,9 +24,12 @@ public class MemoryCacheFeature(IModule module) : FeatureBase(module)
Services
.AddMemoryCache()
- .AddSingleton()
.AddSingleton()
.AddSingleton()
;
+ // Keep local script/template caches independent of the workflow provider.
+ Services.TryAddSingleton();
+ Services.TryAddSingleton();
+ Services.TryAddSingleton();
}
-}
\ No newline at end of file
+}
diff --git a/src/modules/Elsa.Caching/Models/CacheEntryOptions.cs b/src/modules/Elsa.Caching/Models/CacheEntryOptions.cs
new file mode 100644
index 0000000..6ddd495
--- /dev/null
+++ b/src/modules/Elsa.Caching/Models/CacheEntryOptions.cs
@@ -0,0 +1,9 @@
+namespace Elsa.Caching.Models;
+
+/// Provider-independent lifetime and invalidation dependencies.
+public class CacheEntryOptions
+{
+ public TimeSpan? AbsoluteExpirationRelativeToNow { get; set; }
+ public TimeSpan? SlidingExpiration { get; set; }
+ public ICollection Tags { get; } = new List();
+}
diff --git a/src/modules/Elsa.Caching/Services/MemoryCacheConsistencyStore.cs b/src/modules/Elsa.Caching/Services/MemoryCacheConsistencyStore.cs
new file mode 100644
index 0000000..018913a
--- /dev/null
+++ b/src/modules/Elsa.Caching/Services/MemoryCacheConsistencyStore.cs
@@ -0,0 +1,34 @@
+namespace Elsa.Caching.Services;
+
+public class MemoryCacheConsistencyStore : ICacheConsistencyStore
+{
+ private readonly object _sync = new();
+ private readonly Dictionary _pending = new();
+ private long _revision;
+ private long _routeRevision;
+ public bool IsDurable => false;
+ public Task ReadAsync(CancellationToken cancellationToken = default)
+ {
+ lock (_sync) return Task.FromResult(new CacheConsistencyState(_revision.ToString(), _pending.Values.Any(x => x == "workflow"), _routeRevision.ToString(), _pending.Count != 0));
+ }
+ public Task BeginMutationAsync(CancellationToken cancellationToken = default, string scope = "workflow")
+ {
+ lock (_sync)
+ {
+ var id = Guid.NewGuid().ToString("N");
+ _pending.Add(id, scope);
+ if (scope == "workflow") _revision++;
+ _routeRevision++;
+ return Task.FromResult(id);
+ }
+ }
+ public Task CompleteMutationAsync(string mutationId, CancellationToken cancellationToken = default)
+ {
+ lock (_sync)
+ {
+ if (_pending.Remove(mutationId, out var scope) && scope == "workflow") _revision++;
+ _routeRevision++;
+ }
+ return Task.CompletedTask;
+ }
+}
diff --git a/src/modules/Elsa.Caching/Services/MemoryCacheProvider.cs b/src/modules/Elsa.Caching/Services/MemoryCacheProvider.cs
new file mode 100644
index 0000000..1ef47df
--- /dev/null
+++ b/src/modules/Elsa.Caching/Services/MemoryCacheProvider.cs
@@ -0,0 +1,32 @@
+using Elsa.Caching.Models;
+using Microsoft.Extensions.Caching.Memory;
+
+namespace Elsa.Caching.Services;
+
+public class MemoryCacheProvider(IMemoryCache cache, IChangeTokenSignaler signaler) : ICacheProvider
+{
+ public bool IsShared => false;
+ public Task> GetAsync(string key, CancellationToken cancellationToken = default)
+ {
+ var found = cache.TryGetValue(key, out T? value);
+ return Task.FromResult(new CacheResult(found, value));
+ }
+
+ public Task SetAsync(string key, T? value, CacheEntryOptions options, CancellationToken cancellationToken = default)
+ {
+ var memoryOptions = new MemoryCacheEntryOptions
+ {
+ AbsoluteExpirationRelativeToNow = options.AbsoluteExpirationRelativeToNow,
+ SlidingExpiration = options.SlidingExpiration
+ };
+ foreach (var tag in options.Tags) memoryOptions.AddExpirationToken(signaler.GetToken(tag));
+ cache.Set(key, value, memoryOptions);
+ return Task.CompletedTask;
+ }
+
+ public Task RemoveAsync(string key, CancellationToken cancellationToken = default)
+ {
+ cache.Remove(key);
+ return Task.CompletedTask;
+ }
+}
diff --git a/src/modules/Elsa.Caching/Services/ProviderCacheManager.cs b/src/modules/Elsa.Caching/Services/ProviderCacheManager.cs
new file mode 100644
index 0000000..00c2628
--- /dev/null
+++ b/src/modules/Elsa.Caching/Services/ProviderCacheManager.cs
@@ -0,0 +1,122 @@
+using System.Security.Cryptography;
+using System.Text;
+using System.Transactions;
+using Elsa.Caching.Features;
+using Elsa.Caching.Models;
+using Elsa.Caching.Options;
+using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Primitives;
+using Microsoft.Extensions.Logging;
+
+namespace Elsa.Caching.Services;
+
+/// Coordinates cache payloads using an authoritative revision, independent of the provider.
+public class ProviderCacheManager(ICacheProvider provider, ICacheConsistencyStore consistency,
+ IChangeTokenSignaler signaler, IOptions options, CacheFeature feature, IMemoryCache memoryCache,
+ ILogger logger) : ICacheManager
+{
+ public IOptions CachingOptions => options;
+ public bool IsShared => provider.IsShared;
+ public bool IsEnabled => feature.Enabled;
+ public IChangeToken GetToken(string key) => signaler.GetToken(key);
+ public Task GetConsistencyStateAsync(CancellationToken cancellationToken = default) => consistency.ReadAsync(cancellationToken);
+
+ public async Task BeginMutationAsync(CancellationToken cancellationToken = default)
+ => await BeginMutationCoreAsync("workflow", cancellationToken);
+ public Task BeginRouteMutationAsync(CancellationToken cancellationToken = default)
+ => BeginMutationCoreAsync("routes", cancellationToken);
+ public async Task CompleteMutationAsync(string id, CancellationToken cancellationToken = default)
+ {
+ // Begin registered the transaction-completed callback. Do not close a fence before the outer commit.
+ if (Transaction.Current != null) return;
+ await consistency.CompleteMutationAsync(id, cancellationToken);
+ }
+
+ private async Task BeginMutationCoreAsync(string scope, CancellationToken cancellationToken)
+ {
+ var transaction = Transaction.Current;
+ string id;
+ using (new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled))
+ id = await consistency.BeginMutationAsync(cancellationToken, scope);
+ if (transaction != null)
+ {
+ void CompleteAfterTransaction(TransactionStatus status)
+ {
+ if (status is not (TransactionStatus.Committed or TransactionStatus.Aborted)) return;
+ try
+ {
+ using var suppressed = new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled);
+ consistency.CompleteMutationAsync(id, CancellationToken.None).GetAwaiter().GetResult();
+ }
+ catch (Exception ex) { logger.LogError(ex, "Workflow cache mutation {MutationId} remains pending after transaction completion.", id); }
+ }
+ transaction.TransactionCompleted += (_, args) => CompleteAfterTransaction(args.Transaction!.TransactionInformation.Status);
+ if (transaction.TransactionInformation.Status != TransactionStatus.Active)
+ CompleteAfterTransaction(transaction.TransactionInformation.Status);
+ }
+ return id;
+ }
+
+ public async ValueTask TriggerTokenAsync(string key, CancellationToken cancellationToken = default)
+ {
+ var id = await BeginMutationAsync(cancellationToken);
+ await CompleteMutationAsync(id, cancellationToken);
+ await signaler.TriggerTokenAsync(key, cancellationToken);
+ }
+
+ public async Task GetOrCreateAsync(object key, Func> factory)
+ {
+ if (Transaction.Current != null) { using var transactionEntry = new CompatibilityCacheEntry(key); return await factory(transactionEntry); }
+ if (!feature.Enabled) { using var uncached = new CompatibilityCacheEntry(key); return await factory(uncached); }
+ if (!IsShared) return await memoryCache.GetOrCreateAsync(key, factory);
+ // Compatibility entry for older extensions. New consumers use CacheEntryOptions.
+ using var entry = new CompatibilityCacheEntry(key);
+ return await GetOrCreateAsync(key.ToString()!, () => factory(entry), entry.Options);
+ }
+
+ public async Task GetOrCreateAsync(string key, Func> factory, CacheEntryOptions entryOptions, CancellationToken cancellationToken = default)
+ {
+ if (!feature.Enabled || Transaction.Current != null) return await factory();
+ if (IsShared && !consistency.IsDurable)
+ throw new InvalidOperationException("Shared Elsa caching requires a durable ICacheConsistencyStore. Configure the EF Core consistency store.");
+ var before = await consistency.ReadAsync(cancellationToken);
+ if (before.HasPendingMutations) return await factory();
+ var storageKey = CreateKey(key, before.Revision);
+ var cached = await provider.GetAsync(storageKey, cancellationToken);
+ if (cached.Found) return cached.Value;
+ var value = await factory();
+ var after = await consistency.ReadAsync(cancellationToken);
+ if (!after.HasPendingMutations && after.Revision == before.Revision)
+ {
+ if (entryOptions.AbsoluteExpirationRelativeToNow == null && entryOptions.SlidingExpiration == null)
+ entryOptions.AbsoluteExpirationRelativeToNow = options.Value.CacheDuration;
+ await provider.SetAsync(storageKey, value, entryOptions, cancellationToken);
+ }
+ return value;
+ }
+
+ public void Remove(string key) => TriggerTokenAsync(key).AsTask().GetAwaiter().GetResult();
+ private static string CreateKey(string key, string revision)
+ => $"v1:{revision}:" + Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes($"{typeof(T).FullName}:{key}")));
+
+ private sealed class CompatibilityCacheEntry(object key) : ICacheEntry
+ {
+ public CacheEntryOptions Options { get; } = new();
+ public object Key => key;
+ public object? Value { get; set; }
+ private DateTimeOffset? _absoluteExpiration;
+ public DateTimeOffset? AbsoluteExpiration
+ {
+ get => _absoluteExpiration;
+ set { _absoluteExpiration = value; Options.AbsoluteExpirationRelativeToNow = value - DateTimeOffset.UtcNow; }
+ }
+ public TimeSpan? AbsoluteExpirationRelativeToNow { get => Options.AbsoluteExpirationRelativeToNow; set => Options.AbsoluteExpirationRelativeToNow = value; }
+ public TimeSpan? SlidingExpiration { get => Options.SlidingExpiration; set => Options.SlidingExpiration = value; }
+ public IList ExpirationTokens { get; } = new List();
+ public IList PostEvictionCallbacks { get; } = new List();
+ public CacheItemPriority Priority { get; set; }
+ public long? Size { get; set; }
+ public void Dispose() { }
+ }
+}
diff --git a/src/modules/Elsa.Common/Elsa.Common.csproj b/src/modules/Elsa.Common/Elsa.Common.csproj
index c883ffd..543a083 100644
--- a/src/modules/Elsa.Common/Elsa.Common.csproj
+++ b/src/modules/Elsa.Common/Elsa.Common.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Common
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides services and utility functions common to Elsa modules.
diff --git a/src/modules/Elsa.Connections.Api/Elsa.Connections.Api.csproj b/src/modules/Elsa.Connections.Api/Elsa.Connections.Api.csproj
index f22b59b..452bb69 100644
--- a/src/modules/Elsa.Connections.Api/Elsa.Connections.Api.csproj
+++ b/src/modules/Elsa.Connections.Api/Elsa.Connections.Api.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Connections.Api
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides Activity Connection API endpoints
elsa module activity connection abstraction api
diff --git a/src/modules/Elsa.Connections.Core/Elsa.Connections.Core.csproj b/src/modules/Elsa.Connections.Core/Elsa.Connections.Core.csproj
index f88a704..82b9185 100644
--- a/src/modules/Elsa.Connections.Core/Elsa.Connections.Core.csproj
+++ b/src/modules/Elsa.Connections.Core/Elsa.Connections.Core.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Connections.Core
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides an connection framework to use connection abtraction in activities
elsa module connection abstratction
diff --git a/src/modules/Elsa.Connections.Models/Elsa.Connections.Models.csproj b/src/modules/Elsa.Connections.Models/Elsa.Connections.Models.csproj
index 3d1bb3b..d774e46 100644
--- a/src/modules/Elsa.Connections.Models/Elsa.Connections.Models.csproj
+++ b/src/modules/Elsa.Connections.Models/Elsa.Connections.Models.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Connections.Models
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides models that can be shared between the API and API clients
elsa module connections api
diff --git a/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite/Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite.csproj b/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite/Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite.csproj
index 843aa4a..0bc8b11 100644
--- a/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite/Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite.csproj
+++ b/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite/Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides an EF Core migrations for SQLite for the Connection Persistence module.
elsa module persistence efcore entity framework core sqlite
diff --git a/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore/Elsa.Connections.Persistence.EntityFrameworkCore.csproj b/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore/Elsa.Connections.Persistence.EntityFrameworkCore.csproj
index cd3f8a8..92abf6b 100644
--- a/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore/Elsa.Connections.Persistence.EntityFrameworkCore.csproj
+++ b/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore/Elsa.Connections.Persistence.EntityFrameworkCore.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Connections.Persistence.EntityFrameworkCore
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides an Entity Framework Core provider for the persistence layer of the Connection module
elsa connection persistence efcore entity framework core
diff --git a/src/modules/Elsa.Connections.Persistence/Elsa.Connections.Persistence.csproj b/src/modules/Elsa.Connections.Persistence/Elsa.Connections.Persistence.csproj
index dff80d9..6e1dacd 100644
--- a/src/modules/Elsa.Connections.Persistence/Elsa.Connections.Persistence.csproj
+++ b/src/modules/Elsa.Connections.Persistence/Elsa.Connections.Persistence.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Connections.Persistence
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides Connections persistence services
elsa module connections persistence
diff --git a/src/modules/Elsa.Dapper.Migrations/Elsa.Dapper.Migrations.csproj b/src/modules/Elsa.Dapper.Migrations/Elsa.Dapper.Migrations.csproj
index 72bc008..7eeb056 100644
--- a/src/modules/Elsa.Dapper.Migrations/Elsa.Dapper.Migrations.csproj
+++ b/src/modules/Elsa.Dapper.Migrations/Elsa.Dapper.Migrations.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Dapper.Migrations
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides Dapper migrations for Dapper perisstence providers from various modules.
diff --git a/src/modules/Elsa.Dapper/Elsa.Dapper.csproj b/src/modules/Elsa.Dapper/Elsa.Dapper.csproj
index 097e6f8..084be2d 100644
--- a/src/modules/Elsa.Dapper/Elsa.Dapper.csproj
+++ b/src/modules/Elsa.Dapper/Elsa.Dapper.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Dapper
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides Dapper implementations of various persistence abstractions from various modules.
diff --git a/src/modules/Elsa.Dsl/Elsa.Dsl.csproj b/src/modules/Elsa.Dsl/Elsa.Dsl.csproj
index 69401f5..6a7d716 100644
--- a/src/modules/Elsa.Dsl/Elsa.Dsl.csproj
+++ b/src/modules/Elsa.Dsl/Elsa.Dsl.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Dsl
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides a custom DSL to write workflows with in a syntax similar to JavaScript but with native keywords translating to workflow functionality.
diff --git a/src/modules/Elsa.Elasticsearch/Elsa.Elasticsearch.csproj b/src/modules/Elsa.Elasticsearch/Elsa.Elasticsearch.csproj
index 872115c..c0e779f 100644
--- a/src/modules/Elsa.Elasticsearch/Elsa.Elasticsearch.csproj
+++ b/src/modules/Elsa.Elasticsearch/Elsa.Elasticsearch.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Elasticsearch
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides Elasticsearch implementation for workflow state persistence.
diff --git a/src/modules/Elsa.Email/Elsa.Email.csproj b/src/modules/Elsa.Email/Elsa.Email.csproj
index eaaf040..64911ca 100644
--- a/src/modules/Elsa.Email/Elsa.Email.csproj
+++ b/src/modules/Elsa.Email/Elsa.Email.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Email
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides a SendEmail activity that lets you send emails from a workflow.
diff --git a/src/modules/Elsa.EntityFrameworkCore.Common/Elsa.EntityFrameworkCore.Common.csproj b/src/modules/Elsa.EntityFrameworkCore.Common/Elsa.EntityFrameworkCore.Common.csproj
index d6c5658..b0ae971 100644
--- a/src/modules/Elsa.EntityFrameworkCore.Common/Elsa.EntityFrameworkCore.Common.csproj
+++ b/src/modules/Elsa.EntityFrameworkCore.Common/Elsa.EntityFrameworkCore.Common.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.EntityFrameworkCore.Common
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides common Entity Framework Core types for implementing EF Core persistence of varipus ELsa modules.
diff --git a/src/modules/Elsa.EntityFrameworkCore.MySql/Elsa.EntityFrameworkCore.MySql.csproj b/src/modules/Elsa.EntityFrameworkCore.MySql/Elsa.EntityFrameworkCore.MySql.csproj
index bd2c9ec..a2bfb9c 100644
--- a/src/modules/Elsa.EntityFrameworkCore.MySql/Elsa.EntityFrameworkCore.MySql.csproj
+++ b/src/modules/Elsa.EntityFrameworkCore.MySql/Elsa.EntityFrameworkCore.MySql.csproj
@@ -5,7 +5,7 @@
Caspian.Elsa.EntityFrameworkCore.MySql
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides MySQL EF Core migrations for various modules.
diff --git a/src/modules/Elsa.EntityFrameworkCore.Oracle/Elsa.EntityFrameworkCore.Oracle.csproj b/src/modules/Elsa.EntityFrameworkCore.Oracle/Elsa.EntityFrameworkCore.Oracle.csproj
index 171ea61..b984ac1 100644
--- a/src/modules/Elsa.EntityFrameworkCore.Oracle/Elsa.EntityFrameworkCore.Oracle.csproj
+++ b/src/modules/Elsa.EntityFrameworkCore.Oracle/Elsa.EntityFrameworkCore.Oracle.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.EntityFrameworkCore.Oracle
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides Oracle EF Core migrations for various modules.
diff --git a/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Elsa.EntityFrameworkCore.PostgreSql.csproj b/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Elsa.EntityFrameworkCore.PostgreSql.csproj
index e4eb390..de7358a 100644
--- a/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Elsa.EntityFrameworkCore.PostgreSql.csproj
+++ b/src/modules/Elsa.EntityFrameworkCore.PostgreSql/Elsa.EntityFrameworkCore.PostgreSql.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.EntityFrameworkCore.PostgreSql
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides PostgreSQL EF Core migrations for various modules.
diff --git a/src/modules/Elsa.EntityFrameworkCore.SqlServer/Elsa.EntityFrameworkCore.SqlServer.csproj b/src/modules/Elsa.EntityFrameworkCore.SqlServer/Elsa.EntityFrameworkCore.SqlServer.csproj
index 89aeaef..6c69a48 100644
--- a/src/modules/Elsa.EntityFrameworkCore.SqlServer/Elsa.EntityFrameworkCore.SqlServer.csproj
+++ b/src/modules/Elsa.EntityFrameworkCore.SqlServer/Elsa.EntityFrameworkCore.SqlServer.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.EntityFrameworkCore.SqlServer
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides SQL Server EF Core migrations for various modules.
diff --git a/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Management/20261007000000_AddCacheMutationFences.cs b/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Management/20261007000000_AddCacheMutationFences.cs
new file mode 100644
index 0000000..433c2e2
--- /dev/null
+++ b/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Management/20261007000000_AddCacheMutationFences.cs
@@ -0,0 +1,26 @@
+using Elsa.EntityFrameworkCore.Modules.Management;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace Elsa.EntityFrameworkCore.SqlServer.Migrations.Management;
+
+[DbContext(typeof(ManagementElsaDbContext))]
+[Migration("20261007000000_AddCacheMutationFences")]
+public class AddCacheMutationFences : Migration
+{
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable("CacheMutations", schema: "Elsa", columns: table => new
+ {
+ Id = table.Column(nullable: false).Annotation("SqlServer:Identity", "1, 1"),
+ MutationId = table.Column(maxLength: 32, nullable: false),
+ Scope = table.Column(maxLength: 32, nullable: false),
+ OwnerInstance = table.Column(maxLength: 256, nullable: false),
+ IsCompleted = table.Column(nullable: false),
+ CreatedAt = table.Column(nullable: false)
+ }, constraints: table => table.PrimaryKey("PK_CacheMutations", x => x.Id));
+ migrationBuilder.CreateIndex("IX_CacheMutations_MutationId", "CacheMutations", "MutationId", "Elsa", unique: true);
+ migrationBuilder.CreateIndex("IX_CacheMutations_IsCompleted", "CacheMutations", "IsCompleted", "Elsa");
+ }
+ protected override void Down(MigrationBuilder migrationBuilder) => migrationBuilder.DropTable("CacheMutations", "Elsa");
+}
diff --git a/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Management/ManagementElsaDbContextModelSnapshot.cs b/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Management/ManagementElsaDbContextModelSnapshot.cs
index 5a2f2fc..c5b2322 100644
--- a/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Management/ManagementElsaDbContextModelSnapshot.cs
+++ b/src/modules/Elsa.EntityFrameworkCore.SqlServer/Migrations/Management/ManagementElsaDbContextModelSnapshot.cs
@@ -1,4 +1,4 @@
-//
+//
using System;
using Elsa.EntityFrameworkCore.Modules.Management;
using Microsoft.EntityFrameworkCore;
@@ -23,6 +23,21 @@ namespace Elsa.EntityFrameworkCore.SqlServer.Migrations.Management
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+ modelBuilder.Entity("Elsa.EntityFrameworkCore.Modules.Management.CacheMutation", b =>
+ {
+ b.Property("Id").ValueGeneratedOnAdd().HasColumnType("bigint");
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+ b.Property("MutationId").IsRequired().HasMaxLength(32).HasColumnType("nvarchar(32)");
+ b.Property("Scope").IsRequired().HasMaxLength(32).HasColumnType("nvarchar(32)");
+ b.Property("OwnerInstance").IsRequired().HasMaxLength(256).HasColumnType("nvarchar(256)");
+ b.Property("IsCompleted").HasColumnType("bit");
+ b.Property("CreatedAt").HasColumnType("datetimeoffset");
+ b.HasKey("Id");
+ b.HasIndex("MutationId").IsUnique();
+ b.HasIndex("IsCompleted");
+ b.ToTable("CacheMutations", "Elsa");
+ });
+
modelBuilder.Entity("Elsa.Workflows.Management.Entities.WorkflowDefinition", b =>
{
b.Property("Id")
diff --git a/src/modules/Elsa.EntityFrameworkCore.Sqlite/Elsa.EntityFrameworkCore.Sqlite.csproj b/src/modules/Elsa.EntityFrameworkCore.Sqlite/Elsa.EntityFrameworkCore.Sqlite.csproj
index e072969..4694d71 100644
--- a/src/modules/Elsa.EntityFrameworkCore.Sqlite/Elsa.EntityFrameworkCore.Sqlite.csproj
+++ b/src/modules/Elsa.EntityFrameworkCore.Sqlite/Elsa.EntityFrameworkCore.Sqlite.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.EntityFrameworkCore.Sqlite
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides SQLite EF Core migrations for various modules.
diff --git a/src/modules/Elsa.EntityFrameworkCore/Elsa.EntityFrameworkCore.csproj b/src/modules/Elsa.EntityFrameworkCore/Elsa.EntityFrameworkCore.csproj
index 23f4557..13a3aa4 100644
--- a/src/modules/Elsa.EntityFrameworkCore/Elsa.EntityFrameworkCore.csproj
+++ b/src/modules/Elsa.EntityFrameworkCore/Elsa.EntityFrameworkCore.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.EntityFrameworkCore
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides Entity Framework Core implementations of various abstractions from various modules.
diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/CacheMutation.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/CacheMutation.cs
new file mode 100644
index 0000000..63e9275
--- /dev/null
+++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/CacheMutation.cs
@@ -0,0 +1,13 @@
+namespace Elsa.EntityFrameworkCore.Modules.Management;
+
+/// Durable write fence. Pending records must never expire automatically.
+public class CacheMutation
+{
+ public static string CurrentOwnerInstance { get; } = $"{Environment.MachineName}:{Environment.ProcessId}:{Guid.NewGuid():N}";
+ public long Id { get; set; }
+ public string MutationId { get; set; } = Guid.NewGuid().ToString("N");
+ public string Scope { get; set; } = "workflow";
+ public string OwnerInstance { get; set; } = CurrentOwnerInstance;
+ public bool IsCompleted { get; set; }
+ public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
+}
diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/DbContext.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/DbContext.cs
index 79ab138..2fcb6ad 100644
--- a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/DbContext.cs
+++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/DbContext.cs
@@ -24,6 +24,7 @@ public class ManagementElsaDbContext : ElsaDbContextBase
/// The workflow instances.
///
public DbSet WorkflowInstances { get; set; } = default!;
+ public DbSet CacheMutations { get; set; } = default!;
///
protected override void OnModelCreating(ModelBuilder modelBuilder)
@@ -34,6 +35,17 @@ public class ManagementElsaDbContext : ElsaDbContextBase
var config = new Configurations();
modelBuilder.ApplyConfiguration(config);
modelBuilder.ApplyConfiguration(config);
+ modelBuilder.Entity(b =>
+ {
+ b.ToTable("CacheMutations");
+ b.HasKey(x => x.Id);
+ b.Property(x => x.Id).ValueGeneratedOnAdd();
+ b.Property(x => x.MutationId).HasMaxLength(32).IsRequired();
+ b.HasIndex(x => x.MutationId).IsUnique();
+ b.Property(x => x.Scope).HasMaxLength(32).IsRequired();
+ b.Property(x => x.OwnerInstance).HasMaxLength(256).IsRequired();
+ b.HasIndex(x => x.IsCompleted);
+ });
base.OnModelCreating(modelBuilder);
}
diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/EFCoreCacheConsistencyStore.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/EFCoreCacheConsistencyStore.cs
new file mode 100644
index 0000000..1aced04
--- /dev/null
+++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/EFCoreCacheConsistencyStore.cs
@@ -0,0 +1,57 @@
+using Elsa.Caching;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using System.Transactions;
+
+namespace Elsa.EntityFrameworkCore.Modules.Management;
+
+/// Reads SQL before cache access, so missed messages and Redis failover cannot revive old revisions.
+public class EFCoreCacheConsistencyStore(IServiceScopeFactory scopes) : ICacheConsistencyStore
+{
+ public bool IsDurable => true;
+ public async Task ReadAsync(CancellationToken cancellationToken = default)
+ {
+ using var suppressed = new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled);
+ using var serviceScope = scopes.CreateScope();
+ var factory = serviceScope.ServiceProvider.GetRequiredService>();
+ await using var db = await factory.CreateDbContextAsync(cancellationToken);
+ // One SQL statement gives a consistent view of the revision and pending fences.
+ var state = await db.CacheMutations.AsNoTracking().GroupBy(_ => 1)
+ .Select(g => new { Revision = g.Max(x => x.Scope == "workflow" ? (long?)x.Id : null) ?? 0,
+ Pending = g.Any(x => !x.IsCompleted && x.Scope == "workflow"),
+ RouteRevision = g.Max(x => x.Id), PendingRoutes = g.Any(x => !x.IsCompleted) })
+ .SingleOrDefaultAsync(cancellationToken);
+ return new CacheConsistencyState(state?.Revision.ToString() ?? "0", state?.Pending ?? false, state?.RouteRevision.ToString() ?? "0", state?.PendingRoutes ?? false);
+ }
+ public async Task BeginMutationAsync(CancellationToken cancellationToken = default, string scope = "workflow")
+ {
+ using var suppressed = new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled);
+ if (scope is not ("workflow" or "routes")) throw new ArgumentException("Unknown workflow cache mutation scope.", nameof(scope));
+ using var serviceScope = scopes.CreateScope();
+ var factory = serviceScope.ServiceProvider.GetRequiredService>();
+ await using var db = await factory.CreateDbContextAsync(cancellationToken);
+ var mutation = new CacheMutation { Scope = scope };
+ db.CacheMutations.Add(mutation);
+ await db.SaveChangesAsync(cancellationToken);
+ return mutation.MutationId;
+ }
+ public async Task CompleteMutationAsync(string mutationId, CancellationToken cancellationToken = default)
+ {
+ using var suppressed = new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled);
+ using var serviceScope = scopes.CreateScope();
+ var factory = serviceScope.ServiceProvider.GetRequiredService>();
+ await using var db = await factory.CreateDbContextAsync(cancellationToken);
+ var mutation = await db.CacheMutations.SingleOrDefaultAsync(x => x.MutationId == mutationId, cancellationToken);
+ if (mutation == null || mutation.IsCompleted) return;
+ mutation.IsCompleted = true;
+ // A begin marker can commit out of identity order. Always allocate a new revision AFTER the data write.
+ // Closing the fence and publishing that revision must commit in the same SQL transaction.
+ db.CacheMutations.Add(new CacheMutation { Scope = mutation.Scope, IsCompleted = true });
+ await db.SaveChangesAsync(cancellationToken);
+ // Never delete a row newer than the maximum observed for its scope: another node may complete meanwhile.
+ var latest = await db.CacheMutations.GroupBy(x => x.Scope)
+ .Select(g => new { Scope = g.Key, Maximum = g.Max(x => x.Id) }).ToListAsync(cancellationToken);
+ foreach (var item in latest)
+ await db.CacheMutations.Where(x => x.IsCompleted && x.Scope == item.Scope && x.Id < item.Maximum).ExecuteDeleteAsync(cancellationToken);
+ }
+}
diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowDefinitionPersistenceFeature.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowDefinitionPersistenceFeature.cs
index 3b808a9..b4c2425 100644
--- a/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowDefinitionPersistenceFeature.cs
+++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Management/WorkflowDefinitionPersistenceFeature.cs
@@ -3,6 +3,7 @@ using Elsa.Features.Services;
using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.Management.Features;
using Microsoft.Extensions.DependencyInjection;
+using Elsa.Caching.Features;
namespace Elsa.EntityFrameworkCore.Modules.Management;
@@ -16,6 +17,10 @@ public class EFCoreWorkflowDefinitionPersistenceFeature(IModule module) : Persis
///
public override void Configure()
{
+ Module.Configure(feature => feature.ConsistencyStore = sp =>
+ sp.GetRequiredService().IsShared
+ ? ActivatorUtilities.CreateInstance(sp)
+ : sp.GetRequiredService());
Module.Configure(feature =>
{
feature.WorkflowDefinitionStore = sp => sp.GetRequiredService();
@@ -28,4 +33,4 @@ public class EFCoreWorkflowDefinitionPersistenceFeature(IModule module) : Persis
base.Apply();
AddEntityStore();
}
-}
\ No newline at end of file
+}
diff --git a/src/modules/Elsa.Environments/Elsa.Environments.csproj b/src/modules/Elsa.Environments/Elsa.Environments.csproj
index 6a8b215..c606b56 100644
--- a/src/modules/Elsa.Environments/Elsa.Environments.csproj
+++ b/src/modules/Elsa.Environments/Elsa.Environments.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Environments
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides API endpoints for client applications to enumerate available environments.
diff --git a/src/modules/Elsa.Expressions/Elsa.Expressions.csproj b/src/modules/Elsa.Expressions/Elsa.Expressions.csproj
index e930a3d..8a485c2 100644
--- a/src/modules/Elsa.Expressions/Elsa.Expressions.csproj
+++ b/src/modules/Elsa.Expressions/Elsa.Expressions.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Expressions
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides infrastructure for registering and executing workflow expressions.
diff --git a/src/modules/Elsa.FileStorage/Elsa.FileStorage.csproj b/src/modules/Elsa.FileStorage/Elsa.FileStorage.csproj
index 1abaf72..2918bb1 100644
--- a/src/modules/Elsa.FileStorage/Elsa.FileStorage.csproj
+++ b/src/modules/Elsa.FileStorage/Elsa.FileStorage.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.FileStorage
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides activities to save and load files to and from a confogurable storage provider.
diff --git a/src/modules/Elsa.Hangfire/Elsa.Hangfire.csproj b/src/modules/Elsa.Hangfire/Elsa.Hangfire.csproj
index ba8611d..ee7fe79 100644
--- a/src/modules/Elsa.Hangfire/Elsa.Hangfire.csproj
+++ b/src/modules/Elsa.Hangfire/Elsa.Hangfire.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Hangfire
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides Hangfire integration and an implementation of Elsa's IJobQueue implementation.
diff --git a/src/modules/Elsa.Hosting.Management/Elsa.Hosting.Management.csproj b/src/modules/Elsa.Hosting.Management/Elsa.Hosting.Management.csproj
index 2c8f4ab..275c85e 100644
--- a/src/modules/Elsa.Hosting.Management/Elsa.Hosting.Management.csproj
+++ b/src/modules/Elsa.Hosting.Management/Elsa.Hosting.Management.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Hosting.Management
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides hosting management functionality.
diff --git a/src/modules/Elsa.Http/Contracts/IRouteTable.cs b/src/modules/Elsa.Http/Contracts/IRouteTable.cs
index b82ce1c..f746681 100644
--- a/src/modules/Elsa.Http/Contracts/IRouteTable.cs
+++ b/src/modules/Elsa.Http/Contracts/IRouteTable.cs
@@ -5,6 +5,12 @@ namespace Elsa.Http;
///
public interface IRouteTable : IEnumerable
{
+ /// Replaces the local snapshot. Implementations should publish it atomically.
+ void Replace(IEnumerable routes)
+ {
+ RemoveRange(this.Select(x => x.Route).ToArray());
+ foreach (var route in routes) Add(route);
+ }
///
/// Adds a route to the table.
///
@@ -34,4 +40,4 @@ public interface IRouteTable : IEnumerable
///
/// The routes to remove.
void RemoveRange(IEnumerable routes);
-}
\ No newline at end of file
+}
diff --git a/src/modules/Elsa.Http/Elsa.Http.csproj b/src/modules/Elsa.Http/Elsa.Http.csproj
index 4e7ad08..5d8815a 100644
--- a/src/modules/Elsa.Http/Elsa.Http.csproj
+++ b/src/modules/Elsa.Http/Elsa.Http.csproj
@@ -1,10 +1,10 @@
-
+
Caspian.Elsa.Http
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides HTTP activities to listen for and send out HTTP requests.
diff --git a/src/modules/Elsa.Http/Extensions/ModuleExtensions.cs b/src/modules/Elsa.Http/Extensions/ModuleExtensions.cs
index 7898a48..1c7aa8c 100644
--- a/src/modules/Elsa.Http/Extensions/ModuleExtensions.cs
+++ b/src/modules/Elsa.Http/Extensions/ModuleExtensions.cs
@@ -26,4 +26,13 @@ public static class ModuleExtensions
feature.Module.Configure(configure);
return feature;
}
-}
\ No newline at end of file
+ public static HttpFeature UseCache(this HttpFeature feature,
+ Microsoft.Extensions.Configuration.IConfigurationSection section, Action? configure = null)
+ {
+ return feature.UseCache(cache =>
+ {
+ cache.UseConfiguration(section);
+ configure?.Invoke(cache);
+ });
+ }
+}
diff --git a/src/modules/Elsa.Http/Features/HttpCacheFeature.cs b/src/modules/Elsa.Http/Features/HttpCacheFeature.cs
index 4dbc59b..9e087e9 100644
--- a/src/modules/Elsa.Http/Features/HttpCacheFeature.cs
+++ b/src/modules/Elsa.Http/Features/HttpCacheFeature.cs
@@ -1,4 +1,5 @@
using Elsa.Features.Abstractions;
+using Elsa.Caching.Features;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
using Elsa.Http.Handlers;
@@ -13,7 +14,7 @@ namespace Elsa.Http.Features;
///
[DependsOn(typeof(HttpFeature))]
[DependsOn(typeof(CachingWorkflowDefinitionsFeature))]
-public class HttpCacheFeature : FeatureBase
+public class HttpCacheFeature : CacheConsumerFeature
{
///
public HttpCacheFeature(IModule module) : base(module)
@@ -28,4 +29,4 @@ public class HttpCacheFeature : FeatureBase
.Decorate()
.AddNotificationHandler();
}
-}
\ No newline at end of file
+}
diff --git a/src/modules/Elsa.Http/Features/HttpFeature.cs b/src/modules/Elsa.Http/Features/HttpFeature.cs
index 80e0e51..f3aec14 100644
--- a/src/modules/Elsa.Http/Features/HttpFeature.cs
+++ b/src/modules/Elsa.Http/Features/HttpFeature.cs
@@ -167,6 +167,8 @@ public class HttpFeature(IModule module) : FeatureBase(module)
.AddScoped()
.AddScoped()
.AddScoped()
+ .AddScoped()
+ .AddSingleton()
.AddScoped()
.AddScoped(ContentTypeProvider)
.AddHttpContextAccessor()
@@ -253,4 +255,4 @@ public class HttpFeature(IModule module) : FeatureBase(module)
options.AddTypeAlias("Downloadable[]");
});
}
-}
\ No newline at end of file
+}
diff --git a/src/modules/Elsa.Http/Middleware/HttpWorkflowsMiddleware.cs b/src/modules/Elsa.Http/Middleware/HttpWorkflowsMiddleware.cs
index 668d188..070aea8 100644
--- a/src/modules/Elsa.Http/Middleware/HttpWorkflowsMiddleware.cs
+++ b/src/modules/Elsa.Http/Middleware/HttpWorkflowsMiddleware.cs
@@ -36,6 +36,8 @@ public class HttpWorkflowsMiddleware(RequestDelegate next, IOptions()
+ .EnsureFreshAsync(httpContext.RequestAborted);
var matchingPath = GetMatchingRoute(serviceProvider, path).Route;
var basePath = options.Value.BasePath?.ToString().NormalizeRoute();
@@ -370,4 +372,4 @@ public class HttpWorkflowsMiddleware(RequestDelegate next, IOptions();
return bookmarkHasher.Hash(HttpStimulusNames.HttpEndpoint, bookmarkPayload);
}
-}
\ No newline at end of file
+}
diff --git a/src/modules/Elsa.Http/Services/CachingHttpWorkflowLookupService.cs b/src/modules/Elsa.Http/Services/CachingHttpWorkflowLookupService.cs
index b4d2e63..ae5f82e 100644
--- a/src/modules/Elsa.Http/Services/CachingHttpWorkflowLookupService.cs
+++ b/src/modules/Elsa.Http/Services/CachingHttpWorkflowLookupService.cs
@@ -11,7 +11,8 @@ namespace Elsa.Http.Services;
public class CachingHttpWorkflowLookupService(
IHttpWorkflowLookupService decoratedService,
IHttpWorkflowsCacheManager cacheManager,
- ITenantAccessor tenantAccessor) : IHttpWorkflowLookupService
+ ITenantAccessor tenantAccessor,
+ Elsa.Workflows.Management.IWorkflowDefinitionService definitionService) : IHttpWorkflowLookupService
{
///
public async Task FindWorkflowAsync(string bookmarkHash, CancellationToken cancellationToken = default)
@@ -21,6 +22,21 @@ public class CachingHttpWorkflowLookupService(
var tenantIdPrefix = !string.IsNullOrEmpty(tenantId) ? $"{tenantId}:" : string.Empty;
var key = $"{tenantIdPrefix}http-workflow:{bookmarkHash}";
var cache = cacheManager.Cache;
+ if (!cache.IsEnabled) return await decoratedService.FindWorkflowAsync(bookmarkHash, cancellationToken);
+ if (cache.IsShared)
+ {
+ // Redis stores the definition version reference and trigger data, never an executable graph.
+ var entryOptions = new Elsa.Caching.Models.CacheEntryOptions { SlidingExpiration = cache.CachingOptions.Value.CacheDuration };
+ var cached = await cache.GetOrCreateAsync(key, async () =>
+ {
+ var found = await decoratedService.FindWorkflowAsync(bookmarkHash, cancellationToken);
+ return found == null ? null : new CachedHttpLookup(found.WorkflowGraph?.Workflow.Identity.Id, found.Triggers);
+ }, entryOptions, cancellationToken);
+ if (cached == null) return null;
+ var graph = cached.DefinitionVersionId == null ? null
+ : await definitionService.FindWorkflowGraphAsync(cached.DefinitionVersionId, cancellationToken);
+ return new HttpWorkflowLookupResult(graph, cached.Triggers);
+ }
return await cache.GetOrCreateAsync(key, async entry =>
{
var cachingOptions = cache.CachingOptions.Value;
@@ -43,4 +59,5 @@ public class CachingHttpWorkflowLookupService(
return result;
});
}
-}
\ No newline at end of file
+ public record CachedHttpLookup(string? DefinitionVersionId, ICollection Triggers);
+}
diff --git a/src/modules/Elsa.Http/Services/DefaultRouteTableUpdater.cs b/src/modules/Elsa.Http/Services/DefaultRouteTableUpdater.cs
index bca5468..6765c5f 100644
--- a/src/modules/Elsa.Http/Services/DefaultRouteTableUpdater.cs
+++ b/src/modules/Elsa.Http/Services/DefaultRouteTableUpdater.cs
@@ -23,17 +23,29 @@ public class DefaultRouteTableUpdater(
var stimulusName = HttpStimulusNames.HttpEndpoint;
var triggerFilter = new TriggerFilter
{
- Name = stimulusName
+ Name = stimulusName,
+ TenantAgnostic = true
};
var bookmarkFilter = new BookmarkFilter
{
- Name = stimulusName
+ Name = stimulusName,
+ TenantAgnostic = true
};
var triggers = (await triggerStore.FindManyAsync(triggerFilter, cancellationToken)).ToList();
var bookmarks = (await bookmarkStore.FindManyAsync(bookmarkFilter, cancellationToken)).ToList();
- await AddRoutesAsync(triggers, cancellationToken);
- await AddRoutesAsync(bookmarks, cancellationToken);
+ var routes = new List();
+ foreach (var trigger in Filter(triggers))
+ {
+ var context = new HttpEndpointRouteProviderContext(trigger.GetPayload(), trigger.TenantId, cancellationToken);
+ routes.AddRange(await httpEndpointRoutesProvider.GetRoutesAsync(context));
+ }
+ foreach (var bookmark in Filter(bookmarks))
+ {
+ var context = new HttpEndpointRouteProviderContext(bookmark.GetPayload(), bookmark.TenantId, cancellationToken);
+ routes.AddRange(await httpEndpointRoutesProvider.GetRoutesAsync(context));
+ }
+ routeTable.Replace(routes);
}
public async Task AddRoutesAsync(IEnumerable triggers, CancellationToken cancellationToken = default)
@@ -107,4 +119,4 @@ public class DefaultRouteTableUpdater(
var stimulusName = HttpStimulusNames.HttpEndpoint;
return bookmarks.Where(x => x.Name == stimulusName && x.Payload != null);
}
-}
\ No newline at end of file
+}
diff --git a/src/modules/Elsa.Http/Services/RouteTable.cs b/src/modules/Elsa.Http/Services/RouteTable.cs
index 67260a7..687cb2b 100644
--- a/src/modules/Elsa.Http/Services/RouteTable.cs
+++ b/src/modules/Elsa.Http/Services/RouteTable.cs
@@ -12,6 +12,13 @@ public class RouteTable(IMemoryCache cache, ILogger logger) : IRoute
private static readonly object Key = new();
private ConcurrentDictionary Routes => cache.GetOrCreate(Key, _ => new ConcurrentDictionary())!;
+ public void Replace(IEnumerable routes)
+ {
+ var snapshot = new ConcurrentDictionary();
+ foreach (var route in routes.Where(x => !x.Route.Contains("//")))
+ snapshot.TryAdd(route.Route.NormalizeRoute(), route);
+ cache.Set(Key, snapshot);
+ }
///
public void Add(string route)
@@ -56,4 +63,4 @@ public class RouteTable(IMemoryCache cache, ILogger logger) : IRoute
public IEnumerator GetEnumerator() => Routes.Values.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
-}
\ No newline at end of file
+}
diff --git a/src/modules/Elsa.Http/Services/RouteTableRevisionRefresher.cs b/src/modules/Elsa.Http/Services/RouteTableRevisionRefresher.cs
new file mode 100644
index 0000000..7eb866a
--- /dev/null
+++ b/src/modules/Elsa.Http/Services/RouteTableRevisionRefresher.cs
@@ -0,0 +1,34 @@
+using Elsa.Caching;
+
+namespace Elsa.Http.Services;
+
+/// A local route snapshot with a revision checked against SQL on each inbound workflow request.
+public class RouteTableRevisionState : IDisposable
+{
+ public SemaphoreSlim Gate { get; } = new(1, 1);
+ public string? Revision { get; set; }
+ public void Dispose() => Gate.Dispose();
+}
+
+public class RouteTableRevisionRefresher(ICacheManager cache, IRouteTableUpdater updater, RouteTableRevisionState state)
+{
+ public async Task EnsureFreshAsync(CancellationToken cancellationToken)
+ {
+ if (!cache.IsShared) return;
+ // A process-wide route snapshot must not be built from a request's ambient transaction snapshot.
+ using var suppressed = new System.Transactions.TransactionScope(System.Transactions.TransactionScopeOption.Suppress,
+ System.Transactions.TransactionScopeAsyncFlowOption.Enabled);
+ var before = await cache.GetConsistencyStateAsync(cancellationToken);
+ if (!before.HasPendingRouteMutations && (before.RouteRevision ?? before.Revision) == state.Revision) return;
+ await state.Gate.WaitAsync(cancellationToken);
+ try
+ {
+ before = await cache.GetConsistencyStateAsync(cancellationToken);
+ if (!before.HasPendingRouteMutations && (before.RouteRevision ?? before.Revision) == state.Revision) return;
+ await updater.UpdateAsync(cancellationToken);
+ var after = await cache.GetConsistencyStateAsync(cancellationToken);
+ state.Revision = !after.HasPendingRouteMutations && (before.RouteRevision ?? before.Revision) == (after.RouteRevision ?? after.Revision) ? after.RouteRevision ?? after.Revision : null;
+ }
+ finally { state.Gate.Release(); }
+ }
+}
diff --git a/src/modules/Elsa.IO.Compression/Elsa.IO.Compression.csproj b/src/modules/Elsa.IO.Compression/Elsa.IO.Compression.csproj
index 62000c5..950a1b1 100644
--- a/src/modules/Elsa.IO.Compression/Elsa.IO.Compression.csproj
+++ b/src/modules/Elsa.IO.Compression/Elsa.IO.Compression.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.IO.Compression
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides compression and archiving activities for Elsa Workflows.
diff --git a/src/modules/Elsa.IO.Http/Elsa.IO.Http.csproj b/src/modules/Elsa.IO.Http/Elsa.IO.Http.csproj
index 7426539..d7f84e2 100644
--- a/src/modules/Elsa.IO.Http/Elsa.IO.Http.csproj
+++ b/src/modules/Elsa.IO.Http/Elsa.IO.Http.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.IO.Http
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides http capabilities to IO modules in Elsa Workflows.
diff --git a/src/modules/Elsa.IO/Elsa.IO.csproj b/src/modules/Elsa.IO/Elsa.IO.csproj
index b187af2..6c606cb 100644
--- a/src/modules/Elsa.IO/Elsa.IO.csproj
+++ b/src/modules/Elsa.IO/Elsa.IO.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.IO
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides IO services for resolving various content types to streams.
diff --git a/src/modules/Elsa.Identity/Elsa.Identity.csproj b/src/modules/Elsa.Identity/Elsa.Identity.csproj
index 2c0b2e2..3b0f818 100644
--- a/src/modules/Elsa.Identity/Elsa.Identity.csproj
+++ b/src/modules/Elsa.Identity/Elsa.Identity.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Identity
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides a default identity solution.
diff --git a/src/modules/Elsa.JavaScript.Libraries/Elsa.JavaScript.Libraries.csproj b/src/modules/Elsa.JavaScript.Libraries/Elsa.JavaScript.Libraries.csproj
index f039407..9d69d0d 100644
--- a/src/modules/Elsa.JavaScript.Libraries/Elsa.JavaScript.Libraries.csproj
+++ b/src/modules/Elsa.JavaScript.Libraries/Elsa.JavaScript.Libraries.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.JavaScript.Libraries
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides the following JavaScript libraries for Elsa:
diff --git a/src/modules/Elsa.JavaScript/Elsa.JavaScript.csproj b/src/modules/Elsa.JavaScript/Elsa.JavaScript.csproj
index 964cf23..3c23e09 100644
--- a/src/modules/Elsa.JavaScript/Elsa.JavaScript.csproj
+++ b/src/modules/Elsa.JavaScript/Elsa.JavaScript.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.JavaScript
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides a JavaScript expression provider.
diff --git a/src/modules/Elsa.Kafka/Elsa.Kafka.csproj b/src/modules/Elsa.Kafka/Elsa.Kafka.csproj
index b1de00e..0dca7ee 100644
--- a/src/modules/Elsa.Kafka/Elsa.Kafka.csproj
+++ b/src/modules/Elsa.Kafka/Elsa.Kafka.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Kafka
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides Apache Kafka integration and activities.
diff --git a/src/modules/Elsa.KeyValues/Elsa.KeyValues.csproj b/src/modules/Elsa.KeyValues/Elsa.KeyValues.csproj
index 97975a8..b7ed2e4 100644
--- a/src/modules/Elsa.KeyValues/Elsa.KeyValues.csproj
+++ b/src/modules/Elsa.KeyValues/Elsa.KeyValues.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.KeyValues
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides a store for holding key value records.
diff --git a/src/modules/Elsa.Labels/Elsa.Labels.csproj b/src/modules/Elsa.Labels/Elsa.Labels.csproj
index a82ae2a..b0b966b 100644
--- a/src/modules/Elsa.Labels/Elsa.Labels.csproj
+++ b/src/modules/Elsa.Labels/Elsa.Labels.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Labels
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides funtionality to tag workflows with labels and filter by them.
diff --git a/src/modules/Elsa.Liquid/Elsa.Liquid.csproj b/src/modules/Elsa.Liquid/Elsa.Liquid.csproj
index fd2a0e8..b717a0c 100644
--- a/src/modules/Elsa.Liquid/Elsa.Liquid.csproj
+++ b/src/modules/Elsa.Liquid/Elsa.Liquid.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Liquid
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides a Liquid expression provider.
diff --git a/src/modules/Elsa.MassTransit.AzureServiceBus/Elsa.MassTransit.AzureServiceBus.csproj b/src/modules/Elsa.MassTransit.AzureServiceBus/Elsa.MassTransit.AzureServiceBus.csproj
index 5596c82..4c0ae55 100644
--- a/src/modules/Elsa.MassTransit.AzureServiceBus/Elsa.MassTransit.AzureServiceBus.csproj
+++ b/src/modules/Elsa.MassTransit.AzureServiceBus/Elsa.MassTransit.AzureServiceBus.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.MassTransit.AzureServiceBus
Caspian.
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides integration of Azure Service Bus with MassTransit's integration of Elsa.
diff --git a/src/modules/Elsa.MassTransit.RabbitMq/Elsa.MassTransit.RabbitMq.csproj b/src/modules/Elsa.MassTransit.RabbitMq/Elsa.MassTransit.RabbitMq.csproj
index f2f7be4..04e9f5c 100644
--- a/src/modules/Elsa.MassTransit.RabbitMq/Elsa.MassTransit.RabbitMq.csproj
+++ b/src/modules/Elsa.MassTransit.RabbitMq/Elsa.MassTransit.RabbitMq.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.MassTransit.RabbitMq
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides integration of RabbitMQ with MassTransit's integration of Elsa.
diff --git a/src/modules/Elsa.MassTransit/Elsa.MassTransit.csproj b/src/modules/Elsa.MassTransit/Elsa.MassTransit.csproj
index 0902853..18babef 100644
--- a/src/modules/Elsa.MassTransit/Elsa.MassTransit.csproj
+++ b/src/modules/Elsa.MassTransit/Elsa.MassTransit.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.MassTransit
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides integration with MassTransit and comes with a MassTransit implementation of Elsa's IServiceBus abstraction.
diff --git a/src/modules/Elsa.MongoDb/Elsa.MongoDb.csproj b/src/modules/Elsa.MongoDb/Elsa.MongoDb.csproj
index db333eb..75406ef 100644
--- a/src/modules/Elsa.MongoDb/Elsa.MongoDb.csproj
+++ b/src/modules/Elsa.MongoDb/Elsa.MongoDb.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.MongoDb
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides MongoDB implementations of various abstractions from various modules.
diff --git a/src/modules/Elsa.OpenTelemetry/Elsa.OpenTelemetry.csproj b/src/modules/Elsa.OpenTelemetry/Elsa.OpenTelemetry.csproj
index 0602c75..c24641c 100644
--- a/src/modules/Elsa.OpenTelemetry/Elsa.OpenTelemetry.csproj
+++ b/src/modules/Elsa.OpenTelemetry/Elsa.OpenTelemetry.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.OpenTelemetry
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides OpenTelemetry sources for tracing workflow and activity execution.
diff --git a/src/modules/Elsa.ProtoActor.Core/Elsa.ProtoActor.Core.csproj b/src/modules/Elsa.ProtoActor.Core/Elsa.ProtoActor.Core.csproj
index 10968ed..25cc775 100644
--- a/src/modules/Elsa.ProtoActor.Core/Elsa.ProtoActor.Core.csproj
+++ b/src/modules/Elsa.ProtoActor.Core/Elsa.ProtoActor.Core.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.ProtoActor.Core
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides ProtoActor services to the system. Other modules can add actors to the ActorSystem.
diff --git a/src/modules/Elsa.Python/Elsa.Python.csproj b/src/modules/Elsa.Python/Elsa.Python.csproj
index f70e0c0..6581550 100644
--- a/src/modules/Elsa.Python/Elsa.Python.csproj
+++ b/src/modules/Elsa.Python/Elsa.Python.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Python
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides a Python expression provider.
diff --git a/src/modules/Elsa.Quartz.EntityFrameworkCore.MySql/Elsa.Quartz.EntityFrameworkCore.MySql.csproj b/src/modules/Elsa.Quartz.EntityFrameworkCore.MySql/Elsa.Quartz.EntityFrameworkCore.MySql.csproj
index 48e8844..4ccb29f 100644
--- a/src/modules/Elsa.Quartz.EntityFrameworkCore.MySql/Elsa.Quartz.EntityFrameworkCore.MySql.csproj
+++ b/src/modules/Elsa.Quartz.EntityFrameworkCore.MySql/Elsa.Quartz.EntityFrameworkCore.MySql.csproj
@@ -5,7 +5,7 @@
Caspian.Elsa.Quartz.EntityFrameworkCore.MySql
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides EF Core migrations for Quartz.NET.
diff --git a/src/modules/Elsa.Quartz.EntityFrameworkCore.PostgreSql/Elsa.Quartz.EntityFrameworkCore.PostgreSql.csproj b/src/modules/Elsa.Quartz.EntityFrameworkCore.PostgreSql/Elsa.Quartz.EntityFrameworkCore.PostgreSql.csproj
index 7ff97fe..5c3cb7d 100644
--- a/src/modules/Elsa.Quartz.EntityFrameworkCore.PostgreSql/Elsa.Quartz.EntityFrameworkCore.PostgreSql.csproj
+++ b/src/modules/Elsa.Quartz.EntityFrameworkCore.PostgreSql/Elsa.Quartz.EntityFrameworkCore.PostgreSql.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Quartz.EntityFrameworkCore.PostgreSql
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides EF Core migrations for Quartz.NET.
diff --git a/src/modules/Elsa.Quartz.EntityFrameworkCore.SqlServer/Elsa.Quartz.EntityFrameworkCore.SqlServer.csproj b/src/modules/Elsa.Quartz.EntityFrameworkCore.SqlServer/Elsa.Quartz.EntityFrameworkCore.SqlServer.csproj
index 15fdc73..687514d 100644
--- a/src/modules/Elsa.Quartz.EntityFrameworkCore.SqlServer/Elsa.Quartz.EntityFrameworkCore.SqlServer.csproj
+++ b/src/modules/Elsa.Quartz.EntityFrameworkCore.SqlServer/Elsa.Quartz.EntityFrameworkCore.SqlServer.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Quartz.EntityFrameworkCore.SqlServer
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides EF Core migrations for Quartz.NET.
diff --git a/src/modules/Elsa.Quartz.EntityFrameworkCore.Sqlite/Elsa.Quartz.EntityFrameworkCore.Sqlite.csproj b/src/modules/Elsa.Quartz.EntityFrameworkCore.Sqlite/Elsa.Quartz.EntityFrameworkCore.Sqlite.csproj
index 2fcfe88..3492e5e 100644
--- a/src/modules/Elsa.Quartz.EntityFrameworkCore.Sqlite/Elsa.Quartz.EntityFrameworkCore.Sqlite.csproj
+++ b/src/modules/Elsa.Quartz.EntityFrameworkCore.Sqlite/Elsa.Quartz.EntityFrameworkCore.Sqlite.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Quartz.EntityFrameworkCore.Sqlite
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides EF Core migrations for Quartz.NET.
diff --git a/src/modules/Elsa.Quartz/Elsa.Quartz.csproj b/src/modules/Elsa.Quartz/Elsa.Quartz.csproj
index 1c53c48..a63bdc5 100644
--- a/src/modules/Elsa.Quartz/Elsa.Quartz.csproj
+++ b/src/modules/Elsa.Quartz/Elsa.Quartz.csproj
@@ -5,7 +5,7 @@
Caspian.Elsa.Quartz
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides integration with the Quartz.NET library and provide am implementation of Elsa's IJobScheduler using Quartz.NET.
diff --git a/src/modules/Elsa.Resilience.Core/Elsa.Resilience.Core.csproj b/src/modules/Elsa.Resilience.Core/Elsa.Resilience.Core.csproj
index f017bce..56838bd 100644
--- a/src/modules/Elsa.Resilience.Core/Elsa.Resilience.Core.csproj
+++ b/src/modules/Elsa.Resilience.Core/Elsa.Resilience.Core.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Resilience.Core
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides core services and types for implementing resilient services and activities.
diff --git a/src/modules/Elsa.Resilience/Elsa.Resilience.csproj b/src/modules/Elsa.Resilience/Elsa.Resilience.csproj
index 1011437..902a091 100644
--- a/src/modules/Elsa.Resilience/Elsa.Resilience.csproj
+++ b/src/modules/Elsa.Resilience/Elsa.Resilience.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Resilience
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides features and services for implementing resilient services and activities.
diff --git a/src/modules/Elsa.Retention/Elsa.Retention.csproj b/src/modules/Elsa.Retention/Elsa.Retention.csproj
index bbac337..12dbfce 100644
--- a/src/modules/Elsa.Retention/Elsa.Retention.csproj
+++ b/src/modules/Elsa.Retention/Elsa.Retention.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Retention
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides retention options for workflows.
diff --git a/src/modules/Elsa.SasTokens/Elsa.SasTokens.csproj b/src/modules/Elsa.SasTokens/Elsa.SasTokens.csproj
index 3549fc6..7798d2c 100644
--- a/src/modules/Elsa.SasTokens/Elsa.SasTokens.csproj
+++ b/src/modules/Elsa.SasTokens/Elsa.SasTokens.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.SasTokens
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides services to generate SAS tokens.
diff --git a/src/modules/Elsa.Scheduling/Elsa.Scheduling.csproj b/src/modules/Elsa.Scheduling/Elsa.Scheduling.csproj
index 16f7285..e61ce5a 100644
--- a/src/modules/Elsa.Scheduling/Elsa.Scheduling.csproj
+++ b/src/modules/Elsa.Scheduling/Elsa.Scheduling.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Scheduling
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides scheduling activities such as Timer, Delay and StartAt.
diff --git a/src/modules/Elsa.Secrets.Api/Elsa.Secrets.Api.csproj b/src/modules/Elsa.Secrets.Api/Elsa.Secrets.Api.csproj
index 5b570b9..ce4d65b 100644
--- a/src/modules/Elsa.Secrets.Api/Elsa.Secrets.Api.csproj
+++ b/src/modules/Elsa.Secrets.Api/Elsa.Secrets.Api.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Secrets.Api
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides Secrets API endpoints
elsa module secrets api
diff --git a/src/modules/Elsa.Secrets.Core/Elsa.Secrets.Core.csproj b/src/modules/Elsa.Secrets.Core/Elsa.Secrets.Core.csproj
index a1d8e1e..af902ff 100644
--- a/src/modules/Elsa.Secrets.Core/Elsa.Secrets.Core.csproj
+++ b/src/modules/Elsa.Secrets.Core/Elsa.Secrets.Core.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Secrets.Core
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides services to get secrets.
diff --git a/src/modules/Elsa.Secrets.Management/Elsa.Secrets.Management.csproj b/src/modules/Elsa.Secrets.Management/Elsa.Secrets.Management.csproj
index 8d2a3b1..d50c103 100644
--- a/src/modules/Elsa.Secrets.Management/Elsa.Secrets.Management.csproj
+++ b/src/modules/Elsa.Secrets.Management/Elsa.Secrets.Management.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Secrets.Management
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides services to manage secrets.
diff --git a/src/modules/Elsa.Secrets.Models/Elsa.Secrets.Models.csproj b/src/modules/Elsa.Secrets.Models/Elsa.Secrets.Models.csproj
index e0a590c..9977236 100644
--- a/src/modules/Elsa.Secrets.Models/Elsa.Secrets.Models.csproj
+++ b/src/modules/Elsa.Secrets.Models/Elsa.Secrets.Models.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Secrets.Models
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides models that can be shared between the API and API clients
elsa module secrets api
diff --git a/src/modules/Elsa.Secrets.Persistence.EntityFrameworkCore.PostgreSql/Elsa.Secrets.Persistence.EntityFrameworkCore.PostgreSql.csproj b/src/modules/Elsa.Secrets.Persistence.EntityFrameworkCore.PostgreSql/Elsa.Secrets.Persistence.EntityFrameworkCore.PostgreSql.csproj
index 1619ab7..a933ca9 100644
--- a/src/modules/Elsa.Secrets.Persistence.EntityFrameworkCore.PostgreSql/Elsa.Secrets.Persistence.EntityFrameworkCore.PostgreSql.csproj
+++ b/src/modules/Elsa.Secrets.Persistence.EntityFrameworkCore.PostgreSql/Elsa.Secrets.Persistence.EntityFrameworkCore.PostgreSql.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Secrets.Persistence.EntityFrameworkCore.PostgreSql
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides PostgreSql persistence for the Elsa Secrets module using Entity Framework Core.
elsa module secrets persistence efcore PostgreSql
diff --git a/src/modules/Elsa.Secrets.Persistence.EntityFrameworkCore.SqlServer/Elsa.Secrets.Persistence.EntityFrameworkCore.SqlServer.csproj b/src/modules/Elsa.Secrets.Persistence.EntityFrameworkCore.SqlServer/Elsa.Secrets.Persistence.EntityFrameworkCore.SqlServer.csproj
index 93d59ea..6aa3539 100644
--- a/src/modules/Elsa.Secrets.Persistence.EntityFrameworkCore.SqlServer/Elsa.Secrets.Persistence.EntityFrameworkCore.SqlServer.csproj
+++ b/src/modules/Elsa.Secrets.Persistence.EntityFrameworkCore.SqlServer/Elsa.Secrets.Persistence.EntityFrameworkCore.SqlServer.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Secrets.Persistence.EntityFrameworkCore.SqlServer
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides SqlServer persistence for the Elsa Secrets module using Entity Framework Core.
elsa module secrets persistence efcore SqlServer
diff --git a/src/modules/Elsa.Secrets.Persistence.EntityFrameworkCore.Sqlite/Elsa.Secrets.Persistence.EntityFrameworkCore.Sqlite.csproj b/src/modules/Elsa.Secrets.Persistence.EntityFrameworkCore.Sqlite/Elsa.Secrets.Persistence.EntityFrameworkCore.Sqlite.csproj
index 070dcb0..4fb9fe7 100644
--- a/src/modules/Elsa.Secrets.Persistence.EntityFrameworkCore.Sqlite/Elsa.Secrets.Persistence.EntityFrameworkCore.Sqlite.csproj
+++ b/src/modules/Elsa.Secrets.Persistence.EntityFrameworkCore.Sqlite/Elsa.Secrets.Persistence.EntityFrameworkCore.Sqlite.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Secrets.Persistence.EntityFrameworkCore.Sqlite
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides SQLite persistence for the Elsa Secrets module using Entity Framework Core.
elsa module secrets persistence efcore sqlite
diff --git a/src/modules/Elsa.Secrets.Persistence.EntityFrameworkCore/Elsa.Secrets.Persistence.EntityFrameworkCore.csproj b/src/modules/Elsa.Secrets.Persistence.EntityFrameworkCore/Elsa.Secrets.Persistence.EntityFrameworkCore.csproj
index c7f5d79..f5dee21 100644
--- a/src/modules/Elsa.Secrets.Persistence.EntityFrameworkCore/Elsa.Secrets.Persistence.EntityFrameworkCore.csproj
+++ b/src/modules/Elsa.Secrets.Persistence.EntityFrameworkCore/Elsa.Secrets.Persistence.EntityFrameworkCore.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Secrets.Persistence.EntityFrameworkCore
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides an Entity Framework Core provider for the persistence layer of the Secrets module
elsa module security secrets persistence efcore entity framework core
diff --git a/src/modules/Elsa.Secrets.Scripting/Elsa.Secrets.Scripting.csproj b/src/modules/Elsa.Secrets.Scripting/Elsa.Secrets.Scripting.csproj
index 580bbf5..6547b73 100644
--- a/src/modules/Elsa.Secrets.Scripting/Elsa.Secrets.Scripting.csproj
+++ b/src/modules/Elsa.Secrets.Scripting/Elsa.Secrets.Scripting.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Secrets.Scripting
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides scripting integration with various languages like JavaScript, C#, Python and Liquid.
elsa module secrets scripting
diff --git a/src/modules/Elsa.Sql.MySql/Elsa.Sql.MySql.csproj b/src/modules/Elsa.Sql.MySql/Elsa.Sql.MySql.csproj
index cf7365d..0c1f4b9 100644
--- a/src/modules/Elsa.Sql.MySql/Elsa.Sql.MySql.csproj
+++ b/src/modules/Elsa.Sql.MySql/Elsa.Sql.MySql.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Sql.MySql
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides client implementations for interacting with MySql databases.
diff --git a/src/modules/Elsa.Sql.PostgreSql/Elsa.Sql.PostgreSql.csproj b/src/modules/Elsa.Sql.PostgreSql/Elsa.Sql.PostgreSql.csproj
index 7ddfe6f..903a849 100644
--- a/src/modules/Elsa.Sql.PostgreSql/Elsa.Sql.PostgreSql.csproj
+++ b/src/modules/Elsa.Sql.PostgreSql/Elsa.Sql.PostgreSql.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Sql.PostgreSql
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides client implementations for interacting with PostgreSql databases.
diff --git a/src/modules/Elsa.Sql.SqlServer/Elsa.Sql.SqlServer.csproj b/src/modules/Elsa.Sql.SqlServer/Elsa.Sql.SqlServer.csproj
index e1da09c..da34aeb 100644
--- a/src/modules/Elsa.Sql.SqlServer/Elsa.Sql.SqlServer.csproj
+++ b/src/modules/Elsa.Sql.SqlServer/Elsa.Sql.SqlServer.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Sql.SqlServer
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides client implementations for interacting with Microsoft SQL Server databases.
diff --git a/src/modules/Elsa.Sql.Sqlite/Elsa.Sql.Sqlite.csproj b/src/modules/Elsa.Sql.Sqlite/Elsa.Sql.Sqlite.csproj
index f1385ba..cdb135d 100644
--- a/src/modules/Elsa.Sql.Sqlite/Elsa.Sql.Sqlite.csproj
+++ b/src/modules/Elsa.Sql.Sqlite/Elsa.Sql.Sqlite.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Sql.Sqlite
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides client implementations for interacting with Sqlite databases.
diff --git a/src/modules/Elsa.Sql/Elsa.Sql.csproj b/src/modules/Elsa.Sql/Elsa.Sql.csproj
index 730eec0..2a1652e 100644
--- a/src/modules/Elsa.Sql/Elsa.Sql.csproj
+++ b/src/modules/Elsa.Sql/Elsa.Sql.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Sql
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides activities to interact with sql databases.
diff --git a/src/modules/Elsa.Tenants.AspNetCore/Elsa.Tenants.AspNetCore.csproj b/src/modules/Elsa.Tenants.AspNetCore/Elsa.Tenants.AspNetCore.csproj
index 07f35d6..990f8ff 100644
--- a/src/modules/Elsa.Tenants.AspNetCore/Elsa.Tenants.AspNetCore.csproj
+++ b/src/modules/Elsa.Tenants.AspNetCore/Elsa.Tenants.AspNetCore.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Tenants.AspNetCore
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides multitenancy middleware
diff --git a/src/modules/Elsa.Tenants/Elsa.Tenants.csproj b/src/modules/Elsa.Tenants/Elsa.Tenants.csproj
index 881f657..7483e7f 100644
--- a/src/modules/Elsa.Tenants/Elsa.Tenants.csproj
+++ b/src/modules/Elsa.Tenants/Elsa.Tenants.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Tenants
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides multitenancy services.
diff --git a/src/modules/Elsa.Webhooks/Elsa.Webhooks.csproj b/src/modules/Elsa.Webhooks/Elsa.Webhooks.csproj
index c694f16..2d3a9a5 100644
--- a/src/modules/Elsa.Webhooks/Elsa.Webhooks.csproj
+++ b/src/modules/Elsa.Webhooks/Elsa.Webhooks.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Webhooks
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides a way to register webhook endpoints that should be invoked when certain event occur.
diff --git a/src/modules/Elsa.WorkflowContexts/Elsa.WorkflowContexts.csproj b/src/modules/Elsa.WorkflowContexts/Elsa.WorkflowContexts.csproj
index 4b9cfd9..e6c2e51 100644
--- a/src/modules/Elsa.WorkflowContexts/Elsa.WorkflowContexts.csproj
+++ b/src/modules/Elsa.WorkflowContexts/Elsa.WorkflowContexts.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.WorkflowContexts
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides workflow context APIs to Elsa workflows.
diff --git a/src/modules/Elsa.WorkflowProviders.BlobStorage/Elsa.WorkflowProviders.BlobStorage.csproj b/src/modules/Elsa.WorkflowProviders.BlobStorage/Elsa.WorkflowProviders.BlobStorage.csproj
index 0b49f32..90041c8 100644
--- a/src/modules/Elsa.WorkflowProviders.BlobStorage/Elsa.WorkflowProviders.BlobStorage.csproj
+++ b/src/modules/Elsa.WorkflowProviders.BlobStorage/Elsa.WorkflowProviders.BlobStorage.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.WorkflowProviders.BlobStorage
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides a workflow definition provider that loads workflow definitions from files.
diff --git a/src/modules/Elsa.Workflows.Api/Elsa.Workflows.Api.csproj b/src/modules/Elsa.Workflows.Api/Elsa.Workflows.Api.csproj
index 1baee8f..b7a3285 100644
--- a/src/modules/Elsa.Workflows.Api/Elsa.Workflows.Api.csproj
+++ b/src/modules/Elsa.Workflows.Api/Elsa.Workflows.Api.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Workflows.Api
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides API endpoints for client applications such as the Elsa Designer web component.
diff --git a/src/modules/Elsa.Workflows.Core/Elsa.Workflows.Core.csproj b/src/modules/Elsa.Workflows.Core/Elsa.Workflows.Core.csproj
index d3642c6..774eebd 100644
--- a/src/modules/Elsa.Workflows.Core/Elsa.Workflows.Core.csproj
+++ b/src/modules/Elsa.Workflows.Core/Elsa.Workflows.Core.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Workflows.Core
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Elsa.Workflows
diff --git a/src/modules/Elsa.Workflows.Management/Elsa.Workflows.Management.csproj b/src/modules/Elsa.Workflows.Management/Elsa.Workflows.Management.csproj
index a34b0e7..48586f1 100644
--- a/src/modules/Elsa.Workflows.Management/Elsa.Workflows.Management.csproj
+++ b/src/modules/Elsa.Workflows.Management/Elsa.Workflows.Management.csproj
@@ -1,10 +1,10 @@
-
+
Caspian.Elsa.Workflows.Management
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides workflow management functionality.
diff --git a/src/modules/Elsa.Workflows.Management/Extensions/ModuleExtensions.cs b/src/modules/Elsa.Workflows.Management/Extensions/ModuleExtensions.cs
index dc6a1dc..236731f 100644
--- a/src/modules/Elsa.Workflows.Management/Extensions/ModuleExtensions.cs
+++ b/src/modules/Elsa.Workflows.Management/Extensions/ModuleExtensions.cs
@@ -89,4 +89,13 @@ public static class ModuleExtensions
feature.Module.Configure(configure);
return feature;
}
-}
\ No newline at end of file
+ public static WorkflowManagementFeature UseCache(this WorkflowManagementFeature feature,
+ Microsoft.Extensions.Configuration.IConfigurationSection section, Action? configure = null)
+ {
+ return feature.UseCache(cache =>
+ {
+ cache.UseConfiguration(section);
+ configure?.Invoke(cache);
+ });
+ }
+}
diff --git a/src/modules/Elsa.Workflows.Management/Features/CachingWorkflowDefinitionsFeature.cs b/src/modules/Elsa.Workflows.Management/Features/CachingWorkflowDefinitionsFeature.cs
index df939d6..a5e7622 100644
--- a/src/modules/Elsa.Workflows.Management/Features/CachingWorkflowDefinitionsFeature.cs
+++ b/src/modules/Elsa.Workflows.Management/Features/CachingWorkflowDefinitionsFeature.cs
@@ -1,4 +1,6 @@
using Elsa.Features.Abstractions;
+using Elsa.Caching.Features;
+using Elsa.Features.Attributes;
using Elsa.Features.Services;
using Elsa.Workflows.Management.Handlers;
using Elsa.Workflows.Management.Handlers.Notifications;
@@ -11,7 +13,8 @@ namespace Elsa.Workflows.Management.Features;
///
/// Configures workflow definition caching.
///
-public class CachingWorkflowDefinitionsFeature : FeatureBase
+[DependsOn(typeof(CacheFeature))]
+public class CachingWorkflowDefinitionsFeature : CacheConsumerFeature
{
///
public CachingWorkflowDefinitionsFeature(IModule module) : base(module)
@@ -26,4 +29,4 @@ public class CachingWorkflowDefinitionsFeature : FeatureBase
Services.Decorate();
Services.AddNotificationHandler();
}
-}
\ No newline at end of file
+}
diff --git a/src/modules/Elsa.Workflows.Management/Handlers/Notifications/EvictWorkflowDefinitionServiceCache.cs b/src/modules/Elsa.Workflows.Management/Handlers/Notifications/EvictWorkflowDefinitionServiceCache.cs
index f3802e9..7de4bef 100644
--- a/src/modules/Elsa.Workflows.Management/Handlers/Notifications/EvictWorkflowDefinitionServiceCache.cs
+++ b/src/modules/Elsa.Workflows.Management/Handlers/Notifications/EvictWorkflowDefinitionServiceCache.cs
@@ -12,39 +12,39 @@ namespace Elsa.Workflows.Management.Handlers.Notifications;
///
[UsedImplicitly]
internal class EvictWorkflowDefinitionServiceCache(IWorkflowDefinitionCacheManager workflowDefinitionCacheManager) :
- INotificationHandler,
- INotificationHandler,
- INotificationHandler,
- INotificationHandler,
- INotificationHandler
+ INotificationHandler,
+ INotificationHandler,
+ INotificationHandler,
+ INotificationHandler,
+ INotificationHandler
{
///
- public async Task HandleAsync(WorkflowDefinitionPublishing notification, CancellationToken cancellationToken)
+ public async Task HandleAsync(WorkflowDefinitionPublished notification, CancellationToken cancellationToken)
{
await workflowDefinitionCacheManager.EvictWorkflowDefinitionAsync(notification.WorkflowDefinition.DefinitionId, cancellationToken);
}
///
- public async Task HandleAsync(WorkflowDefinitionRetracting notification, CancellationToken cancellationToken)
+ public async Task HandleAsync(WorkflowDefinitionRetracted notification, CancellationToken cancellationToken)
{
await workflowDefinitionCacheManager.EvictWorkflowDefinitionAsync(notification.WorkflowDefinition.DefinitionId, cancellationToken);
}
///
- public async Task HandleAsync(WorkflowDefinitionDeleting notification, CancellationToken cancellationToken)
+ public async Task HandleAsync(WorkflowDefinitionDeleted notification, CancellationToken cancellationToken)
{
await workflowDefinitionCacheManager.EvictWorkflowDefinitionAsync(notification.DefinitionId, cancellationToken);
}
///
- public async Task HandleAsync(WorkflowDefinitionsDeleting notification, CancellationToken cancellationToken)
+ public async Task HandleAsync(WorkflowDefinitionsDeleted notification, CancellationToken cancellationToken)
{
foreach (var definitionId in notification.DefinitionIds)
await workflowDefinitionCacheManager.EvictWorkflowDefinitionAsync(definitionId, cancellationToken);
}
///
- public async Task HandleAsync(WorkflowDefinitionVersionsUpdating notification, CancellationToken cancellationToken)
+ public async Task HandleAsync(WorkflowDefinitionVersionsUpdated notification, CancellationToken cancellationToken)
{
foreach (var definition in notification.WorkflowDefinitions)
await workflowDefinitionCacheManager.EvictWorkflowDefinitionAsync(definition.DefinitionId, cancellationToken);
diff --git a/src/modules/Elsa.Workflows.Management/Services/CachingWorkflowDefinitionService.cs b/src/modules/Elsa.Workflows.Management/Services/CachingWorkflowDefinitionService.cs
index f8936d8..cd15f7d 100644
--- a/src/modules/Elsa.Workflows.Management/Services/CachingWorkflowDefinitionService.cs
+++ b/src/modules/Elsa.Workflows.Management/Services/CachingWorkflowDefinitionService.cs
@@ -3,7 +3,7 @@ using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.Management.Filters;
using Elsa.Workflows.Models;
using JetBrains.Annotations;
-using Microsoft.Extensions.Caching.Memory;
+using Elsa.Caching.Models;
namespace Elsa.Workflows.Management.Services;
@@ -61,6 +61,7 @@ public class CachingWorkflowDefinitionService(IWorkflowDefinitionService decorat
///
public async Task FindWorkflowGraphAsync(string definitionId, VersionOptions versionOptions, CancellationToken cancellationToken = default)
{
+ if (cacheManager.Cache.IsShared) return await decoratedService.FindWorkflowGraphAsync(definitionId, versionOptions, cancellationToken);
var cacheKey = cacheManager.CreateWorkflowVersionCacheKey(definitionId, versionOptions);
return await GetFromCacheAsync(cacheKey,
() => decoratedService.FindWorkflowGraphAsync(definitionId, versionOptions, cancellationToken),
@@ -70,6 +71,7 @@ public class CachingWorkflowDefinitionService(IWorkflowDefinitionService decorat
///
public async Task FindWorkflowGraphAsync(string definitionVersionId, CancellationToken cancellationToken = default)
{
+ if (cacheManager.Cache.IsShared) return await decoratedService.FindWorkflowGraphAsync(definitionVersionId, cancellationToken);
var cacheKey = cacheManager.CreateWorkflowVersionCacheKey(definitionVersionId);
return await GetFromCacheAsync(cacheKey,
() => decoratedService.FindWorkflowGraphAsync(definitionVersionId, cancellationToken),
@@ -90,6 +92,7 @@ public class CachingWorkflowDefinitionService(IWorkflowDefinitionService decorat
///
public async Task FindWorkflowGraphAsync(WorkflowDefinitionFilter filter, CancellationToken cancellationToken = default)
{
+ if (cacheManager.Cache.IsShared) return await decoratedService.FindWorkflowGraphAsync(filter, cancellationToken);
var cacheKey = cacheManager.CreateWorkflowFilterCacheKey(filter);
return await GetFromCacheAsync(cacheKey,
() => decoratedService.FindWorkflowGraphAsync(filter, cancellationToken),
@@ -99,18 +102,15 @@ public class CachingWorkflowDefinitionService(IWorkflowDefinitionService decorat
private async Task GetFromCacheAsync(string cacheKey, Func> getObjectFunc, Func getChangeTokenKeyFunc)
{
var cache = cacheManager.Cache;
- return await cache.GetOrCreateAsync(cacheKey, async entry =>
+ var options = new CacheEntryOptions { AbsoluteExpirationRelativeToNow = cache.CachingOptions.Value.CacheDuration };
+ options.Tags.Add(typeof(Elsa.Workflows.Management.Stores.CachingWorkflowDefinitionStore).FullName!);
+ // The shared consistency revision also invalidates cached misses.
+ return await cache.GetOrCreateAsync(cacheKey, async () =>
{
- entry.SetAbsoluteExpiration(cache.CachingOptions.Value.CacheDuration);
var obj = await getObjectFunc();
-
- if (obj == null)
- return default;
-
- var changeTokenKeyInput = getChangeTokenKeyFunc(obj);
- var changeTokenKey = cacheManager.CreateWorkflowDefinitionChangeTokenKey(changeTokenKeyInput);
- entry.AddExpirationToken(cache.GetToken(changeTokenKey));
+ if (obj != null)
+ options.Tags.Add(cacheManager.CreateWorkflowDefinitionChangeTokenKey(getChangeTokenKeyFunc(obj)));
return obj;
- });
+ }, options);
}
-}
\ No newline at end of file
+}
diff --git a/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionCacheManager.cs b/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionCacheManager.cs
index 8fdbd6d..ee43835 100644
--- a/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionCacheManager.cs
+++ b/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionCacheManager.cs
@@ -5,36 +5,36 @@ using Elsa.Workflows.Management.Filters;
namespace Elsa.Workflows.Management.Services;
///
-public class WorkflowDefinitionCacheManager(ICacheManager cache, IHasher hasher) : IWorkflowDefinitionCacheManager
+public class WorkflowDefinitionCacheManager(ICacheManager cache, IHasher hasher, Elsa.Common.Multitenancy.ITenantAccessor tenantAccessor) : IWorkflowDefinitionCacheManager
{
///
public ICacheManager Cache => cache;
///
- public string CreateWorkflowDefinitionVersionCacheKey(string definitionId, VersionOptions versionOptions) => $"WorkflowDefinition:{definitionId}:{versionOptions}";
+ public string CreateWorkflowDefinitionVersionCacheKey(string definitionId, VersionOptions versionOptions) => $"{tenantAccessor.Tenant?.Id}:WorkflowDefinition:{definitionId}:{versionOptions}";
///
public string CreateWorkflowDefinitionFilterCacheKey(WorkflowDefinitionFilter filter)
{
var hash = hasher.Hash(filter);
- return $"WorkflowDefinition:{hash}";
+ return $"{tenantAccessor.Tenant?.Id}:WorkflowDefinition:{hash}";
}
///
- public string CreateWorkflowVersionCacheKey(string definitionId, VersionOptions versionOptions) => $"Workflow:{definitionId}:{versionOptions}";
+ public string CreateWorkflowVersionCacheKey(string definitionId, VersionOptions versionOptions) => $"{tenantAccessor.Tenant?.Id}:Workflow:{definitionId}:{versionOptions}";
///
- public string CreateWorkflowVersionCacheKey(string definitionVersionId) => $"Workflow:{definitionVersionId}";
+ public string CreateWorkflowVersionCacheKey(string definitionVersionId) => $"{tenantAccessor.Tenant?.Id}:Workflow:{definitionVersionId}";
///
public string CreateWorkflowFilterCacheKey(WorkflowDefinitionFilter filter)
{
var hash = hasher.Hash(filter);
- return $"Workflow:{hash}";
+ return $"{tenantAccessor.Tenant?.Id}:Workflow:{hash}";
}
///
- public string CreateWorkflowDefinitionVersionCacheKey(string definitionVersionId) => $"WorkflowDefinition:{definitionVersionId}";
+ public string CreateWorkflowDefinitionVersionCacheKey(string definitionVersionId) => $"{tenantAccessor.Tenant?.Id}:WorkflowDefinition:{definitionVersionId}";
///
public string CreateWorkflowDefinitionChangeTokenKey(string definitionId) => $"WorkflowChangeToken:{definitionId}";
diff --git a/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionPublisher.cs b/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionPublisher.cs
index 88640c6..464aca2 100644
--- a/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionPublisher.cs
+++ b/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionPublisher.cs
@@ -1,4 +1,7 @@
using Elsa.Common;
+using Elsa.Caching;
+using Medallion.Threading;
+using Microsoft.Extensions.DependencyInjection;
using Elsa.Common.Entities;
using Elsa.Common.Models;
using Elsa.Mediator.Contracts;
@@ -20,7 +23,8 @@ public class WorkflowDefinitionPublisher(
IIdentityGenerator identityGenerator,
IActivitySerializer activitySerializer,
IMediator mediator,
- ISystemClock systemClock)
+ ISystemClock systemClock,
+ IServiceProvider? serviceProvider = null)
: IWorkflowDefinitionPublisher
{
///
@@ -90,6 +94,11 @@ public class WorkflowDefinitionPublisher(
if (validationErrors.Any())
return new(false, validationErrors, new([]));
+ var locks = serviceProvider?.GetService();
+ await using var publishLock = locks == null ? null
+ : await locks.AcquireLockAsync($"workflow-definition:{definition.DefinitionId}", cancellationToken: cancellationToken);
+ var cache = serviceProvider?.GetService();
+ var mutation = cache == null ? null : await cache.BeginMutationAsync(cancellationToken);
await mediator.SendAsync(new WorkflowDefinitionPublishing(definition), cancellationToken);
var definitionId = definition.DefinitionId;
@@ -117,7 +126,8 @@ public class WorkflowDefinitionPublisher(
await workflowDefinitionStore.SaveAsync(definition, cancellationToken);
var affectedWorkflows = new AffectedWorkflows(new List());
- await mediator.SendAsync(new WorkflowDefinitionPublished(definition, affectedWorkflows), cancellationToken);
+ await mediator.SendAsync(new WorkflowDefinitionPublished(definition, affectedWorkflows), CancellationToken.None);
+ if (cache != null && mutation != null) await cache.CompleteMutationAsync(mutation, CancellationToken.None);
return new(true, validationErrors, affectedWorkflows);
}
@@ -221,4 +231,4 @@ public class WorkflowDefinitionPublisher(
return definition;
}
-}
\ No newline at end of file
+}
diff --git a/src/modules/Elsa.Workflows.Management/Stores/CachingWorkflowDefinitionStore.cs b/src/modules/Elsa.Workflows.Management/Stores/CachingWorkflowDefinitionStore.cs
index 796a557..8c86b54 100644
--- a/src/modules/Elsa.Workflows.Management/Stores/CachingWorkflowDefinitionStore.cs
+++ b/src/modules/Elsa.Workflows.Management/Stores/CachingWorkflowDefinitionStore.cs
@@ -4,7 +4,8 @@ using Elsa.Common.Multitenancy;
using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.Management.Filters;
using Elsa.Workflows.Management.Models;
-using Microsoft.Extensions.Caching.Memory;
+using Elsa.Caching.Models;
+using Elsa.Extensions;
namespace Elsa.Workflows.Management.Stores;
@@ -18,7 +19,7 @@ public class CachingWorkflowDefinitionStore(IWorkflowDefinitionStore decoratedSt
///
public async Task FindAsync(WorkflowDefinitionFilter filter, CancellationToken cancellationToken = default)
{
- var cacheKey = hasher.Hash(filter);
+ var cacheKey = hasher.Hash(nameof(FindAsync), filter);
return await GetOrCreateAsync(cacheKey, () => decoratedStore.FindAsync(filter, cancellationToken));
}
@@ -95,22 +96,22 @@ public class CachingWorkflowDefinitionStore(IWorkflowDefinitionStore decoratedSt
///
public async Task SaveAsync(WorkflowDefinition definition, CancellationToken cancellationToken = default)
{
- await decoratedStore.SaveAsync(definition, cancellationToken);
- await cacheManager.TriggerTokenAsync(CacheInvalidationTokenKey, cancellationToken);
+ await cacheManager.MutateAsync(() => decoratedStore.SaveAsync(definition, cancellationToken), cancellationToken);
+ await cacheManager.TriggerTokenAsync(CacheInvalidationTokenKey, CancellationToken.None);
}
///
public async Task SaveManyAsync(IEnumerable definitions, CancellationToken cancellationToken = default)
{
- await decoratedStore.SaveManyAsync(definitions, cancellationToken);
- await cacheManager.TriggerTokenAsync(CacheInvalidationTokenKey, cancellationToken);
+ await cacheManager.MutateAsync(() => decoratedStore.SaveManyAsync(definitions, cancellationToken), cancellationToken);
+ await cacheManager.TriggerTokenAsync(CacheInvalidationTokenKey, CancellationToken.None);
}
///
public async Task DeleteAsync(WorkflowDefinitionFilter filter, CancellationToken cancellationToken = default)
{
- var result = await decoratedStore.DeleteAsync(filter, cancellationToken);
- await cacheManager.TriggerTokenAsync(CacheInvalidationTokenKey, cancellationToken);
+ var result = await cacheManager.MutateAsync(() => decoratedStore.DeleteAsync(filter, cancellationToken), cancellationToken);
+ await cacheManager.TriggerTokenAsync(CacheInvalidationTokenKey, CancellationToken.None);
return result;
}
@@ -139,13 +140,9 @@ public class CachingWorkflowDefinitionStore(IWorkflowDefinitionStore decoratedSt
{
var tenantId = tenantAccessor.Tenant?.Id;
var tenantIdPrefix = !string.IsNullOrEmpty(tenantId) ? $"{tenantId}:" : string.Empty;
- var internalKey = $"{tenantIdPrefix}{typeof(T).Name}:{key}";
- return await cacheManager.GetOrCreateAsync(internalKey, async entry =>
- {
- var invalidationRequestToken = cacheManager.GetToken(CacheInvalidationTokenKey);
- entry.AddExpirationToken(invalidationRequestToken);
- entry.SetSlidingExpiration(cacheManager.CachingOptions.Value.CacheDuration);
- return await factory();
- });
+ var internalKey = $"{tenantIdPrefix}{typeof(T).FullName}:{key}";
+ var options = new CacheEntryOptions { SlidingExpiration = cacheManager.CachingOptions.Value.CacheDuration };
+ options.Tags.Add(CacheInvalidationTokenKey);
+ return await cacheManager.GetOrCreateAsync(internalKey, factory, options);
}
-}
\ No newline at end of file
+}
diff --git a/src/modules/Elsa.Workflows.Runtime.Distributed/Elsa.Workflows.Runtime.Distributed.csproj b/src/modules/Elsa.Workflows.Runtime.Distributed/Elsa.Workflows.Runtime.Distributed.csproj
index a989a60..99d377c 100644
--- a/src/modules/Elsa.Workflows.Runtime.Distributed/Elsa.Workflows.Runtime.Distributed.csproj
+++ b/src/modules/Elsa.Workflows.Runtime.Distributed/Elsa.Workflows.Runtime.Distributed.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Workflows.Runtime.Distributed
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides distributed workflow runtime functionality.
diff --git a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Elsa.Workflows.Runtime.ProtoActor.csproj b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Elsa.Workflows.Runtime.ProtoActor.csproj
index 1aa0607..ce368bc 100644
--- a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Elsa.Workflows.Runtime.ProtoActor.csproj
+++ b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Elsa.Workflows.Runtime.ProtoActor.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Workflows.Runtime.ProtoActor
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides a workflow runtime using ProtoActor and its out-of-the-box memory provider.
diff --git a/src/modules/Elsa.Workflows.Runtime/Elsa.Workflows.Runtime.csproj b/src/modules/Elsa.Workflows.Runtime/Elsa.Workflows.Runtime.csproj
index e261795..710974a 100644
--- a/src/modules/Elsa.Workflows.Runtime/Elsa.Workflows.Runtime.csproj
+++ b/src/modules/Elsa.Workflows.Runtime/Elsa.Workflows.Runtime.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa.Workflows.Runtime
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Provides workflow runtime functionality.
diff --git a/src/modules/Elsa.Workflows.Runtime/Extensions/ModuleExtensions.cs b/src/modules/Elsa.Workflows.Runtime/Extensions/ModuleExtensions.cs
index c909010..4a8f791 100644
--- a/src/modules/Elsa.Workflows.Runtime/Extensions/ModuleExtensions.cs
+++ b/src/modules/Elsa.Workflows.Runtime/Extensions/ModuleExtensions.cs
@@ -63,4 +63,13 @@ public static class ModuleExtensions
feature.Module.Configure(configure);
return feature;
}
-}
\ No newline at end of file
+ public static WorkflowRuntimeFeature UseCache(this WorkflowRuntimeFeature feature,
+ Microsoft.Extensions.Configuration.IConfigurationSection section, Action? configure = null)
+ {
+ return feature.UseCache(cache =>
+ {
+ cache.UseConfiguration(section);
+ configure?.Invoke(cache);
+ });
+ }
+}
diff --git a/src/modules/Elsa.Workflows.Runtime/Features/CachingWorkflowRuntimeFeature.cs b/src/modules/Elsa.Workflows.Runtime/Features/CachingWorkflowRuntimeFeature.cs
index a4d2f58..dcbb0c1 100644
--- a/src/modules/Elsa.Workflows.Runtime/Features/CachingWorkflowRuntimeFeature.cs
+++ b/src/modules/Elsa.Workflows.Runtime/Features/CachingWorkflowRuntimeFeature.cs
@@ -11,8 +11,8 @@ namespace Elsa.Workflows.Runtime.Features;
///
/// Installs and configures workflow runtime caching features.
///
-[DependsOn(typeof(MemoryCacheFeature))]
-public class CachingWorkflowRuntimeFeature : FeatureBase
+[DependsOn(typeof(CacheFeature))]
+public class CachingWorkflowRuntimeFeature : CacheConsumerFeature
{
///
public CachingWorkflowRuntimeFeature(IModule module) : base(module)
@@ -25,9 +25,10 @@ public class CachingWorkflowRuntimeFeature : FeatureBase
Services
// Decorators.
.Decorate()
+ .Decorate()
// Handlers.
.AddNotificationHandler()
.AddNotificationHandler();
}
-}
\ No newline at end of file
+}
diff --git a/src/modules/Elsa.Workflows.Runtime/Stores/BookmarkConsistencyStore.cs b/src/modules/Elsa.Workflows.Runtime/Stores/BookmarkConsistencyStore.cs
new file mode 100644
index 0000000..37ba4c7
--- /dev/null
+++ b/src/modules/Elsa.Workflows.Runtime/Stores/BookmarkConsistencyStore.cs
@@ -0,0 +1,21 @@
+using Elsa.Caching;
+using Elsa.Extensions;
+using Elsa.Workflows.Runtime.Entities;
+using Elsa.Workflows.Runtime.Filters;
+
+namespace Elsa.Workflows.Runtime.Stores;
+
+/// Bookmarks stay in SQL; a mutation advances the shared revision for local HTTP route tables.
+public class BookmarkConsistencyStore(IBookmarkStore decoratedStore, ICacheManager cache) : IBookmarkStore
+{
+ public async ValueTask SaveAsync(StoredBookmark record, CancellationToken cancellationToken = default)
+ => await cache.MutateRoutesAsync(async () => await decoratedStore.SaveAsync(record, cancellationToken), cancellationToken);
+ public async ValueTask SaveManyAsync(IEnumerable records, CancellationToken cancellationToken)
+ => await cache.MutateRoutesAsync(async () => await decoratedStore.SaveManyAsync(records, cancellationToken), cancellationToken);
+ public ValueTask FindAsync(BookmarkFilter filter, CancellationToken cancellationToken = default)
+ => decoratedStore.FindAsync(filter, cancellationToken);
+ public ValueTask> FindManyAsync(BookmarkFilter filter, CancellationToken cancellationToken = default)
+ => decoratedStore.FindManyAsync(filter, cancellationToken);
+ public async ValueTask DeleteAsync(BookmarkFilter filter, CancellationToken cancellationToken = default)
+ => await cache.MutateRoutesAsync(async () => await decoratedStore.DeleteAsync(filter, cancellationToken), cancellationToken);
+}
diff --git a/src/modules/Elsa.Workflows.Runtime/Stores/CachingTriggerStore.cs b/src/modules/Elsa.Workflows.Runtime/Stores/CachingTriggerStore.cs
index 5fed21c..bec19df 100644
--- a/src/modules/Elsa.Workflows.Runtime/Stores/CachingTriggerStore.cs
+++ b/src/modules/Elsa.Workflows.Runtime/Stores/CachingTriggerStore.cs
@@ -4,7 +4,8 @@ using Elsa.Common.Multitenancy;
using Elsa.Workflows.Runtime.Entities;
using Elsa.Workflows.Runtime.Filters;
using Elsa.Workflows.Runtime.OrderDefinitions;
-using Microsoft.Extensions.Caching.Memory;
+using Elsa.Caching.Models;
+using Elsa.Extensions;
namespace Elsa.Workflows.Runtime.Stores;
@@ -21,15 +22,15 @@ public class CachingTriggerStore(ITriggerStore decoratedStore, ICacheManager cac
///
public async ValueTask SaveAsync(StoredTrigger record, CancellationToken cancellationToken = default)
{
- await cacheManager.TriggerTokenAsync(CacheInvalidationTokenKey, cancellationToken);
- await decoratedStore.SaveAsync(record, cancellationToken);
+ await cacheManager.MutateAsync(async () => await decoratedStore.SaveAsync(record, cancellationToken), cancellationToken);
+ await cacheManager.TriggerTokenAsync(CacheInvalidationTokenKey, CancellationToken.None);
}
///
public async ValueTask SaveManyAsync(IEnumerable records, CancellationToken cancellationToken = default)
{
- await cacheManager.TriggerTokenAsync(CacheInvalidationTokenKey, cancellationToken);
- await decoratedStore.SaveManyAsync(records, cancellationToken);
+ await cacheManager.MutateAsync(async () => await decoratedStore.SaveManyAsync(records, cancellationToken), cancellationToken);
+ await cacheManager.TriggerTokenAsync(CacheInvalidationTokenKey, CancellationToken.None);
}
///
@@ -48,41 +49,38 @@ public class CachingTriggerStore(ITriggerStore decoratedStore, ICacheManager cac
public async ValueTask> FindManyAsync(TriggerFilter filter, PageArgs pageArgs, CancellationToken cancellationToken = default)
{
- var cacheKey = hasher.Hash(filter);
+ var cacheKey = hasher.Hash(filter, pageArgs);
return (await GetOrCreateAsync(cacheKey, async () => await decoratedStore.FindManyAsync(filter, pageArgs, cancellationToken)))!;
}
public async ValueTask> FindManyAsync(TriggerFilter filter, PageArgs pageArgs, StoredTriggerOrder order, CancellationToken cancellationToken = default)
{
- var cacheKey = hasher.Hash(filter);
+ var cacheKey = hasher.Hash(filter, pageArgs, order);
return (await GetOrCreateAsync(cacheKey, async () => await decoratedStore.FindManyAsync(filter, pageArgs, order, cancellationToken)))!;
}
///
public async ValueTask ReplaceAsync(IEnumerable removed, IEnumerable added, CancellationToken cancellationToken = default)
{
- await cacheManager.TriggerTokenAsync(CacheInvalidationTokenKey, cancellationToken);
- await decoratedStore.ReplaceAsync(removed, added, cancellationToken);
+ await cacheManager.MutateAsync(async () => await decoratedStore.ReplaceAsync(removed, added, cancellationToken), cancellationToken);
+ await cacheManager.TriggerTokenAsync(CacheInvalidationTokenKey, CancellationToken.None);
}
///
public async ValueTask DeleteManyAsync(TriggerFilter filter, CancellationToken cancellationToken = default)
{
- await cacheManager.TriggerTokenAsync(CacheInvalidationTokenKey, cancellationToken);
- return await decoratedStore.DeleteManyAsync(filter, cancellationToken);
+ var result = await cacheManager.MutateAsync(async () => await decoratedStore.DeleteManyAsync(filter, cancellationToken), cancellationToken);
+ await cacheManager.TriggerTokenAsync(CacheInvalidationTokenKey, CancellationToken.None);
+ return result;
}
private async Task GetOrCreateAsync(string key, Func> factory)
{
var tenantId = tenantAccessor.Tenant?.Id;
var tenantIdPrefix = !string.IsNullOrEmpty(tenantId) ? $"{tenantId}:" : string.Empty;
- var internalKey = $"{tenantIdPrefix}{typeof(T).Name}:{key}";
- return await cacheManager.GetOrCreateAsync(internalKey, async entry =>
- {
- var invalidationRequestToken = cacheManager.GetToken(CacheInvalidationTokenKey);
- entry.AddExpirationToken(invalidationRequestToken);
- entry.SetAbsoluteExpiration(cacheManager.CachingOptions.Value.CacheDuration);
- return await factory();
- });
+ var internalKey = $"{tenantIdPrefix}{typeof(T).FullName}:{key}";
+ var options = new CacheEntryOptions { AbsoluteExpirationRelativeToNow = cacheManager.CachingOptions.Value.CacheDuration };
+ options.Tags.Add(CacheInvalidationTokenKey);
+ return await cacheManager.GetOrCreateAsync(internalKey, factory, options);
}
-}
\ No newline at end of file
+}
diff --git a/src/modules/Elsa/Elsa.csproj b/src/modules/Elsa/Elsa.csproj
index 31fc759..d6b7d7c 100644
--- a/src/modules/Elsa/Elsa.csproj
+++ b/src/modules/Elsa/Elsa.csproj
@@ -4,7 +4,7 @@
Caspian.Elsa
Caspian
Behzad Alizadeh
- 1.1.0-preview.8
+ 1.1.0-preview.11
True
Bundles the most commonly-used packages when building an Elsa workflows application.
diff --git a/test/integration/Elsa.Caching.ProcessTests/Elsa.Caching.ProcessTests.csproj b/test/integration/Elsa.Caching.ProcessTests/Elsa.Caching.ProcessTests.csproj
new file mode 100644
index 0000000..9715238
--- /dev/null
+++ b/test/integration/Elsa.Caching.ProcessTests/Elsa.Caching.ProcessTests.csproj
@@ -0,0 +1,9 @@
+
+ Exefalse
+
+
+
+
+
+
+
diff --git a/test/integration/Elsa.Caching.ProcessTests/Program.cs b/test/integration/Elsa.Caching.ProcessTests/Program.cs
new file mode 100644
index 0000000..90633ab
--- /dev/null
+++ b/test/integration/Elsa.Caching.ProcessTests/Program.cs
@@ -0,0 +1,41 @@
+using Elsa.Caching;
+using Elsa.Caching.Models;
+using Elsa.Caching.UnitTests;
+using Microsoft.Extensions.DependencyInjection;
+
+if (args.Length != 3) throw new ArgumentException("Usage: write|read|invalidate|read-updated|pending|bypass");
+using var host = CacheTestHost.Create(args[0], true, args[1]);
+await CacheTestHost.EnsureCreatedAsync(host);
+var cache = host.GetRequiredService();
+var options = new CacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(2) };
+switch (args[2])
+{
+ case "write":
+ Console.WriteLine(await cache.GetOrCreateAsync("definition", () => Task.FromResult("from-first-process"), options));
+ break;
+ case "read":
+ var value = await cache.GetOrCreateAsync("definition", () => throw new Exception("Second process did not share Redis"), options);
+ if (value != "from-first-process") throw new Exception("Unexpected cache payload");
+ Console.WriteLine("PASS: payload shared across independent processes");
+ break;
+ case "invalidate":
+ var mutation = await cache.BeginMutationAsync();
+ await cache.CompleteMutationAsync(mutation);
+ Console.WriteLine(await cache.GetOrCreateAsync("definition", () => Task.FromResult("updated"), options));
+ break;
+ case "pending":
+ await cache.BeginMutationAsync();
+ Console.WriteLine("Pending durable fence recorded; process exiting");
+ break;
+ case "read-updated":
+ var updated = await cache.GetOrCreateAsync("definition", () => throw new Exception("Updated payload was not shared"), options);
+ if (updated != "updated") throw new Exception("Old payload survived another process's mutation");
+ Console.WriteLine("PASS: another process observes the updated revision");
+ break;
+ case "bypass":
+ var fresh = await cache.GetOrCreateAsync("definition", () => Task.FromResult("sql-current"), options);
+ if (fresh != "sql-current") throw new Exception("Stale Redis payload was used after an unfinished mutation");
+ Console.WriteLine("PASS: unfinished mutation forces SQL reads after process restart");
+ break;
+ default: throw new ArgumentException("Unknown mode");
+}
diff --git a/test/unit/Elsa.Caching.UnitTests/CacheProviderTests.cs b/test/unit/Elsa.Caching.UnitTests/CacheProviderTests.cs
new file mode 100644
index 0000000..f3c4120
--- /dev/null
+++ b/test/unit/Elsa.Caching.UnitTests/CacheProviderTests.cs
@@ -0,0 +1,417 @@
+using Elsa.Caching.Features;
+using Elsa.Caching.Models;
+using Elsa.Caching.Redis.Options;
+using Elsa.EntityFrameworkCore.Modules.Management;
+using Elsa.Extensions;
+using Elsa.Http.Services;
+using Elsa.Workflows.Management.Entities;
+using Elsa.Workflows.Runtime.Entities;
+using Elsa.Common.Models;
+using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.EntityFrameworkCore;
+using Xunit;
+
+namespace Elsa.Caching.UnitTests;
+
+public class RedisFactAttribute : FactAttribute
+{
+ public RedisFactAttribute()
+ {
+ if (Environment.GetEnvironmentVariable("REDIS_TEST_CONNECTION") == null)
+ Skip = "Set REDIS_TEST_CONNECTION to run integration tests against an isolated Redis server.";
+ }
+}
+
+public class CacheProviderTests
+{
+ private static CacheEntryOptions Lifetime => new() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1) };
+ private static IConfigurationSection Configuration(string provider) => new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ ["Workflow:Cache:Provider"] = provider,
+ ["Workflow:Cache:Duration"] = "00:03:00",
+ ["Workflow:Cache:Redis:Database"] = "2",
+ ["Workflow:Cache:Redis:KeyPrefix"] = "configured:"
+ }).Build().GetSection("Workflow:Cache");
+
+ [Fact]
+ public async Task Default_memory_caches_and_keeps_local_objects()
+ {
+ using var host = CacheTestHost.Create();
+ var cache = host.GetRequiredService();
+ var calls = 0;
+ Assert.Equal(1, await cache.GetOrCreateAsync("a", () => Task.FromResult(++calls), Lifetime));
+ Assert.Equal(1, await cache.GetOrCreateAsync("a", () => Task.FromResult(++calls), Lifetime));
+ var local = host.GetRequiredService();
+ var runner = new Func(() => 3);
+ local.Set("compiled-script", runner);
+ await cache.TriggerTokenAsync("definition");
+ Assert.Same(runner, local.Get>("compiled-script"));
+ Assert.Equal(2, await cache.GetOrCreateAsync("a", () => Task.FromResult(++calls), Lifetime));
+ }
+
+ [Fact]
+ public void Configuration_is_overridden_only_by_explicit_fluent_values()
+ {
+ using var host = CacheTestHost.Create(configure: cache =>
+ {
+ cache.UseConfiguration(Configuration("Memory"));
+ cache.UseRedis(redis => redis.KeyPrefix = "fluent:");
+ });
+ var options = host.GetRequiredService();
+ Assert.Equal(2, options.Database);
+ Assert.Equal("fluent:", options.KeyPrefix);
+ Assert.Equal(TimeSpan.FromMinutes(3), host.GetRequiredService().Duration);
+ Assert.True(host.GetRequiredService().IsShared);
+ }
+
+ [Fact]
+ public void Fluent_memory_can_override_configured_redis()
+ {
+ using var host = CacheTestHost.Create(configure: cache => { cache.UseConfiguration(Configuration("Redis")); cache.UseMemory(); });
+ Assert.False(host.GetRequiredService().IsShared);
+ }
+
+ [Fact]
+ public void Explicit_connection_factory_overrides_configured_connection_string()
+ {
+ Func factory = _ => throw new Exception("Not used during registration");
+ using var host = CacheTestHost.Create(configure: c => c.UseRedis(o =>
+ {
+ o.ConnectionString = "configured-host:6379";
+ o.ConnectionFactory = factory;
+ }));
+ Assert.Same(factory, host.GetRequiredService().ConnectionFactory);
+ }
+
+ [Fact]
+ public void Unknown_or_conflicting_providers_fail_clearly()
+ {
+ Assert.Throws(() => CacheTestHost.Create(configure: c => c.UseConfiguration(Configuration("Missing"))));
+ Assert.Throws(() => CacheTestHost.Create(configure: c => { c.UseMemory(); c.UseRedis(); }));
+ }
+
+ [Fact]
+ public async Task Shared_provider_requires_durable_consistency()
+ {
+ using var host = CacheTestHost.Create(configure: c => c.UseRedis());
+ await Assert.ThrowsAsync(() => host.GetRequiredService()
+ .GetOrCreateAsync("a", () => Task.FromResult(1), Lifetime));
+ }
+
+ [Fact]
+ public async Task Disabled_cache_always_calls_factory()
+ {
+ using var host = CacheTestHost.Create(configure: c => c.Enabled = false);
+ var cache = host.GetRequiredService();
+ var calls = 0;
+ await cache.GetOrCreateAsync("a", () => Task.FromResult(++calls), Lifetime);
+ await cache.GetOrCreateAsync("a", () => Task.FromResult(++calls), Lifetime);
+ Assert.Equal(2, calls);
+ }
+
+ [Fact]
+ public async Task Filling_old_data_during_a_mutation_cannot_resurrect_it()
+ {
+ using var host = CacheTestHost.Create();
+ var cache = host.GetRequiredService();
+ var captured = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ var fill = cache.GetOrCreateAsync("definition", async () => { captured.SetResult(); await release.Task; return "old"; }, Lifetime);
+ await captured.Task;
+ var mutation = await cache.BeginMutationAsync();
+ await cache.CompleteMutationAsync(mutation);
+ release.SetResult();
+ Assert.Equal("old", await fill); // An already-running request may finish with its captured version.
+ Assert.Equal("new", await cache.GetOrCreateAsync("definition", () => Task.FromResult("new"), Lifetime));
+ }
+
+ [Fact]
+ public void Route_snapshot_replacement_removes_deleted_routes()
+ {
+ using var host = CacheTestHost.Create();
+ var table = ActivatorUtilities.CreateInstance(host);
+ table.Add("/old");
+ table.Replace(new[] { new Elsa.Http.HttpRouteData("/new") });
+ Assert.Equal("/new", Assert.Single(table).Route);
+ }
+
+ [Fact]
+ public void Sql_server_migration_generates_the_durable_fence_table()
+ {
+ var services = new ServiceCollection();
+ services.AddDbContextFactory(options => options.UseSqlServer(
+ "Server=(local);Database=workflow-cache-script;Integrated Security=true;TrustServerCertificate=true",
+ sql => sql.MigrationsAssembly(typeof(Elsa.EntityFrameworkCore.SqlServer.Migrations.Management.AddCacheMutationFences).Assembly.FullName)));
+ using var host = services.BuildServiceProvider();
+ using var context = host.GetRequiredService>().CreateDbContext();
+ var migrator = ((Microsoft.EntityFrameworkCore.Infrastructure.IInfrastructure)context.Database).Instance.GetRequiredService();
+ var script = migrator.GenerateScript("20250818170340_AddExternalUserId", "20261007000000_AddCacheMutationFences");
+ Assert.Contains("CREATE TABLE [Elsa].[CacheMutations]", script);
+ Assert.Contains("[Scope] nvarchar(32)", script);
+ Assert.Contains("IDENTITY", script);
+ }
+
+ [RedisFact]
+ public async Task Nodes_share_payload_and_cached_null_is_invalidated()
+ {
+ var path = Path.Combine(Path.GetTempPath(), "elsa-cache-" + Guid.NewGuid().ToString("N") + ".db");
+ var prefix = "workflow-tests:" + Guid.NewGuid().ToString("N") + ":";
+ using var first = CacheTestHost.Create(path, true, prefix);
+ using var second = CacheTestHost.Create(path, true, prefix);
+ await CacheTestHost.EnsureCreatedAsync(first);
+ var a = first.GetRequiredService