Add shell middleware, call‑stack tracking, and workflow reference graph APIs (#7333)
* refactor(deps): use local CShells project refs Replace CShells NuGet package references with direct project references to the local CShells source to enable developing and testing against local changes and simplify build integration across modules. * Handle assembly load errors in feature discovery Added error handling for assembly load failures in feature discovery to improve resilience. Also updated configuration for identity token options and removed unused service bus consumer dependencies. Simplified project structure by moving and cleaning up `Directory.Build.targets` files. * Refactor configuration and service extension methods. Moved `ShellSettingsExtensions` and `ShellConfiguration` to `CShells.Abstractions` for better modularity. Added new `ServiceCollectionFeatureExtensions` to improve options registration. Updated appsettings and references to support these changes. * Introduce ManagementServiceCollectionExtensions to streamline activity and variable registration Added `ManagementServiceCollectionExtensions` for registering Elsa activity types and variable descriptors, providing a modular and shell-feature-compatible approach to configuration. Updated relevant features to utilize these new extension methods, enhancing code modularity and reducing redundancy. * Add resilience strategy registration to HTTP feature Introduced `ResilienceServiceCollectionExtensions` to register resilience strategies within the `Elsa.Resilience.Core` module. Updated `HttpFeature` to incorporate resilience strategies, enhancing HTTP-related resilience configuration leveraging the new extension methods. * Add new configuration options to JavaScriptFeature Implemented multiple properties in `JavaScriptFeature` to enhance JavaScript execution: `AllowClrAccess`, `AllowConfigurationAccess`, `ScriptCacheTimeout`, `DisableWrappers`, and `DisableVariableCopying`. These additions enable more flexible and secure configuration of the Jint JavaScript engine. * refactor(workflows): unify graph caching Resolve workflow definitions first and store graphs under stable per-version-ID cache keys so different lookup paths share entries. Centralize cache creation and change-token registration to remove duplicated caching logic. Skip materializer-unavailable definitions to avoid caching null graphs and simplify flow. * refactor(tests): centralize default IDs and materializer setup Introduce constants for default definition and version IDs, and materializer name. Refactor tests to use these constants, streamline graph and definition resolution, and improve cache key creation by sharing logic across tests. Extend tests to check scenarios with unavailable materializers, ensuring caching only occurs for valid cases. * extend(tests): enhance cache key verification in AutoUpdateTests Added checks for both workflow definition and version cache keys in AutoUpdateTests to ensure comprehensive cache validation, improving test reliability and coverage. * refactor(projects): update CShells project paths and solution configuration Revised project reference paths in `Elsa.ModularServer.Web.csproj` for CShells projects and updated `Elsa.sln` to include new CShells projects, streamlining project organization and build configuration. * Add `IWorkflowReferenceGraphBuilder` to `WorkflowManagementFeature`; rename `ResilienceShellFeature` to `ResilienceFeature`. * Refactor `HttpFeature` to use `IMiddlewareShellFeature`, include `HttpWorkflowsMiddleware`, and update `HttpActivityOptions` defaults. * Add `AddTypeAlias` and `AddVariableTypeAndAlias` extension methods to service collections - Introduced `AddTypeAlias<T>` method in `ServiceCollectionExtensions.cs` for adding type aliases. - Added `AddVariableTypeAndAlias<T>` method in `ManagementServiceCollectionExtensions.cs` to add variable types with aliases. * Update CShells package versions to 0.0.11 and replace ProjectReferences with PackageReferences in project files
This commit is contained in:
parent
a79d6a257e
commit
11fec1c85d
7
Directory.Build.targets
Normal file
7
Directory.Build.targets
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<Project>
|
||||||
|
<!-- Use project references instead of NuGet packages for local development -->
|
||||||
|
<PropertyGroup>
|
||||||
|
<UseProjectReferences>true</UseProjectReferences>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
|
|
||||||
|
|
@ -100,12 +100,12 @@
|
||||||
<PackageVersion Include="coverlet.collector" Version="6.0.4" PrivateAssets="All"/>
|
<PackageVersion Include="coverlet.collector" Version="6.0.4" PrivateAssets="All"/>
|
||||||
<PackageVersion Include="coverlet.msbuild" Version="6.0.4" PrivateAssets="All"/>
|
<PackageVersion Include="coverlet.msbuild" Version="6.0.4" PrivateAssets="All"/>
|
||||||
<PackageVersion Include="Cronos" Version="0.11.1"/>
|
<PackageVersion Include="Cronos" Version="0.11.1"/>
|
||||||
<PackageVersion Include="CShells" Version="0.0.9"/>
|
<PackageVersion Include="CShells" Version="0.0.11"/>
|
||||||
<PackageVersion Include="CShells.Abstractions" Version="0.0.9"/>
|
<PackageVersion Include="CShells.Abstractions" Version="0.0.11"/>
|
||||||
<PackageVersion Include="CShells.AspNetCore" Version="0.0.9"/>
|
<PackageVersion Include="CShells.AspNetCore" Version="0.0.11"/>
|
||||||
<PackageVersion Include="CShells.AspNetCore.Abstractions" Version="0.0.9"/>
|
<PackageVersion Include="CShells.AspNetCore.Abstractions" Version="0.0.11"/>
|
||||||
<PackageVersion Include="CShells.FastEndpoints" Version="0.0.9"/>
|
<PackageVersion Include="CShells.FastEndpoints" Version="0.0.11"/>
|
||||||
<PackageVersion Include="CShells.FastEndpoints.Abstractions" Version="0.0.9"/>
|
<PackageVersion Include="CShells.FastEndpoints.Abstractions" Version="0.0.11"/>
|
||||||
<PackageVersion Include="Datadog.Trace.Bundle" Version="3.32.0"/>
|
<PackageVersion Include="Datadog.Trace.Bundle" Version="3.32.0"/>
|
||||||
<PackageVersion Include="DistributedLock" Version="2.7.1"/>
|
<PackageVersion Include="DistributedLock" Version="2.7.1"/>
|
||||||
<PackageVersion Include="DistributedLock.Core" Version="1.0.8"/>
|
<PackageVersion Include="DistributedLock.Core" Version="1.0.8"/>
|
||||||
|
|
|
||||||
44
Elsa.sln
44
Elsa.sln
|
|
@ -337,6 +337,20 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Http.IntegrationTests"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Tenants.UnitTests", "test\unit\Elsa.Tenants.UnitTests\Elsa.Tenants.UnitTests.csproj", "{DC476900-D836-4920-A696-CF8796668723}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Tenants.UnitTests", "test\unit\Elsa.Tenants.UnitTests\Elsa.Tenants.UnitTests.csproj", "{DC476900-D836-4920-A696-CF8796668723}"
|
||||||
EndProject
|
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
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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}.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.ActiveCfg = Release|Any CPU
|
||||||
{DC476900-D836-4920-A696-CF8796668723}.Release|Any CPU.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
@ -719,6 +757,12 @@ Global
|
||||||
{A3C07D5B-2A30-494E-B9BC-4B1594B31ABC} = {18453B51-25EB-4317-A4B3-B10518252E92}
|
{A3C07D5B-2A30-494E-B9BC-4B1594B31ABC} = {18453B51-25EB-4317-A4B3-B10518252E92}
|
||||||
{8C4F6A2D-1E9F-4B3C-9D8E-7F5A6B4C3D2E} = {1B8D5897-902E-4632-8698-E89CAF3DDF54}
|
{8C4F6A2D-1E9F-4B3C-9D8E-7F5A6B4C3D2E} = {1B8D5897-902E-4632-8698-E89CAF3DDF54}
|
||||||
{DC476900-D836-4920-A696-CF8796668723} = {18453B51-25EB-4317-A4B3-B10518252E92}
|
{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
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E}
|
SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CShells.AspNetCore"/>
|
<PackageReference Include="CShells.AspNetCore" />
|
||||||
<PackageReference Include="CShells.FastEndpoints"/>
|
<PackageReference Include="CShells.FastEndpoints" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\modules\Elsa.Identity\Elsa.Identity.csproj" />
|
<ProjectReference Include="..\..\modules\Elsa.Identity\Elsa.Identity.csproj" />
|
||||||
|
|
|
||||||
|
|
@ -26,13 +26,49 @@ namespace Elsa.Expressions.JavaScript.ShellFeatures;
|
||||||
public class JavaScriptFeature : IShellFeature
|
public class JavaScriptFeature : IShellFeature
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 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
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Action<JintOptions> JintOptions { get; set; } = _ => { };
|
public bool AllowClrAccess { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enables access to .NET configuration via the <c>getConfig</c> function.
|
||||||
|
/// Do not enable if you are executing workflows from untrusted sources (e.g user defined workflows).
|
||||||
|
/// </summary>
|
||||||
|
public bool AllowConfigurationAccess { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The timeout for script caching.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The <c>ScriptCacheTimeout</c> 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 <c>ScriptCacheTimeout</c> is <c>null</c>, 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.
|
||||||
|
/// </remarks>
|
||||||
|
public TimeSpan? ScriptCacheTimeout { get; set; } = TimeSpan.FromDays(1);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Disables the generation of variable wrappers. E.g. <c>getMyVariable()</c> will no longer be available for variables. Instead, you can only access variables using <c>getVariable("MyVariable")</c> function.
|
||||||
|
/// This is useful if your application requires the use of invalid JavaScript variable names.
|
||||||
|
/// </summary>
|
||||||
|
public bool DisableWrappers { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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 <c>variables.MyVariable</c> syntax.
|
||||||
|
/// </summary>
|
||||||
|
public bool DisableVariableCopying { get; set; }
|
||||||
|
|
||||||
public void ConfigureServices(IServiceCollection services)
|
public void ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.Configure(JintOptions);
|
services.Configure<JintOptions>(options =>
|
||||||
|
{
|
||||||
|
options.AllowClrAccess = AllowClrAccess;
|
||||||
|
options.AllowConfigurationAccess = AllowConfigurationAccess;
|
||||||
|
options.ScriptCacheTimeout = ScriptCacheTimeout;
|
||||||
|
options.DisableWrappers = DisableWrappers;
|
||||||
|
options.DisableVariableCopying = DisableVariableCopying;
|
||||||
|
});
|
||||||
|
|
||||||
// JavaScript services.
|
// JavaScript services.
|
||||||
services
|
services
|
||||||
|
|
|
||||||
|
|
@ -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<T>(this IServiceCollection services, string alias)
|
||||||
|
{
|
||||||
|
services.Configure<ExpressionOptions>(options => options.AddTypeAlias<T>(alias));
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.
|
/// An ASP.NET middleware component that tries to match the inbound request path to an associated workflow and then run that workflow.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public class HttpWorkflowsMiddleware(RequestDelegate next, IOptions<HttpActivityOptions> options)
|
public class HttpWorkflowsMiddleware(RequestDelegate next)
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Attempts to match the inbound request path to an associated workflow and then run that workflow.
|
/// Attempts to match the inbound request path to an associated workflow and then run that workflow.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Serialize<TValue>(TValue, JsonSerializerOptions)")]
|
[RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Serialize<TValue>(TValue, JsonSerializerOptions)")]
|
||||||
public async Task InvokeAsync(HttpContext httpContext, IServiceProvider serviceProvider)
|
public async Task InvokeAsync(
|
||||||
|
HttpContext httpContext,
|
||||||
|
IServiceProvider serviceProvider,
|
||||||
|
IOptions<HttpActivityOptions> options,
|
||||||
|
IHttpWorkflowLookupService httpWorkflowLookupService)
|
||||||
{
|
{
|
||||||
var path = httpContext.Request.Path.Value!.NormalizeRoute();
|
var path = httpContext.Request.Path.Value!.NormalizeRoute();
|
||||||
var matchingPath = GetMatchingRoute(serviceProvider, path).Route;
|
var matchingPath = GetMatchingRoute(serviceProvider, path).Route;
|
||||||
|
|
@ -63,7 +67,6 @@ public class HttpWorkflowsMiddleware(RequestDelegate next, IOptions<HttpActivity
|
||||||
var cancellationToken = httpContext.RequestAborted;
|
var cancellationToken = httpContext.RequestAborted;
|
||||||
var request = httpContext.Request;
|
var request = httpContext.Request;
|
||||||
var method = request.Method.ToLowerInvariant();
|
var method = request.Method.ToLowerInvariant();
|
||||||
var httpWorkflowLookupService = serviceProvider.GetRequiredService<IHttpWorkflowLookupService>();
|
|
||||||
var workflowInstanceId = await GetWorkflowInstanceIdAsync(serviceProvider, httpContext, cancellationToken);
|
var workflowInstanceId = await GetWorkflowInstanceIdAsync(serviceProvider, httpContext, cancellationToken);
|
||||||
var correlationId = await GetCorrelationIdAsync(serviceProvider, httpContext, cancellationToken);
|
var correlationId = await GetCorrelationIdAsync(serviceProvider, httpContext, cancellationToken);
|
||||||
var bookmarkHash = ComputeBookmarkHash(serviceProvider, matchingPath, method);
|
var bookmarkHash = ComputeBookmarkHash(serviceProvider, matchingPath, method);
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ public class HttpActivityOptions
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 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.
|
/// 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Uri BaseUrl { get; set; } = null!;
|
public Uri BaseUrl { get; set; } = new Uri("https://localhost:5001");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The prefix used for API routes.
|
/// 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
|
/// A boolean to opt-in if you want to call CompleteAsync() in the HttpResponse Activity
|
||||||
/// and not waiting for the end of the HttpMiddleware.
|
/// and not waiting for the end of the HttpMiddleware.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Boolean WriteHttpResponseSynchronously { get; set; } = false;
|
public bool WriteHttpResponseSynchronously { get; set; } = false;
|
||||||
}
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using CShells.AspNetCore.Features;
|
||||||
using CShells.Features;
|
using CShells.Features;
|
||||||
using Elsa.Expressions.Options;
|
using Elsa.Expressions.Options;
|
||||||
using Elsa.Extensions;
|
using Elsa.Extensions;
|
||||||
|
|
@ -6,20 +7,27 @@ using Elsa.Http.ContentWriters;
|
||||||
using Elsa.Http.DownloadableContentHandlers;
|
using Elsa.Http.DownloadableContentHandlers;
|
||||||
using Elsa.Http.FileCaches;
|
using Elsa.Http.FileCaches;
|
||||||
using Elsa.Http.Handlers;
|
using Elsa.Http.Handlers;
|
||||||
|
using Elsa.Http.Middleware;
|
||||||
using Elsa.Http.Options;
|
using Elsa.Http.Options;
|
||||||
using Elsa.Http.Parsers;
|
using Elsa.Http.Parsers;
|
||||||
using Elsa.Http.PortResolvers;
|
using Elsa.Http.PortResolvers;
|
||||||
|
using Elsa.Http.Resilience;
|
||||||
using Elsa.Http.Selectors;
|
using Elsa.Http.Selectors;
|
||||||
using Elsa.Http.Services;
|
using Elsa.Http.Services;
|
||||||
using Elsa.Http.Tasks;
|
using Elsa.Http.Tasks;
|
||||||
using Elsa.Http.TriggerPayloadValidators;
|
using Elsa.Http.TriggerPayloadValidators;
|
||||||
using Elsa.Http.UIHints;
|
using Elsa.Http.UIHints;
|
||||||
|
using Elsa.Resilience.Extensions;
|
||||||
using Elsa.Workflows;
|
using Elsa.Workflows;
|
||||||
|
using Elsa.Workflows.Management.Extensions;
|
||||||
using FluentStorage;
|
using FluentStorage;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.AspNetCore.StaticFiles;
|
using Microsoft.AspNetCore.StaticFiles;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Elsa.Http.ShellFeatures;
|
namespace Elsa.Http.ShellFeatures;
|
||||||
|
|
@ -32,12 +40,12 @@ namespace Elsa.Http.ShellFeatures;
|
||||||
Description = "Provides HTTP-related activities and services for workflow execution",
|
Description = "Provides HTTP-related activities and services for workflow execution",
|
||||||
DependsOn = ["HttpJavaScript", "Resilience"])]
|
DependsOn = ["HttpJavaScript", "Resilience"])]
|
||||||
[UsedImplicitly]
|
[UsedImplicitly]
|
||||||
public class HttpFeature : IShellFeature
|
public class HttpFeature : IMiddlewareShellFeature
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A delegate to configure <see cref="HttpActivityOptions"/>.
|
/// The <see cref="HttpActivityOptions"/> to configure.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Action<HttpActivityOptions>? ConfigureHttpOptions { get; set; }
|
public HttpActivityOptions HttpActivityOptions { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A delegate to configure <see cref="HttpFileCacheOptions"/>.
|
/// A delegate to configure <see cref="HttpFileCacheOptions"/>.
|
||||||
|
|
@ -99,15 +107,34 @@ public class HttpFeature : IShellFeature
|
||||||
|
|
||||||
public void ConfigureServices(IServiceCollection services)
|
public void ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
var configureOptions = ConfigureHttpOptions ?? (options =>
|
// Register HTTP activities.
|
||||||
{
|
services.AddActivitiesFrom<HttpFeature>();
|
||||||
options.BasePath = "/workflows";
|
|
||||||
options.BaseUrl = new Uri("http://localhost");
|
// 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<HttpResilienceStrategy>();
|
||||||
|
|
||||||
var configureFileCacheOptions = ConfigureHttpFileCacheOptions ?? (options => { options.TimeToLive = TimeSpan.FromDays(7); });
|
var configureFileCacheOptions = ConfigureHttpFileCacheOptions ?? (options => { options.TimeToLive = TimeSpan.FromDays(7); });
|
||||||
|
|
||||||
services.Configure(configureOptions);
|
services.Configure<HttpActivityOptions>(options =>
|
||||||
|
{
|
||||||
|
options.BasePath = HttpActivityOptions.BasePath;
|
||||||
|
options.BaseUrl = HttpActivityOptions.BaseUrl;
|
||||||
|
options.AvailableContentTypes = HttpActivityOptions.AvailableContentTypes;
|
||||||
|
options.WriteHttpResponseSynchronously = HttpActivityOptions.WriteHttpResponseSynchronously;
|
||||||
|
});
|
||||||
|
|
||||||
services.Configure(configureFileCacheOptions);
|
services.Configure(configureFileCacheOptions);
|
||||||
|
|
||||||
var httpClientBuilder = services.AddHttpClient<SendHttpRequestBase>(HttpClient);
|
var httpClientBuilder = services.AddHttpClient<SendHttpRequestBase>(HttpClient);
|
||||||
|
|
@ -207,5 +234,10 @@ public class HttpFeature : IShellFeature
|
||||||
options.AddTypeAlias<Downloadable[]>("Downloadable[]");
|
options.AddTypeAlias<Downloadable[]>("Downloadable[]");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void UseMiddleware(IApplicationBuilder app, IHostEnvironment? environment)
|
||||||
|
{
|
||||||
|
app.UseWorkflows();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using AspNetCore.Authentication.ApiKey;
|
using AspNetCore.Authentication.ApiKey;
|
||||||
|
using CShells.Configuration;
|
||||||
using CShells.FastEndpoints.Features;
|
using CShells.FastEndpoints.Features;
|
||||||
using CShells.Features;
|
using CShells.Features;
|
||||||
using Elsa.Common.Multitenancy;
|
using Elsa.Common.Multitenancy;
|
||||||
|
|
@ -10,6 +11,7 @@ using Elsa.Identity.Options;
|
||||||
using Elsa.Identity.Providers;
|
using Elsa.Identity.Providers;
|
||||||
using Elsa.Identity.Services;
|
using Elsa.Identity.Services;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace Elsa.Identity.ShellFeatures;
|
namespace Elsa.Identity.ShellFeatures;
|
||||||
|
|
@ -26,8 +28,7 @@ public class IdentityFeature : IFastEndpointsShellFeature
|
||||||
{
|
{
|
||||||
public void ConfigureServices(IServiceCollection services)
|
public void ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
// Configure options - Note: SigningKey must be configured by the application for security
|
services.AddOptions<IdentityTokenOptions>().Configure<ShellConfiguration>((options, config) => config.GetSection("Identity").Bind(options));
|
||||||
services.AddOptions<IdentityTokenOptions>().BindConfiguration("Identity");
|
|
||||||
services.Configure<ApiKeyOptions>(ApiKeyDefaults.AuthenticationScheme, options =>
|
services.Configure<ApiKeyOptions>(ApiKeyDefaults.AuthenticationScheme, options =>
|
||||||
{
|
{
|
||||||
options.Realm = "Elsa Workflows";
|
options.Realm = "Elsa Workflows";
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ using Microsoft.Extensions.Options;
|
||||||
namespace Elsa.Persistence.EFCore;
|
namespace Elsa.Persistence.EFCore;
|
||||||
|
|
||||||
public abstract class PersistenceShellFeatureBase<TDbContext> : IShellFeature
|
public abstract class PersistenceShellFeatureBase<TDbContext> : IShellFeature
|
||||||
where TDbContext : ElsaDbContextBase
|
where TDbContext : DbContext
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether to use context pooling.
|
/// Gets or sets a value indicating whether to use context pooling.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
using Elsa.Resilience.Options;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
// ReSharper disable once CheckNamespace
|
||||||
|
namespace Elsa.Resilience.Extensions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="IServiceCollection"/> extension methods for registering resilience strategy types
|
||||||
|
/// via <see cref="ResilienceOptions"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static class ResilienceServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Registers a resilience strategy type with <see cref="ResilienceOptions"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddResilienceStrategy<T>(this IServiceCollection services) =>
|
||||||
|
services.Configure<ResilienceOptions>(options => options.StrategyTypes.Add(typeof(T)));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers multiple resilience strategy types with <see cref="ResilienceOptions"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddResilienceStrategies(this IServiceCollection services, IEnumerable<Type> types) =>
|
||||||
|
services.Configure<ResilienceOptions>(options =>
|
||||||
|
{
|
||||||
|
foreach (var type in types)
|
||||||
|
options.StrategyTypes.Add(type);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -14,7 +14,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||||
namespace Elsa.Resilience.ShellFeatures;
|
namespace Elsa.Resilience.ShellFeatures;
|
||||||
|
|
||||||
[ShellFeature]
|
[ShellFeature]
|
||||||
public class ResilienceShellFeature : IFastEndpointsShellFeature
|
public class ResilienceFeature : IFastEndpointsShellFeature
|
||||||
{
|
{
|
||||||
public void ConfigureServices(IServiceCollection services)
|
public void ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
|
|
@ -37,7 +37,7 @@ public class ResilienceShellFeature : IFastEndpointsShellFeature
|
||||||
.AddScoped<IRetryAttemptRecorder, ActivityExecutionContextRetryAttemptRecorder>()
|
.AddScoped<IRetryAttemptRecorder, ActivityExecutionContextRetryAttemptRecorder>()
|
||||||
.AddScoped<IRetryAttemptReader, ActivityExecutionContextRetryAttemptReader>()
|
.AddScoped<IRetryAttemptReader, ActivityExecutionContextRetryAttemptReader>()
|
||||||
.AddScoped<ActivityExecutionContextRetryAttemptReader>()
|
.AddScoped<ActivityExecutionContextRetryAttemptReader>()
|
||||||
.AddHandlersFrom<ResilienceShellFeature>();
|
.AddHandlersFrom<ResilienceFeature>();
|
||||||
|
|
||||||
// Register transient exception detection infrastructure
|
// Register transient exception detection infrastructure
|
||||||
services
|
services
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
using CShells.Features;
|
using CShells.Features;
|
||||||
using Elsa.Common.Multitenancy;
|
using Elsa.Common.Multitenancy;
|
||||||
using Elsa.Common.RecurringTasks;
|
using Elsa.Common.ShellFeatures;
|
||||||
using Elsa.Extensions;
|
using Elsa.Extensions;
|
||||||
using Elsa.Scheduling.Bookmarks;
|
using Elsa.Scheduling.Bookmarks;
|
||||||
using Elsa.Scheduling.Handlers;
|
using Elsa.Scheduling.Handlers;
|
||||||
using Elsa.Scheduling.HostedServices;
|
using Elsa.Scheduling.HostedServices;
|
||||||
using Elsa.Scheduling.Services;
|
using Elsa.Scheduling.Services;
|
||||||
using Elsa.Scheduling.TriggerPayloadValidators;
|
using Elsa.Scheduling.TriggerPayloadValidators;
|
||||||
|
using Elsa.Workflows.Management.Extensions;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
|
@ -18,7 +19,7 @@ namespace Elsa.Scheduling.ShellFeatures;
|
||||||
[ShellFeature(
|
[ShellFeature(
|
||||||
DisplayName = "Scheduling",
|
DisplayName = "Scheduling",
|
||||||
Description = "Provides scheduling capabilities for workflows including cron and delay-based triggers",
|
Description = "Provides scheduling capabilities for workflows including cron and delay-based triggers",
|
||||||
DependsOn = ["SystemClock"])]
|
DependsOn = [typeof(SystemClockFeature)])]
|
||||||
[UsedImplicitly]
|
[UsedImplicitly]
|
||||||
public class SchedulingFeature : IShellFeature
|
public class SchedulingFeature : IShellFeature
|
||||||
{
|
{
|
||||||
|
|
@ -47,9 +48,8 @@ public class SchedulingFeature : IShellFeature
|
||||||
.AddScoped(WorkflowScheduler)
|
.AddScoped(WorkflowScheduler)
|
||||||
.AddBackgroundTask<CreateSchedulesBackgroundTask>()
|
.AddBackgroundTask<CreateSchedulesBackgroundTask>()
|
||||||
.AddHandlersFrom<ScheduleWorkflows>()
|
.AddHandlersFrom<ScheduleWorkflows>()
|
||||||
|
.AddTriggerPayloadValidator<CronTriggerPayloadValidator, CronTriggerPayload>()
|
||||||
//Trigger payload validators.
|
.AddActivitiesFrom<SchedulingFeature>();
|
||||||
.AddTriggerPayloadValidator<CronTriggerPayloadValidator, CronTriggerPayload>();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,7 @@ public class WorkflowsFeature : IShellFeature
|
||||||
.AddScoped<IActivityResolver, SwitchActivityResolver>()
|
.AddScoped<IActivityResolver, SwitchActivityResolver>()
|
||||||
.AddSerializationOptionsConfigurator<AdditionalConvertersConfigurator>()
|
.AddSerializationOptionsConfigurator<AdditionalConvertersConfigurator>()
|
||||||
.AddSerializationOptionsConfigurator<CustomConstructorConfigurator>()
|
.AddSerializationOptionsConfigurator<CustomConstructorConfigurator>()
|
||||||
|
.AddSerializationOptionsConfigurator<FlowchartSerializationOptionConfigurator>()
|
||||||
|
|
||||||
// Domain event handlers.
|
// Domain event handlers.
|
||||||
.AddHandlersFrom<WorkflowsFeature>()
|
.AddHandlersFrom<WorkflowsFeature>()
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="IServiceCollection"/> extension methods for registering Elsa activity types
|
||||||
|
/// and variable descriptors via <see cref="ManagementOptions"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// These extensions are the shell-feature-compatible replacement for calling
|
||||||
|
/// <c>WorkflowManagementFeature.AddActivitiesFrom<T>()</c> in the old-style feature system.
|
||||||
|
/// Because <c>services.Configure<ManagementOptions></c> is additive, multiple features
|
||||||
|
/// can independently register activities without any coupling to each other.
|
||||||
|
/// </remarks>
|
||||||
|
public static class ManagementServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Activities
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers the supplied activity <paramref name="types"/> with <see cref="ManagementOptions"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddActivities(this IServiceCollection services, IEnumerable<Type> types) =>
|
||||||
|
services.Configure<ManagementOptions>(options =>
|
||||||
|
{
|
||||||
|
foreach (var type in types)
|
||||||
|
options.ActivityTypes.Add(type);
|
||||||
|
});
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers a single activity type <typeparamref name="TActivity"/> with <see cref="ManagementOptions"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddActivity<TActivity>(this IServiceCollection services)
|
||||||
|
where TActivity : IActivity =>
|
||||||
|
services.AddActivities([typeof(TActivity)]);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Scans <paramref name="assembly"/> and registers every concrete, non-generic
|
||||||
|
/// <see cref="IActivity"/> implementation found.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Scans the assembly that contains <typeparamref name="TMarker"/> and registers every
|
||||||
|
/// concrete, non-generic <see cref="IActivity"/> implementation found.
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddActivitiesFrom<TMarker>(this IServiceCollection services) =>
|
||||||
|
services.AddActivitiesFrom(typeof(TMarker).Assembly);
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Variable descriptors
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers the supplied <paramref name="descriptors"/> with <see cref="ManagementOptions"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddVariableDescriptors(
|
||||||
|
this IServiceCollection services,
|
||||||
|
IEnumerable<VariableDescriptor> descriptors) =>
|
||||||
|
services.Configure<ManagementOptions>(options =>
|
||||||
|
{
|
||||||
|
foreach (var descriptor in descriptors)
|
||||||
|
options.VariableDescriptors.Add(descriptor);
|
||||||
|
});
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers a single variable descriptor.
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddVariableDescriptor(
|
||||||
|
this IServiceCollection services,
|
||||||
|
VariableDescriptor descriptor) =>
|
||||||
|
services.AddVariableDescriptors([descriptor]);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers a variable descriptor for <typeparamref name="T"/> with the given
|
||||||
|
/// <paramref name="category"/> and optional <paramref name="description"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddVariableDescriptor<T>(
|
||||||
|
this IServiceCollection services,
|
||||||
|
string category,
|
||||||
|
string? description = null) =>
|
||||||
|
services.AddVariableDescriptor(new(typeof(T), category, description));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a variable type and its alias to the specified service collection.
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddVariableTypeAndAlias<T>(this IServiceCollection services, string alias, string category)
|
||||||
|
{
|
||||||
|
return services
|
||||||
|
.AddVariableDescriptor<T>(category)
|
||||||
|
.AddTypeAlias<T>(alias);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -87,10 +87,11 @@ public class CachingWorkflowDefinitionService(
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<WorkflowGraph?> FindWorkflowGraphAsync(WorkflowDefinitionFilter filter, CancellationToken cancellationToken = default)
|
public async Task<WorkflowGraph?> FindWorkflowGraphAsync(WorkflowDefinitionFilter filter, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var cacheKey = cacheManager.CreateWorkflowFilterCacheKey(filter);
|
// Resolve the definition first (already cached by FindWorkflowDefinitionAsync) so the
|
||||||
return await FindFromCacheAsync(cacheKey,
|
// WorkflowGraph ends up under the stable per-version-ID key rather than a filter-hash key
|
||||||
() => decoratedService.FindWorkflowGraphAsync(filter, cancellationToken),
|
// that can never be shared with the other FindWorkflowGraphAsync overloads.
|
||||||
x => x.Workflow.Identity.DefinitionId);
|
var definition = await FindWorkflowDefinitionAsync(filter, cancellationToken);
|
||||||
|
return await FindWorkflowGraphForDefinitionAsync(definition, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|
@ -100,12 +101,9 @@ public class CachingWorkflowDefinitionService(
|
||||||
var workflowGraphs = new List<WorkflowGraph>();
|
var workflowGraphs = new List<WorkflowGraph>();
|
||||||
foreach (var workflowDefinition in workflowDefinitions)
|
foreach (var workflowDefinition in workflowDefinitions)
|
||||||
{
|
{
|
||||||
var cacheKey = cacheManager.CreateWorkflowVersionCacheKey(workflowDefinition.Id);
|
var workflowGraph = await FindWorkflowGraphForDefinitionAsync(workflowDefinition, cancellationToken);
|
||||||
var workflowGraph = await GetFromCacheAsync(
|
if (workflowGraph != null)
|
||||||
cacheKey,
|
workflowGraphs.Add(workflowGraph);
|
||||||
async () => await MaterializeWorkflowAsync(workflowDefinition, cancellationToken),
|
|
||||||
wf => wf.Workflow.Identity.DefinitionId);
|
|
||||||
workflowGraphs.Add(workflowGraph);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return workflowGraphs;
|
return workflowGraphs;
|
||||||
|
|
@ -114,23 +112,17 @@ public class CachingWorkflowDefinitionService(
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<WorkflowGraphFindResult> TryFindWorkflowGraphAsync(string definitionId, VersionOptions versionOptions, CancellationToken cancellationToken = default)
|
public async Task<WorkflowGraphFindResult> TryFindWorkflowGraphAsync(string definitionId, VersionOptions versionOptions, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var cacheKey = cacheManager.CreateWorkflowVersionCacheKey(definitionId, versionOptions);
|
var definition = await FindWorkflowDefinitionAsync(definitionId, versionOptions, cancellationToken);
|
||||||
var result = await GetFromCacheAsync(cacheKey,
|
var workflowGraph = await FindWorkflowGraphForDefinitionAsync(definition, cancellationToken);
|
||||||
() => decoratedService.TryFindWorkflowGraphAsync(definitionId, versionOptions, cancellationToken),
|
return new(definition, workflowGraph);
|
||||||
x => x.WorkflowDefinition?.DefinitionId);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<WorkflowGraphFindResult> TryFindWorkflowGraphAsync(string definitionVersionId, CancellationToken cancellationToken = default)
|
public async Task<WorkflowGraphFindResult> TryFindWorkflowGraphAsync(string definitionVersionId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var cacheKey = cacheManager.CreateWorkflowVersionCacheKey(definitionVersionId);
|
var definition = await FindWorkflowDefinitionAsync(definitionVersionId, cancellationToken);
|
||||||
var result = await GetFromCacheAsync(cacheKey,
|
var workflowGraph = await FindWorkflowGraphForDefinitionAsync(definition, cancellationToken);
|
||||||
() => decoratedService.TryFindWorkflowGraphAsync(definitionVersionId, cancellationToken),
|
return new(definition, workflowGraph);
|
||||||
x => x.WorkflowDefinition?.DefinitionId);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|
@ -143,12 +135,9 @@ public class CachingWorkflowDefinitionService(
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<WorkflowGraphFindResult> TryFindWorkflowGraphAsync(WorkflowDefinitionFilter filter, CancellationToken cancellationToken = default)
|
public async Task<WorkflowGraphFindResult> TryFindWorkflowGraphAsync(WorkflowDefinitionFilter filter, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var cacheKey = cacheManager.CreateWorkflowFilterCacheKey(filter);
|
var definition = await FindWorkflowDefinitionAsync(filter, cancellationToken);
|
||||||
var result = await GetFromCacheAsync(cacheKey,
|
var workflowGraph = await FindWorkflowGraphForDefinitionAsync(definition, cancellationToken);
|
||||||
() => decoratedService.TryFindWorkflowGraphAsync(filter, cancellationToken),
|
return new(definition, workflowGraph);
|
||||||
x => x.WorkflowDefinition?.DefinitionId);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|
@ -158,50 +147,54 @@ public class CachingWorkflowDefinitionService(
|
||||||
var results = new List<WorkflowGraphFindResult>();
|
var results = new List<WorkflowGraphFindResult>();
|
||||||
foreach (var workflowDefinition in workflowDefinitions)
|
foreach (var workflowDefinition in workflowDefinitions)
|
||||||
{
|
{
|
||||||
if (!materializerRegistry.IsMaterializerAvailable(workflowDefinition.MaterializerName))
|
var workflowGraph = await FindWorkflowGraphForDefinitionAsync(workflowDefinition, cancellationToken);
|
||||||
{
|
results.Add(new WorkflowGraphFindResult(workflowDefinition, workflowGraph));
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<T?> FindFromCacheAsync<T>(string cacheKey, Func<Task<T?>> getObjectFunc, Func<T, string> getChangeTokenKeyFunc) where T : class
|
private async Task<T?> FindFromCacheAsync<T>(string cacheKey, Func<Task<T?>> getObjectFunc, Func<T, string> getChangeTokenKeyFunc) where T : class
|
||||||
{
|
|
||||||
return await GetFromCacheAsync(
|
|
||||||
cacheKey,
|
|
||||||
getObjectFunc,
|
|
||||||
obj => obj != null ? getChangeTokenKeyFunc(obj) : null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<T> GetFromCacheAsync<T>(string cacheKey, Func<Task<T>> getObjectFunc, Func<T, string?> getChangeTokenKeyFunc)
|
|
||||||
{
|
{
|
||||||
var cache = cacheManager.Cache;
|
var cache = cacheManager.Cache;
|
||||||
return await cache.GetOrCreateAsync(cacheKey, async entry =>
|
return await cache.FindOrCreateAsync(cacheKey, async entry =>
|
||||||
{
|
{
|
||||||
entry.SetAbsoluteExpiration(cache.CachingOptions.Value.CacheDuration);
|
entry.SetAbsoluteExpiration(cache.CachingOptions.Value.CacheDuration);
|
||||||
var obj = await getObjectFunc();
|
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));
|
entry.AddExpirationToken(cache.GetToken(changeTokenKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the <see cref="WorkflowGraph"/> for the given definition, using the per-version-ID cache entry
|
||||||
|
/// that is shared with <see cref="FindWorkflowGraphAsync(string,CancellationToken)"/>.
|
||||||
|
/// Returns <c>null</c> when the definition is <c>null</c> or its materializer is unavailable.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<WorkflowGraph?> 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -11,6 +11,7 @@ using Elsa.Workflows.Features;
|
||||||
using Elsa.Workflows.Management.Activities.WorkflowDefinitionActivity;
|
using Elsa.Workflows.Management.Activities.WorkflowDefinitionActivity;
|
||||||
using Elsa.Workflows.Management.Contracts;
|
using Elsa.Workflows.Management.Contracts;
|
||||||
using Elsa.Workflows.Management.Entities;
|
using Elsa.Workflows.Management.Entities;
|
||||||
|
using Elsa.Workflows.Management.Extensions;
|
||||||
using Elsa.Workflows.Management.Features;
|
using Elsa.Workflows.Management.Features;
|
||||||
using Elsa.Workflows.Management.Handlers.Notifications;
|
using Elsa.Workflows.Management.Handlers.Notifications;
|
||||||
using Elsa.Workflows.Management.Mappers;
|
using Elsa.Workflows.Management.Mappers;
|
||||||
|
|
@ -94,6 +95,7 @@ public class WorkflowManagementFeature : IShellFeature
|
||||||
.AddScoped<IWorkflowSerializer, WorkflowSerializer>()
|
.AddScoped<IWorkflowSerializer, WorkflowSerializer>()
|
||||||
.AddScoped<IWorkflowValidator, WorkflowValidator>()
|
.AddScoped<IWorkflowValidator, WorkflowValidator>()
|
||||||
.AddScoped<IWorkflowReferenceQuery, DefaultWorkflowReferenceQuery>()
|
.AddScoped<IWorkflowReferenceQuery, DefaultWorkflowReferenceQuery>()
|
||||||
|
.AddScoped<IWorkflowReferenceGraphBuilder, WorkflowReferenceGraphBuilder>()
|
||||||
.AddScoped<IWorkflowDefinitionPublisher, WorkflowDefinitionPublisher>()
|
.AddScoped<IWorkflowDefinitionPublisher, WorkflowDefinitionPublisher>()
|
||||||
.AddScoped<IWorkflowDefinitionImporter, WorkflowDefinitionImporter>()
|
.AddScoped<IWorkflowDefinitionImporter, WorkflowDefinitionImporter>()
|
||||||
.AddScoped<IWorkflowDefinitionManager, WorkflowDefinitionManager>()
|
.AddScoped<IWorkflowDefinitionManager, WorkflowDefinitionManager>()
|
||||||
|
|
@ -118,42 +120,14 @@ public class WorkflowManagementFeature : IShellFeature
|
||||||
.AddNotificationHandler<DeleteWorkflowInstances>()
|
.AddNotificationHandler<DeleteWorkflowInstances>()
|
||||||
.AddNotificationHandler<RefreshActivityRegistry>()
|
.AddNotificationHandler<RefreshActivityRegistry>()
|
||||||
.AddNotificationHandler<UpdateConsumingWorkflows>()
|
.AddNotificationHandler<UpdateConsumingWorkflows>()
|
||||||
.AddNotificationHandler<ValidateWorkflow>()
|
.AddNotificationHandler<ValidateWorkflow>();
|
||||||
;
|
|
||||||
|
|
||||||
AddActivitiesFrom<WorkflowsFeature>();
|
// Register built-in activities from the Workflows and WorkflowManagement assemblies.
|
||||||
AddActivitiesFrom<WorkflowManagementFeature>();
|
services
|
||||||
|
.AddActivitiesFrom<WorkflowsFeature>()
|
||||||
|
.AddActivitiesFrom<WorkflowManagementFeature>();
|
||||||
|
|
||||||
services.Configure<ManagementOptions>(options =>
|
// Register the default variable descriptors declared on this feature.
|
||||||
{
|
services.AddVariableDescriptors(VariableDescriptors);
|
||||||
foreach (var activityType in ActivityTypes.Distinct())
|
|
||||||
options.ActivityTypes.Add(activityType);
|
|
||||||
|
|
||||||
foreach (var descriptor in VariableDescriptors.DistinctBy(x => x.Type))
|
|
||||||
options.VariableDescriptors.Add(descriptor);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A set of activity types to make available to the system.
|
|
||||||
/// </summary>
|
|
||||||
private HashSet<Type> ActivityTypes { get; } = [];
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adds the specified activity types to the system.
|
|
||||||
/// </summary>
|
|
||||||
private WorkflowManagementFeature AddActivities(IEnumerable<Type> activityTypes)
|
|
||||||
{
|
|
||||||
ActivityTypes.AddRange(activityTypes);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
private WorkflowManagementFeature AddActivitiesFrom<TMarker>()
|
|
||||||
{
|
|
||||||
var activityTypes = typeof(TMarker).Assembly.GetExportedTypes()
|
|
||||||
.Where(x => typeof(IActivity).IsAssignableFrom(x) && x is { IsAbstract: false, IsInterface: false, IsGenericType: false })
|
|
||||||
.ToList();
|
|
||||||
return AddActivities(activityTypes);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -15,6 +15,6 @@ public class ElsaFeature : IShellFeature
|
||||||
public void ConfigureServices(IServiceCollection services)
|
public void ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
// Register the shell-based feature provider to bridge shell features to the Elsa feature API
|
// Register the shell-based feature provider to bridge shell features to the Elsa feature API
|
||||||
services.AddSingleton<Elsa.Features.Contracts.IInstalledFeatureProvider, Elsa.Features.Services.ShellInstalledFeatureProvider>();
|
services.AddSingleton<Features.Contracts.IInstalledFeatureProvider, Features.Services.ShellInstalledFeatureProvider>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -70,8 +70,10 @@ public class AutoUpdateTests : AppComponentTest
|
||||||
{
|
{
|
||||||
Id = ParentDefinitionVersionId
|
Id = ParentDefinitionVersionId
|
||||||
};
|
};
|
||||||
var parentVersionCacheKey = _definitionCacheManager.CreateWorkflowFilterCacheKey(parentWorkflowDefinitionFilter);
|
var parentDefinitionCacheKey = _definitionCacheManager.CreateWorkflowDefinitionFilterCacheKey(parentWorkflowDefinitionFilter);
|
||||||
Assert.True(_cache.TryGetValue(parentVersionCacheKey, out _));
|
var parentGraphCacheKey = _definitionCacheManager.CreateWorkflowVersionCacheKey(ParentDefinitionVersionId);
|
||||||
|
Assert.True(_cache.TryGetValue(parentDefinitionCacheKey, out _));
|
||||||
|
Assert.True(_cache.TryGetValue(parentGraphCacheKey, out _));
|
||||||
|
|
||||||
// Set change tokens.
|
// Set change tokens.
|
||||||
_httpChangeToken = _workflowCacheManager.CreateWorkflowDefinitionChangeTokenKey(ParentDefinitionId);
|
_httpChangeToken = _workflowCacheManager.CreateWorkflowDefinitionChangeTokenKey(ParentDefinitionId);
|
||||||
|
|
@ -88,7 +90,8 @@ public class AutoUpdateTests : AppComponentTest
|
||||||
|
|
||||||
Assert.False(_cache.TryGetValue($"http-workflow:{hash}", out _));
|
Assert.False(_cache.TryGetValue($"http-workflow:{hash}", out _));
|
||||||
Assert.False(_cache.TryGetValue($"IEnumerable`1:{hashedFilter}", 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)
|
private void OnChangeTokenSignalTriggered(object? sender, TriggerChangeTokenSignalEventArgs args)
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,10 @@ namespace Elsa.Workflows.Management.UnitTests.Services;
|
||||||
|
|
||||||
public class CachingWorkflowDefinitionServiceTests
|
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<IWorkflowDefinitionService>();
|
private readonly IWorkflowDefinitionService _decoratedService = Substitute.For<IWorkflowDefinitionService>();
|
||||||
private readonly IWorkflowDefinitionCacheManager _cacheManager = Substitute.For<IWorkflowDefinitionCacheManager>();
|
private readonly IWorkflowDefinitionCacheManager _cacheManager = Substitute.For<IWorkflowDefinitionCacheManager>();
|
||||||
private readonly IWorkflowDefinitionStore _workflowDefinitionStore = Substitute.For<IWorkflowDefinitionStore>();
|
private readonly IWorkflowDefinitionStore _workflowDefinitionStore = Substitute.For<IWorkflowDefinitionStore>();
|
||||||
|
|
@ -30,11 +34,9 @@ public class CachingWorkflowDefinitionServiceTests
|
||||||
public async Task MaterializeWorkflowAsync_DelegatesToDecoratedService()
|
public async Task MaterializeWorkflowAsync_DelegatesToDecoratedService()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var definition = TestHelpers.CreateWorkflowDefinition("def-1", "materializer");
|
var definition = TestHelpers.CreateWorkflowDefinition(DefaultDefinitionId, DefaultMaterializerName);
|
||||||
var (_, workflowGraph) = CreateWorkflowAndGraph("def-1");
|
var (_, workflowGraph) = CreateWorkflowAndGraph(DefaultDefinitionId);
|
||||||
|
|
||||||
_decoratedService.MaterializeWorkflowAsync(definition, Arg.Any<CancellationToken>()).Returns(workflowGraph);
|
_decoratedService.MaterializeWorkflowAsync(definition, Arg.Any<CancellationToken>()).Returns(workflowGraph);
|
||||||
|
|
||||||
var service = CreateService();
|
var service = CreateService();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -49,42 +51,35 @@ public class CachingWorkflowDefinitionServiceTests
|
||||||
public async Task FindWorkflowDefinitionAsync_ByDefinitionIdAndVersionOptions_CreatesCacheKey()
|
public async Task FindWorkflowDefinitionAsync_ByDefinitionIdAndVersionOptions_CreatesCacheKey()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var definition = TestHelpers.CreateWorkflowDefinition("def-1", "materializer");
|
var definition = TestHelpers.CreateWorkflowDefinition(DefaultDefinitionId, DefaultMaterializerName);
|
||||||
var cacheKey = "cache-key-1";
|
_cacheManager.CreateWorkflowDefinitionVersionCacheKey(DefaultDefinitionId, VersionOptions.Published).Returns("cache-key");
|
||||||
|
_decoratedService.FindWorkflowDefinitionAsync(DefaultDefinitionId, VersionOptions.Published, Arg.Any<CancellationToken>()).Returns(definition);
|
||||||
_cacheManager.CreateWorkflowDefinitionVersionCacheKey("def-1", VersionOptions.Published).Returns(cacheKey);
|
|
||||||
_decoratedService.FindWorkflowDefinitionAsync("def-1", VersionOptions.Published, Arg.Any<CancellationToken>())
|
|
||||||
.Returns(definition);
|
|
||||||
|
|
||||||
var service = CreateService();
|
var service = CreateService();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await service.FindWorkflowDefinitionAsync("def-1", VersionOptions.Published);
|
var result = await service.FindWorkflowDefinitionAsync(DefaultDefinitionId, VersionOptions.Published);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Same(definition, result);
|
Assert.Same(definition, result);
|
||||||
_cacheManager.Received(1).CreateWorkflowDefinitionVersionCacheKey("def-1", VersionOptions.Published);
|
_cacheManager.Received(1).CreateWorkflowDefinitionVersionCacheKey(DefaultDefinitionId, VersionOptions.Published);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task FindWorkflowDefinitionAsync_ByDefinitionVersionId_CreatesCacheKey()
|
public async Task FindWorkflowDefinitionAsync_ByDefinitionVersionId_CreatesCacheKey()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var definition = TestHelpers.CreateWorkflowDefinition("def-1", "materializer");
|
var definition = TestHelpers.CreateWorkflowDefinition(DefaultDefinitionId, DefaultMaterializerName);
|
||||||
definition.Id = "version-id-1";
|
definition.Id = DefaultVersionId;
|
||||||
var cacheKey = "cache-key-version-1";
|
_cacheManager.CreateWorkflowDefinitionVersionCacheKey(DefaultVersionId).Returns("cache-key");
|
||||||
|
_decoratedService.FindWorkflowDefinitionAsync(DefaultVersionId, Arg.Any<CancellationToken>()).Returns(definition);
|
||||||
_cacheManager.CreateWorkflowDefinitionVersionCacheKey("version-id-1").Returns(cacheKey);
|
|
||||||
_decoratedService.FindWorkflowDefinitionAsync("version-id-1", Arg.Any<CancellationToken>()).Returns(definition);
|
|
||||||
|
|
||||||
var service = CreateService();
|
var service = CreateService();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await service.FindWorkflowDefinitionAsync("version-id-1");
|
var result = await service.FindWorkflowDefinitionAsync(DefaultVersionId);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Same(definition, result);
|
Assert.Same(definition, result);
|
||||||
_cacheManager.Received(1).CreateWorkflowDefinitionVersionCacheKey("version-id-1");
|
_cacheManager.Received(1).CreateWorkflowDefinitionVersionCacheKey(DefaultVersionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -172,17 +167,18 @@ public class CachingWorkflowDefinitionServiceTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task FindWorkflowGraphAsync_ByHandle_ConvertsTFilterAndDelegates()
|
public async Task FindWorkflowGraphAsync_ByHandle_ConvertsTFilterAndResolvesDefinitionAndGraph()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (_, workflowGraph) = CreateWorkflowAndGraph("def-1");
|
var definition = TestHelpers.CreateWorkflowDefinition(DefaultDefinitionId, DefaultMaterializerName);
|
||||||
var handle = WorkflowDefinitionHandle.ByDefinitionId("def-1", VersionOptions.Latest);
|
definition.Id = DefaultVersionId;
|
||||||
var cacheKey = "cache-key-filter";
|
var (_, workflowGraph) = CreateWorkflowAndGraph(DefaultDefinitionId);
|
||||||
|
var handle = WorkflowDefinitionHandle.ByDefinitionId(DefaultDefinitionId, VersionOptions.Latest);
|
||||||
_cacheManager.CreateWorkflowFilterCacheKey(Arg.Any<WorkflowDefinitionFilter>()).Returns(cacheKey);
|
|
||||||
_decoratedService.FindWorkflowGraphAsync(Arg.Any<WorkflowDefinitionFilter>(), Arg.Any<CancellationToken>())
|
|
||||||
.Returns(workflowGraph);
|
|
||||||
|
|
||||||
|
_cacheManager.CreateWorkflowDefinitionFilterCacheKey(Arg.Any<WorkflowDefinitionFilter>()).Returns("cache-key-def");
|
||||||
|
_cacheManager.CreateWorkflowVersionCacheKey(DefaultVersionId).Returns("cache-key-graph");
|
||||||
|
_decoratedService.FindWorkflowDefinitionAsync(Arg.Any<WorkflowDefinitionFilter>(), Arg.Any<CancellationToken>()).Returns(definition);
|
||||||
|
SetupDefinitionAndGraphMaterialization(definition, workflowGraph);
|
||||||
var service = CreateService();
|
var service = CreateService();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -190,20 +186,22 @@ public class CachingWorkflowDefinitionServiceTests
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Same(workflowGraph, result);
|
Assert.Same(workflowGraph, result);
|
||||||
await _decoratedService.Received(1).FindWorkflowGraphAsync(Arg.Any<WorkflowDefinitionFilter>(), Arg.Any<CancellationToken>());
|
await _decoratedService.Received(1).FindWorkflowDefinitionAsync(Arg.Any<WorkflowDefinitionFilter>(), Arg.Any<CancellationToken>());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task FindWorkflowGraphAsync_ByFilter_CreatesCacheKey()
|
public async Task FindWorkflowGraphAsync_ByFilter_ResolvesDefinitionFirstAndCachesGraph()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (_, workflowGraph) = CreateWorkflowAndGraph("def-1");
|
var definition = TestHelpers.CreateWorkflowDefinition(DefaultDefinitionId, DefaultMaterializerName);
|
||||||
var filter = new WorkflowDefinitionFilter { DefinitionId = "def-1" };
|
definition.Id = DefaultVersionId;
|
||||||
var cacheKey = "cache-key-filter";
|
var (_, workflowGraph) = CreateWorkflowAndGraph(DefaultDefinitionId);
|
||||||
|
var filter = new WorkflowDefinitionFilter { DefinitionId = DefaultDefinitionId };
|
||||||
_cacheManager.CreateWorkflowFilterCacheKey(filter).Returns(cacheKey);
|
|
||||||
_decoratedService.FindWorkflowGraphAsync(filter, Arg.Any<CancellationToken>()).Returns(workflowGraph);
|
|
||||||
|
|
||||||
|
_cacheManager.CreateWorkflowDefinitionFilterCacheKey(filter).Returns("cache-key-def");
|
||||||
|
_cacheManager.CreateWorkflowVersionCacheKey(DefaultVersionId).Returns("cache-key-graph");
|
||||||
|
_decoratedService.FindWorkflowDefinitionAsync(filter, Arg.Any<CancellationToken>()).Returns(definition);
|
||||||
|
SetupDefinitionAndGraphMaterialization(definition, workflowGraph);
|
||||||
var service = CreateService();
|
var service = CreateService();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -211,31 +209,50 @@ public class CachingWorkflowDefinitionServiceTests
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Same(workflowGraph, result);
|
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<CancellationToken>()).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<WorkflowDefinition>(), Arg.Any<CancellationToken>());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task FindWorkflowGraphsAsync_WithMultipleDefinitions_CachesEachGraph()
|
public async Task FindWorkflowGraphsAsync_WithMultipleDefinitions_CachesEachGraph()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var definition1 = TestHelpers.CreateWorkflowDefinition("def-1", "materializer");
|
var definition1 = TestHelpers.CreateWorkflowDefinition("def-1", DefaultMaterializerName);
|
||||||
definition1.Id = "id-1";
|
definition1.Id = "id-1";
|
||||||
var definition2 = TestHelpers.CreateWorkflowDefinition("def-2", "materializer");
|
var definition2 = TestHelpers.CreateWorkflowDefinition("def-2", DefaultMaterializerName);
|
||||||
definition2.Id = "id-2";
|
definition2.Id = "id-2";
|
||||||
var definitions = new[] { definition1, definition2 };
|
|
||||||
|
|
||||||
var (_, graph1) = CreateWorkflowAndGraph("def-1");
|
var (_, graph1) = CreateWorkflowAndGraph("def-1");
|
||||||
var (_, graph2) = CreateWorkflowAndGraph("def-2");
|
var (_, graph2) = CreateWorkflowAndGraph("def-2");
|
||||||
|
|
||||||
var filter = new WorkflowDefinitionFilter();
|
var filter = new WorkflowDefinitionFilter();
|
||||||
_workflowDefinitionStore.FindManyAsync(filter, Arg.Any<CancellationToken>()).Returns(definitions);
|
|
||||||
|
|
||||||
|
_workflowDefinitionStore.FindManyAsync(filter, Arg.Any<CancellationToken>()).Returns(new[] { definition1, definition2 });
|
||||||
_cacheManager.CreateWorkflowVersionCacheKey("id-1").Returns("cache-key-1");
|
_cacheManager.CreateWorkflowVersionCacheKey("id-1").Returns("cache-key-1");
|
||||||
_cacheManager.CreateWorkflowVersionCacheKey("id-2").Returns("cache-key-2");
|
_cacheManager.CreateWorkflowVersionCacheKey("id-2").Returns("cache-key-2");
|
||||||
|
SetupDefinitionAndGraphMaterialization(definition1, graph1);
|
||||||
_decoratedService.MaterializeWorkflowAsync(definition1, Arg.Any<CancellationToken>()).Returns(graph1);
|
SetupDefinitionAndGraphMaterialization(definition2, graph2);
|
||||||
_decoratedService.MaterializeWorkflowAsync(definition2, Arg.Any<CancellationToken>()).Returns(graph2);
|
|
||||||
|
|
||||||
var service = CreateService();
|
var service = CreateService();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -251,106 +268,146 @@ public class CachingWorkflowDefinitionServiceTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task TryFindWorkflowGraphAsync_ByDefinitionIdAndVersionOptions_CreatesCacheKey()
|
public async Task FindWorkflowGraphsAsync_SkipsDefinitionsWithUnavailableMaterializer()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var findResult = CreateWorkflowGraphFindResult("def-1");
|
var definition1 = TestHelpers.CreateWorkflowDefinition("def-1", "available-materializer");
|
||||||
var cacheKey = "cache-key-try";
|
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);
|
var (_, graph1) = CreateWorkflowAndGraph("def-1");
|
||||||
_decoratedService.TryFindWorkflowGraphAsync("def-1", VersionOptions.Published, Arg.Any<CancellationToken>())
|
|
||||||
.Returns(findResult);
|
var filter = new WorkflowDefinitionFilter();
|
||||||
|
_workflowDefinitionStore.FindManyAsync(filter, Arg.Any<CancellationToken>()).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<CancellationToken>()).Returns(graph1);
|
||||||
|
|
||||||
var service = CreateService();
|
var service = CreateService();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await service.TryFindWorkflowGraphAsync("def-1", VersionOptions.Published);
|
var result = await service.FindWorkflowGraphsAsync(filter);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Same(findResult, result);
|
var graphs = result.ToList();
|
||||||
_cacheManager.Received(1).CreateWorkflowVersionCacheKey("def-1", VersionOptions.Published);
|
Assert.Single(graphs);
|
||||||
|
Assert.Same(graph1, graphs[0]);
|
||||||
|
await _decoratedService.DidNotReceive().MaterializeWorkflowAsync(definition2, Arg.Any<CancellationToken>());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task TryFindWorkflowGraphAsync_ByDefinitionVersionId_CreatesCacheKey()
|
public async Task TryFindWorkflowGraphAsync_ByDefinitionIdAndVersionOptions_ResolvesDefinitionAndGraph()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var findResult = CreateWorkflowGraphFindResult("def-1");
|
var definition = TestHelpers.CreateWorkflowDefinition(DefaultDefinitionId, DefaultMaterializerName);
|
||||||
var cacheKey = "cache-key-try-version";
|
definition.Id = DefaultVersionId;
|
||||||
|
var (_, workflowGraph) = CreateWorkflowAndGraph(DefaultDefinitionId);
|
||||||
_cacheManager.CreateWorkflowVersionCacheKey("version-id-1").Returns(cacheKey);
|
|
||||||
_decoratedService.TryFindWorkflowGraphAsync("version-id-1", Arg.Any<CancellationToken>()).Returns(findResult);
|
|
||||||
|
|
||||||
|
_cacheManager.CreateWorkflowDefinitionVersionCacheKey(DefaultDefinitionId, VersionOptions.Published).Returns("cache-key-def");
|
||||||
|
_cacheManager.CreateWorkflowVersionCacheKey(DefaultVersionId).Returns("cache-key-graph");
|
||||||
|
_decoratedService.FindWorkflowDefinitionAsync(DefaultDefinitionId, VersionOptions.Published, Arg.Any<CancellationToken>()).Returns(definition);
|
||||||
|
SetupDefinitionAndGraphMaterialization(definition, workflowGraph);
|
||||||
var service = CreateService();
|
var service = CreateService();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await service.TryFindWorkflowGraphAsync("version-id-1");
|
var result = await service.TryFindWorkflowGraphAsync(DefaultDefinitionId, VersionOptions.Published);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Same(findResult, result);
|
Assert.Same(definition, result.WorkflowDefinition);
|
||||||
_cacheManager.Received(1).CreateWorkflowVersionCacheKey("version-id-1");
|
Assert.Same(workflowGraph, result.WorkflowGraph);
|
||||||
|
_cacheManager.Received(1).CreateWorkflowDefinitionVersionCacheKey(DefaultDefinitionId, VersionOptions.Published);
|
||||||
|
_cacheManager.Received(1).CreateWorkflowVersionCacheKey(DefaultVersionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task TryFindWorkflowGraphAsync_ByHandle_ConvertsTFilterAndDelegates()
|
public async Task TryFindWorkflowGraphAsync_ByDefinitionVersionId_ResolvesDefinitionAndGraph()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var findResult = CreateWorkflowGraphFindResult("def-1");
|
var definition = TestHelpers.CreateWorkflowDefinition(DefaultDefinitionId, DefaultMaterializerName);
|
||||||
var handle = WorkflowDefinitionHandle.ByDefinitionId("def-1", VersionOptions.Latest);
|
definition.Id = DefaultVersionId;
|
||||||
var cacheKey = "cache-key-filter";
|
var (_, workflowGraph) = CreateWorkflowAndGraph(DefaultDefinitionId);
|
||||||
|
|
||||||
_cacheManager.CreateWorkflowFilterCacheKey(Arg.Any<WorkflowDefinitionFilter>()).Returns(cacheKey);
|
_cacheManager.CreateWorkflowDefinitionVersionCacheKey(DefaultVersionId).Returns("cache-key-def");
|
||||||
_decoratedService.TryFindWorkflowGraphAsync(Arg.Any<WorkflowDefinitionFilter>(), Arg.Any<CancellationToken>())
|
_cacheManager.CreateWorkflowVersionCacheKey(DefaultVersionId).Returns("cache-key-graph");
|
||||||
.Returns(findResult);
|
_decoratedService.FindWorkflowDefinitionAsync(DefaultVersionId, Arg.Any<CancellationToken>()).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<WorkflowDefinitionFilter>()).Returns("cache-key-def");
|
||||||
|
_cacheManager.CreateWorkflowVersionCacheKey(DefaultVersionId).Returns("cache-key-graph");
|
||||||
|
_decoratedService.FindWorkflowDefinitionAsync(Arg.Any<WorkflowDefinitionFilter>(), Arg.Any<CancellationToken>()).Returns(definition);
|
||||||
|
SetupDefinitionAndGraphMaterialization(definition, workflowGraph);
|
||||||
var service = CreateService();
|
var service = CreateService();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await service.TryFindWorkflowGraphAsync(handle);
|
var result = await service.TryFindWorkflowGraphAsync(handle);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Same(findResult, result);
|
Assert.Same(definition, result.WorkflowDefinition);
|
||||||
await _decoratedService.Received(1).TryFindWorkflowGraphAsync(Arg.Any<WorkflowDefinitionFilter>(), Arg.Any<CancellationToken>());
|
Assert.Same(workflowGraph, result.WorkflowGraph);
|
||||||
|
await _decoratedService.Received(1).FindWorkflowDefinitionAsync(Arg.Any<WorkflowDefinitionFilter>(), Arg.Any<CancellationToken>());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task TryFindWorkflowGraphAsync_ByFilter_CreatesCacheKey()
|
public async Task TryFindWorkflowGraphAsync_ByFilter_ResolvesDefinitionAndGraph()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var findResult = CreateWorkflowGraphFindResult("def-1");
|
var definition = TestHelpers.CreateWorkflowDefinition(DefaultDefinitionId, DefaultMaterializerName);
|
||||||
var filter = new WorkflowDefinitionFilter { DefinitionId = "def-1" };
|
definition.Id = DefaultVersionId;
|
||||||
var cacheKey = "cache-key-filter";
|
var (_, workflowGraph) = CreateWorkflowAndGraph(DefaultDefinitionId);
|
||||||
|
var filter = new WorkflowDefinitionFilter { DefinitionId = DefaultDefinitionId };
|
||||||
_cacheManager.CreateWorkflowFilterCacheKey(filter).Returns(cacheKey);
|
|
||||||
_decoratedService.TryFindWorkflowGraphAsync(filter, Arg.Any<CancellationToken>()).Returns(findResult);
|
|
||||||
|
|
||||||
|
_cacheManager.CreateWorkflowDefinitionFilterCacheKey(filter).Returns("cache-key-def");
|
||||||
|
_cacheManager.CreateWorkflowVersionCacheKey(DefaultVersionId).Returns("cache-key-graph");
|
||||||
|
_decoratedService.FindWorkflowDefinitionAsync(filter, Arg.Any<CancellationToken>()).Returns(definition);
|
||||||
|
SetupDefinitionAndGraphMaterialization(definition, workflowGraph);
|
||||||
var service = CreateService();
|
var service = CreateService();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await service.TryFindWorkflowGraphAsync(filter);
|
var result = await service.TryFindWorkflowGraphAsync(filter);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Same(findResult, result);
|
Assert.Same(definition, result.WorkflowDefinition);
|
||||||
_cacheManager.Received(1).CreateWorkflowFilterCacheKey(filter);
|
Assert.Same(workflowGraph, result.WorkflowGraph);
|
||||||
|
_cacheManager.Received(1).CreateWorkflowDefinitionFilterCacheKey(filter);
|
||||||
|
_cacheManager.Received(1).CreateWorkflowVersionCacheKey(DefaultVersionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task TryFindWorkflowGraphsAsync_WithAvailableMaterializer_CachesGraph()
|
public async Task TryFindWorkflowGraphsAsync_WithAvailableMaterializer_CachesGraph()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var definition = TestHelpers.CreateWorkflowDefinition("def-1", "materializer");
|
var definition = TestHelpers.CreateWorkflowDefinition(DefaultDefinitionId, DefaultMaterializerName);
|
||||||
definition.Id = "id-1";
|
definition.Id = "id-1";
|
||||||
var definitions = new[] { definition };
|
var (_, graph) = CreateWorkflowAndGraph(DefaultDefinitionId);
|
||||||
|
|
||||||
var (_, graph) = CreateWorkflowAndGraph("def-1");
|
|
||||||
|
|
||||||
var filter = new WorkflowDefinitionFilter();
|
var filter = new WorkflowDefinitionFilter();
|
||||||
_workflowDefinitionStore.FindManyAsync(filter, Arg.Any<CancellationToken>()).Returns(definitions);
|
|
||||||
|
|
||||||
_cacheManager.CreateWorkflowVersionCacheKey("id-1").Returns("cache-key-1");
|
|
||||||
_materializerRegistry.IsMaterializerAvailable("materializer").Returns(true);
|
|
||||||
_decoratedService.MaterializeWorkflowAsync(definition, Arg.Any<CancellationToken>()).Returns(graph);
|
|
||||||
|
|
||||||
|
_workflowDefinitionStore.FindManyAsync(filter, Arg.Any<CancellationToken>()).Returns(new[] { definition });
|
||||||
|
_cacheManager.CreateWorkflowVersionCacheKey("id-1").Returns("cache-key");
|
||||||
|
SetupDefinitionAndGraphMaterialization(definition, graph);
|
||||||
var service = CreateService();
|
var service = CreateService();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -367,16 +424,14 @@ public class CachingWorkflowDefinitionServiceTests
|
||||||
public async Task TryFindWorkflowGraphsAsync_WithUnavailableMaterializer_ReturnsNullGraph()
|
public async Task TryFindWorkflowGraphsAsync_WithUnavailableMaterializer_ReturnsNullGraph()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var definition = TestHelpers.CreateWorkflowDefinition("def-1", "unavailable-materializer");
|
const string unavailableMaterializer = "unavailable-materializer";
|
||||||
|
var definition = TestHelpers.CreateWorkflowDefinition(DefaultDefinitionId, unavailableMaterializer);
|
||||||
definition.Id = "id-1";
|
definition.Id = "id-1";
|
||||||
var definitions = new[] { definition };
|
|
||||||
|
|
||||||
var filter = new WorkflowDefinitionFilter();
|
var filter = new WorkflowDefinitionFilter();
|
||||||
_workflowDefinitionStore.FindManyAsync(filter, Arg.Any<CancellationToken>()).Returns(definitions);
|
|
||||||
|
|
||||||
_cacheManager.CreateWorkflowVersionCacheKey("id-1").Returns("cache-key-1");
|
|
||||||
_materializerRegistry.IsMaterializerAvailable("unavailable-materializer").Returns(false);
|
|
||||||
|
|
||||||
|
_workflowDefinitionStore.FindManyAsync(filter, Arg.Any<CancellationToken>()).Returns(new[] { definition });
|
||||||
|
_cacheManager.CreateWorkflowVersionCacheKey("id-1").Returns("cache-key");
|
||||||
|
_materializerRegistry.IsMaterializerAvailable(unavailableMaterializer).Returns(false);
|
||||||
var service = CreateService();
|
var service = CreateService();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -476,12 +531,16 @@ public class CachingWorkflowDefinitionServiceTests
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
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);
|
materializerName ??= definition.MaterializerName;
|
||||||
var (_, workflowGraph) = CreateWorkflowAndGraph(definitionId);
|
_materializerRegistry.IsMaterializerAvailable(materializerName).Returns(true);
|
||||||
return new(definition, workflowGraph);
|
_decoratedService.MaterializeWorkflowAsync(definition, Arg.Any<CancellationToken>()).Returns(workflowGraph);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue