diff --git a/src/modules/Elsa.MassTransit/Messages/WorkflowDefinitionCreated.cs b/src/modules/Elsa.MassTransit/Messages/WorkflowDefinitionCreated.cs index bd8b2b452..4638f7574 100644 --- a/src/modules/Elsa.MassTransit/Messages/WorkflowDefinitionCreated.cs +++ b/src/modules/Elsa.MassTransit/Messages/WorkflowDefinitionCreated.cs @@ -5,6 +5,13 @@ namespace Elsa.MassTransit.Messages; /// public class WorkflowDefinitionCreated(string id, bool usableAsActivity) { - public string Id { get; set; } = id; - public bool UsableAsActivity { get; set; } = usableAsActivity; + /// + /// The ID of the created workflow definition. + /// + public string Id { get; } = id; + + /// + /// Whether the created workflow definition is usable as an activity. + /// + public bool UsableAsActivity { get; } = usableAsActivity; } \ No newline at end of file diff --git a/src/modules/Elsa.MassTransit/Messages/WorkflowDefinitionDeleted.cs b/src/modules/Elsa.MassTransit/Messages/WorkflowDefinitionDeleted.cs index bc3c286a1..ac7d54348 100644 --- a/src/modules/Elsa.MassTransit/Messages/WorkflowDefinitionDeleted.cs +++ b/src/modules/Elsa.MassTransit/Messages/WorkflowDefinitionDeleted.cs @@ -5,5 +5,8 @@ namespace Elsa.MassTransit.Messages; /// public class WorkflowDefinitionDeleted(string id) { - public string Id { get; set; } = id; + /// + /// The ID of the deleted workflow definition. + /// + public string Id { get; } = id; } \ No newline at end of file diff --git a/src/modules/Elsa.MassTransit/Messages/WorkflowDefinitionPublished.cs b/src/modules/Elsa.MassTransit/Messages/WorkflowDefinitionPublished.cs index 86f219da1..9c77f2fc0 100644 --- a/src/modules/Elsa.MassTransit/Messages/WorkflowDefinitionPublished.cs +++ b/src/modules/Elsa.MassTransit/Messages/WorkflowDefinitionPublished.cs @@ -1,7 +1,17 @@ namespace Elsa.MassTransit.Messages; +/// +/// Represents a distributed message that is triggered when a workflow definition is published. +/// public class WorkflowDefinitionPublished(string id, bool usableAsActivity) { - public string Id { get; set; } = id; - public bool UsableAsActivity { get; set; } = usableAsActivity; + /// + /// The ID of the published workflow definition. + /// + public string Id { get; } = id; + + /// + /// Whether the published workflow definition is usable as an activity. + /// + public bool UsableAsActivity { get; } = usableAsActivity; } \ No newline at end of file diff --git a/src/modules/Elsa.Scheduling/Services/DefaultTriggerScheduler.cs b/src/modules/Elsa.Scheduling/Services/DefaultTriggerScheduler.cs index 50735ab85..b626d72c0 100644 --- a/src/modules/Elsa.Scheduling/Services/DefaultTriggerScheduler.cs +++ b/src/modules/Elsa.Scheduling/Services/DefaultTriggerScheduler.cs @@ -91,7 +91,7 @@ public class DefaultTriggerScheduler(IWorkflowScheduler workflowScheduler, ISyst } catch (FormatException ex) { - logger.LogWarning($"Cron expression format error: {ex.Message}. CronExpression: {cronExpression}"); + logger.LogWarning(ex,"Cron expression format error: {exceptionMessage}. CronExpression: {cronExpression}", ex.Message, cronExpression); } } } diff --git a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs index c8d77efbd..3af335fbe 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs @@ -5,6 +5,7 @@ using Elsa.Expressions.Models; using Elsa.Extensions; using Elsa.Workflows.Activities; using Elsa.Workflows.Contracts; +using Elsa.Workflows.Exceptions; using Elsa.Workflows.Memory; using Elsa.Workflows.Models; using Elsa.Workflows.Options; @@ -580,7 +581,7 @@ public partial class WorkflowExecutionContext : IExecutionContext /// public async Task CreateActivityExecutionContext(IActivity activity, ActivityInvocationOptions? options = default) { - var activityDescriptor = await ActivityRegistryLookup.Find(activity) ?? throw new Exception($"Activity with type {activity.Type} not found in registry"); + var activityDescriptor = await ActivityRegistryLookup.Find(activity) ?? throw new ActivityNotFoundException(activity.Type); var tag = options?.Tag; var parentContext = options?.Owner; var parentExpressionExecutionContext = parentContext?.ExpressionExecutionContext ?? ExpressionExecutionContext; diff --git a/src/modules/Elsa.Workflows.Core/Contracts/IActivityRegistry.cs b/src/modules/Elsa.Workflows.Core/Contracts/IActivityRegistry.cs index c3ed1630f..4a7f44287 100644 --- a/src/modules/Elsa.Workflows.Core/Contracts/IActivityRegistry.cs +++ b/src/modules/Elsa.Workflows.Core/Contracts/IActivityRegistry.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Elsa.Workflows.Models; namespace Elsa.Workflows.Contracts; @@ -74,7 +75,7 @@ public interface IActivityRegistry : IActivityProvider /// /// The activity type to register. /// An optional cancellation token. - Task RegisterAsync(Type activityType, CancellationToken cancellationToken = default); + Task RegisterAsync([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type activityType, CancellationToken cancellationToken = default); /// /// Registers multiple activity types. diff --git a/src/modules/Elsa.Workflows.Core/Exceptions/ActivityNotFoundException.cs b/src/modules/Elsa.Workflows.Core/Exceptions/ActivityNotFoundException.cs index b7f2be040..caad78481 100644 --- a/src/modules/Elsa.Workflows.Core/Exceptions/ActivityNotFoundException.cs +++ b/src/modules/Elsa.Workflows.Core/Exceptions/ActivityNotFoundException.cs @@ -5,6 +5,12 @@ namespace Elsa.Workflows.Exceptions; /// public class ActivityNotFoundException : Exception { + /// + public ActivityNotFoundException(string missingTypeName) : base($"Activity type '{missingTypeName}' could not be found.") + { + MissingTypeName = missingTypeName; + } + /// public ActivityNotFoundException(string missingTypeName, int missingTypeVersion) : base($"Activity type '{missingTypeName}' version '{missingTypeVersion}' could not be found.") { diff --git a/src/modules/Elsa.Workflows.Core/Services/ActivityRegistry.cs b/src/modules/Elsa.Workflows.Core/Services/ActivityRegistry.cs index 8e596007b..beaea748f 100644 --- a/src/modules/Elsa.Workflows.Core/Services/ActivityRegistry.cs +++ b/src/modules/Elsa.Workflows.Core/Services/ActivityRegistry.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; using Elsa.Workflows.Contracts; using Elsa.Workflows.Helpers; using Elsa.Workflows.Models; @@ -48,7 +49,7 @@ public class ActivityRegistry(IActivityDescriber activityDescriber, IEnumerable< } /// - public async Task RegisterAsync(Type activityType, CancellationToken cancellationToken) + public async Task RegisterAsync([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type activityType, CancellationToken cancellationToken) { var activityTypeName = ActivityTypeNameHelper.GenerateTypeName(activityType); @@ -70,6 +71,7 @@ public class ActivityRegistry(IActivityDescriber activityDescriber, IEnumerable< /// public ValueTask> GetDescriptorsAsync(CancellationToken cancellationToken = default) => new(_manualActivityDescriptors); + /// public async Task RefreshDescriptors(IEnumerable activityProviders, CancellationToken cancellationToken = default) { var providersDictionary = new ConcurrentDictionary>(); @@ -94,7 +96,7 @@ public class ActivityRegistry(IActivityDescriber activityDescriber, IEnumerable< Add(descriptor, _activityDescriptors, target); } - private void Add(ActivityDescriptor descriptor, ConcurrentDictionary<(string Type, int Version), ActivityDescriptor> activityDescriptors, ICollection providerDescriptors) + private void Add(ActivityDescriptor? descriptor, ConcurrentDictionary<(string Type, int Version), ActivityDescriptor> activityDescriptors, ICollection providerDescriptors) { if (descriptor is null) { diff --git a/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowActivities/AutoUpdateTests.cs b/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowActivities/AutoUpdateTests.cs index 040ff1726..8276dd1ff 100644 --- a/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowActivities/AutoUpdateTests.cs +++ b/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowActivities/AutoUpdateTests.cs @@ -3,7 +3,6 @@ using Elsa.Http; using Elsa.Http.Bookmarks; using Elsa.Http.Contracts; using Elsa.Workflows.Contracts; -using Elsa.Workflows.Helpers; using Elsa.Workflows.Management.Contracts; using Elsa.Workflows.Runtime.Filters; using Microsoft.Extensions.Caching.Memory; @@ -14,7 +13,6 @@ namespace Elsa.Workflows.ComponentTests.Scenarios.WorkflowActivities; public class AutoUpdateTests : AppComponentTest { private readonly IMemoryCache _cache; - private readonly IBookmarkHasher _bookmarkHasher; private readonly IHasher _hasher; private readonly IWorkflowDefinitionCacheManager _definitionCacheManager; private readonly IWorkflowDefinitionPublisher _publisher; @@ -26,9 +24,9 @@ public class AutoUpdateTests : AppComponentTest private readonly IHttpWorkflowsCacheManager _httpCacheManager; private readonly IWorkflowDefinitionCacheManager _workflowCacheManager; - private string _httpChangeToken; - private string _triggerChangeToken; - private string _graphChangeToken; + private string? _httpChangeToken; + private string? _triggerChangeToken; + private string? _graphChangeToken; private static readonly object HttpChangeTokenSignal = new(); private static readonly object TriggerChangeTokenSignal = new(); @@ -41,7 +39,6 @@ public class AutoUpdateTests : AppComponentTest public AutoUpdateTests(App app) : base(app) { _cache = Scope.ServiceProvider.GetRequiredService(); - _bookmarkHasher = Scope.ServiceProvider.GetRequiredService(); _hasher = Scope.ServiceProvider.GetRequiredService(); _definitionCacheManager = Scope.ServiceProvider.GetRequiredService(); _publisher = Scope.ServiceProvider.GetRequiredService(); diff --git a/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowActivities/DeleteWorkflowTests.cs b/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowActivities/DeleteWorkflowTests.cs index 249eab3fb..47949871c 100644 --- a/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowActivities/DeleteWorkflowTests.cs +++ b/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowActivities/DeleteWorkflowTests.cs @@ -1,5 +1,4 @@ using Elsa.Workflows.Contracts; -using Elsa.Workflows.Management.Activities.WorkflowDefinitionActivity; using Elsa.Workflows.Management.Contracts; using Microsoft.Extensions.DependencyInjection; @@ -28,43 +27,42 @@ public class DeleteWorkflowTests : AppComponentTest [Fact] public async Task DeleteWorkflow() { - EnsureWorkflowInRegistry(_scope1); + EnsureWorkflowInRegistry(_scope1, Workflows.DeleteWorkflow.Type); var workflowDefinitionManager = _scope1.ServiceProvider.GetRequiredService(); await workflowDefinitionManager.DeleteByDefinitionIdAsync(Workflows.DeleteWorkflow.DefinitionId); - WorkflowTypeDeletedFromRegistry(_scope1); + WorkflowTypeDeletedFromRegistry(_scope1, Workflows.DeleteWorkflow.Type); } [Fact(Skip = "Clustered tests are interfering with other event driven tests")] public async Task DeleteWorkflow_Clustered() { - EnsureWorkflowInRegistry(_scope1); - EnsureWorkflowInRegistry(_scope2); - EnsureWorkflowInRegistry(_scope3); + EnsureWorkflowInRegistry(_scope1, Workflows.DeleteWorkflowClustered.Type); + EnsureWorkflowInRegistry(_scope2, Workflows.DeleteWorkflowClustered.Type); + EnsureWorkflowInRegistry(_scope3, Workflows.DeleteWorkflowClustered.Type); var workflowDefinitionManager = _scope1.ServiceProvider.GetRequiredService(); - await workflowDefinitionManager.DeleteByDefinitionIdAsync(Workflows.DeleteWorkflow.DefinitionId); + await workflowDefinitionManager.DeleteByDefinitionIdAsync(Workflows.DeleteWorkflowClustered.DefinitionId); - WorkflowTypeDeletedFromRegistry(_scope1); + WorkflowTypeDeletedFromRegistry(_scope1, Workflows.DeleteWorkflowClustered.Type); await _signalManager.WaitAsync(WorkflowDeletedSignal); - WorkflowTypeDeletedFromRegistry(_scope2); - WorkflowTypeDeletedFromRegistry(_scope3); + WorkflowTypeDeletedFromRegistry(_scope2, Workflows.DeleteWorkflowClustered.Type); + WorkflowTypeDeletedFromRegistry(_scope3, Workflows.DeleteWorkflowClustered.Type); } - private void EnsureWorkflowInRegistry(IServiceScope scope) + private static void EnsureWorkflowInRegistry(IServiceScope scope, string type) { var activityRegistry = scope.ServiceProvider.GetRequiredService(); - var descriptor = activityRegistry.Find(Workflows.DeleteWorkflow.Type); - if (descriptor is null) - activityRegistry.Add(typeof(WorkflowDefinitionActivityProvider), descriptor); + var descriptor = activityRegistry.Find(type); + Assert.NotNull(descriptor); } - private void WorkflowTypeDeletedFromRegistry(IServiceScope scope) + private static void WorkflowTypeDeletedFromRegistry(IServiceScope scope, string type) { var activityRegistry = scope.ServiceProvider.GetRequiredService(); - var descriptor = activityRegistry.Find(Workflows.DeleteWorkflow.Type); + var descriptor = activityRegistry.Find(type); Assert.Null(descriptor); } diff --git a/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowActivities/Workflows/DeleteWorkflow_Clustered.cs b/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowActivities/Workflows/DeleteWorkflow_Clustered.cs new file mode 100644 index 000000000..00b1722eb --- /dev/null +++ b/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowActivities/Workflows/DeleteWorkflow_Clustered.cs @@ -0,0 +1,26 @@ +using Elsa.Scheduling.Activities; +using Elsa.Workflows.Activities; +using Elsa.Workflows.Contracts; + +namespace Elsa.Workflows.ComponentTests.Scenarios.WorkflowActivities.Workflows; + +public class DeleteWorkflowClustered : WorkflowBase +{ + public static readonly string DefinitionId = Guid.NewGuid().ToString(); + public static readonly string Type = nameof(DeleteWorkflowClustered); + + protected override void Build(IWorkflowBuilder builder) + { + builder.Name = Type; + builder.WithDefinitionId(DefinitionId); + builder.WorkflowOptions.UsableAsActivity = true; + builder.Root = new Sequence + { + Activities = + { + new Delay(TimeSpan.FromMilliseconds(250)), + new WriteLine("This workflow will be deleted!") + } + }; + } +} \ No newline at end of file