diff --git a/Elsa.sln.DotSettings b/Elsa.sln.DotSettings index 19414abf6..b291186ee 100644 --- a/Elsa.sln.DotSettings +++ b/Elsa.sln.DotSettings @@ -12,10 +12,12 @@ EF True True + True True True True True + True True True True diff --git a/src/modules/Elsa.Http/Handlers/InvalidateHttpWorkflowsCache.cs b/src/modules/Elsa.Http/Handlers/InvalidateHttpWorkflowsCache.cs index bb87afe99..f07a32072 100644 --- a/src/modules/Elsa.Http/Handlers/InvalidateHttpWorkflowsCache.cs +++ b/src/modules/Elsa.Http/Handlers/InvalidateHttpWorkflowsCache.cs @@ -95,10 +95,8 @@ public class InvalidateHttpWorkflowsCache( /// public async Task HandleAsync(WorkflowDefinitionsReloaded notification, CancellationToken cancellationToken) { - foreach (var workflowDefinitionId in notification.WorkflowDefinitionIds) - { - await InvalidateCacheAsync(workflowDefinitionId); - } + foreach (var reloadedWorkflowDefinition in notification.ReloadedWorkflowDefinitions) + await InvalidateCacheAsync(reloadedWorkflowDefinition.DefinitionId); } private async Task InvalidateCacheAsync(string workflowDefinitionId) @@ -119,9 +117,9 @@ public class InvalidateHttpWorkflowsCache( private async Task InvalidateTriggerCacheAsync(IEnumerable triggers, CancellationToken cancellationToken) { - foreach (StoredTrigger trigger in triggers) + foreach (var trigger in triggers) { - if (trigger?.Payload is HttpEndpointBookmarkPayload httpPayload) + if (trigger.Payload is HttpEndpointBookmarkPayload httpPayload) { var hash = httpWorkflowsCacheManager.ComputeBookmarkHash(httpPayload.Path, httpPayload.Method); await httpWorkflowsCacheManager.EvictTriggerAsync(hash, cancellationToken); diff --git a/src/modules/Elsa.MassTransit/Consumers/WorkflowDefinitionEventsConsumer.cs b/src/modules/Elsa.MassTransit/Consumers/WorkflowDefinitionEventsConsumer.cs index d43d11172..4f7821a63 100644 --- a/src/modules/Elsa.MassTransit/Consumers/WorkflowDefinitionEventsConsumer.cs +++ b/src/modules/Elsa.MassTransit/Consumers/WorkflowDefinitionEventsConsumer.cs @@ -92,7 +92,7 @@ public class WorkflowDefinitionEventsConsumer(IWorkflowDefinitionActivityRegistr public async Task Consume(ConsumeContext context) { var message = context.Message; - var notification = new Elsa.Workflows.Runtime.Notifications.WorkflowDefinitionsReloaded(message.WorkflowDefinitionIds); + var notification = new Elsa.Workflows.Runtime.Notifications.WorkflowDefinitionsReloaded(message.ReloadedWorkflowDefinitions); AmbientConsumerScope.IsWorkflowDefinitionEventsConsumer = true; await notificationSender.SendAsync(notification, context.CancellationToken); AmbientConsumerScope.IsWorkflowDefinitionEventsConsumer = false; diff --git a/src/modules/Elsa.MassTransit/Handlers/DistributedWorkflowDefinitionNotificationsHandler.cs b/src/modules/Elsa.MassTransit/Handlers/DistributedWorkflowDefinitionNotificationsHandler.cs index 991eb0914..10e654479 100644 --- a/src/modules/Elsa.MassTransit/Handlers/DistributedWorkflowDefinitionNotificationsHandler.cs +++ b/src/modules/Elsa.MassTransit/Handlers/DistributedWorkflowDefinitionNotificationsHandler.cs @@ -88,8 +88,8 @@ public class DistributedWorkflowDefinitionNotificationsHandler(IBus bus) : if (AmbientConsumerScope.IsWorkflowDefinitionEventsConsumer) return Task.CompletedTask; - var definitionIds = notification.WorkflowDefinitionIds; - var message = new Distributed.WorkflowDefinitionsReloaded(definitionIds); + var reloadedWorkflowDefinitions = notification.ReloadedWorkflowDefinitions; + var message = new Distributed.WorkflowDefinitionsReloaded(reloadedWorkflowDefinitions); return bus.Publish(message, cancellationToken); } } \ No newline at end of file diff --git a/src/modules/Elsa.MassTransit/Messages/WorkflowDefinitionsReloaded.cs b/src/modules/Elsa.MassTransit/Messages/WorkflowDefinitionsReloaded.cs index bba459390..138492683 100644 --- a/src/modules/Elsa.MassTransit/Messages/WorkflowDefinitionsReloaded.cs +++ b/src/modules/Elsa.MassTransit/Messages/WorkflowDefinitionsReloaded.cs @@ -1,8 +1,10 @@ -namespace Elsa.MassTransit.Messages; +using Elsa.Workflows.Runtime.Models; + +namespace Elsa.MassTransit.Messages; /// Represents a message that indicates that the specified workflow definitions have been reloaded. -public class WorkflowDefinitionsReloaded(ICollection workflowDefinitionIds) +public class WorkflowDefinitionsReloaded(ICollection reloadedWorkflowDefinitions) { - /// The workflow definition IDs that have been reloaded. - public ICollection WorkflowDefinitionIds { get; set; } = workflowDefinitionIds; + /// The reloaded workflow definitions. + public ICollection ReloadedWorkflowDefinitions { get; set; } = reloadedWorkflowDefinitions; } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Management/Contracts/IWorkflowDefinitionActivityRegistryUpdater.cs b/src/modules/Elsa.Workflows.Management/Contracts/IWorkflowDefinitionActivityRegistryUpdater.cs index 454ac3dbe..826b37c8c 100644 --- a/src/modules/Elsa.Workflows.Management/Contracts/IWorkflowDefinitionActivityRegistryUpdater.cs +++ b/src/modules/Elsa.Workflows.Management/Contracts/IWorkflowDefinitionActivityRegistryUpdater.cs @@ -8,9 +8,9 @@ public interface IWorkflowDefinitionActivityRegistryUpdater /// /// Tries to add a workflow as an activity to the registry. /// - /// The ID of the workflow definition. + /// The version ID of the workflow definition. /// The cancellation token. - Task AddToRegistry(string workflowDefinitionId, CancellationToken cancellationToken = default); + Task AddToRegistry(string workflowDefinitionVersionId, CancellationToken cancellationToken = default); /// /// Removes workflow definition activities from the . @@ -18,7 +18,6 @@ public interface IWorkflowDefinitionActivityRegistryUpdater /// The ID of the workflow definition to remove. void RemoveDefinitionFromRegistry(string workflowDefinitionId); - /// /// Removes a workflow definition version activity from the . /// diff --git a/src/modules/Elsa.Workflows.Management/Handlers/DeleteWorkflowInstances.cs b/src/modules/Elsa.Workflows.Management/Handlers/DeleteWorkflowInstances.cs index 1a0178476..66141f945 100644 --- a/src/modules/Elsa.Workflows.Management/Handlers/DeleteWorkflowInstances.cs +++ b/src/modules/Elsa.Workflows.Management/Handlers/DeleteWorkflowInstances.cs @@ -2,12 +2,14 @@ using Elsa.Mediator.Contracts; using Elsa.Workflows.Management.Contracts; using Elsa.Workflows.Management.Filters; using Elsa.Workflows.Management.Notifications; +using JetBrains.Annotations; namespace Elsa.Workflows.Management.Handlers; /// /// Deletes workflow instances when a workflow definition or version is deleted. /// +[UsedImplicitly] public class DeleteWorkflowInstances : INotificationHandler, INotificationHandler, diff --git a/src/modules/Elsa.Workflows.Runtime/Contracts/IWorkflowDefinitionStorePopulator.cs b/src/modules/Elsa.Workflows.Runtime/Contracts/IWorkflowDefinitionStorePopulator.cs index aeb5c198b..5cdf956dc 100644 --- a/src/modules/Elsa.Workflows.Runtime/Contracts/IWorkflowDefinitionStorePopulator.cs +++ b/src/modules/Elsa.Workflows.Runtime/Contracts/IWorkflowDefinitionStorePopulator.cs @@ -1,4 +1,5 @@ using Elsa.Workflows.Management.Contracts; +using Elsa.Workflows.Management.Entities; using Elsa.Workflows.Runtime.Models; namespace Elsa.Workflows.Runtime.Contracts; @@ -12,27 +13,27 @@ public interface IWorkflowDefinitionStorePopulator /// Populates the with workflow definitions provided from implementations. /// /// The cancellation token. - Task> PopulateStoreAsync(CancellationToken cancellationToken = default); + Task> PopulateStoreAsync(CancellationToken cancellationToken = default); /// /// Populates the with workflow definitions provided from implementations. /// /// Whether to index triggers. /// The cancellation token. - Task> PopulateStoreAsync(bool indexTriggers, CancellationToken cancellationToken = default); + Task> PopulateStoreAsync(bool indexTriggers, CancellationToken cancellationToken = default); /// /// Adds a workflow definition to the store. /// /// A materialized workflow. /// An optional cancellation token. - Task AddAsync(MaterializedWorkflow materializedWorkflow, CancellationToken cancellationToken = default); - + Task AddAsync(MaterializedWorkflow materializedWorkflow, CancellationToken cancellationToken = default); + /// /// Adds a workflow definition to the store. /// /// A materialized workflow. - /// /// Whether to index triggers. + /// Whether to index triggers. /// An optional cancellation token. - Task AddAsync(MaterializedWorkflow materializedWorkflow, bool indexTriggers, CancellationToken cancellationToken = default); + Task AddAsync(MaterializedWorkflow materializedWorkflow, bool indexTriggers, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Contracts/IWorkflowDefinitionsReloader.cs b/src/modules/Elsa.Workflows.Runtime/Contracts/IWorkflowDefinitionsReloader.cs index e2cc2987e..f17d06edf 100644 --- a/src/modules/Elsa.Workflows.Runtime/Contracts/IWorkflowDefinitionsReloader.cs +++ b/src/modules/Elsa.Workflows.Runtime/Contracts/IWorkflowDefinitionsReloader.cs @@ -1,8 +1,8 @@ namespace Elsa.Workflows.Runtime.Contracts; -/// Reloads all workflows by re-invoking the populator. +/// Reloads all workflows by invoking the populator. public interface IWorkflowDefinitionsReloader { - /// Reloads all workflows by re-invoking the populator. + /// Reloads all workflows by invoking the populator. Task ReloadWorkflowDefinitionsAsync(CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Contracts/IWorkflowRuntime.cs b/src/modules/Elsa.Workflows.Runtime/Contracts/IWorkflowRuntime.cs index a60d31f88..be218983b 100644 --- a/src/modules/Elsa.Workflows.Runtime/Contracts/IWorkflowRuntime.cs +++ b/src/modules/Elsa.Workflows.Runtime/Contracts/IWorkflowRuntime.cs @@ -15,7 +15,7 @@ namespace Elsa.Workflows.Runtime.Contracts; public interface IWorkflowRuntime { /// - /// Returns a value whether or not the specified workflow definition can create a new instance. + /// Returns a value whether the specified workflow definition can create a new instance. /// Task CanStartWorkflowAsync(string definitionId, StartWorkflowRuntimeParams? options = default); diff --git a/src/modules/Elsa.Workflows.Runtime/Features/CachingWorkflowRuntimeFeature.cs b/src/modules/Elsa.Workflows.Runtime/Features/CachingWorkflowRuntimeFeature.cs index 9c0e26e73..bb31de5d4 100644 --- a/src/modules/Elsa.Workflows.Runtime/Features/CachingWorkflowRuntimeFeature.cs +++ b/src/modules/Elsa.Workflows.Runtime/Features/CachingWorkflowRuntimeFeature.cs @@ -26,6 +26,7 @@ public class CachingWorkflowRuntimeFeature : FeatureBase .Decorate() // Handlers. - .AddNotificationHandler(); + .AddNotificationHandler() + .AddNotificationHandler(); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs b/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs index 75a41922e..6bdbb7d99 100644 --- a/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs +++ b/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs @@ -7,7 +7,6 @@ using Elsa.Features.Attributes; using Elsa.Features.Services; using Elsa.Workflows.Contracts; using Elsa.Workflows.Management.Contracts; -using Elsa.Workflows.Management.Handlers; using Elsa.Workflows.Management.Services; using Elsa.Workflows.Runtime.ActivationValidators; using Elsa.Workflows.Runtime.Contracts; @@ -272,12 +271,12 @@ public class WorkflowRuntimeFeature : FeatureBase .AddNotificationHandler() .AddNotificationHandler() .AddNotificationHandler() - .AddNotificationHandler() .AddNotificationHandler() .AddNotificationHandler() .AddNotificationHandler() .AddNotificationHandler() .AddNotificationHandler() + .AddNotificationHandler() // Workflow activation strategies. .AddScoped() diff --git a/src/modules/Elsa.Workflows.Runtime/Handlers/InvalidateWorkflowsCache.cs b/src/modules/Elsa.Workflows.Runtime/Handlers/InvalidateWorkflowsCache.cs index fb6dc6d45..d2d52613c 100644 --- a/src/modules/Elsa.Workflows.Runtime/Handlers/InvalidateWorkflowsCache.cs +++ b/src/modules/Elsa.Workflows.Runtime/Handlers/InvalidateWorkflowsCache.cs @@ -6,7 +6,7 @@ using JetBrains.Annotations; namespace Elsa.Workflows.Runtime.Handlers; /// -/// A notification handler that invalidates workflows cache when workflow definitions are reloaded. +/// A notification handler that invalidates the workflow cache when workflow definitions are reloaded. /// /// /// The class implements the INotificationHandler interface and is responsible for handling WorkflowDefinitionsReloaded notifications. @@ -18,9 +18,7 @@ public class InvalidateWorkflowsCache(IWorkflowDefinitionCacheManager workflowDe /// public async Task HandleAsync(WorkflowDefinitionsReloaded notification, CancellationToken cancellationToken) { - foreach (var workflowDefinitionId in notification.WorkflowDefinitionIds) - { - await workflowDefinitionCacheManager.EvictWorkflowDefinitionAsync(workflowDefinitionId, cancellationToken); - } + foreach (var reloadedWorkflowDefinition in notification.ReloadedWorkflowDefinitions) + await workflowDefinitionCacheManager.EvictWorkflowDefinitionAsync(reloadedWorkflowDefinition.DefinitionId, cancellationToken); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Handlers/RefreshActivityRegistry.cs b/src/modules/Elsa.Workflows.Runtime/Handlers/RefreshActivityRegistry.cs new file mode 100644 index 000000000..607751270 --- /dev/null +++ b/src/modules/Elsa.Workflows.Runtime/Handlers/RefreshActivityRegistry.cs @@ -0,0 +1,31 @@ +using Elsa.Mediator.Contracts; +using Elsa.Workflows.Contracts; +using Elsa.Workflows.Management.Activities.WorkflowDefinitionActivity; +using Elsa.Workflows.Management.Contracts; +using Elsa.Workflows.Management.Entities; +using Elsa.Workflows.Runtime.Notifications; +using JetBrains.Annotations; + +namespace Elsa.Workflows.Runtime.Handlers; + +/// Refreshes the for the provider whenever workflow definitions are reloaded. +[PublicAPI] +public class RefreshActivityRegistry(IWorkflowDefinitionActivityRegistryUpdater workflowDefinitionActivityRegistryUpdater) : INotificationHandler +{ + /// + public async Task HandleAsync(WorkflowDefinitionsReloaded notification, CancellationToken cancellationToken) + { + foreach (var reloadedWorkflowDefinition in notification.ReloadedWorkflowDefinitions) + await UpdateDefinition(reloadedWorkflowDefinition.DefinitionVersionId, reloadedWorkflowDefinition.UsableAsActivity); + } + + private Task UpdateDefinition(string definitionVersionId, bool? usableAsActivity) + { + // A workflow should remain in the activity registry unless no longer being marked as an activity. + if (usableAsActivity.GetValueOrDefault()) + return workflowDefinitionActivityRegistryUpdater.AddToRegistry(definitionVersionId); + + workflowDefinitionActivityRegistryUpdater.RemoveDefinitionVersionFromRegistry(definitionVersionId); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Models/ReloadedWorkflowDefinition.cs b/src/modules/Elsa.Workflows.Runtime/Models/ReloadedWorkflowDefinition.cs new file mode 100644 index 000000000..7a30f1f3b --- /dev/null +++ b/src/modules/Elsa.Workflows.Runtime/Models/ReloadedWorkflowDefinition.cs @@ -0,0 +1,30 @@ +using Elsa.Workflows.Management.Entities; + +namespace Elsa.Workflows.Runtime.Models; + +/// +/// Represents a reloaded workflow definition with necessary properties for +/// identification, versioning, and usability status as an activity. +/// +/// The unique identifier for the workflow definition. +/// The unique identifier for the specific version of the workflow definition. +/// The version number of the workflow definition. +/// Indicates whether the workflow definition can be used as an activity. +public record ReloadedWorkflowDefinition(string DefinitionId, string DefinitionVersionId, int Version, bool UsableAsActivity) +{ + /// + /// Creates an instance of from a given . + /// + /// The workflow definition used to create the reloaded workflow definition. + /// A new instance of . + public static ReloadedWorkflowDefinition FromDefinition(WorkflowDefinition workflowDefinition) + { + return new ReloadedWorkflowDefinition + ( + workflowDefinition.DefinitionId, + workflowDefinition.Id, + workflowDefinition.Version, + workflowDefinition.Options.UsableAsActivity ?? false + ); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Notifications/WorkflowDefinitionsReloaded.cs b/src/modules/Elsa.Workflows.Runtime/Notifications/WorkflowDefinitionsReloaded.cs index 882fa7a51..336347ff4 100644 --- a/src/modules/Elsa.Workflows.Runtime/Notifications/WorkflowDefinitionsReloaded.cs +++ b/src/modules/Elsa.Workflows.Runtime/Notifications/WorkflowDefinitionsReloaded.cs @@ -1,6 +1,7 @@ using Elsa.Mediator.Contracts; +using Elsa.Workflows.Runtime.Models; namespace Elsa.Workflows.Runtime.Notifications; /// Published when workflow definitions have been reloaded. -public record WorkflowDefinitionsReloaded(ICollection WorkflowDefinitionIds) : INotification; +public record WorkflowDefinitionsReloaded(ICollection ReloadedWorkflowDefinitions) : INotification; \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Services/DefaultWorkflowDefinitionStorePopulator.cs b/src/modules/Elsa.Workflows.Runtime/Services/DefaultWorkflowDefinitionStorePopulator.cs index cba487b5a..fad0f8759 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/DefaultWorkflowDefinitionStorePopulator.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/DefaultWorkflowDefinitionStorePopulator.cs @@ -49,42 +49,47 @@ public class DefaultWorkflowDefinitionStorePopulator : IWorkflowDefinitionStoreP } /// - public Task> PopulateStoreAsync(CancellationToken cancellationToken = default) + public Task> PopulateStoreAsync(CancellationToken cancellationToken = default) { return PopulateStoreAsync(true, cancellationToken); } /// - public async Task> PopulateStoreAsync(bool indexTriggers, CancellationToken cancellationToken = default) + public async Task> PopulateStoreAsync(bool indexTriggers, CancellationToken cancellationToken = default) { var providers = _workflowDefinitionProviders(); - var workflowDefinitionIds = new List(); + var workflowDefinitions = new List(); + foreach (var provider in providers) { var results = await provider.GetWorkflowsAsync(cancellationToken).AsTask().ToList(); - workflowDefinitionIds.AddRange(results.Select(w => w.Workflow.Id)); - - foreach (var result in results) await AddAsync(result, indexTriggers, cancellationToken); + foreach (var result in results) + { + var workflowDefinition = await AddAsync(result, indexTriggers, cancellationToken); + workflowDefinitions.Add(workflowDefinition); + } } - return workflowDefinitionIds; + return workflowDefinitions; } /// - public Task AddAsync(MaterializedWorkflow materializedWorkflow, CancellationToken cancellationToken = default) + public Task AddAsync(MaterializedWorkflow materializedWorkflow, CancellationToken cancellationToken = default) { return AddAsync(materializedWorkflow, true, cancellationToken); } /// - public async Task AddAsync(MaterializedWorkflow materializedWorkflow, bool indexTriggers, CancellationToken cancellationToken = default) + public async Task AddAsync(MaterializedWorkflow materializedWorkflow, bool indexTriggers, CancellationToken cancellationToken = default) { await AssignIdentities(materializedWorkflow.Workflow, cancellationToken); - await AddOrUpdateAsync(materializedWorkflow, cancellationToken); + var workflowDefinition = await AddOrUpdateAsync(materializedWorkflow, cancellationToken); if (indexTriggers) await IndexTriggersAsync(materializedWorkflow, cancellationToken); + + return workflowDefinition; } private async Task AssignIdentities(Workflow workflow, CancellationToken cancellationToken) @@ -92,13 +97,13 @@ public class DefaultWorkflowDefinitionStorePopulator : IWorkflowDefinitionStoreP await _identityGraphService.AssignIdentitiesAsync(workflow, cancellationToken); } - private async Task AddOrUpdateAsync(MaterializedWorkflow materializedWorkflow, CancellationToken cancellationToken = default) + private async Task AddOrUpdateAsync(MaterializedWorkflow materializedWorkflow, CancellationToken cancellationToken = default) { await _semaphore.WaitAsync(cancellationToken); try { - await AddOrUpdateCoreAsync(materializedWorkflow, cancellationToken); + return await AddOrUpdateCoreAsync(materializedWorkflow, cancellationToken); } finally { @@ -106,7 +111,7 @@ public class DefaultWorkflowDefinitionStorePopulator : IWorkflowDefinitionStoreP } } - private async Task AddOrUpdateCoreAsync(MaterializedWorkflow materializedWorkflow, CancellationToken cancellationToken = default) + private async Task AddOrUpdateCoreAsync(MaterializedWorkflow materializedWorkflow, CancellationToken cancellationToken = default) { var workflow = materializedWorkflow.Workflow; var definitionId = workflow.Identity.DefinitionId; @@ -178,7 +183,7 @@ public class DefaultWorkflowDefinitionStorePopulator : IWorkflowDefinitionStoreP if (existingDefinitionVersion is null && workflowDefinitionsToSave.Any(w => w.Id == workflowDefinition.Id)) { _logger.LogInformation("Workflow with ID {WorkflowId} already exists", workflowDefinition.Id); - return; + return workflowDefinition; } workflowDefinitionsToSave.Add(workflowDefinition); @@ -194,7 +199,7 @@ public class DefaultWorkflowDefinitionStorePopulator : IWorkflowDefinitionStoreP } await _workflowDefinitionStore.SaveManyAsync(workflowDefinitionsToSave, cancellationToken); - return; + return workflowDefinition; async Task UpdateIsLatest() { diff --git a/src/modules/Elsa.Workflows.Runtime/Services/WorkflowDefinitionsReloader.cs b/src/modules/Elsa.Workflows.Runtime/Services/WorkflowDefinitionsReloader.cs index 3afadafd2..f1a179e51 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/WorkflowDefinitionsReloader.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/WorkflowDefinitionsReloader.cs @@ -1,5 +1,6 @@ using Elsa.Mediator.Contracts; using Elsa.Workflows.Runtime.Contracts; +using Elsa.Workflows.Runtime.Models; using Elsa.Workflows.Runtime.Notifications; namespace Elsa.Workflows.Runtime.Services; @@ -10,8 +11,9 @@ public class WorkflowDefinitionsReloader(IWorkflowDefinitionStorePopulator workf /// public async Task ReloadWorkflowDefinitionsAsync(CancellationToken cancellationToken) { - var definitionIds = await workflowDefinitionStorePopulator.PopulateStoreAsync(true, cancellationToken); - var notification = new WorkflowDefinitionsReloaded(definitionIds); + var workflowDefinitions = await workflowDefinitionStorePopulator.PopulateStoreAsync(true, cancellationToken); + var reloadedWorkflowDefinitions = workflowDefinitions.Select(ReloadedWorkflowDefinition.FromDefinition).ToList(); + var notification = new WorkflowDefinitionsReloaded(reloadedWorkflowDefinitions); await notificationSender.SendAsync(notification, cancellationToken); } } \ No newline at end of file diff --git a/test/component/Elsa.Workflows.ComponentTests/Elsa.Workflows.ComponentTests.csproj b/test/component/Elsa.Workflows.ComponentTests/Elsa.Workflows.ComponentTests.csproj index c6588808f..deb69e04d 100644 --- a/test/component/Elsa.Workflows.ComponentTests/Elsa.Workflows.ComponentTests.csproj +++ b/test/component/Elsa.Workflows.ComponentTests/Elsa.Workflows.ComponentTests.csproj @@ -21,9 +21,6 @@ - - Always - Always @@ -81,6 +78,9 @@ Always + + Always + diff --git a/test/component/Elsa.Workflows.ComponentTests/Helpers/Fixtures/WorkflowServer.cs b/test/component/Elsa.Workflows.ComponentTests/Helpers/Fixtures/WorkflowServer.cs index aaaa665b4..a026a35a4 100644 --- a/test/component/Elsa.Workflows.ComponentTests/Helpers/Fixtures/WorkflowServer.cs +++ b/test/component/Elsa.Workflows.ComponentTests/Helpers/Fixtures/WorkflowServer.cs @@ -6,8 +6,11 @@ using Elsa.Extensions; using Elsa.Identity.Providers; using Elsa.MassTransit.Extensions; using Elsa.Workflows.ComponentTests.Consumers; +using Elsa.Workflows.ComponentTests.Helpers.Materializers; using Elsa.Workflows.ComponentTests.Helpers.Services; +using Elsa.Workflows.ComponentTests.Helpers.WorkflowProviders; using Elsa.Workflows.ComponentTests.Services; +using Elsa.Workflows.Management.Contracts; using FluentStorage; using Hangfire.Annotations; using Microsoft.AspNetCore.Hosting; @@ -94,11 +97,15 @@ public class WorkflowServer(Infrastructure infrastructure, string url) : WebAppl builder.ConfigureTestServices(services => { - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddNotificationHandlersFrom(); + services + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddScoped() + .AddNotificationHandlersFrom() + .AddWorkflowDefinitionProvider() + ; }); } diff --git a/test/component/Elsa.Workflows.ComponentTests/Helpers/Materializers/TestWorkflowMaterializer.cs b/test/component/Elsa.Workflows.ComponentTests/Helpers/Materializers/TestWorkflowMaterializer.cs new file mode 100644 index 000000000..e011422c4 --- /dev/null +++ b/test/component/Elsa.Workflows.ComponentTests/Helpers/Materializers/TestWorkflowMaterializer.cs @@ -0,0 +1,26 @@ +using Elsa.Workflows.Activities; +using Elsa.Workflows.ComponentTests.Helpers.WorkflowProviders; +using Elsa.Workflows.Management.Contracts; +using Elsa.Workflows.Management.Entities; +using Elsa.Workflows.Runtime.Contracts; + +namespace Elsa.Workflows.ComponentTests.Helpers.Materializers; + +/// A workflow materializer that deserializes workflows created from . +public class TestWorkflowMaterializer(IEnumerable workflowProviders) : IWorkflowMaterializer +{ + /// The name of the materializer. + public const string MaterializerName = "Test"; + + /// + public string Name => MaterializerName; + + /// + public ValueTask MaterializeAsync(WorkflowDefinition definition, CancellationToken cancellationToken = default) + { + var testProvider = (TestWorkflowProvider)workflowProviders.Single(x => x is TestWorkflowProvider); + var materializedWorkflow = testProvider.MaterializedWorkflows.First(x => x.Workflow.Identity.Id == definition.Id); + + return ValueTask.FromResult(materializedWorkflow.Workflow); + } +} \ No newline at end of file diff --git a/test/component/Elsa.Workflows.ComponentTests/Helpers/WorkflowProviders/TestWorkflowProvider.cs b/test/component/Elsa.Workflows.ComponentTests/Helpers/WorkflowProviders/TestWorkflowProvider.cs new file mode 100644 index 000000000..5473cf8eb --- /dev/null +++ b/test/component/Elsa.Workflows.ComponentTests/Helpers/WorkflowProviders/TestWorkflowProvider.cs @@ -0,0 +1,14 @@ +using Elsa.Workflows.Runtime.Contracts; +using Elsa.Workflows.Runtime.Models; + +namespace Elsa.Workflows.ComponentTests.Helpers.WorkflowProviders; + +public class TestWorkflowProvider : IWorkflowProvider +{ + public string Name => "Test"; + public ICollection MaterializedWorkflows { get; set; } = new List(); + public ValueTask> GetWorkflowsAsync(CancellationToken cancellationToken = default) + { + return new(MaterializedWorkflows); + } +} \ No newline at end of file diff --git a/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowDefinitionReload/ReloadWorkflowTests.cs b/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowDefinitionReload/ReloadWorkflowTests.cs new file mode 100644 index 000000000..120f0fe42 --- /dev/null +++ b/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowDefinitionReload/ReloadWorkflowTests.cs @@ -0,0 +1,113 @@ +using System.Net; +using Elsa.Common.Models; +using Elsa.Workflows.Activities; +using Elsa.Workflows.ComponentTests.Helpers.Materializers; +using Elsa.Workflows.ComponentTests.Helpers.WorkflowProviders; +using Elsa.Workflows.Contracts; +using Elsa.Workflows.Management; +using Elsa.Workflows.Management.Contracts; +using Elsa.Workflows.Management.Materializers; +using Elsa.Workflows.Runtime.Contracts; +using Elsa.Workflows.Runtime.Models; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Workflows.ComponentTests.Scenarios.WorkflowDefinitionReload; + +public class ReloadWorkflowTests : AppComponentTest +{ + private readonly IWorkflowDefinitionManager _workflowDefinitionManager; + private readonly IWorkflowDefinitionsReloader _workflowDefinitionsReloader; + private readonly IWorkflowBuilderFactory _workflowBuilderFactory; + private readonly TestWorkflowProvider _testWorkflowProvider; + private readonly IWorkflowDefinitionService _workflowDefinitionService; + private readonly IActivityRegistry _activityRegistry; + + public ReloadWorkflowTests(App app) : base(app) + { + _workflowDefinitionManager = Scope.ServiceProvider.GetRequiredService(); + _workflowDefinitionsReloader = Scope.ServiceProvider.GetRequiredService(); + _workflowBuilderFactory = Scope.ServiceProvider.GetRequiredService(); + _workflowDefinitionService = Scope.ServiceProvider.GetRequiredService(); + _activityRegistry = Scope.ServiceProvider.GetRequiredService(); + var workflowProviders = Scope.ServiceProvider.GetRequiredService>(); + _testWorkflowProvider = (TestWorkflowProvider)workflowProviders.First(x => x is TestWorkflowProvider); + } + + [Fact] + public async Task Reloading_AfterRemovingTheWorkflow_ShouldMakeWorkflowReachableAgain() + { + var client = WorkflowServer.CreateHttpWorkflowClient(); + await _workflowDefinitionManager.DeleteByDefinitionIdAsync("f68b09bc-2013-4617-b82f-d76b6819a624", CancellationToken.None); + var firstResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "reload-test")); + await _workflowDefinitionsReloader.ReloadWorkflowDefinitionsAsync(CancellationToken.None); + var secondResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "reload-test")); + Assert.Equal(HttpStatusCode.NotFound, firstResponse.StatusCode); + Assert.Equal(HttpStatusCode.OK, secondResponse.StatusCode); + } + + [Fact] + public async Task Reloading_AfterUpdatingSourceProvider_ShouldRefreshCaches() + { + var definitionId = Guid.NewGuid().ToString(); + var definitionVersionId1 = Guid.NewGuid().ToString(); + var workflowV1 = await BuildWorkflowAsync(definitionId, definitionVersionId1, 1); + + // Set up the initial workflow version. + _testWorkflowProvider.MaterializedWorkflows = [workflowV1]; + await _workflowDefinitionsReloader.ReloadWorkflowDefinitionsAsync(); + var definitionV1 = await _workflowDefinitionService.FindWorkflowGraphAsync(definitionId, VersionOptions.Latest); + Assert.Equal(definitionVersionId1, definitionV1!.Workflow.Identity.Id); + + // Simulate the workflow provider to have a new version available. + var definitionVersionId2 = Guid.NewGuid().ToString(); + var workflowV2 = await BuildWorkflowAsync(definitionId, definitionVersionId2, 2); + _testWorkflowProvider.MaterializedWorkflows = [workflowV1, workflowV2]; + + // Reload the workflow definitions. + await _workflowDefinitionsReloader.ReloadWorkflowDefinitionsAsync(); + + // Assert that the workflow definition service finds the updated workflow version. + var definitionV2 = await _workflowDefinitionService.FindWorkflowGraphAsync(definitionId, VersionOptions.Latest); + Assert.Equal(definitionVersionId2, definitionV2!.Workflow.Identity.Id); + } + + [Fact] + public async Task Reloading_AfterUpdatingSourceProvider_ShouldRefreshActivityRegistry() + { + var definitionId = Guid.NewGuid().ToString(); + var definitionVersionId1 = Guid.NewGuid().ToString(); + var workflowV1 = await BuildWorkflowAsync(definitionId, definitionVersionId1, 1); + + // Set up the initial workflow version. + _testWorkflowProvider.MaterializedWorkflows = [workflowV1]; + await _workflowDefinitionsReloader.ReloadWorkflowDefinitionsAsync(); + var activityV1 = _activityRegistry.Find(workflowV1.Workflow.Name!); + Assert.Equal(1, activityV1!.Version); + + // Simulate the workflow provider to have a new version available. + var definitionVersionId2 = Guid.NewGuid().ToString(); + var workflowV2 = await BuildWorkflowAsync(definitionId, definitionVersionId2, 2); + _testWorkflowProvider.MaterializedWorkflows = [workflowV1, workflowV2]; + + // Reload the workflow definitions. + await _workflowDefinitionsReloader.ReloadWorkflowDefinitionsAsync(); + + // Assert that the activity registry contains a new activity descriptor representing the new workflow version. + var activityV2 = _activityRegistry.Find(workflowV2.Workflow.Name!)!; + Assert.Equal(2, activityV2.Version); + } + + private async Task BuildWorkflowAsync(string definitionId, string definitionVersionId, int version) + { + var builder = _workflowBuilderFactory.CreateBuilder(); + builder.DefinitionId = definitionId; + builder.Id = definitionVersionId; + builder.Version = version; + builder.Name = Guid.NewGuid().ToString(); + builder.Root = new WriteLine($"Version {version}"); + builder.WorkflowOptions.UsableAsActivity = true; + var workflow = await builder.BuildWorkflowAsync(); + workflow.Name = builder.Name; + return new MaterializedWorkflow(workflow, _testWorkflowProvider.Name, TestWorkflowMaterializer.MaterializerName); + } +} \ No newline at end of file diff --git a/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowDefinitionReload/RemoveReloadWorkflowTests.cs b/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowDefinitionReload/RemoveReloadWorkflowTests.cs deleted file mode 100644 index 390c58d14..000000000 --- a/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowDefinitionReload/RemoveReloadWorkflowTests.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Net; -using Elsa.Workflows.Management.Contracts; -using Elsa.Workflows.Runtime.Contracts; -using Microsoft.Extensions.DependencyInjection; - -namespace Elsa.Workflows.ComponentTests.Scenarios.WorkflowDefinitionReload; - -public class RemoveReloadWorkflowTests : AppComponentTest -{ - private readonly IWorkflowDefinitionManager _workflowDefinitionManager; - private readonly IWorkflowDefinitionsReloader _workflowDefinitionsReloader; - - public RemoveReloadWorkflowTests(App app) : base(app) - { - _workflowDefinitionManager = Scope.ServiceProvider.GetRequiredService(); - _workflowDefinitionsReloader = Scope.ServiceProvider.GetRequiredService(); - } - - [Fact] - public async Task RemovingTheWorkflowThenReload_WorkflowShouldBeReachableAgain() - { - var client = WorkflowServer.CreateHttpWorkflowClient(); - - var result = await _workflowDefinitionManager.DeleteByDefinitionIdAsync("f68b09bc-2013-4617-b82f-d76b6819a624", CancellationToken.None); - - var firstResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "reload-test")); - - await _workflowDefinitionsReloader.ReloadWorkflowDefinitionsAsync(CancellationToken.None); - - var secondResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "reload-test")); - - Assert.Equal(HttpStatusCode.NotFound, firstResponse.StatusCode); - Assert.Equal(HttpStatusCode.OK, secondResponse.StatusCode); - } -} \ No newline at end of file diff --git a/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowDefinitionReload/http-workflow.json b/test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowDefinitionReload/Workflows/http-workflow.json similarity index 100% rename from test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowDefinitionReload/http-workflow.json rename to test/component/Elsa.Workflows.ComponentTests/Scenarios/WorkflowDefinitionReload/Workflows/http-workflow.json