diff --git a/src/modules/Elsa.Dsl.ElsaScript/Compiler/ElsaScriptCompiler.cs b/src/modules/Elsa.Dsl.ElsaScript/Compiler/ElsaScriptCompiler.cs index 8cb52e32f..65a53fb89 100644 --- a/src/modules/Elsa.Dsl.ElsaScript/Compiler/ElsaScriptCompiler.cs +++ b/src/modules/Elsa.Dsl.ElsaScript/Compiler/ElsaScriptCompiler.cs @@ -202,8 +202,9 @@ public class ElsaScriptCompiler(IActivityRegistryLookupService activityRegistryL else { // No positional arguments, use default constructor - var activityConstructorContext = new ActivityConstructorContext(activityDescriptor, ActivityActivator.Create); - activity = activityDescriptor.Constructor(activityConstructorContext); + var activityConstructorContext = new ActivityConstructorContext(activityDescriptor, (t) => new(ActivityActivator.Create(t))); + var activityConstructionResult = activityDescriptor.Constructor(activityConstructorContext); + activity = activityConstructionResult.Activity; } // Set named argument properties diff --git a/src/modules/Elsa.Workflows.Core/Exceptions/InvalidActivityDescriptorInputException.cs b/src/modules/Elsa.Workflows.Core/Exceptions/InvalidActivityDescriptorInputException.cs new file mode 100644 index 000000000..ddccd3787 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Exceptions/InvalidActivityDescriptorInputException.cs @@ -0,0 +1,5 @@ +namespace Elsa.Workflows.Exceptions; + +public sealed class InvalidActivityDescriptorInputException(string message) : Exception(message) +{ +} diff --git a/src/modules/Elsa.Workflows.Core/Models/ActivityConstructionResult.cs b/src/modules/Elsa.Workflows.Core/Models/ActivityConstructionResult.cs new file mode 100644 index 000000000..b90e64f22 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Models/ActivityConstructionResult.cs @@ -0,0 +1,17 @@ +namespace Elsa.Workflows.Models; + +public class ActivityConstructionResult(IActivity activity, IEnumerable? exceptions = null) +{ + public bool HasExceptions => Exceptions.Any(); + + public IActivity Activity { get; } = activity; + public IEnumerable Exceptions { get; } = exceptions ?? []; + + public ActivityConstructionResult Cast() where TActivity : IActivity => new ((TActivity)Activity, Exceptions); +} + +public sealed class ActivityConstructionResult(TActivity activity, IEnumerable? exceptions = null) : ActivityConstructionResult(activity, exceptions) + where TActivity : IActivity +{ + public new TActivity Activity { get; } = activity; +} diff --git a/src/modules/Elsa.Workflows.Core/Models/ActivityConstructorContext.cs b/src/modules/Elsa.Workflows.Core/Models/ActivityConstructorContext.cs index 38f35415b..78d9f1ca3 100644 --- a/src/modules/Elsa.Workflows.Core/Models/ActivityConstructorContext.cs +++ b/src/modules/Elsa.Workflows.Core/Models/ActivityConstructorContext.cs @@ -3,15 +3,17 @@ using System.Text.Json.Nodes; using Elsa.Expressions.Helpers; using Elsa.Extensions; using Elsa.Workflows.Attributes; +using Elsa.Workflows.Exceptions; using Elsa.Workflows.Memory; using Humanizer; +using Microsoft.Extensions.Logging; namespace Elsa.Workflows.Models; -public record ActivityConstructorContext(ActivityDescriptor ActivityDescriptor, Func ActivityFactory) +public record ActivityConstructorContext(ActivityDescriptor ActivityDescriptor, Func ActivityFactory) { - public T CreateActivity() where T : IActivity => (T)ActivityFactory(typeof(T)); - public IActivity CreateActivity(Type type) => ActivityFactory(type); + public ActivityConstructionResult CreateActivity() where T : IActivity => ActivityFactory(typeof(T)).Cast(); + public ActivityConstructionResult CreateActivity(Type type) => ActivityFactory(type); } public static class JsonActivityConstructorContextHelper @@ -21,13 +23,15 @@ public static class JsonActivityConstructorContextHelper return new ActivityConstructorContext(activityDescriptor, type => CreateActivity(activityDescriptor, type, element, serializerOptions)); } - public static T CreateActivity(ActivityDescriptor activityDescriptor, JsonElement element, JsonSerializerOptions serializerOptions) where T : IActivity + public static ActivityConstructionResult CreateActivity(ActivityDescriptor activityDescriptor, JsonElement element, JsonSerializerOptions serializerOptions) where T : IActivity { - return (T)CreateActivity(activityDescriptor, typeof(T), element, serializerOptions); + return CreateActivity(activityDescriptor, typeof(T), element, serializerOptions).Cast(); } - public static IActivity CreateActivity(ActivityDescriptor activityDescriptor, Type type, JsonElement element, JsonSerializerOptions serializerOptions) + public static ActivityConstructionResult CreateActivity(ActivityDescriptor activityDescriptor, Type type, JsonElement element, JsonSerializerOptions serializerOptions) { + var exceptions = new List(); + // 1) Grab the raw text var raw = element.GetRawText(); @@ -56,14 +60,14 @@ public static class JsonActivityConstructorContextHelper composite.Setup(); // 9) Existing synthetic inputs/outputs routines, using the cleanedElement - ReadSyntheticInputs(activityDescriptor, activity, cleanedElement, serializerOptions); + ReadSyntheticInputs(activityDescriptor, activity, cleanedElement, serializerOptions, exceptions); ReadSyntheticOutputs(activityDescriptor, activity, cleanedElement); // 10) Finally re‑apply those flags activity.SetCanStartWorkflow(canStartWorkflow); activity.SetRunAsynchronously(runAsynchronously); - return activity; + return new(activity, exceptions); } /// @@ -140,7 +144,7 @@ public static class JsonActivityConstructorContextHelper } } - private static void ReadSyntheticInputs(ActivityDescriptor activityDescriptor, IActivity activity, JsonElement activityRoot, JsonSerializerOptions options) + private static void ReadSyntheticInputs(ActivityDescriptor activityDescriptor, IActivity activity, JsonElement activityRoot, JsonSerializerOptions options, List exceptions) { foreach (var inputDescriptor in activityDescriptor.Inputs.Where(x => x.IsSynthetic)) { @@ -152,6 +156,15 @@ public static class JsonActivityConstructorContextHelper if (!activityRoot.TryGetProperty(propertyName, out var propertyElement) || propertyElement.ValueKind == JsonValueKind.Null || propertyElement.ValueKind == JsonValueKind.Undefined) continue; + if(propertyElement.ValueKind == JsonValueKind.Object && !propertyElement.TryGetProperty("typeName", out var _)) + { + var exception = new InvalidActivityDescriptorInputException( + $"Activity descriptor '{activityDescriptor.Name}' has invalid input property '{propertyName}'; missing required property 'typeName'" + ); + exceptions.Add(exception); + continue; + } + var isWrapped = propertyElement.ValueKind == JsonValueKind.Object && propertyElement.GetProperty("typeName").ValueKind != JsonValueKind.Undefined; if (isWrapped) diff --git a/src/modules/Elsa.Workflows.Core/Models/ActivityDescriptor.cs b/src/modules/Elsa.Workflows.Core/Models/ActivityDescriptor.cs index 8b6cf8675..950566795 100644 --- a/src/modules/Elsa.Workflows.Core/Models/ActivityDescriptor.cs +++ b/src/modules/Elsa.Workflows.Core/Models/ActivityDescriptor.cs @@ -69,7 +69,7 @@ public class ActivityDescriptor /// Instantiates a concrete instance of an . /// [JsonIgnore] - public Func Constructor { get; set; } = null!; + public Func Constructor { get; set; } = null!; /// /// The kind of activity. diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Converters/ActivityJsonConverter.cs b/src/modules/Elsa.Workflows.Core/Serialization/Converters/ActivityJsonConverter.cs index a07f8268e..125bfe847 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Converters/ActivityJsonConverter.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Converters/ActivityJsonConverter.cs @@ -7,6 +7,7 @@ using Elsa.Workflows.Helpers; using Elsa.Workflows.Models; using Elsa.Workflows.Serialization.Helpers; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace Elsa.Workflows.Serialization.Converters; @@ -38,13 +39,14 @@ public class ActivityJsonConverter( } var clonedOptions = GetClonedOptions(options); - // If the activity type is not found, create a NotFoundActivity instead. if (activityDescriptor == null) { var notFoundActivityDescriptor = activityRegistry.Find()!; - var notFoundActivity = JsonActivityConstructorContextHelper.CreateActivity(notFoundActivityDescriptor, activityRoot, clonedOptions); + var notFoundActivityResult = JsonActivityConstructorContextHelper.CreateActivity(notFoundActivityDescriptor, activityRoot, clonedOptions); + LogExceptionsIfAny(notFoundActivityResult); + var notFoundActivity = notFoundActivityResult.Activity; notFoundActivity.Type = notFoundActivityTypeName; notFoundActivity.Version = 1; notFoundActivity.MissingTypeName = activityTypeName; @@ -56,8 +58,20 @@ public class ActivityJsonConverter( } var context = JsonActivityConstructorContextHelper.Create(activityDescriptor, activityRoot, clonedOptions); - var activity = activityDescriptor.Constructor(context); - return activity; + var activityResult = activityDescriptor.Constructor(context); + LogExceptionsIfAny(activityResult); + + return activityResult.Activity; + } + + void LogExceptionsIfAny(ActivityConstructionResult result) + { + if (!result.HasExceptions) + return; + + var logger = serviceProvider.GetRequiredService>(); + foreach (var exception in result.Exceptions) + logger.LogWarning("An exception was thrown while constructing activity with id '{activityId}': {Message}", result.Activity.Id, exception.Message); } /// diff --git a/src/modules/Elsa.Workflows.Core/Services/ActivityDescriber.cs b/src/modules/Elsa.Workflows.Core/Services/ActivityDescriber.cs index 2fd774cca..8498bc4eb 100644 --- a/src/modules/Elsa.Workflows.Core/Services/ActivityDescriber.cs +++ b/src/modules/Elsa.Workflows.Core/Services/ActivityDescriber.cs @@ -92,9 +92,9 @@ public class ActivityDescriber(IPropertyDefaultValueResolver defaultValueResolve }, Constructor = context => { - var activity = context.CreateActivity(activityType); - activity.Type = fullTypeName; - return activity; + var activityResult = context.CreateActivity(activityType); + activityResult.Activity.Type = fullTypeName; + return activityResult; }, }; diff --git a/src/modules/Elsa.Workflows.Core/Services/ActivityFactory.cs b/src/modules/Elsa.Workflows.Core/Services/ActivityFactory.cs index f4cf6a097..bcc81bf92 100644 --- a/src/modules/Elsa.Workflows.Core/Services/ActivityFactory.cs +++ b/src/modules/Elsa.Workflows.Core/Services/ActivityFactory.cs @@ -8,6 +8,6 @@ public class ActivityFactory : IActivityFactory /// public IActivity Create(Type type, ActivityConstructorContext context) { - return context.CreateActivity(type); + return context.CreateActivity(type).Activity; } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Management/Activities/WorkflowDefinitionActivity/WorkflowDefinitionActivityDescriptorFactory.cs b/src/modules/Elsa.Workflows.Management/Activities/WorkflowDefinitionActivity/WorkflowDefinitionActivityDescriptorFactory.cs index be3504c5e..f240d075e 100644 --- a/src/modules/Elsa.Workflows.Management/Activities/WorkflowDefinitionActivity/WorkflowDefinitionActivityDescriptorFactory.cs +++ b/src/modules/Elsa.Workflows.Management/Activities/WorkflowDefinitionActivity/WorkflowDefinitionActivityDescriptorFactory.cs @@ -57,7 +57,8 @@ public class WorkflowDefinitionActivityDescriptorFactory }, Constructor = context => { - var activity = context.CreateActivity(); + var activityResult = context.CreateActivity(); + var activity = activityResult.Activity; activity.Type = typeName; activity.WorkflowDefinitionId = definition.DefinitionId; activity.WorkflowDefinitionVersionId = definition.Id; @@ -65,7 +66,7 @@ public class WorkflowDefinitionActivityDescriptorFactory activity.LatestAvailablePublishedVersion = latestPublishedDefinition?.Version ?? definition.Version; activity.LatestAvailablePublishedVersionId = latestPublishedDefinition?.Id ?? definition.Id; - return activity; + return activityResult; } }; } diff --git a/src/modules/Elsa.Workflows.Management/Services/HostMethodActivityDescriber.cs b/src/modules/Elsa.Workflows.Management/Services/HostMethodActivityDescriber.cs index c96db73db..f496e33a3 100644 --- a/src/modules/Elsa.Workflows.Management/Services/HostMethodActivityDescriber.cs +++ b/src/modules/Elsa.Workflows.Management/Services/HostMethodActivityDescriber.cs @@ -57,12 +57,13 @@ public class HostMethodActivityDescriber(IActivityDescriber activityDescriber) : descriptor.Constructor = context => { - var activity = context.CreateActivity(); + var activityResult = context.CreateActivity(); + var activity = activityResult.Activity; activity.Type = activityTypeName; activity.HostType = hostType; activity.MethodName = methodName; activity.RunAsynchronously ??= descriptor.RunAsynchronously; - return activity; + return activityResult; }; descriptor.Inputs.Clear(); diff --git a/src/modules/Elsa.Workflows.Runtime/Services/BackgroundWorkflowCancellationDispatcher.cs b/src/modules/Elsa.Workflows.Runtime/Services/BackgroundWorkflowCancellationDispatcher.cs index 761f8396f..238b7f0b8 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/BackgroundWorkflowCancellationDispatcher.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/BackgroundWorkflowCancellationDispatcher.cs @@ -9,7 +9,7 @@ using Elsa.Workflows.Runtime.Responses; namespace Elsa.Workflows.Runtime; /// -/// Dispatches workflow cancellation requests to a local background worker. +/// Dispatches workflow cancellation requests to a local background worker. /// public class BackgroundWorkflowCancellationDispatcher(ICommandSender commandSender, ITenantAccessor tenantAccessor) : IWorkflowCancellationDispatcher { diff --git a/test/unit/Elsa.Workflows.Core.UnitTests/Models/JsonActivityConstructorContextHelperTests.cs b/test/unit/Elsa.Workflows.Core.UnitTests/Models/JsonActivityConstructorContextHelperTests.cs index fe9f84b94..fab8da0b6 100644 --- a/test/unit/Elsa.Workflows.Core.UnitTests/Models/JsonActivityConstructorContextHelperTests.cs +++ b/test/unit/Elsa.Workflows.Core.UnitTests/Models/JsonActivityConstructorContextHelperTests.cs @@ -1,6 +1,7 @@  using System.Text.Json; using System.Text.Json.Serialization; +using Elsa.Workflows.Exceptions; using Elsa.Workflows.Models; namespace Elsa.Workflows.Core.UnitTests.Models; @@ -14,7 +15,7 @@ public sealed class JsonActivityConstructorContextHelperTests [InlineData("Metadata")] [InlineData("_CustomProperties")] [InlineData("CustomProperties")] - public void When_InputName_Is_ReservedKeyWord_Then_ThrowsException_WhenReadingInputs(string reservedInputName) + public void When_InputName_Is_ReservedKeyWord_Then_AddsException_WhenReadingInputs(string reservedInputName) { // Arrange var activityDescriptor = new ActivityDescriptor @@ -31,15 +32,15 @@ public sealed class JsonActivityConstructorContextHelperTests }; var jsonElement = GetJsonElementWithReservedInputName(reservedInputName); - // Act & Assert - Assert.Throws(() => - { - _ = JsonActivityConstructorContextHelper.CreateActivity( - activityDescriptor, - jsonElement, - _serializerOptions - ); - }); + // Act + var result = JsonActivityConstructorContextHelper.CreateActivity( + activityDescriptor, + jsonElement, + _serializerOptions + ); + + // Assert + Assert.Single(result.Exceptions, e => e is InvalidActivityDescriptorInputException); } private static JsonElement GetJsonElementWithReservedInputName(string inputName) diff --git a/test/unit/Elsa.Workflows.Core.UnitTests/Serialization/Converters/ActivityJsonConverterTests.cs b/test/unit/Elsa.Workflows.Core.UnitTests/Serialization/Converters/ActivityJsonConverterTests.cs index d8969c243..54ce2cd71 100644 --- a/test/unit/Elsa.Workflows.Core.UnitTests/Serialization/Converters/ActivityJsonConverterTests.cs +++ b/test/unit/Elsa.Workflows.Core.UnitTests/Serialization/Converters/ActivityJsonConverterTests.cs @@ -125,7 +125,7 @@ public sealed class ActivityJsonConverterTests static IActivityRegistry CreateActivityRegistry(string typeName, IActivity activity, int? version = null) { - var descriptor = new ActivityDescriptor { Constructor = _ => activity }; + var descriptor = new ActivityDescriptor { Constructor = _ => new(activity) }; var activityRegistry = Substitute.For(); if (version.HasValue) @@ -140,7 +140,7 @@ public sealed class ActivityJsonConverterTests { var descriptor = new ActivityDescriptor { - Constructor = _ => WorkflowAsActivity, + Constructor = _ => new(WorkflowAsActivity), CustomProperties = { [customPropertyName] = customPropertyValue } }; @@ -334,6 +334,11 @@ public sealed class ActivityJsonConverterTests private static ActivityJsonConverter CreateSut(IActivityRegistry activityRegistry) { var expressionDescriptorRegistry = new ExpressionDescriptorRegistry([]); + var serviceProvider = Substitute.For(); + serviceProvider + .GetService(typeof(ILogger))! + .Returns(CreateLogger()); + return new( activityRegistry, expressionDescriptorRegistry, @@ -342,7 +347,7 @@ public sealed class ActivityJsonConverterTests new(expressionDescriptorRegistry), CreateLogger() ), - Substitute.For() + serviceProvider ); }