diff --git a/src/clients/Elsa.Api.Client/Shared/Enums/LogPersistenceEvaluationMode.cs b/src/clients/Elsa.Api.Client/Shared/Enums/LogPersistenceEvaluationMode.cs new file mode 100644 index 000000000..1f4a18a17 --- /dev/null +++ b/src/clients/Elsa.Api.Client/Shared/Enums/LogPersistenceEvaluationMode.cs @@ -0,0 +1,7 @@ +namespace Elsa.Api.Client.Shared.Enums; + +public enum LogPersistenceEvaluationMode +{ + Strategy, + Expression +} \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Shared/Models/LogPersistenceConfiguration.cs b/src/clients/Elsa.Api.Client/Shared/Models/LogPersistenceConfiguration.cs new file mode 100644 index 000000000..ac4531b50 --- /dev/null +++ b/src/clients/Elsa.Api.Client/Shared/Models/LogPersistenceConfiguration.cs @@ -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; } +} \ No newline at end of file diff --git a/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithCommonTypes.cs b/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithCommonTypes.cs index a296d1d7a..c0a303024 100644 --- a/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithCommonTypes.cs +++ b/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithCommonTypes.cs @@ -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(); engine.RegisterType(); engine.RegisterType(); + engine.RegisterType(); return Task.CompletedTask; } diff --git a/src/modules/Elsa.JavaScript/Providers/VariableTypeDefinitionProvider.cs b/src/modules/Elsa.JavaScript/Providers/VariableTypeDefinitionProvider.cs index d56e6c255..a5888b87b 100644 --- a/src/modules/Elsa.JavaScript/Providers/VariableTypeDefinitionProvider.cs +++ b/src/modules/Elsa.JavaScript/Providers/VariableTypeDefinitionProvider.cs @@ -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(); diff --git a/src/modules/Elsa.JavaScript/Services/TypeAliasRegistry.cs b/src/modules/Elsa.JavaScript/Services/TypeAliasRegistry.cs index 77af847c9..71ca4c238 100644 --- a/src/modules/Elsa.JavaScript/Services/TypeAliasRegistry.cs +++ b/src/modules/Elsa.JavaScript/Services/TypeAliasRegistry.cs @@ -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("Date"); this.RegisterType("Date"); this.RegisterType>("ObjectDictionary"); + this.RegisterType("LogPersistenceMode"); } /// diff --git a/src/modules/Elsa.JavaScript/TypeDefinitions/Services/TypeDefinitionDocumentRenderer.cs b/src/modules/Elsa.JavaScript/TypeDefinitions/Services/TypeDefinitionDocumentRenderer.cs index 103b074ab..f5699e6f5 100644 --- a/src/modules/Elsa.JavaScript/TypeDefinitions/Services/TypeDefinitionDocumentRenderer.cs +++ b/src/modules/Elsa.JavaScript/TypeDefinitions/Services/TypeDefinitionDocumentRenderer.cs @@ -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 parameters) => string.Join(", ", parameters.Select(RenderParameter)); diff --git a/src/modules/Elsa.JavaScript/TypeDefinitions/Services/TypeDescriber.cs b/src/modules/Elsa.JavaScript/TypeDefinitions/Services/TypeDescriber.cs index 65e76b403..af4a85b74 100644 --- a/src/modules/Elsa.JavaScript/TypeDefinitions/Services/TypeDescriber.cs +++ b/src/modules/Elsa.JavaScript/TypeDefinitions/Services/TypeDescriber.cs @@ -36,6 +36,9 @@ public class TypeDescriber : ITypeDescriber private IEnumerable 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 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" }; } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Extensions/DictionaryExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/DictionaryExtensions.cs index 4731da51c..bc1bb6c44 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/DictionaryExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/DictionaryExtensions.cs @@ -9,6 +9,18 @@ public static class DictionaryExtensions public static bool TryGetValue(this IDictionary dictionary, IEnumerable keys, out T value) => dictionary.TryGetValue(keys, out value); public static bool TryGetValue(this IDictionary dictionary, string key, out T value) => dictionary.TryGetValue(key, out value); + public static bool TryGetValue(this IDictionary 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(this IDictionary 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(this IDictionary dictionary, TKey key) => ConvertValue(dictionary[key]); public static T? GetValue(this IDictionary dictionary, string key) => ConvertValue(dictionary[key]); + public static T? GetValueOrDefault(this IDictionary dictionary, TKey key, Func defaultValueFactory) => TryGetValue(dictionary, key, out var value) ? value : defaultValueFactory(); public static T? GetValueOrDefault(this IDictionary dictionary, TKey key, Func defaultValueFactory) => TryGetValue(dictionary, key, out var value) ? value : defaultValueFactory(); public static T? GetValueOrDefault(this IDictionary dictionary, TKey key) => GetValueOrDefault(dictionary, key, () => default); diff --git a/src/modules/Elsa.Workflows.Management/Features/WorkflowManagementFeature.cs b/src/modules/Elsa.Workflows.Management/Features/WorkflowManagementFeature.cs index 7a57818d8..a81d8fd47 100644 --- a/src/modules/Elsa.Workflows.Management/Features/WorkflowManagementFeature.cs +++ b/src/modules/Elsa.Workflows.Management/Features/WorkflowManagementFeature.cs @@ -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.") ]; /// diff --git a/src/modules/Elsa.Workflows.Runtime/Elsa.Workflows.Runtime.csproj.DotSettings b/src/modules/Elsa.Workflows.Runtime/Elsa.Workflows.Runtime.csproj.DotSettings index e0fbdd65a..9b4d77e2f 100644 --- a/src/modules/Elsa.Workflows.Runtime/Elsa.Workflows.Runtime.csproj.DotSettings +++ b/src/modules/Elsa.Workflows.Runtime/Elsa.Workflows.Runtime.csproj.DotSettings @@ -1,6 +1,7 @@  True True + True True True True diff --git a/src/modules/Elsa.Workflows.Runtime/Enums/LogPersistenceEvaluationMode.cs b/src/modules/Elsa.Workflows.Runtime/Enums/LogPersistenceEvaluationMode.cs new file mode 100644 index 000000000..75aea73c0 --- /dev/null +++ b/src/modules/Elsa.Workflows.Runtime/Enums/LogPersistenceEvaluationMode.cs @@ -0,0 +1,7 @@ +namespace Elsa.Workflows.Runtime; + +public enum LogPersistenceEvaluationMode +{ + Strategy, + Expression +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Models/LogPersistenceConfiguration.cs b/src/modules/Elsa.Workflows.Runtime/Models/LogPersistenceConfiguration.cs new file mode 100644 index 000000000..19d8b6e12 --- /dev/null +++ b/src/modules/Elsa.Workflows.Runtime/Models/LogPersistenceConfiguration.cs @@ -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; } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Services/DefaultActivityExecutionMapper.cs b/src/modules/Elsa.Workflows.Runtime/Services/DefaultActivityExecutionMapper.cs index 2fe92dce7..c04958358 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/DefaultActivityExecutionMapper.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/DefaultActivityExecutionMapper.cs @@ -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; /// public class DefaultActivityExecutionMapper : IActivityExecutionMapper { + private readonly JsonSerializerOptions _logPersistenceConfigSerializerOptions; private readonly IOptions _options; + private readonly IExpressionEvaluator _expressionEvaluator; private readonly IDictionary _logPersistenceStrategies; - public DefaultActivityExecutionMapper(IOptions options, ILogPersistenceStrategyService logPersistenceStrategyService) + public DefaultActivityExecutionMapper( + IOptions 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"; /// public async Task 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>(LegacyLogPersistenceModeKey, () => new Dictionary()); - var activityPersistenceProperties = source.Activity.CustomProperties.GetValueOrDefault>(LogPersistenceStrategyKey, () => new Dictionary()); + var activityPersistenceProperties = source.Activity.CustomProperties.GetValueOrDefault>(LogPersistenceConfigKey, () => new Dictionary()); var payload = GetPayload(source); var outputs = GetOutputs(source); outputs = await StorePropertyUsingPersistenceMode( + source.ExpressionExecutionContext, outputs, legacyActivityPersistenceProperties!.GetValueOrDefault("outputs", () => new Dictionary())!, activityPersistenceProperties!.GetValueOrDefault("outputs", () => new Dictionary())!, @@ -65,7 +87,8 @@ public class DefaultActivityExecutionMapper : IActivityExecutionMapper cancellationToken); var inputs = await StorePropertyUsingPersistenceMode( - source.ActivityState, + source.ExpressionExecutionContext, + source.ActivityState, legacyActivityPersistenceProperties!.GetValueOrDefault("inputs", () => new Dictionary())!, activityPersistenceProperties!.GetValueOrDefault("inputs", () => new Dictionary())!, activityPersistencePropertyDefault, @@ -92,13 +115,13 @@ public class DefaultActivityExecutionMapper : IActivityExecutionMapper }; } - private async Task GetDefaultPersistenceModeAsync(IDictionary customProperties, Func defaultFactory, CancellationToken cancellationToken) + private async Task GetDefaultPersistenceModeAsync(ExpressionExecutionContext expressionExecutionContext, IDictionary customProperties, Func defaultFactory, CancellationToken cancellationToken) { var legacyProperties = customProperties.GetValueOrDefault>(LegacyLogPersistenceModeKey, () => new Dictionary()); - var properties = customProperties.GetValueOrDefault>(LogPersistenceStrategyKey, () => new Dictionary()); - var defaultPersistenceStrategyTypeName = properties!.GetValueOrDefault("default"); + var properties = customProperties.GetValueOrDefault>(LogPersistenceConfigKey, () => new Dictionary()); + 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> StorePropertyUsingPersistenceMode( + ExpressionExecutionContext expressionExecutionContext, IDictionary state, - IDictionary persistenceModeConfiguration, + IDictionary obsoletePersistenceModeConfiguration, IDictionary 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(json, _logPersistenceConfigSerializerOptions); + + return config; + } + + private async Task EvaluateLogPersistenceConfigAsync(LogPersistenceConfiguration? config, ExpressionExecutionContext executionContext, Func 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(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 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 GetOutputs(ActivityExecutionContext source) { var activity = source.Activity; @@ -193,7 +249,7 @@ public class DefaultActivityExecutionMapper : IActivityExecutionMapper return default; }); - + return outputs; } } \ No newline at end of file