diff --git a/Directory.Build.targets b/Directory.Build.targets
new file mode 100644
index 000000000..dcef467e1
--- /dev/null
+++ b/Directory.Build.targets
@@ -0,0 +1,7 @@
+
+
+
+ true
+
+
+
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 81b2493bc..d47a5fe18 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -100,12 +100,12 @@
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/Elsa.sln b/Elsa.sln
index c043709ee..35c603d07 100644
--- a/Elsa.sln
+++ b/Elsa.sln
@@ -337,6 +337,20 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Http.IntegrationTests"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Tenants.UnitTests", "test\unit\Elsa.Tenants.UnitTests\Elsa.Tenants.UnitTests.csproj", "{DC476900-D836-4920-A696-CF8796668723}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "cshells", "cshells", "{E85D181A-2633-440B-AF1E-2DA31C910BFD}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CShells.Abstractions", "..\cshells\src\CShells.Abstractions\CShells.Abstractions.csproj", "{FD6D8905-1181-4524-8241-2BFD61BA92E4}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CShells", "..\cshells\src\CShells\CShells.csproj", "{3DDA30C9-E530-4CFD-A383-9B12DBCC6ED4}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CShells.AspNetCore.Abstractions", "..\cshells\src\CShells.AspNetCore.Abstractions\CShells.AspNetCore.Abstractions.csproj", "{F0B9F468-4FB8-497D-B0AC-4567F3126D91}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CShells.AspNetCore", "..\cshells\src\CShells.AspNetCore\CShells.AspNetCore.csproj", "{85AFD6A7-BFBA-46BC-8701-90565C9C7473}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CShells.FastEndpoints.Abstractions", "..\cshells\src\CShells.FastEndpoints.Abstractions\CShells.FastEndpoints.Abstractions.csproj", "{80137E60-7C6D-411B-808A-092BA05E5B04}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CShells.FastEndpoints", "..\cshells\src\CShells.FastEndpoints\CShells.FastEndpoints.csproj", "{3C64C25E-F825-42CC-A696-9CD30F13B9A6}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -613,6 +627,30 @@ Global
{DC476900-D836-4920-A696-CF8796668723}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DC476900-D836-4920-A696-CF8796668723}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DC476900-D836-4920-A696-CF8796668723}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FD6D8905-1181-4524-8241-2BFD61BA92E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FD6D8905-1181-4524-8241-2BFD61BA92E4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FD6D8905-1181-4524-8241-2BFD61BA92E4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FD6D8905-1181-4524-8241-2BFD61BA92E4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3DDA30C9-E530-4CFD-A383-9B12DBCC6ED4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3DDA30C9-E530-4CFD-A383-9B12DBCC6ED4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3DDA30C9-E530-4CFD-A383-9B12DBCC6ED4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3DDA30C9-E530-4CFD-A383-9B12DBCC6ED4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F0B9F468-4FB8-497D-B0AC-4567F3126D91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F0B9F468-4FB8-497D-B0AC-4567F3126D91}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F0B9F468-4FB8-497D-B0AC-4567F3126D91}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F0B9F468-4FB8-497D-B0AC-4567F3126D91}.Release|Any CPU.Build.0 = Release|Any CPU
+ {85AFD6A7-BFBA-46BC-8701-90565C9C7473}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {85AFD6A7-BFBA-46BC-8701-90565C9C7473}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {85AFD6A7-BFBA-46BC-8701-90565C9C7473}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {85AFD6A7-BFBA-46BC-8701-90565C9C7473}.Release|Any CPU.Build.0 = Release|Any CPU
+ {80137E60-7C6D-411B-808A-092BA05E5B04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {80137E60-7C6D-411B-808A-092BA05E5B04}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {80137E60-7C6D-411B-808A-092BA05E5B04}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {80137E60-7C6D-411B-808A-092BA05E5B04}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3C64C25E-F825-42CC-A696-9CD30F13B9A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3C64C25E-F825-42CC-A696-9CD30F13B9A6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3C64C25E-F825-42CC-A696-9CD30F13B9A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3C64C25E-F825-42CC-A696-9CD30F13B9A6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -719,6 +757,12 @@ Global
{A3C07D5B-2A30-494E-B9BC-4B1594B31ABC} = {18453B51-25EB-4317-A4B3-B10518252E92}
{8C4F6A2D-1E9F-4B3C-9D8E-7F5A6B4C3D2E} = {1B8D5897-902E-4632-8698-E89CAF3DDF54}
{DC476900-D836-4920-A696-CF8796668723} = {18453B51-25EB-4317-A4B3-B10518252E92}
+ {FD6D8905-1181-4524-8241-2BFD61BA92E4} = {E85D181A-2633-440B-AF1E-2DA31C910BFD}
+ {3DDA30C9-E530-4CFD-A383-9B12DBCC6ED4} = {E85D181A-2633-440B-AF1E-2DA31C910BFD}
+ {F0B9F468-4FB8-497D-B0AC-4567F3126D91} = {E85D181A-2633-440B-AF1E-2DA31C910BFD}
+ {85AFD6A7-BFBA-46BC-8701-90565C9C7473} = {E85D181A-2633-440B-AF1E-2DA31C910BFD}
+ {80137E60-7C6D-411B-808A-092BA05E5B04} = {E85D181A-2633-440B-AF1E-2DA31C910BFD}
+ {3C64C25E-F825-42CC-A696-9CD30F13B9A6} = {E85D181A-2633-440B-AF1E-2DA31C910BFD}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E}
diff --git a/src/apps/Elsa.ModularServer.Web/Elsa.ModularServer.Web.csproj b/src/apps/Elsa.ModularServer.Web/Elsa.ModularServer.Web.csproj
index d2f791b73..246b58bcd 100644
--- a/src/apps/Elsa.ModularServer.Web/Elsa.ModularServer.Web.csproj
+++ b/src/apps/Elsa.ModularServer.Web/Elsa.ModularServer.Web.csproj
@@ -1,7 +1,7 @@
-
-
+
+
diff --git a/src/modules/Elsa.Expressions.JavaScript/ShellFeatures/JavaScriptFeature.cs b/src/modules/Elsa.Expressions.JavaScript/ShellFeatures/JavaScriptFeature.cs
index e4cc1fad4..9d3d225f1 100644
--- a/src/modules/Elsa.Expressions.JavaScript/ShellFeatures/JavaScriptFeature.cs
+++ b/src/modules/Elsa.Expressions.JavaScript/ShellFeatures/JavaScriptFeature.cs
@@ -26,13 +26,49 @@ namespace Elsa.Expressions.JavaScript.ShellFeatures;
public class JavaScriptFeature : IShellFeature
{
///
- /// Configures the Jint options.
+ /// Enables access to any .NET class. Do not enable if you are executing workflows from untrusted sources (e.g. user defined workflows).
+ ///
+ /// See Jint docs for more: https://github.com/sebastienros/jint#accessing-net-assemblies-and-classes
///
- public Action JintOptions { get; set; } = _ => { };
+ public bool AllowClrAccess { get; set; }
+
+ ///
+ /// Enables access to .NET configuration via the getConfig function.
+ /// Do not enable if you are executing workflows from untrusted sources (e.g user defined workflows).
+ ///
+ public bool AllowConfigurationAccess { get; set; }
+
+ ///
+ /// The timeout for script caching.
+ ///
+ ///
+ /// The ScriptCacheTimeout property specifies the duration for which the scripts are cached in the Jint JavaScript engine. When a script is executed, it is compiled and cached for future use. This caching improves performance by avoiding repetitive compilation of the same script.
+ /// If the value of ScriptCacheTimeout is null, the scripts are cached indefinitely. If a time value is specified, the scripts will be purged from the cache after they've been unused for the specified duration and recompiled on next use.
+ ///
+ public TimeSpan? ScriptCacheTimeout { get; set; } = TimeSpan.FromDays(1);
+
+ ///
+ /// Disables the generation of variable wrappers. E.g. getMyVariable() will no longer be available for variables. Instead, you can only access variables using getVariable("MyVariable") function.
+ /// This is useful if your application requires the use of invalid JavaScript variable names.
+ ///
+ public bool DisableWrappers { get; set; }
+
+ ///
+ /// Disables copying workflow variables into the Jint engine and copying them back into the workflow execution context.
+ /// Disabling this option will increase performance but will also prevent you from accessing workflow variables from within JavaScript expressions using the variables.MyVariable syntax.
+ ///
+ public bool DisableVariableCopying { get; set; }
public void ConfigureServices(IServiceCollection services)
{
- services.Configure(JintOptions);
+ services.Configure(options =>
+ {
+ options.AllowClrAccess = AllowClrAccess;
+ options.AllowConfigurationAccess = AllowConfigurationAccess;
+ options.ScriptCacheTimeout = ScriptCacheTimeout;
+ options.DisableWrappers = DisableWrappers;
+ options.DisableVariableCopying = DisableVariableCopying;
+ });
// JavaScript services.
services
diff --git a/src/modules/Elsa.Expressions/Extensions/ServiceCollectionExtensions.cs b/src/modules/Elsa.Expressions/Extensions/ServiceCollectionExtensions.cs
new file mode 100644
index 000000000..a159f8fe3
--- /dev/null
+++ b/src/modules/Elsa.Expressions/Extensions/ServiceCollectionExtensions.cs
@@ -0,0 +1,14 @@
+using Elsa.Expressions.Options;
+using Elsa.Extensions;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Elsa.Expressions.Extensions;
+
+public static class ServiceCollectionExtensions
+{
+ public static IServiceCollection AddTypeAlias(this IServiceCollection services, string alias)
+ {
+ services.Configure(options => options.AddTypeAlias(alias));
+ return services;
+ }
+}
\ 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 668d18863..cf6f932be 100644
--- a/src/modules/Elsa.Http/Middleware/HttpWorkflowsMiddleware.cs
+++ b/src/modules/Elsa.Http/Middleware/HttpWorkflowsMiddleware.cs
@@ -27,13 +27,17 @@ namespace Elsa.Http.Middleware;
/// An ASP.NET middleware component that tries to match the inbound request path to an associated workflow and then run that workflow.
///
[PublicAPI]
-public class HttpWorkflowsMiddleware(RequestDelegate next, IOptions options)
+public class HttpWorkflowsMiddleware(RequestDelegate next)
{
///
/// Attempts to match the inbound request path to an associated workflow and then run that workflow.
///
[RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Serialize(TValue, JsonSerializerOptions)")]
- public async Task InvokeAsync(HttpContext httpContext, IServiceProvider serviceProvider)
+ public async Task InvokeAsync(
+ HttpContext httpContext,
+ IServiceProvider serviceProvider,
+ IOptions options,
+ IHttpWorkflowLookupService httpWorkflowLookupService)
{
var path = httpContext.Request.Path.Value!.NormalizeRoute();
var matchingPath = GetMatchingRoute(serviceProvider, path).Route;
@@ -63,7 +67,6 @@ public class HttpWorkflowsMiddleware(RequestDelegate next, IOptions();
var workflowInstanceId = await GetWorkflowInstanceIdAsync(serviceProvider, httpContext, cancellationToken);
var correlationId = await GetCorrelationIdAsync(serviceProvider, httpContext, cancellationToken);
var bookmarkHash = ComputeBookmarkHash(serviceProvider, matchingPath, method);
diff --git a/src/modules/Elsa.Http/Options/HttpActivityOptions.cs b/src/modules/Elsa.Http/Options/HttpActivityOptions.cs
index 1d0c75f49..e86a259bd 100644
--- a/src/modules/Elsa.Http/Options/HttpActivityOptions.cs
+++ b/src/modules/Elsa.Http/Options/HttpActivityOptions.cs
@@ -15,7 +15,7 @@ public class HttpActivityOptions
///
/// The base URL of the server. This should be set to the same value at which the Elsa Server is publicly available. It will be used when generating absolute URLs need to be generated by activities such as SendEmail.
///
- public Uri BaseUrl { get; set; } = null!;
+ public Uri BaseUrl { get; set; } = new Uri("https://localhost:5001");
///
/// The prefix used for API routes.
@@ -42,5 +42,5 @@ public class HttpActivityOptions
/// A boolean to opt-in if you want to call CompleteAsync() in the HttpResponse Activity
/// and not waiting for the end of the HttpMiddleware.
///
- public Boolean WriteHttpResponseSynchronously { get; set; } = false;
+ public bool WriteHttpResponseSynchronously { get; set; } = false;
}
\ No newline at end of file
diff --git a/src/modules/Elsa.Http/ShellFeatures/HttpFeature.cs b/src/modules/Elsa.Http/ShellFeatures/HttpFeature.cs
index 519590aad..1d360ceb7 100644
--- a/src/modules/Elsa.Http/ShellFeatures/HttpFeature.cs
+++ b/src/modules/Elsa.Http/ShellFeatures/HttpFeature.cs
@@ -1,3 +1,4 @@
+using CShells.AspNetCore.Features;
using CShells.Features;
using Elsa.Expressions.Options;
using Elsa.Extensions;
@@ -6,20 +7,27 @@ using Elsa.Http.ContentWriters;
using Elsa.Http.DownloadableContentHandlers;
using Elsa.Http.FileCaches;
using Elsa.Http.Handlers;
+using Elsa.Http.Middleware;
using Elsa.Http.Options;
using Elsa.Http.Parsers;
using Elsa.Http.PortResolvers;
+using Elsa.Http.Resilience;
using Elsa.Http.Selectors;
using Elsa.Http.Services;
using Elsa.Http.Tasks;
using Elsa.Http.TriggerPayloadValidators;
using Elsa.Http.UIHints;
+using Elsa.Resilience.Extensions;
using Elsa.Workflows;
+using Elsa.Workflows.Management.Extensions;
using FluentStorage;
using JetBrains.Annotations;
+using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
namespace Elsa.Http.ShellFeatures;
@@ -32,12 +40,12 @@ namespace Elsa.Http.ShellFeatures;
Description = "Provides HTTP-related activities and services for workflow execution",
DependsOn = ["HttpJavaScript", "Resilience"])]
[UsedImplicitly]
-public class HttpFeature : IShellFeature
+public class HttpFeature : IMiddlewareShellFeature
{
///
- /// A delegate to configure .
+ /// The to configure.
///
- public Action? ConfigureHttpOptions { get; set; }
+ public HttpActivityOptions HttpActivityOptions { get; set; } = new();
///
/// A delegate to configure .
@@ -99,15 +107,34 @@ public class HttpFeature : IShellFeature
public void ConfigureServices(IServiceCollection services)
{
- var configureOptions = ConfigureHttpOptions ?? (options =>
- {
- options.BasePath = "/workflows";
- options.BaseUrl = new Uri("http://localhost");
- });
+ // Register HTTP activities.
+ services.AddActivitiesFrom();
+
+ // Register HTTP variable types.
+ services.AddVariableDescriptors([
+ new(typeof(HttpRouteData), "HTTP", null),
+ new(typeof(HttpRequest), "HTTP", null),
+ new(typeof(HttpResponse), "HTTP", null),
+ new(typeof(HttpResponseMessage), "HTTP", null),
+ new(typeof(HttpHeaders), "HTTP", null),
+ new(typeof(IFormFile), "HTTP", null),
+ new(typeof(HttpFile), "HTTP", null),
+ new(typeof(Downloadable), "HTTP", null),
+ ]);
+
+ // Register the HTTP resilience strategy.
+ services.AddResilienceStrategy();
var configureFileCacheOptions = ConfigureHttpFileCacheOptions ?? (options => { options.TimeToLive = TimeSpan.FromDays(7); });
- services.Configure(configureOptions);
+ services.Configure(options =>
+ {
+ options.BasePath = HttpActivityOptions.BasePath;
+ options.BaseUrl = HttpActivityOptions.BaseUrl;
+ options.AvailableContentTypes = HttpActivityOptions.AvailableContentTypes;
+ options.WriteHttpResponseSynchronously = HttpActivityOptions.WriteHttpResponseSynchronously;
+ });
+
services.Configure(configureFileCacheOptions);
var httpClientBuilder = services.AddHttpClient(HttpClient);
@@ -207,5 +234,10 @@ public class HttpFeature : IShellFeature
options.AddTypeAlias("Downloadable[]");
});
}
-}
+ ///
+ public void UseMiddleware(IApplicationBuilder app, IHostEnvironment? environment)
+ {
+ app.UseWorkflows();
+ }
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Identity/ShellFeatures/IdentityFeature.cs b/src/modules/Elsa.Identity/ShellFeatures/IdentityFeature.cs
index 702e645c0..a704843c1 100644
--- a/src/modules/Elsa.Identity/ShellFeatures/IdentityFeature.cs
+++ b/src/modules/Elsa.Identity/ShellFeatures/IdentityFeature.cs
@@ -1,4 +1,5 @@
using AspNetCore.Authentication.ApiKey;
+using CShells.Configuration;
using CShells.FastEndpoints.Features;
using CShells.Features;
using Elsa.Common.Multitenancy;
@@ -10,6 +11,7 @@ using Elsa.Identity.Options;
using Elsa.Identity.Providers;
using Elsa.Identity.Services;
using JetBrains.Annotations;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Identity.ShellFeatures;
@@ -26,8 +28,7 @@ public class IdentityFeature : IFastEndpointsShellFeature
{
public void ConfigureServices(IServiceCollection services)
{
- // Configure options - Note: SigningKey must be configured by the application for security
- services.AddOptions().BindConfiguration("Identity");
+ services.AddOptions().Configure((options, config) => config.GetSection("Identity").Bind(options));
services.Configure(ApiKeyDefaults.AuthenticationScheme, options =>
{
options.Realm = "Elsa Workflows";
diff --git a/src/modules/Elsa.Persistence.EFCore.Common/PersistenceShellFeatureBase.cs b/src/modules/Elsa.Persistence.EFCore.Common/PersistenceShellFeatureBase.cs
index b16893798..22b371416 100644
--- a/src/modules/Elsa.Persistence.EFCore.Common/PersistenceShellFeatureBase.cs
+++ b/src/modules/Elsa.Persistence.EFCore.Common/PersistenceShellFeatureBase.cs
@@ -11,7 +11,7 @@ using Microsoft.Extensions.Options;
namespace Elsa.Persistence.EFCore;
public abstract class PersistenceShellFeatureBase : IShellFeature
- where TDbContext : ElsaDbContextBase
+ where TDbContext : DbContext
{
///
/// Gets or sets a value indicating whether to use context pooling.
diff --git a/src/modules/Elsa.Resilience.Core/Extensions/ResilienceServiceCollectionExtensions.cs b/src/modules/Elsa.Resilience.Core/Extensions/ResilienceServiceCollectionExtensions.cs
new file mode 100644
index 000000000..089cbb034
--- /dev/null
+++ b/src/modules/Elsa.Resilience.Core/Extensions/ResilienceServiceCollectionExtensions.cs
@@ -0,0 +1,29 @@
+using Elsa.Resilience.Options;
+using Microsoft.Extensions.DependencyInjection;
+
+// ReSharper disable once CheckNamespace
+namespace Elsa.Resilience.Extensions;
+
+///
+/// extension methods for registering resilience strategy types
+/// via .
+///
+public static class ResilienceServiceCollectionExtensions
+{
+ ///
+ /// Registers a resilience strategy type with .
+ ///
+ public static IServiceCollection AddResilienceStrategy(this IServiceCollection services) =>
+ services.Configure(options => options.StrategyTypes.Add(typeof(T)));
+
+ ///
+ /// Registers multiple resilience strategy types with .
+ ///
+ public static IServiceCollection AddResilienceStrategies(this IServiceCollection services, IEnumerable types) =>
+ services.Configure(options =>
+ {
+ foreach (var type in types)
+ options.StrategyTypes.Add(type);
+ });
+}
+
diff --git a/src/modules/Elsa.Resilience/ShellFeatures/ResilienceShellFeature.cs b/src/modules/Elsa.Resilience/ShellFeatures/ResilienceFeature.cs
similarity index 94%
rename from src/modules/Elsa.Resilience/ShellFeatures/ResilienceShellFeature.cs
rename to src/modules/Elsa.Resilience/ShellFeatures/ResilienceFeature.cs
index 7087c4d3b..e77e4668b 100644
--- a/src/modules/Elsa.Resilience/ShellFeatures/ResilienceShellFeature.cs
+++ b/src/modules/Elsa.Resilience/ShellFeatures/ResilienceFeature.cs
@@ -14,7 +14,7 @@ using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Resilience.ShellFeatures;
[ShellFeature]
-public class ResilienceShellFeature : IFastEndpointsShellFeature
+public class ResilienceFeature : IFastEndpointsShellFeature
{
public void ConfigureServices(IServiceCollection services)
{
@@ -37,7 +37,7 @@ public class ResilienceShellFeature : IFastEndpointsShellFeature
.AddScoped()
.AddScoped()
.AddScoped()
- .AddHandlersFrom();
+ .AddHandlersFrom();
// Register transient exception detection infrastructure
services
diff --git a/src/modules/Elsa.Scheduling/ShellFeatures/SchedulingFeature.cs b/src/modules/Elsa.Scheduling/ShellFeatures/SchedulingFeature.cs
index 7473a0b11..149094b04 100644
--- a/src/modules/Elsa.Scheduling/ShellFeatures/SchedulingFeature.cs
+++ b/src/modules/Elsa.Scheduling/ShellFeatures/SchedulingFeature.cs
@@ -1,12 +1,13 @@
using CShells.Features;
using Elsa.Common.Multitenancy;
-using Elsa.Common.RecurringTasks;
+using Elsa.Common.ShellFeatures;
using Elsa.Extensions;
using Elsa.Scheduling.Bookmarks;
using Elsa.Scheduling.Handlers;
using Elsa.Scheduling.HostedServices;
using Elsa.Scheduling.Services;
using Elsa.Scheduling.TriggerPayloadValidators;
+using Elsa.Workflows.Management.Extensions;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
@@ -18,7 +19,7 @@ namespace Elsa.Scheduling.ShellFeatures;
[ShellFeature(
DisplayName = "Scheduling",
Description = "Provides scheduling capabilities for workflows including cron and delay-based triggers",
- DependsOn = ["SystemClock"])]
+ DependsOn = [typeof(SystemClockFeature)])]
[UsedImplicitly]
public class SchedulingFeature : IShellFeature
{
@@ -47,9 +48,8 @@ public class SchedulingFeature : IShellFeature
.AddScoped(WorkflowScheduler)
.AddBackgroundTask()
.AddHandlersFrom()
-
- //Trigger payload validators.
- .AddTriggerPayloadValidator();
+ .AddTriggerPayloadValidator()
+ .AddActivitiesFrom();
}
}
diff --git a/src/modules/Elsa.Workflows.Core/ShellFeatures/WorkflowsFeature.cs b/src/modules/Elsa.Workflows.Core/ShellFeatures/WorkflowsFeature.cs
index 4e88b9ef2..7181efa16 100644
--- a/src/modules/Elsa.Workflows.Core/ShellFeatures/WorkflowsFeature.cs
+++ b/src/modules/Elsa.Workflows.Core/ShellFeatures/WorkflowsFeature.cs
@@ -120,6 +120,7 @@ public class WorkflowsFeature : IShellFeature
.AddScoped()
.AddSerializationOptionsConfigurator()
.AddSerializationOptionsConfigurator()
+ .AddSerializationOptionsConfigurator()
// Domain event handlers.
.AddHandlersFrom()
diff --git a/src/modules/Elsa.Workflows.Management/Extensions/ManagementServiceCollectionExtensions.cs b/src/modules/Elsa.Workflows.Management/Extensions/ManagementServiceCollectionExtensions.cs
new file mode 100644
index 000000000..0eadcf65b
--- /dev/null
+++ b/src/modules/Elsa.Workflows.Management/Extensions/ManagementServiceCollectionExtensions.cs
@@ -0,0 +1,105 @@
+using System.Reflection;
+using Elsa.Expressions.Extensions;
+using Elsa.Workflows.Management.Models;
+using Elsa.Workflows.Management.Options;
+using Microsoft.Extensions.DependencyInjection;
+
+// ReSharper disable once CheckNamespace
+namespace Elsa.Workflows.Management.Extensions;
+
+///
+/// extension methods for registering Elsa activity types
+/// and variable descriptors via .
+///
+///
+/// These extensions are the shell-feature-compatible replacement for calling
+/// WorkflowManagementFeature.AddActivitiesFrom<T>() in the old-style feature system.
+/// Because services.Configure<ManagementOptions> is additive, multiple features
+/// can independently register activities without any coupling to each other.
+///
+public static class ManagementServiceCollectionExtensions
+{
+ // -------------------------------------------------------------------------
+ // Activities
+ // -------------------------------------------------------------------------
+
+ ///
+ /// Registers the supplied activity with .
+ ///
+ public static IServiceCollection AddActivities(this IServiceCollection services, IEnumerable types) =>
+ services.Configure(options =>
+ {
+ foreach (var type in types)
+ options.ActivityTypes.Add(type);
+ });
+
+ ///
+ /// Registers a single activity type with .
+ ///
+ public static IServiceCollection AddActivity(this IServiceCollection services)
+ where TActivity : IActivity =>
+ services.AddActivities([typeof(TActivity)]);
+
+ ///
+ /// Scans and registers every concrete, non-generic
+ /// implementation found.
+ ///
+ public static IServiceCollection AddActivitiesFrom(this IServiceCollection services, Assembly assembly)
+ {
+ var types = assembly.GetExportedTypes()
+ .Where(t => typeof(IActivity).IsAssignableFrom(t)
+ && t is { IsAbstract: false, IsInterface: false, IsGenericTypeDefinition: false });
+ return services.AddActivities(types);
+ }
+
+ ///
+ /// Scans the assembly that contains and registers every
+ /// concrete, non-generic implementation found.
+ ///
+ public static IServiceCollection AddActivitiesFrom(this IServiceCollection services) =>
+ services.AddActivitiesFrom(typeof(TMarker).Assembly);
+
+ // -------------------------------------------------------------------------
+ // Variable descriptors
+ // -------------------------------------------------------------------------
+
+ ///
+ /// Registers the supplied with .
+ ///
+ public static IServiceCollection AddVariableDescriptors(
+ this IServiceCollection services,
+ IEnumerable descriptors) =>
+ services.Configure(options =>
+ {
+ foreach (var descriptor in descriptors)
+ options.VariableDescriptors.Add(descriptor);
+ });
+
+ ///
+ /// Registers a single variable descriptor.
+ ///
+ public static IServiceCollection AddVariableDescriptor(
+ this IServiceCollection services,
+ VariableDescriptor descriptor) =>
+ services.AddVariableDescriptors([descriptor]);
+
+ ///
+ /// Registers a variable descriptor for with the given
+ /// and optional .
+ ///
+ public static IServiceCollection AddVariableDescriptor(
+ this IServiceCollection services,
+ string category,
+ string? description = null) =>
+ services.AddVariableDescriptor(new(typeof(T), category, description));
+
+ ///
+ /// Adds a variable type and its alias to the specified service collection.
+ ///
+ public static IServiceCollection AddVariableTypeAndAlias(this IServiceCollection services, string alias, string category)
+ {
+ return services
+ .AddVariableDescriptor(category)
+ .AddTypeAlias(alias);
+ }
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Management/Services/CachingWorkflowDefinitionService.cs b/src/modules/Elsa.Workflows.Management/Services/CachingWorkflowDefinitionService.cs
index 578c1fe84..cc1555bd5 100644
--- a/src/modules/Elsa.Workflows.Management/Services/CachingWorkflowDefinitionService.cs
+++ b/src/modules/Elsa.Workflows.Management/Services/CachingWorkflowDefinitionService.cs
@@ -87,10 +87,11 @@ public class CachingWorkflowDefinitionService(
///
public async Task FindWorkflowGraphAsync(WorkflowDefinitionFilter filter, CancellationToken cancellationToken = default)
{
- var cacheKey = cacheManager.CreateWorkflowFilterCacheKey(filter);
- return await FindFromCacheAsync(cacheKey,
- () => decoratedService.FindWorkflowGraphAsync(filter, cancellationToken),
- x => x.Workflow.Identity.DefinitionId);
+ // Resolve the definition first (already cached by FindWorkflowDefinitionAsync) so the
+ // WorkflowGraph ends up under the stable per-version-ID key rather than a filter-hash key
+ // that can never be shared with the other FindWorkflowGraphAsync overloads.
+ var definition = await FindWorkflowDefinitionAsync(filter, cancellationToken);
+ return await FindWorkflowGraphForDefinitionAsync(definition, cancellationToken);
}
///
@@ -100,12 +101,9 @@ public class CachingWorkflowDefinitionService(
var workflowGraphs = new List();
foreach (var workflowDefinition in workflowDefinitions)
{
- var cacheKey = cacheManager.CreateWorkflowVersionCacheKey(workflowDefinition.Id);
- var workflowGraph = await GetFromCacheAsync(
- cacheKey,
- async () => await MaterializeWorkflowAsync(workflowDefinition, cancellationToken),
- wf => wf.Workflow.Identity.DefinitionId);
- workflowGraphs.Add(workflowGraph);
+ var workflowGraph = await FindWorkflowGraphForDefinitionAsync(workflowDefinition, cancellationToken);
+ if (workflowGraph != null)
+ workflowGraphs.Add(workflowGraph);
}
return workflowGraphs;
@@ -114,23 +112,17 @@ public class CachingWorkflowDefinitionService(
///
public async Task TryFindWorkflowGraphAsync(string definitionId, VersionOptions versionOptions, CancellationToken cancellationToken = default)
{
- var cacheKey = cacheManager.CreateWorkflowVersionCacheKey(definitionId, versionOptions);
- var result = await GetFromCacheAsync(cacheKey,
- () => decoratedService.TryFindWorkflowGraphAsync(definitionId, versionOptions, cancellationToken),
- x => x.WorkflowDefinition?.DefinitionId);
-
- return result;
+ var definition = await FindWorkflowDefinitionAsync(definitionId, versionOptions, cancellationToken);
+ var workflowGraph = await FindWorkflowGraphForDefinitionAsync(definition, cancellationToken);
+ return new(definition, workflowGraph);
}
///
public async Task TryFindWorkflowGraphAsync(string definitionVersionId, CancellationToken cancellationToken = default)
{
- var cacheKey = cacheManager.CreateWorkflowVersionCacheKey(definitionVersionId);
- var result = await GetFromCacheAsync(cacheKey,
- () => decoratedService.TryFindWorkflowGraphAsync(definitionVersionId, cancellationToken),
- x => x.WorkflowDefinition?.DefinitionId);
-
- return result;
+ var definition = await FindWorkflowDefinitionAsync(definitionVersionId, cancellationToken);
+ var workflowGraph = await FindWorkflowGraphForDefinitionAsync(definition, cancellationToken);
+ return new(definition, workflowGraph);
}
///
@@ -143,12 +135,9 @@ public class CachingWorkflowDefinitionService(
///
public async Task TryFindWorkflowGraphAsync(WorkflowDefinitionFilter filter, CancellationToken cancellationToken = default)
{
- var cacheKey = cacheManager.CreateWorkflowFilterCacheKey(filter);
- var result = await GetFromCacheAsync(cacheKey,
- () => decoratedService.TryFindWorkflowGraphAsync(filter, cancellationToken),
- x => x.WorkflowDefinition?.DefinitionId);
-
- return result;
+ var definition = await FindWorkflowDefinitionAsync(filter, cancellationToken);
+ var workflowGraph = await FindWorkflowGraphForDefinitionAsync(definition, cancellationToken);
+ return new(definition, workflowGraph);
}
///
@@ -158,50 +147,54 @@ public class CachingWorkflowDefinitionService(
var results = new List();
foreach (var workflowDefinition in workflowDefinitions)
{
- if (!materializerRegistry.IsMaterializerAvailable(workflowDefinition.MaterializerName))
- {
- var unavailableResult = new WorkflowGraphFindResult(workflowDefinition, null);
- results.Add(unavailableResult);
- continue;
- }
-
- var cacheKey = cacheManager.CreateWorkflowVersionCacheKey(workflowDefinition.Id);
- var workflowGraph = await FindFromCacheAsync(
- cacheKey,
- async () => await MaterializeWorkflowAsync(workflowDefinition, cancellationToken),
- wf => wf.Workflow.Identity.DefinitionId);
-
- var result = new WorkflowGraphFindResult(workflowDefinition, workflowGraph);
- results.Add(result);
+ var workflowGraph = await FindWorkflowGraphForDefinitionAsync(workflowDefinition, cancellationToken);
+ results.Add(new WorkflowGraphFindResult(workflowDefinition, workflowGraph));
}
return results;
}
private async Task FindFromCacheAsync(string cacheKey, Func> getObjectFunc, Func getChangeTokenKeyFunc) where T : class
- {
- return await GetFromCacheAsync(
- cacheKey,
- getObjectFunc,
- obj => obj != null ? getChangeTokenKeyFunc(obj) : null);
- }
-
- private async Task GetFromCacheAsync(string cacheKey, Func> getObjectFunc, Func getChangeTokenKeyFunc)
{
var cache = cacheManager.Cache;
- return await cache.GetOrCreateAsync(cacheKey, async entry =>
+ return await cache.FindOrCreateAsync(cacheKey, async entry =>
{
entry.SetAbsoluteExpiration(cache.CachingOptions.Value.CacheDuration);
var obj = await getObjectFunc();
- var changeTokenKeyInput = getChangeTokenKeyFunc(obj);
- if (changeTokenKeyInput != null)
+ if (obj != null)
{
- var changeTokenKey = cacheManager.CreateWorkflowDefinitionChangeTokenKey(changeTokenKeyInput);
+ var changeTokenKey = cacheManager.CreateWorkflowDefinitionChangeTokenKey(getChangeTokenKeyFunc(obj));
entry.AddExpirationToken(cache.GetToken(changeTokenKey));
}
return obj;
});
}
+
+ ///
+ /// Returns the for the given definition, using the per-version-ID cache entry
+ /// that is shared with .
+ /// Returns null when the definition is null or its materializer is unavailable.
+ ///
+ private async Task FindWorkflowGraphForDefinitionAsync(WorkflowDefinition? definition, CancellationToken cancellationToken)
+ {
+ if (definition == null)
+ return null;
+
+ if (!materializerRegistry.IsMaterializerAvailable(definition.MaterializerName))
+ return null;
+
+ var cacheKey = cacheManager.CreateWorkflowVersionCacheKey(definition.Id);
+ var cache = cacheManager.Cache;
+ return await cache.GetOrCreateAsync(cacheKey, async entry =>
+ {
+ entry.SetAbsoluteExpiration(cache.CachingOptions.Value.CacheDuration);
+ var graph = await MaterializeWorkflowAsync(definition, cancellationToken);
+ var changeTokenKey = cacheManager.CreateWorkflowDefinitionChangeTokenKey(graph.Workflow.Identity.DefinitionId);
+ entry.AddExpirationToken(cache.GetToken(changeTokenKey));
+ return graph;
+ });
+ }
+
}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Management/ShellFeatures/WorkflowManagementFeature.cs b/src/modules/Elsa.Workflows.Management/ShellFeatures/WorkflowManagementFeature.cs
index 6f54bfefc..d306789da 100644
--- a/src/modules/Elsa.Workflows.Management/ShellFeatures/WorkflowManagementFeature.cs
+++ b/src/modules/Elsa.Workflows.Management/ShellFeatures/WorkflowManagementFeature.cs
@@ -11,6 +11,7 @@ using Elsa.Workflows.Features;
using Elsa.Workflows.Management.Activities.WorkflowDefinitionActivity;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Entities;
+using Elsa.Workflows.Management.Extensions;
using Elsa.Workflows.Management.Features;
using Elsa.Workflows.Management.Handlers.Notifications;
using Elsa.Workflows.Management.Mappers;
@@ -94,6 +95,7 @@ public class WorkflowManagementFeature : IShellFeature
.AddScoped()
.AddScoped()
.AddScoped()
+ .AddScoped()
.AddScoped()
.AddScoped()
.AddScoped()
@@ -118,42 +120,14 @@ public class WorkflowManagementFeature : IShellFeature
.AddNotificationHandler()
.AddNotificationHandler()
.AddNotificationHandler()
- .AddNotificationHandler()
- ;
+ .AddNotificationHandler();
- AddActivitiesFrom();
- AddActivitiesFrom();
-
+ // Register built-in activities from the Workflows and WorkflowManagement assemblies.
+ services
+ .AddActivitiesFrom()
+ .AddActivitiesFrom();
- services.Configure(options =>
- {
- foreach (var activityType in ActivityTypes.Distinct())
- options.ActivityTypes.Add(activityType);
-
- foreach (var descriptor in VariableDescriptors.DistinctBy(x => x.Type))
- options.VariableDescriptors.Add(descriptor);
- });
- }
-
- ///
- /// A set of activity types to make available to the system.
- ///
- private HashSet ActivityTypes { get; } = [];
-
- ///
- /// Adds the specified activity types to the system.
- ///
- private WorkflowManagementFeature AddActivities(IEnumerable activityTypes)
- {
- ActivityTypes.AddRange(activityTypes);
- return this;
- }
-
- private WorkflowManagementFeature AddActivitiesFrom()
- {
- var activityTypes = typeof(TMarker).Assembly.GetExportedTypes()
- .Where(x => typeof(IActivity).IsAssignableFrom(x) && x is { IsAbstract: false, IsInterface: false, IsGenericType: false })
- .ToList();
- return AddActivities(activityTypes);
+ // Register the default variable descriptors declared on this feature.
+ services.AddVariableDescriptors(VariableDescriptors);
}
}
\ No newline at end of file
diff --git a/src/modules/Elsa/ShellFeatures/ElsaFeature.cs b/src/modules/Elsa/ShellFeatures/ElsaFeature.cs
index 815b06fe6..c441e58ae 100644
--- a/src/modules/Elsa/ShellFeatures/ElsaFeature.cs
+++ b/src/modules/Elsa/ShellFeatures/ElsaFeature.cs
@@ -15,6 +15,6 @@ public class ElsaFeature : IShellFeature
public void ConfigureServices(IServiceCollection services)
{
// Register the shell-based feature provider to bridge shell features to the Elsa feature API
- services.AddSingleton();
+ services.AddSingleton();
}
}
\ No newline at end of file
diff --git a/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowActivities/AutoUpdateTests.cs b/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowActivities/AutoUpdateTests.cs
index fdd30c25e..4252686d0 100644
--- a/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowActivities/AutoUpdateTests.cs
+++ b/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowActivities/AutoUpdateTests.cs
@@ -70,8 +70,10 @@ public class AutoUpdateTests : AppComponentTest
{
Id = ParentDefinitionVersionId
};
- var parentVersionCacheKey = _definitionCacheManager.CreateWorkflowFilterCacheKey(parentWorkflowDefinitionFilter);
- Assert.True(_cache.TryGetValue(parentVersionCacheKey, out _));
+ var parentDefinitionCacheKey = _definitionCacheManager.CreateWorkflowDefinitionFilterCacheKey(parentWorkflowDefinitionFilter);
+ var parentGraphCacheKey = _definitionCacheManager.CreateWorkflowVersionCacheKey(ParentDefinitionVersionId);
+ Assert.True(_cache.TryGetValue(parentDefinitionCacheKey, out _));
+ Assert.True(_cache.TryGetValue(parentGraphCacheKey, out _));
// Set change tokens.
_httpChangeToken = _workflowCacheManager.CreateWorkflowDefinitionChangeTokenKey(ParentDefinitionId);
@@ -88,7 +90,8 @@ public class AutoUpdateTests : AppComponentTest
Assert.False(_cache.TryGetValue($"http-workflow:{hash}", out _));
Assert.False(_cache.TryGetValue($"IEnumerable`1:{hashedFilter}", out _));
- Assert.False(_cache.TryGetValue(parentVersionCacheKey, out _));
+ Assert.False(_cache.TryGetValue(parentDefinitionCacheKey, out _));
+ Assert.False(_cache.TryGetValue(parentGraphCacheKey, out _));
}
private void OnChangeTokenSignalTriggered(object? sender, TriggerChangeTokenSignalEventArgs args)
diff --git a/test/unit/Elsa.Workflows.Management.UnitTests/Services/CachingWorkflowDefinitionServiceTests.cs b/test/unit/Elsa.Workflows.Management.UnitTests/Services/CachingWorkflowDefinitionServiceTests.cs
index 9204667f7..49e7363a1 100644
--- a/test/unit/Elsa.Workflows.Management.UnitTests/Services/CachingWorkflowDefinitionServiceTests.cs
+++ b/test/unit/Elsa.Workflows.Management.UnitTests/Services/CachingWorkflowDefinitionServiceTests.cs
@@ -15,6 +15,10 @@ namespace Elsa.Workflows.Management.UnitTests.Services;
public class CachingWorkflowDefinitionServiceTests
{
+ private const string DefaultDefinitionId = "def-1";
+ private const string DefaultVersionId = "version-id-1";
+ private const string DefaultMaterializerName = "materializer";
+
private readonly IWorkflowDefinitionService _decoratedService = Substitute.For();
private readonly IWorkflowDefinitionCacheManager _cacheManager = Substitute.For();
private readonly IWorkflowDefinitionStore _workflowDefinitionStore = Substitute.For();
@@ -30,11 +34,9 @@ public class CachingWorkflowDefinitionServiceTests
public async Task MaterializeWorkflowAsync_DelegatesToDecoratedService()
{
// Arrange
- var definition = TestHelpers.CreateWorkflowDefinition("def-1", "materializer");
- var (_, workflowGraph) = CreateWorkflowAndGraph("def-1");
-
+ var definition = TestHelpers.CreateWorkflowDefinition(DefaultDefinitionId, DefaultMaterializerName);
+ var (_, workflowGraph) = CreateWorkflowAndGraph(DefaultDefinitionId);
_decoratedService.MaterializeWorkflowAsync(definition, Arg.Any()).Returns(workflowGraph);
-
var service = CreateService();
// Act
@@ -49,42 +51,35 @@ public class CachingWorkflowDefinitionServiceTests
public async Task FindWorkflowDefinitionAsync_ByDefinitionIdAndVersionOptions_CreatesCacheKey()
{
// Arrange
- var definition = TestHelpers.CreateWorkflowDefinition("def-1", "materializer");
- var cacheKey = "cache-key-1";
-
- _cacheManager.CreateWorkflowDefinitionVersionCacheKey("def-1", VersionOptions.Published).Returns(cacheKey);
- _decoratedService.FindWorkflowDefinitionAsync("def-1", VersionOptions.Published, Arg.Any())
- .Returns(definition);
-
+ var definition = TestHelpers.CreateWorkflowDefinition(DefaultDefinitionId, DefaultMaterializerName);
+ _cacheManager.CreateWorkflowDefinitionVersionCacheKey(DefaultDefinitionId, VersionOptions.Published).Returns("cache-key");
+ _decoratedService.FindWorkflowDefinitionAsync(DefaultDefinitionId, VersionOptions.Published, Arg.Any()).Returns(definition);
var service = CreateService();
// Act
- var result = await service.FindWorkflowDefinitionAsync("def-1", VersionOptions.Published);
+ var result = await service.FindWorkflowDefinitionAsync(DefaultDefinitionId, VersionOptions.Published);
// Assert
Assert.Same(definition, result);
- _cacheManager.Received(1).CreateWorkflowDefinitionVersionCacheKey("def-1", VersionOptions.Published);
+ _cacheManager.Received(1).CreateWorkflowDefinitionVersionCacheKey(DefaultDefinitionId, VersionOptions.Published);
}
[Fact]
public async Task FindWorkflowDefinitionAsync_ByDefinitionVersionId_CreatesCacheKey()
{
// Arrange
- var definition = TestHelpers.CreateWorkflowDefinition("def-1", "materializer");
- definition.Id = "version-id-1";
- var cacheKey = "cache-key-version-1";
-
- _cacheManager.CreateWorkflowDefinitionVersionCacheKey("version-id-1").Returns(cacheKey);
- _decoratedService.FindWorkflowDefinitionAsync("version-id-1", Arg.Any()).Returns(definition);
-
+ var definition = TestHelpers.CreateWorkflowDefinition(DefaultDefinitionId, DefaultMaterializerName);
+ definition.Id = DefaultVersionId;
+ _cacheManager.CreateWorkflowDefinitionVersionCacheKey(DefaultVersionId).Returns("cache-key");
+ _decoratedService.FindWorkflowDefinitionAsync(DefaultVersionId, Arg.Any()).Returns(definition);
var service = CreateService();
// Act
- var result = await service.FindWorkflowDefinitionAsync("version-id-1");
+ var result = await service.FindWorkflowDefinitionAsync(DefaultVersionId);
// Assert
Assert.Same(definition, result);
- _cacheManager.Received(1).CreateWorkflowDefinitionVersionCacheKey("version-id-1");
+ _cacheManager.Received(1).CreateWorkflowDefinitionVersionCacheKey(DefaultVersionId);
}
[Fact]
@@ -172,17 +167,18 @@ public class CachingWorkflowDefinitionServiceTests
}
[Fact]
- public async Task FindWorkflowGraphAsync_ByHandle_ConvertsTFilterAndDelegates()
+ public async Task FindWorkflowGraphAsync_ByHandle_ConvertsTFilterAndResolvesDefinitionAndGraph()
{
// Arrange
- var (_, workflowGraph) = CreateWorkflowAndGraph("def-1");
- var handle = WorkflowDefinitionHandle.ByDefinitionId("def-1", VersionOptions.Latest);
- var cacheKey = "cache-key-filter";
-
- _cacheManager.CreateWorkflowFilterCacheKey(Arg.Any()).Returns(cacheKey);
- _decoratedService.FindWorkflowGraphAsync(Arg.Any(), Arg.Any())
- .Returns(workflowGraph);
+ var definition = TestHelpers.CreateWorkflowDefinition(DefaultDefinitionId, DefaultMaterializerName);
+ definition.Id = DefaultVersionId;
+ var (_, workflowGraph) = CreateWorkflowAndGraph(DefaultDefinitionId);
+ var handle = WorkflowDefinitionHandle.ByDefinitionId(DefaultDefinitionId, VersionOptions.Latest);
+ _cacheManager.CreateWorkflowDefinitionFilterCacheKey(Arg.Any()).Returns("cache-key-def");
+ _cacheManager.CreateWorkflowVersionCacheKey(DefaultVersionId).Returns("cache-key-graph");
+ _decoratedService.FindWorkflowDefinitionAsync(Arg.Any(), Arg.Any()).Returns(definition);
+ SetupDefinitionAndGraphMaterialization(definition, workflowGraph);
var service = CreateService();
// Act
@@ -190,20 +186,22 @@ public class CachingWorkflowDefinitionServiceTests
// Assert
Assert.Same(workflowGraph, result);
- await _decoratedService.Received(1).FindWorkflowGraphAsync(Arg.Any(), Arg.Any());
+ await _decoratedService.Received(1).FindWorkflowDefinitionAsync(Arg.Any(), Arg.Any());
}
[Fact]
- public async Task FindWorkflowGraphAsync_ByFilter_CreatesCacheKey()
+ public async Task FindWorkflowGraphAsync_ByFilter_ResolvesDefinitionFirstAndCachesGraph()
{
// Arrange
- var (_, workflowGraph) = CreateWorkflowAndGraph("def-1");
- var filter = new WorkflowDefinitionFilter { DefinitionId = "def-1" };
- var cacheKey = "cache-key-filter";
-
- _cacheManager.CreateWorkflowFilterCacheKey(filter).Returns(cacheKey);
- _decoratedService.FindWorkflowGraphAsync(filter, Arg.Any()).Returns(workflowGraph);
+ var definition = TestHelpers.CreateWorkflowDefinition(DefaultDefinitionId, DefaultMaterializerName);
+ definition.Id = DefaultVersionId;
+ var (_, workflowGraph) = CreateWorkflowAndGraph(DefaultDefinitionId);
+ var filter = new WorkflowDefinitionFilter { DefinitionId = DefaultDefinitionId };
+ _cacheManager.CreateWorkflowDefinitionFilterCacheKey(filter).Returns("cache-key-def");
+ _cacheManager.CreateWorkflowVersionCacheKey(DefaultVersionId).Returns("cache-key-graph");
+ _decoratedService.FindWorkflowDefinitionAsync(filter, Arg.Any()).Returns(definition);
+ SetupDefinitionAndGraphMaterialization(definition, workflowGraph);
var service = CreateService();
// Act
@@ -211,31 +209,50 @@ public class CachingWorkflowDefinitionServiceTests
// Assert
Assert.Same(workflowGraph, result);
- _cacheManager.Received(1).CreateWorkflowFilterCacheKey(filter);
+ _cacheManager.Received(1).CreateWorkflowDefinitionFilterCacheKey(filter);
+ _cacheManager.Received(1).CreateWorkflowVersionCacheKey(DefaultVersionId);
+ }
+
+ [Fact]
+ public async Task FindWorkflowGraphAsync_ByFilter_WithUnavailableMaterializer_ReturnsNull()
+ {
+ // Arrange
+ const string unavailableMaterializer = "unavailable-materializer";
+ var definition = TestHelpers.CreateWorkflowDefinition(DefaultDefinitionId, unavailableMaterializer);
+ definition.Id = DefaultVersionId;
+ var filter = new WorkflowDefinitionFilter { DefinitionId = DefaultDefinitionId };
+
+ _cacheManager.CreateWorkflowDefinitionFilterCacheKey(filter).Returns("cache-key");
+ _decoratedService.FindWorkflowDefinitionAsync(filter, Arg.Any()).Returns(definition);
+ _materializerRegistry.IsMaterializerAvailable(unavailableMaterializer).Returns(false);
+ var service = CreateService();
+
+ // Act
+ var result = await service.FindWorkflowGraphAsync(filter);
+
+ // Assert
+ Assert.Null(result);
+ _cacheManager.Received(1).CreateWorkflowDefinitionFilterCacheKey(filter);
+ await _decoratedService.DidNotReceive().MaterializeWorkflowAsync(Arg.Any(), Arg.Any());
}
[Fact]
public async Task FindWorkflowGraphsAsync_WithMultipleDefinitions_CachesEachGraph()
{
// Arrange
- var definition1 = TestHelpers.CreateWorkflowDefinition("def-1", "materializer");
+ var definition1 = TestHelpers.CreateWorkflowDefinition("def-1", DefaultMaterializerName);
definition1.Id = "id-1";
- var definition2 = TestHelpers.CreateWorkflowDefinition("def-2", "materializer");
+ var definition2 = TestHelpers.CreateWorkflowDefinition("def-2", DefaultMaterializerName);
definition2.Id = "id-2";
- var definitions = new[] { definition1, definition2 };
-
var (_, graph1) = CreateWorkflowAndGraph("def-1");
var (_, graph2) = CreateWorkflowAndGraph("def-2");
-
var filter = new WorkflowDefinitionFilter();
- _workflowDefinitionStore.FindManyAsync(filter, Arg.Any()).Returns(definitions);
+ _workflowDefinitionStore.FindManyAsync(filter, Arg.Any()).Returns(new[] { definition1, definition2 });
_cacheManager.CreateWorkflowVersionCacheKey("id-1").Returns("cache-key-1");
_cacheManager.CreateWorkflowVersionCacheKey("id-2").Returns("cache-key-2");
-
- _decoratedService.MaterializeWorkflowAsync(definition1, Arg.Any()).Returns(graph1);
- _decoratedService.MaterializeWorkflowAsync(definition2, Arg.Any()).Returns(graph2);
-
+ SetupDefinitionAndGraphMaterialization(definition1, graph1);
+ SetupDefinitionAndGraphMaterialization(definition2, graph2);
var service = CreateService();
// Act
@@ -251,106 +268,146 @@ public class CachingWorkflowDefinitionServiceTests
}
[Fact]
- public async Task TryFindWorkflowGraphAsync_ByDefinitionIdAndVersionOptions_CreatesCacheKey()
+ public async Task FindWorkflowGraphsAsync_SkipsDefinitionsWithUnavailableMaterializer()
{
// Arrange
- var findResult = CreateWorkflowGraphFindResult("def-1");
- var cacheKey = "cache-key-try";
+ var definition1 = TestHelpers.CreateWorkflowDefinition("def-1", "available-materializer");
+ definition1.Id = "id-1";
+ var definition2 = TestHelpers.CreateWorkflowDefinition("def-2", "unavailable-materializer");
+ definition2.Id = "id-2";
+ var definitions = new[] { definition1, definition2 };
- _cacheManager.CreateWorkflowVersionCacheKey("def-1", VersionOptions.Published).Returns(cacheKey);
- _decoratedService.TryFindWorkflowGraphAsync("def-1", VersionOptions.Published, Arg.Any())
- .Returns(findResult);
+ var (_, graph1) = CreateWorkflowAndGraph("def-1");
+
+ var filter = new WorkflowDefinitionFilter();
+ _workflowDefinitionStore.FindManyAsync(filter, Arg.Any()).Returns(definitions);
+
+ _cacheManager.CreateWorkflowVersionCacheKey("id-1").Returns("cache-key-1");
+ _materializerRegistry.IsMaterializerAvailable("available-materializer").Returns(true);
+ _materializerRegistry.IsMaterializerAvailable("unavailable-materializer").Returns(false);
+ _decoratedService.MaterializeWorkflowAsync(definition1, Arg.Any()).Returns(graph1);
var service = CreateService();
// Act
- var result = await service.TryFindWorkflowGraphAsync("def-1", VersionOptions.Published);
+ var result = await service.FindWorkflowGraphsAsync(filter);
// Assert
- Assert.Same(findResult, result);
- _cacheManager.Received(1).CreateWorkflowVersionCacheKey("def-1", VersionOptions.Published);
+ var graphs = result.ToList();
+ Assert.Single(graphs);
+ Assert.Same(graph1, graphs[0]);
+ await _decoratedService.DidNotReceive().MaterializeWorkflowAsync(definition2, Arg.Any());
}
[Fact]
- public async Task TryFindWorkflowGraphAsync_ByDefinitionVersionId_CreatesCacheKey()
+ public async Task TryFindWorkflowGraphAsync_ByDefinitionIdAndVersionOptions_ResolvesDefinitionAndGraph()
{
// Arrange
- var findResult = CreateWorkflowGraphFindResult("def-1");
- var cacheKey = "cache-key-try-version";
-
- _cacheManager.CreateWorkflowVersionCacheKey("version-id-1").Returns(cacheKey);
- _decoratedService.TryFindWorkflowGraphAsync("version-id-1", Arg.Any()).Returns(findResult);
+ var definition = TestHelpers.CreateWorkflowDefinition(DefaultDefinitionId, DefaultMaterializerName);
+ definition.Id = DefaultVersionId;
+ var (_, workflowGraph) = CreateWorkflowAndGraph(DefaultDefinitionId);
+ _cacheManager.CreateWorkflowDefinitionVersionCacheKey(DefaultDefinitionId, VersionOptions.Published).Returns("cache-key-def");
+ _cacheManager.CreateWorkflowVersionCacheKey(DefaultVersionId).Returns("cache-key-graph");
+ _decoratedService.FindWorkflowDefinitionAsync(DefaultDefinitionId, VersionOptions.Published, Arg.Any()).Returns(definition);
+ SetupDefinitionAndGraphMaterialization(definition, workflowGraph);
var service = CreateService();
// Act
- var result = await service.TryFindWorkflowGraphAsync("version-id-1");
+ var result = await service.TryFindWorkflowGraphAsync(DefaultDefinitionId, VersionOptions.Published);
// Assert
- Assert.Same(findResult, result);
- _cacheManager.Received(1).CreateWorkflowVersionCacheKey("version-id-1");
+ Assert.Same(definition, result.WorkflowDefinition);
+ Assert.Same(workflowGraph, result.WorkflowGraph);
+ _cacheManager.Received(1).CreateWorkflowDefinitionVersionCacheKey(DefaultDefinitionId, VersionOptions.Published);
+ _cacheManager.Received(1).CreateWorkflowVersionCacheKey(DefaultVersionId);
}
[Fact]
- public async Task TryFindWorkflowGraphAsync_ByHandle_ConvertsTFilterAndDelegates()
+ public async Task TryFindWorkflowGraphAsync_ByDefinitionVersionId_ResolvesDefinitionAndGraph()
{
// Arrange
- var findResult = CreateWorkflowGraphFindResult("def-1");
- var handle = WorkflowDefinitionHandle.ByDefinitionId("def-1", VersionOptions.Latest);
- var cacheKey = "cache-key-filter";
+ var definition = TestHelpers.CreateWorkflowDefinition(DefaultDefinitionId, DefaultMaterializerName);
+ definition.Id = DefaultVersionId;
+ var (_, workflowGraph) = CreateWorkflowAndGraph(DefaultDefinitionId);
- _cacheManager.CreateWorkflowFilterCacheKey(Arg.Any()).Returns(cacheKey);
- _decoratedService.TryFindWorkflowGraphAsync(Arg.Any(), Arg.Any())
- .Returns(findResult);
+ _cacheManager.CreateWorkflowDefinitionVersionCacheKey(DefaultVersionId).Returns("cache-key-def");
+ _cacheManager.CreateWorkflowVersionCacheKey(DefaultVersionId).Returns("cache-key-graph");
+ _decoratedService.FindWorkflowDefinitionAsync(DefaultVersionId, Arg.Any()).Returns(definition);
+ SetupDefinitionAndGraphMaterialization(definition, workflowGraph);
+ var service = CreateService();
+ // Act
+ var result = await service.TryFindWorkflowGraphAsync(DefaultVersionId);
+
+ // Assert
+ Assert.Same(definition, result.WorkflowDefinition);
+ Assert.Same(workflowGraph, result.WorkflowGraph);
+ _cacheManager.Received(1).CreateWorkflowDefinitionVersionCacheKey(DefaultVersionId);
+ _cacheManager.Received(1).CreateWorkflowVersionCacheKey(DefaultVersionId);
+ }
+
+ [Fact]
+ public async Task TryFindWorkflowGraphAsync_ByHandle_ConvertsTFilterAndResolvesDefinitionAndGraph()
+ {
+ // Arrange
+ var definition = TestHelpers.CreateWorkflowDefinition(DefaultDefinitionId, DefaultMaterializerName);
+ definition.Id = DefaultVersionId;
+ var (_, workflowGraph) = CreateWorkflowAndGraph(DefaultDefinitionId);
+ var handle = WorkflowDefinitionHandle.ByDefinitionId(DefaultDefinitionId, VersionOptions.Latest);
+
+ _cacheManager.CreateWorkflowDefinitionFilterCacheKey(Arg.Any()).Returns("cache-key-def");
+ _cacheManager.CreateWorkflowVersionCacheKey(DefaultVersionId).Returns("cache-key-graph");
+ _decoratedService.FindWorkflowDefinitionAsync(Arg.Any(), Arg.Any()).Returns(definition);
+ SetupDefinitionAndGraphMaterialization(definition, workflowGraph);
var service = CreateService();
// Act
var result = await service.TryFindWorkflowGraphAsync(handle);
// Assert
- Assert.Same(findResult, result);
- await _decoratedService.Received(1).TryFindWorkflowGraphAsync(Arg.Any(), Arg.Any());
+ Assert.Same(definition, result.WorkflowDefinition);
+ Assert.Same(workflowGraph, result.WorkflowGraph);
+ await _decoratedService.Received(1).FindWorkflowDefinitionAsync(Arg.Any(), Arg.Any());
}
[Fact]
- public async Task TryFindWorkflowGraphAsync_ByFilter_CreatesCacheKey()
+ public async Task TryFindWorkflowGraphAsync_ByFilter_ResolvesDefinitionAndGraph()
{
// Arrange
- var findResult = CreateWorkflowGraphFindResult("def-1");
- var filter = new WorkflowDefinitionFilter { DefinitionId = "def-1" };
- var cacheKey = "cache-key-filter";
-
- _cacheManager.CreateWorkflowFilterCacheKey(filter).Returns(cacheKey);
- _decoratedService.TryFindWorkflowGraphAsync(filter, Arg.Any()).Returns(findResult);
+ var definition = TestHelpers.CreateWorkflowDefinition(DefaultDefinitionId, DefaultMaterializerName);
+ definition.Id = DefaultVersionId;
+ var (_, workflowGraph) = CreateWorkflowAndGraph(DefaultDefinitionId);
+ var filter = new WorkflowDefinitionFilter { DefinitionId = DefaultDefinitionId };
+ _cacheManager.CreateWorkflowDefinitionFilterCacheKey(filter).Returns("cache-key-def");
+ _cacheManager.CreateWorkflowVersionCacheKey(DefaultVersionId).Returns("cache-key-graph");
+ _decoratedService.FindWorkflowDefinitionAsync(filter, Arg.Any()).Returns(definition);
+ SetupDefinitionAndGraphMaterialization(definition, workflowGraph);
var service = CreateService();
// Act
var result = await service.TryFindWorkflowGraphAsync(filter);
// Assert
- Assert.Same(findResult, result);
- _cacheManager.Received(1).CreateWorkflowFilterCacheKey(filter);
+ Assert.Same(definition, result.WorkflowDefinition);
+ Assert.Same(workflowGraph, result.WorkflowGraph);
+ _cacheManager.Received(1).CreateWorkflowDefinitionFilterCacheKey(filter);
+ _cacheManager.Received(1).CreateWorkflowVersionCacheKey(DefaultVersionId);
}
[Fact]
public async Task TryFindWorkflowGraphsAsync_WithAvailableMaterializer_CachesGraph()
{
// Arrange
- var definition = TestHelpers.CreateWorkflowDefinition("def-1", "materializer");
+ var definition = TestHelpers.CreateWorkflowDefinition(DefaultDefinitionId, DefaultMaterializerName);
definition.Id = "id-1";
- var definitions = new[] { definition };
-
- var (_, graph) = CreateWorkflowAndGraph("def-1");
-
+ var (_, graph) = CreateWorkflowAndGraph(DefaultDefinitionId);
var filter = new WorkflowDefinitionFilter();
- _workflowDefinitionStore.FindManyAsync(filter, Arg.Any()).Returns(definitions);
-
- _cacheManager.CreateWorkflowVersionCacheKey("id-1").Returns("cache-key-1");
- _materializerRegistry.IsMaterializerAvailable("materializer").Returns(true);
- _decoratedService.MaterializeWorkflowAsync(definition, Arg.Any()).Returns(graph);
+ _workflowDefinitionStore.FindManyAsync(filter, Arg.Any()).Returns(new[] { definition });
+ _cacheManager.CreateWorkflowVersionCacheKey("id-1").Returns("cache-key");
+ SetupDefinitionAndGraphMaterialization(definition, graph);
var service = CreateService();
// Act
@@ -367,16 +424,14 @@ public class CachingWorkflowDefinitionServiceTests
public async Task TryFindWorkflowGraphsAsync_WithUnavailableMaterializer_ReturnsNullGraph()
{
// Arrange
- var definition = TestHelpers.CreateWorkflowDefinition("def-1", "unavailable-materializer");
+ const string unavailableMaterializer = "unavailable-materializer";
+ var definition = TestHelpers.CreateWorkflowDefinition(DefaultDefinitionId, unavailableMaterializer);
definition.Id = "id-1";
- var definitions = new[] { definition };
-
var filter = new WorkflowDefinitionFilter();
- _workflowDefinitionStore.FindManyAsync(filter, Arg.Any()).Returns(definitions);
-
- _cacheManager.CreateWorkflowVersionCacheKey("id-1").Returns("cache-key-1");
- _materializerRegistry.IsMaterializerAvailable("unavailable-materializer").Returns(false);
+ _workflowDefinitionStore.FindManyAsync(filter, Arg.Any()).Returns(new[] { definition });
+ _cacheManager.CreateWorkflowVersionCacheKey("id-1").Returns("cache-key");
+ _materializerRegistry.IsMaterializerAvailable(unavailableMaterializer).Returns(false);
var service = CreateService();
// Act
@@ -476,12 +531,16 @@ public class CachingWorkflowDefinitionServiceTests
}
///
- /// Creates a workflow graph find result for testing.
+ /// Sets up a complete workflow definition with graph materialization for testing scenarios
+ /// where definition needs to be resolved and materialized into a graph.
///
- private WorkflowGraphFindResult CreateWorkflowGraphFindResult(string definitionId = "def-1", string materializerName = "materializer")
+ private void SetupDefinitionAndGraphMaterialization(
+ WorkflowDefinition definition,
+ WorkflowGraph workflowGraph,
+ string? materializerName = null)
{
- var definition = TestHelpers.CreateWorkflowDefinition(definitionId, materializerName);
- var (_, workflowGraph) = CreateWorkflowAndGraph(definitionId);
- return new(definition, workflowGraph);
+ materializerName ??= definition.MaterializerName;
+ _materializerRegistry.IsMaterializerAvailable(materializerName).Returns(true);
+ _decoratedService.MaterializeWorkflowAsync(definition, Arg.Any()).Returns(workflowGraph);
}
}