Implement Dynamic Expression Evaluation for Log Persistence Mode (#6159)

* Add log persistence configuration and strategy evaluation

Introduced `LogPersistenceConfiguration` to support dynamic strategy and expression-based log persistence evaluations. Updated `Default default values.

* Add LogPersistenceConfiguration and enum support

Introduced LogPersistenceConfiguration class and LogPersistenceEvaluationMode enum in both Elsa.Api.Client and Elsa.Workflows.Runtime. Updated JavaScript services to handle enums correctly and register LogPersistenceMode.

* Rename log persistence key for consistency

Updated the log persistence key from `LogPersistenceStrategyKey` to `LogPersistenceConfigKey` to maintain consistency across the codebase. This change ensures that property access aligns with the updated naming conventions used in the application's configuration.

* Reuse JSON serializer options

* Update log persistence config structure in comments

This commit revises the JSON example in code comments to reflect the updated structure of the log persistence configuration. The changes include updated evaluation modes and strategy types for default, inputs, and outputs sections. These modifications aim to enhance clarity and provide accurate documentation of the expected configuration format.
This commit is contained in:
Sipke Schoorstra 2024-11-28 12:19:38 +01:00 committed by GitHub
parent f8fb7151fc
commit 08632471e3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 176 additions and 37 deletions

View file

@ -0,0 +1,7 @@
namespace Elsa.Api.Client.Shared.Enums;
public enum LogPersistenceEvaluationMode
{
Strategy,
Expression
}

View file

@ -0,0 +1,11 @@
using Elsa.Api.Client.Resources.Scripting.Models;
using Elsa.Api.Client.Shared.Enums;
namespace Elsa.Api.Client.Shared.Models;
public class LogPersistenceConfiguration
{
public LogPersistenceEvaluationMode EvaluationMode { get; set; }
public string? StrategyType { get; set; }
public Expression? Expression { get; set; }
}

View file

@ -1,6 +1,7 @@
using Elsa.Extensions;
using Elsa.JavaScript.Notifications;
using Elsa.Mediator.Contracts;
using Elsa.Workflows.LogPersistence;
using JetBrains.Annotations;
namespace Elsa.JavaScript.Handlers;
@ -22,6 +23,7 @@ public class ConfigureEngineWithCommonTypes : INotificationHandler<EvaluatingJav
engine.RegisterType<TimeSpan>();
engine.RegisterType<Guid>();
engine.RegisterType<Random>();
engine.RegisterType<LogPersistenceMode>();
return Task.CompletedTask;
}

View file

@ -25,7 +25,7 @@ internal class VariableTypeDefinitionProvider(ITypeDescriber typeDescriber) : Ty
var variableTypeQuery =
from variable in variables
let variableType = variable.GetVariableType()
where (variableType.IsClass || variableType.IsInterface) && !variableType.IsPrimitive && !excludedTypes.Any(x => x(variableType))
where (variableType.IsClass || variableType.IsInterface || variableType.IsEnum) && !variableType.IsPrimitive && !excludedTypes.Any(x => x(variableType))
select variableType;
var variableTypes = variableTypeQuery.Distinct();

View file

@ -1,6 +1,7 @@
using System.Dynamic;
using Elsa.JavaScript.Contracts;
using Elsa.JavaScript.Extensions;
using Elsa.Workflows.LogPersistence;
namespace Elsa.JavaScript.Services;
@ -32,6 +33,7 @@ public class TypeAliasRegistry : ITypeAliasRegistry
this.RegisterType<DateOnly>("Date");
this.RegisterType<TimeOnly>("Date");
this.RegisterType<IDictionary<string, object>>("ObjectDictionary");
this.RegisterType<LogPersistenceMode>("LogPersistenceMode");
}
/// <inheritdoc />

View file

@ -40,9 +40,17 @@ public class TypeDefinitionDocumentRenderer : ITypeDefinitionDocumentRenderer
{
output.AppendLine($"declare {typeDefinition.DeclarationKeyword} {typeDefinition.Name} {{");
foreach (var property in typeDefinition.Properties)
Render(property, output);
if (typeDefinition.DeclarationKeyword == "enum")
{
foreach (var property in typeDefinition.Properties)
RenderEnumMember(property, output);
}
else
{
foreach (var property in typeDefinition.Properties)
Render(property, output);
}
foreach (var method in typeDefinition.Methods)
RenderMethod(method, output);
@ -50,6 +58,7 @@ public class TypeDefinitionDocumentRenderer : ITypeDefinitionDocumentRenderer
}
private void Render(PropertyDefinition property, StringBuilder output) => output.AppendLine($"{property.Name}{(property.IsOptional ? "?" : "")}: {property.Type};");
private void RenderEnumMember(PropertyDefinition property, StringBuilder output) => output.AppendLine($"{property.Name} = \"{property.Name}\";");
private void Render(VariableDefinition variable, StringBuilder output) => output.AppendLine($"declare var {variable.Name}: {variable.Type};");
string RenderParameter(ParameterDefinition parameter) => $"{parameter.Name}{(parameter.IsOptional ? "?" : "")}: {parameter.Type}";
string RenderParameters(IEnumerable<ParameterDefinition> parameters) => string.Join(", ", parameters.Select(RenderParameter));

View file

@ -36,6 +36,9 @@ public class TypeDescriber : ITypeDescriber
private IEnumerable<FunctionDefinition> GetMethodDefinitions(Type type)
{
if(type.IsEnum)
yield break;
#pragma warning disable IL2070
var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static).Where(x => !x.IsSpecialName).ToList();
#pragma warning restore IL2070
@ -68,6 +71,22 @@ public class TypeDescriber : ITypeDescriber
private IEnumerable<PropertyDefinition> GetPropertyDefinitions([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type)
{
// If the type is an enum, enumerate its members.
if (type.IsEnum)
{
foreach (var name in Enum.GetNames(type))
{
yield return new PropertyDefinition
{
Name = name,
Type = "string",
IsOptional = false,
};
}
yield break;
}
var properties = type.GetProperties();
foreach (var property in properties)
@ -86,8 +105,8 @@ public class TypeDescriber : ITypeDescriber
{
{ IsInterface: true } => "interface",
{ IsClass: true } => "class",
{ IsValueType: true } => "class",
{ IsEnum: true } => "enum",
{ IsValueType: true, IsEnum: false } => "class",
_ => "interface"
};
}

View file

@ -9,6 +9,18 @@ public static class DictionaryExtensions
public static bool TryGetValue<T>(this IDictionary<string, object> dictionary, IEnumerable<string> keys, out T value) => dictionary.TryGetValue<string, T>(keys, out value);
public static bool TryGetValue<T>(this IDictionary<object, object> dictionary, string key, out T value) => dictionary.TryGetValue<object, T>(key, out value);
public static bool TryGetValue<TKey, T>(this IDictionary<TKey, T> dictionary, TKey key, out T value)
{
if (!dictionary.TryGetValue(key, out var item))
{
value = default!;
return false;
}
value = item;
return true;
}
public static bool TryGetValue<TKey, T>(this IDictionary<TKey, object> dictionary, TKey key, out T value)
{
if (!dictionary.TryGetValue(key, out var item))
@ -38,6 +50,7 @@ public static class DictionaryExtensions
public static T? GetValue<TKey, T>(this IDictionary<TKey, T> dictionary, TKey key) => ConvertValue<T>(dictionary[key]);
public static T? GetValue<T>(this IDictionary<string, object> dictionary, string key) => ConvertValue<T>(dictionary[key]);
public static T? GetValueOrDefault<TKey, T>(this IDictionary<TKey, T> dictionary, TKey key, Func<T?> defaultValueFactory) => TryGetValue(dictionary, key, out var value) ? value : defaultValueFactory();
public static T? GetValueOrDefault<TKey, T>(this IDictionary<TKey, object> dictionary, TKey key, Func<T?> defaultValueFactory) => TryGetValue<TKey, T>(dictionary, key, out var value) ? value : defaultValueFactory();
public static T? GetValueOrDefault<TKey, T>(this IDictionary<TKey, object> dictionary, TKey key) => GetValueOrDefault<TKey, T>(dictionary, key, () => default);

View file

@ -47,6 +47,7 @@ public class WorkflowManagementFeature : FeatureBase
private const string LookupsCategory = "Lookups";
private const string DynamicCategory = "Dynamic";
private const string DataCategory = "Data";
private const string SystemCategory = "System";
private string CompressionAlgorithm { get; set; } = nameof(None);
private LogPersistenceMode LogPersistenceMode { get; set; } = LogPersistenceMode.Include;
@ -86,7 +87,8 @@ public class WorkflowManagementFeature : FeatureBase
new(typeof(JsonNode), DynamicCategory, "A JSON node for reading and writing a JSON structure."),
new(typeof(JsonObject), DynamicCategory, "A JSON object for reading and writing a JSON structure."),
new(typeof(byte[]), DataCategory, "A byte array."),
new(typeof(Stream), DataCategory, "A stream.")
new(typeof(Stream), DataCategory, "A stream."),
new(typeof(LogPersistenceMode), SystemCategory, "A LogPersistenceMode enum value.")
];
/// <summary>

View file

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

View file

@ -0,0 +1,7 @@
namespace Elsa.Workflows.Runtime;
public enum LogPersistenceEvaluationMode
{
Strategy,
Expression
}

View file

@ -0,0 +1,10 @@
using Elsa.Expressions.Models;
namespace Elsa.Workflows.Runtime;
public class LogPersistenceConfiguration
{
public LogPersistenceEvaluationMode EvaluationMode { get; set; }
public string? StrategyType { get; set; }
public Expression? Expression { get; set; }
}

View file

@ -1,9 +1,15 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Elsa.Expressions.Contracts;
using Elsa.Expressions.Models;
using Elsa.Extensions;
using Elsa.Workflows.Activities;
using Elsa.Workflows.LogPersistence;
using Elsa.Workflows.LogPersistence.Strategies;
using Elsa.Workflows.Management.Options;
using Elsa.Workflows.Models;
using Elsa.Workflows.Runtime.Entities;
using Elsa.Workflows.Serialization.Converters;
using Elsa.Workflows.State;
using Humanizer;
using Microsoft.Extensions.Options;
@ -13,17 +19,32 @@ namespace Elsa.Workflows.Runtime;
/// <inheritdoc />
public class DefaultActivityExecutionMapper : IActivityExecutionMapper
{
private readonly JsonSerializerOptions _logPersistenceConfigSerializerOptions;
private readonly IOptions<ManagementOptions> _options;
private readonly IExpressionEvaluator _expressionEvaluator;
private readonly IDictionary<string, ILogPersistenceStrategy> _logPersistenceStrategies;
public DefaultActivityExecutionMapper(IOptions<ManagementOptions> options, ILogPersistenceStrategyService logPersistenceStrategyService)
public DefaultActivityExecutionMapper(
IOptions<ManagementOptions> options,
ILogPersistenceStrategyService logPersistenceStrategyService,
IExpressionEvaluator expressionEvaluator,
IExpressionDescriptorRegistry expressionDescriptorRegistry)
{
_options = options;
_expressionEvaluator = expressionEvaluator;
_logPersistenceStrategies = logPersistenceStrategyService.ListStrategies().ToDictionary(x => x.GetType().GetSimpleAssemblyQualifiedName(), x => x);
_logPersistenceConfigSerializerOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
}.WithConverters(
new ExpressionJsonConverterFactory(expressionDescriptorRegistry),
new JsonStringEnumConverter(),
new ExpandoObjectConverterFactory());
}
private const string LegacyLogPersistenceModeKey = "logPersistenceMode";
private const string LogPersistenceStrategyKey = "logPersistenceStrategy";
private const string LogPersistenceConfigKey = "logPersistenceConfig";
/// <inheritdoc />
public async Task<ActivityExecutionRecord> MapAsync(ActivityExecutionContext source)
@ -40,24 +61,25 @@ public class DefaultActivityExecutionMapper : IActivityExecutionMapper
/* The following JSON structure is expected to be found in the custom properties of the workflow and activity:
* {
* "logPersistenceStrategy": {
* "default": "null",
* "inputs": { k : v },
* "outputs": { k: v }
* "logPersistenceConfig": {
* "default": { "evaluationMode": "Strategy", "strategyType": "Elsa.Workflows.LogPersistence.Strategies.Inherit, Elsa.Workflows.Core", "expression": "..." },
* "inputs": { "input1" : { "evaluationMode": "Strategy", "strategyType": "Elsa.Workflows.LogPersistence.Strategies.Inherit, Elsa.Workflows.Core", "expression": "..." } },
* "outputs": { "output1" : { "evaluationMode": "Strategy", "strategyType": "Elsa.Workflows.LogPersistence.Strategies.Inherit, Elsa.Workflows.Core", "expression": "..." } }
* }
* }
*/
var cancellationToken = source.WorkflowExecutionContext.CancellationToken;
var workflow = (Workflow?)source.GetAncestors().FirstOrDefault(x => x.Activity is Workflow)?.Activity ?? source.WorkflowExecutionContext.Workflow;
var workflowPersistenceProperty = await GetDefaultPersistenceModeAsync(workflow.CustomProperties, () => _options.Value.LogPersistenceMode, cancellationToken);
var activityPersistencePropertyDefault = await GetDefaultPersistenceModeAsync(source.Activity.CustomProperties, () => workflowPersistenceProperty, cancellationToken);
var workflowPersistenceProperty = await GetDefaultPersistenceModeAsync(source.WorkflowExecutionContext.ExpressionExecutionContext!, workflow.CustomProperties, () => _options.Value.LogPersistenceMode, cancellationToken);
var activityPersistencePropertyDefault = await GetDefaultPersistenceModeAsync(source.ExpressionExecutionContext, source.Activity.CustomProperties, () => workflowPersistenceProperty, cancellationToken);
var legacyActivityPersistenceProperties = source.Activity.CustomProperties.GetValueOrDefault<IDictionary<string, object?>>(LegacyLogPersistenceModeKey, () => new Dictionary<string, object?>());
var activityPersistenceProperties = source.Activity.CustomProperties.GetValueOrDefault<IDictionary<string, object?>>(LogPersistenceStrategyKey, () => new Dictionary<string, object?>());
var activityPersistenceProperties = source.Activity.CustomProperties.GetValueOrDefault<IDictionary<string, object?>>(LogPersistenceConfigKey, () => new Dictionary<string, object?>());
var payload = GetPayload(source);
var outputs = GetOutputs(source);
outputs = await StorePropertyUsingPersistenceMode(
source.ExpressionExecutionContext,
outputs,
legacyActivityPersistenceProperties!.GetValueOrDefault("outputs", () => new Dictionary<string, object>())!,
activityPersistenceProperties!.GetValueOrDefault("outputs", () => new Dictionary<string, object>())!,
@ -65,7 +87,8 @@ public class DefaultActivityExecutionMapper : IActivityExecutionMapper
cancellationToken);
var inputs = await StorePropertyUsingPersistenceMode(
source.ActivityState,
source.ExpressionExecutionContext,
source.ActivityState,
legacyActivityPersistenceProperties!.GetValueOrDefault("inputs", () => new Dictionary<string, object>())!,
activityPersistenceProperties!.GetValueOrDefault("inputs", () => new Dictionary<string, object>())!,
activityPersistencePropertyDefault,
@ -92,13 +115,13 @@ public class DefaultActivityExecutionMapper : IActivityExecutionMapper
};
}
private async Task<LogPersistenceMode> GetDefaultPersistenceModeAsync(IDictionary<string, object> customProperties, Func<LogPersistenceMode> defaultFactory, CancellationToken cancellationToken)
private async Task<LogPersistenceMode> GetDefaultPersistenceModeAsync(ExpressionExecutionContext expressionExecutionContext, IDictionary<string, object> customProperties, Func<LogPersistenceMode> defaultFactory, CancellationToken cancellationToken)
{
var legacyProperties = customProperties.GetValueOrDefault<IDictionary<string, object?>>(LegacyLogPersistenceModeKey, () => new Dictionary<string, object?>());
var properties = customProperties.GetValueOrDefault<IDictionary<string, object?>>(LogPersistenceStrategyKey, () => new Dictionary<string, object?>());
var defaultPersistenceStrategyTypeName = properties!.GetValueOrDefault<string>("default");
var properties = customProperties.GetValueOrDefault<IDictionary<string, object>>(LogPersistenceConfigKey, () => new Dictionary<string, object>());
var defaultPersistenceConfigObject = properties?.TryGetValue("default", out var defaultPersistenceConfigObjectValue) == true ? defaultPersistenceConfigObjectValue : null;
if (defaultPersistenceStrategyTypeName == null)
if (defaultPersistenceConfigObject == null)
{
var legacyPersistencePropertyDefault = legacyProperties!.GetValueOrDefault("default", defaultFactory);
@ -107,18 +130,14 @@ public class DefaultActivityExecutionMapper : IActivityExecutionMapper
return legacyPersistencePropertyDefault;
}
var strategy = _logPersistenceStrategies.TryGetValue(defaultPersistenceStrategyTypeName, out var v) ? v : null;
if (strategy == null)
return defaultFactory();
var strategyContext = new LogPersistenceStrategyContext(cancellationToken);
return await strategy.GetPersistenceModeAsync(strategyContext);
var defaultPersistenceConfig = Convert(defaultPersistenceConfigObject);
return await EvaluateLogPersistenceConfigAsync(defaultPersistenceConfig, expressionExecutionContext, defaultFactory, cancellationToken);
}
private async Task<Dictionary<string, object?>> StorePropertyUsingPersistenceMode(
ExpressionExecutionContext expressionExecutionContext,
IDictionary<string, object?> state,
IDictionary<string, object> persistenceModeConfiguration,
IDictionary<string, object> obsoletePersistenceModeConfiguration,
IDictionary<string, object> persistenceStrategyConfiguration,
LogPersistenceMode defaultLogPersistenceMode,
CancellationToken cancellationToken)
@ -128,17 +147,17 @@ public class DefaultActivityExecutionMapper : IActivityExecutionMapper
foreach (var value in state)
{
var propKey = value.Key.Camelize();
var strategy = persistenceStrategyConfiguration.GetValueOrDefault(propKey) is string strategyTypeName ? _logPersistenceStrategies.TryGetValue(strategyTypeName, out var v) ? v : null : null;
var logPersistenceConfigObject = persistenceStrategyConfiguration.GetValueOrDefault(propKey, () => null);
var logPersistenceConfig = Convert(logPersistenceConfigObject);
var mode = defaultLogPersistenceMode;
if (strategy != null)
if (logPersistenceConfig != null)
{
var strategyContext = new LogPersistenceStrategyContext(cancellationToken);
mode = await strategy.GetPersistenceModeAsync(strategyContext);
mode = await EvaluateLogPersistenceConfigAsync(logPersistenceConfig, expressionExecutionContext, () => defaultLogPersistenceMode, cancellationToken);
}
else
{
mode = persistenceModeConfiguration.GetValueOrDefault(propKey, () => defaultLogPersistenceMode);
mode = obsoletePersistenceModeConfiguration.GetValueOrDefault(propKey, () => defaultLogPersistenceMode);
}
if (mode == LogPersistenceMode.Include || mode == LogPersistenceMode.Inherit && defaultLogPersistenceMode is LogPersistenceMode.Include or LogPersistenceMode.Inherit)
@ -148,6 +167,44 @@ public class DefaultActivityExecutionMapper : IActivityExecutionMapper
return result;
}
private LogPersistenceConfiguration? Convert(object? value)
{
if (value == null)
return null;
if (value is LogPersistenceConfiguration c)
return c;
var json = JsonSerializer.Serialize(value);
var config = JsonSerializer.Deserialize<LogPersistenceConfiguration>(json, _logPersistenceConfigSerializerOptions);
return config;
}
private async Task<LogPersistenceMode> EvaluateLogPersistenceConfigAsync(LogPersistenceConfiguration? config, ExpressionExecutionContext executionContext, Func<LogPersistenceMode> defaultMode, CancellationToken cancellationToken)
{
if (config == null)
return defaultMode();
if (config.EvaluationMode == LogPersistenceEvaluationMode.Strategy)
{
var strategyTypeName = config.StrategyType ?? typeof(Inherit).GetSimpleAssemblyQualifiedName();
var strategy = _logPersistenceStrategies.TryGetValue(strategyTypeName, out var v) ? v : null;
if (strategy == null)
return defaultMode();
var strategyContext = new LogPersistenceStrategyContext(cancellationToken);
return await strategy.GetPersistenceModeAsync(strategyContext);
}
if (config.Expression == null)
return defaultMode();
var expression = config.Expression;
return await _expressionEvaluator.EvaluateAsync<LogPersistenceMode>(expression, executionContext);
}
private static ActivityStatus GetAggregateStatus(ActivityExecutionContext context)
{
// If any child activity is faulted, the aggregate status is faulted.
@ -158,7 +215,7 @@ public class DefaultActivityExecutionMapper : IActivityExecutionMapper
return context.Status;
}
private static IDictionary<string, object> GetPayload(ActivityExecutionContext source)
{
var outcomes = source.JournalData.TryGetValue("Outcomes", out var resultValue) ? resultValue as string[] : default;
@ -166,11 +223,10 @@ public class DefaultActivityExecutionMapper : IActivityExecutionMapper
if (outcomes != null)
payload.Add("Outcomes", outcomes);
return payload;
}
// Get the outputs of the activity execution context.
private static IDictionary<string, object?> GetOutputs(ActivityExecutionContext source)
{
var activity = source.Activity;
@ -193,7 +249,7 @@ public class DefaultActivityExecutionMapper : IActivityExecutionMapper
return default;
});
return outputs;
}
}