diff --git a/Directory.Packages.props b/Directory.Packages.props index 6af29175e..990f42ecb 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,7 +4,7 @@ true - 3.5.0 + 3.5.1-preview.1211 9.0.8 diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs index b973b255d..64fbe93c4 100644 --- a/src/apps/Elsa.Server.Web/Program.cs +++ b/src/apps/Elsa.Server.Web/Program.cs @@ -69,8 +69,6 @@ using JetBrains.Annotations; using Medallion.Threading.FileSystem; using Medallion.Threading.Postgres; using Medallion.Threading.Redis; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Logging.Console; using Microsoft.Extensions.Options; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; diff --git a/src/modules/Elsa.Logging/Activities/Log.cs b/src/modules/Elsa.Logging/Activities/Log.cs index 355dc863d..653fc8ff6 100644 --- a/src/modules/Elsa.Logging/Activities/Log.cs +++ b/src/modules/Elsa.Logging/Activities/Log.cs @@ -47,12 +47,22 @@ public class Log : CodeActivity /// [Input(Description = "The log message to emit.")] public Input Message { get; set; } = new(string.Empty); - + + /// + /// Arguments for the templated string message. + /// + [Input(Description = "Values of named or indexed placeholders in the log message.")] + public Input Arguments { get; set; } = null!; + /// /// Additional attributes to include in the log entry. /// - [Input(Description = "Values of named or indexed placeholders in the log message.")] - public Input Arguments { get; set; } = null!; + [Input( + Description = "Flat dictionary of key/value pairs to include as attributes.", + DisplayName = "Attributes", + UIHint = InputUIHints.Dictionary + )] + public Input> Attributes { get; set; } = null!; /// /// The log level. @@ -66,15 +76,6 @@ public class Log : CodeActivity [Input(Description = "The category. Defaults to 'Process'.", DefaultValue = "Process")] public Input Category { get; set; } = new("Process"); - /// - /// Additional attributes to include in the log entry. - /// - [Input( - Description = "Flat dictionary of key/value pairs to include as attributes.", - UIHint = InputUIHints.Dictionary - )] - public Input> Attributes { get; set; } = null!; - /// /// Target sinks to write to. /// @@ -98,8 +99,8 @@ public class Log : CodeActivity // Could be JSON created from e.g., Liquid template. If so, parse it into an ExpandoObject. arguments = TryParseJson(argumentString); } - - var attributes = Attributes.GetOrDefault(context) ?? new Dictionary(); + + var attributes = Attributes.GetOrDefault(context) ?? new Dictionary()!; var sinkNames = SinkNames.GetOrDefault(context) ?? new List(); var category = Category.GetOrDefault(context); if (string.IsNullOrWhiteSpace(category)) category = "Process"; diff --git a/src/modules/Elsa.Logging/Elsa.Logging.csproj b/src/modules/Elsa.Logging/Elsa.Logging.csproj index b4f0b319f..beb7ec790 100644 --- a/src/modules/Elsa.Logging/Elsa.Logging.csproj +++ b/src/modules/Elsa.Logging/Elsa.Logging.csproj @@ -1,4 +1,4 @@ - + @@ -8,9 +8,9 @@ - + - + \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Attributes/InputAttribute.cs b/src/modules/Elsa.Workflows.Core/Attributes/InputAttribute.cs index 98335c6b0..982f409d0 100644 --- a/src/modules/Elsa.Workflows.Core/Attributes/InputAttribute.cs +++ b/src/modules/Elsa.Workflows.Core/Attributes/InputAttribute.cs @@ -78,6 +78,12 @@ public class InputAttribute : Attribute /// public bool AutoEvaluate { get; set; } = true; + /// + /// Specifies the type of a custom evaluator to use for evaluating the input property value. + /// The evaluator type determines how the value for the property is resolved at runtime. + /// + public Type? EvaluatorType { get; set; } + /// /// A value indicating whether this input can be serialized as part of the workflow instance, /// diff --git a/src/modules/Elsa.Workflows.Core/Contexts/ActivityInputEvaluatorContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/ActivityInputEvaluatorContext.cs new file mode 100644 index 000000000..99e1a4572 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Contexts/ActivityInputEvaluatorContext.cs @@ -0,0 +1,12 @@ +using Elsa.Expressions.Contracts; +using Elsa.Expressions.Models; +using Elsa.Workflows.Models; + +namespace Elsa.Workflows; + +public record ActivityInputEvaluatorContext( + ActivityExecutionContext ActivityExecutionContext, + ExpressionExecutionContext ExpressionExecutionContext, + InputDescriptor InputDescriptor, + Input Input, + IExpressionEvaluator ExpressionEvaluator); \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Contracts/IActivityInputEvaluator.cs b/src/modules/Elsa.Workflows.Core/Contracts/IActivityInputEvaluator.cs new file mode 100644 index 000000000..ec94417a4 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Contracts/IActivityInputEvaluator.cs @@ -0,0 +1,6 @@ +namespace Elsa.Workflows; + +public interface IActivityInputEvaluator +{ + Task EvaluateAsync(ActivityInputEvaluatorContext context); +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.InputEvaluation.cs b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.InputEvaluation.cs index f47aa314d..db798e5cf 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.InputEvaluation.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.InputEvaluation.cs @@ -91,9 +91,16 @@ public static partial class ActivityExecutionContextExtensions } else { - var evaluator = context.GetRequiredService(); + var expressionEvaluator = context.GetRequiredService(); var expressionExecutionContext = context.ExpressionExecutionContext; - value = wrappedInput?.Expression != null ? await evaluator.EvaluateAsync(wrappedInput, expressionExecutionContext) : defaultValue; + var inputEvaluatorType = inputDescriptor.EvaluatorType ?? typeof(DefaultActivityInputEvaluator); + + if (wrappedInput?.Expression != null) + { + var inputEvaluator = (IActivityInputEvaluator)context.GetRequiredService(inputEvaluatorType); + var inputEvaluatorContext = new ActivityInputEvaluatorContext(context, expressionExecutionContext, inputDescriptor, wrappedInput, expressionEvaluator); + value = await inputEvaluator.EvaluateAsync(inputEvaluatorContext); + } } var memoryReference = wrappedInput?.MemoryBlockReference(); diff --git a/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs b/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs index cc3f81aad..0608b8d92 100644 --- a/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs +++ b/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs @@ -22,6 +22,7 @@ using Elsa.Workflows.Serialization.Helpers; using Elsa.Workflows.Serialization.Serializers; using Elsa.Workflows.Services; using Elsa.Workflows.UIHints.CheckList; +using Elsa.Workflows.UIHints.Dictionary; using Elsa.Workflows.UIHints.Dropdown; using Elsa.Workflows.UIHints.JsonEditor; using Elsa.Workflows.UIHints.RadioList; @@ -187,6 +188,7 @@ public class WorkflowsFeature : FeatureBase .AddScoped() .AddScoped() .AddScoped() + .AddScoped() // Incident Strategies. .AddTransient() @@ -228,17 +230,17 @@ public class WorkflowsFeature : FeatureBase // Instantiation strategies. .AddScoped() - // UI hints. + // UI. .AddScoped() .AddScoped() .AddScoped() .AddScoped() - - // UI property handlers. .AddScoped() .AddScoped() .AddScoped() .AddScoped() + .AddScoped() + .AddSingleton() // Logger state generators. .AddSingleton(WorkflowLoggerStateGenerator) diff --git a/src/modules/Elsa.Workflows.Core/Models/InputDescriptor.cs b/src/modules/Elsa.Workflows.Core/Models/InputDescriptor.cs index 8c7f24fe0..9d699e9f1 100644 --- a/src/modules/Elsa.Workflows.Core/Models/InputDescriptor.cs +++ b/src/modules/Elsa.Workflows.Core/Models/InputDescriptor.cs @@ -21,19 +21,20 @@ public class InputDescriptor : PropertyDescriptor bool isWrapped, string uiHint, string displayName, - string? description = default, - string? category = default, + string? description = null, + string? category = null, float order = 0, - object? defaultValue = default, + object? defaultValue = null, string? defaultSyntax = "Literal", bool isReadOnly = false, bool isBrowsable = true, bool isSerializable = true, bool isSynthetic = false, bool autoEvaluate = true, - Type? storageDriverType = default, - PropertyInfo? propertyInfo = default, - IDictionary? uiSpecifications = default + Type? evaluatorType = null, + Type? storageDriverType = null, + PropertyInfo? propertyInfo = null, + IDictionary? uiSpecifications = null ) { Name = name; @@ -50,6 +51,7 @@ public class InputDescriptor : PropertyDescriptor DefaultSyntax = defaultSyntax; IsReadOnly = isReadOnly; AutoEvaluate = autoEvaluate; + EvaluatorType = evaluatorType; StorageDriverType = storageDriverType; IsSynthetic = isSynthetic; IsBrowsable = isBrowsable; @@ -66,7 +68,7 @@ public class InputDescriptor : PropertyDescriptor /// /// A string value that hints at what UI control might be used to render in a UI tool. /// - public string UIHint { get; set; } = default!; + public string UIHint { get; set; } = null!; /// /// The category to which this input belongs. Can be used by UI to e.g. render different inputs in different tabs. @@ -104,6 +106,12 @@ public class InputDescriptor : PropertyDescriptor /// True if the expression should be evaluated automatically, false otherwise. Defaults to true. /// public bool AutoEvaluate { get; set; } = true; + + /// + /// Specifies the type of a custom evaluator to use for evaluating the input property value. + /// The evaluator type determines how the value for the property is resolved at runtime. + /// + public Type? EvaluatorType { get; set; } /// /// A dictionary of UI specifications to be used by the UI. diff --git a/src/modules/Elsa.Workflows.Core/Services/ActivityDescriber.cs b/src/modules/Elsa.Workflows.Core/Services/ActivityDescriber.cs index 9533f4652..4810b915f 100644 --- a/src/modules/Elsa.Workflows.Core/Services/ActivityDescriber.cs +++ b/src/modules/Elsa.Workflows.Core/Services/ActivityDescriber.cs @@ -150,8 +150,7 @@ public class ActivityDescriber(IPropertyDefaultValueResolver defaultValueResolve var uiSpecification = await propertyUIHandlerResolver.GetUIPropertiesAsync(propertyInfo, null, cancellationToken); - return new InputDescriptor - ( + return new( inputAttribute?.Name ?? propertyInfo.Name, wrappedPropertyType, propertyInfo.GetValue, @@ -169,7 +168,8 @@ public class ActivityDescriber(IPropertyDefaultValueResolver defaultValueResolve inputAttribute?.IsSerializable ?? true, false, autoEvaluate, - default, + inputAttribute?.EvaluatorType, + null, propertyInfo, uiSpecification ); diff --git a/src/modules/Elsa.Workflows.Core/Services/DefaultActivityInputEvaluator.cs b/src/modules/Elsa.Workflows.Core/Services/DefaultActivityInputEvaluator.cs new file mode 100644 index 000000000..ca89d369f --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Services/DefaultActivityInputEvaluator.cs @@ -0,0 +1,14 @@ +using Elsa.Extensions; + +namespace Elsa.Workflows; + +public class DefaultActivityInputEvaluator : IActivityInputEvaluator +{ + public async Task EvaluateAsync(ActivityInputEvaluatorContext context) + { + var wrappedInput = context.Input; + var evaluator = context.ExpressionEvaluator; + var expressionExecutionContext = context.ExpressionExecutionContext; + return await evaluator.EvaluateAsync(wrappedInput, expressionExecutionContext); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/UIHints/Dictionary/DictionaryUIHintInputModifier.cs b/src/modules/Elsa.Workflows.Core/UIHints/Dictionary/DictionaryUIHintInputModifier.cs new file mode 100644 index 000000000..21e104d4b --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/UIHints/Dictionary/DictionaryUIHintInputModifier.cs @@ -0,0 +1,14 @@ +using Elsa.Workflows.Models; + +namespace Elsa.Workflows.UIHints.Dictionary; + +public class DictionaryUIHintInputModifier : IActivityDescriptorModifier +{ + public void Modify(ActivityDescriptor descriptor) + { + var dictionaryInputs = descriptor.Inputs.Where(x => x.UIHint == InputUIHints.Dictionary).ToList(); + + foreach (var dictionaryInput in dictionaryInputs) + dictionaryInput.EvaluatorType = typeof(DictionaryValueEvaluator); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/UIHints/Dictionary/DictionaryValueEvaluator.cs b/src/modules/Elsa.Workflows.Core/UIHints/Dictionary/DictionaryValueEvaluator.cs new file mode 100644 index 000000000..0efcffb67 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/UIHints/Dictionary/DictionaryValueEvaluator.cs @@ -0,0 +1,55 @@ +using System.Text.Json; +using Elsa.Expressions.Models; +using Elsa.Extensions; +using Microsoft.Extensions.Logging; + +namespace Elsa.Workflows.UIHints.Dictionary; + +public class DictionaryValueEvaluator(ILogger logger) : IActivityInputEvaluator +{ + public async Task EvaluateAsync(ActivityInputEvaluatorContext context) + { + var wrappedInput = context.Input; + var evaluator = context.ExpressionEvaluator; + var expressionExecutionContext = context.ExpressionExecutionContext; + var inputDescriptor = context.InputDescriptor; + var defaultValue = inputDescriptor.DefaultValue; + var value = wrappedInput.Expression != null ? await evaluator.EvaluateAsync(wrappedInput, expressionExecutionContext) : defaultValue; + if (value is IDictionary dictionary && inputDescriptor.UIHint == InputUIHints.Dictionary) + { + var tempDictionary = new Dictionary(dictionary.Count); + foreach (var dict in dictionary) + { + if (dict.Value is not JsonElement json) + { + // Not a JSON object, so just use the value as-is. + tempDictionary[dict.Key] = dict.Value; + continue; + } + + // JSON object, so extract the type and value properties. + var hasType = json.TryGetProperty("type", out var typeProperty); + var hasValue = json.TryGetProperty("value", out var valueProperty); + + if (!hasType || !hasValue) + { + // Skip this entry or handle as needed (e.g., log, throw, etc.) + logger.LogWarning("Dictionary entry is missing type or value property: {Json}", JsonSerializer.Serialize(json)); + continue; + } + + // Evaluate the expression. + var expression = new Expression(typeProperty.ToString(), valueProperty.ToString()); + var val = await evaluator.EvaluateAsync(expression, expressionExecutionContext); + + // Add the evaluated value to the dictionary. + tempDictionary[dict.Key] = val; + } + + // Replace the original dictionary with the evaluated one. + value = tempDictionary; + } + + return value; + } +} \ No newline at end of file