diff --git a/src/modules/Elsa.WorkflowProviders.BlobStorage/Extensions/ModuleExtensions.cs b/src/modules/Elsa.WorkflowProviders.BlobStorage/Extensions/ModuleExtensions.cs index 3fb0c3347..85faeb9d8 100644 --- a/src/modules/Elsa.WorkflowProviders.BlobStorage/Extensions/ModuleExtensions.cs +++ b/src/modules/Elsa.WorkflowProviders.BlobStorage/Extensions/ModuleExtensions.cs @@ -29,7 +29,7 @@ public static class ModuleExtensions /// The module. /// The configuration delegate. /// The module. - public static IModule UseFluentStorageProvider(this IModule module, Action? configure = default) + public static IModule UseFluentStorageProvider(this IModule module, Action? configure = null) { module.Use(configure); return module; diff --git a/src/modules/Elsa.Workflows.Core/Builders/WorkflowBuilder.cs b/src/modules/Elsa.Workflows.Core/Builders/WorkflowBuilder.cs index fb48a93b8..b30d7669e 100644 --- a/src/modules/Elsa.Workflows.Core/Builders/WorkflowBuilder.cs +++ b/src/modules/Elsa.Workflows.Core/Builders/WorkflowBuilder.cs @@ -63,6 +63,12 @@ public class WorkflowBuilder(IActivityVisitor activityVisitor, IIdentityGraphSer DefinitionId = definitionId; return this; } + + public IWorkflowBuilder WithId(string id) + { + Id = id; + return this; + } /// public IWorkflowBuilder WithTenantId(string tenantId) diff --git a/src/modules/Elsa.Workflows.Core/Contracts/IWorkflowBuilder.cs b/src/modules/Elsa.Workflows.Core/Contracts/IWorkflowBuilder.cs index 363fcbf11..fc9a78cdb 100644 --- a/src/modules/Elsa.Workflows.Core/Contracts/IWorkflowBuilder.cs +++ b/src/modules/Elsa.Workflows.Core/Contracts/IWorkflowBuilder.cs @@ -96,6 +96,13 @@ public interface IWorkflowBuilder /// The definition ID to use for the workflow being built. IWorkflowBuilder WithDefinitionId(string definitionId); + /// + /// A fluent method for setting the property. + /// + /// The unique identifier to use for the workflow being built. + /// The current instance for method chaining. + IWorkflowBuilder WithId(string id); + /// /// A fluent method for setting the property. /// diff --git a/src/modules/Elsa.Workflows.Runtime/Activities/Event.cs b/src/modules/Elsa.Workflows.Runtime/Activities/Event.cs index 024e5c492..bbcd5f0ef 100644 --- a/src/modules/Elsa.Workflows.Runtime/Activities/Event.cs +++ b/src/modules/Elsa.Workflows.Runtime/Activities/Event.cs @@ -15,7 +15,7 @@ namespace Elsa.Workflows.Runtime.Activities; [UsedImplicitly] public class Event : Trigger { - internal const string EventInputWorkflowInputKey = "__EventPayloadWorkflowInput"; + public const string EventInputWorkflowInputKey = "__EventPayloadWorkflowInput"; /// internal Event([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line) diff --git a/src/modules/Elsa.Workflows.Runtime/Extensions/DependencyInjectionExtensions.cs b/src/modules/Elsa.Workflows.Runtime/Extensions/DependencyInjectionExtensions.cs index 31e29f8f8..1a32aa76d 100644 --- a/src/modules/Elsa.Workflows.Runtime/Extensions/DependencyInjectionExtensions.cs +++ b/src/modules/Elsa.Workflows.Runtime/Extensions/DependencyInjectionExtensions.cs @@ -10,10 +10,15 @@ namespace Microsoft.Extensions.DependencyInjection; public static class DependencyInjectionExtensions { /// - /// Adds the to the service collection. + /// Adds the specified workflow provider type to the service collection. /// - /// The service collection. - /// The type of the workflow definition provider. - /// The service collection. + /// The type of the workflow provider to add. Must implement . + [Obsolete("Use AddWorkflowsProvider instead.", false)] public static IServiceCollection AddWorkflowDefinitionProvider(this IServiceCollection services) where T : class, IWorkflowsProvider => services.AddScoped(); + + /// + /// Adds the specified workflows provider type to the service collection. + /// + /// The type of the workflow provider to add. Must implement . + public static IServiceCollection AddWorkflowsProvider(this IServiceCollection services) where T : class, IWorkflowsProvider => services.AddScoped(); } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Models/MaterializedWorkflow.cs b/src/modules/Elsa.Workflows.Runtime/Models/MaterializedWorkflow.cs index 909f2e502..4feafd362 100644 --- a/src/modules/Elsa.Workflows.Runtime/Models/MaterializedWorkflow.cs +++ b/src/modules/Elsa.Workflows.Runtime/Models/MaterializedWorkflow.cs @@ -9,4 +9,4 @@ namespace Elsa.Workflows.Runtime; /// The name of the provider that provided the workflow definition. /// The name of the materializer that materialized the workflow. /// The context of the materializer that materialized the workflow. -public record MaterializedWorkflow(Workflow Workflow, string ProviderName, string MaterializerName, object? MaterializerContext = default); \ No newline at end of file +public record MaterializedWorkflow(Workflow Workflow, string ProviderName, string MaterializerName, object? MaterializerContext = null); \ 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 5bd0e7939..d21d298fe 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/DefaultWorkflowDefinitionStorePopulator.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/DefaultWorkflowDefinitionStorePopulator.cs @@ -56,7 +56,7 @@ public class DefaultWorkflowDefinitionStorePopulator : IWorkflowDefinitionStoreP { var providers = _workflowDefinitionProviders(); var workflowDefinitions = new List(); - + foreach (var provider in providers) { var results = await provider.GetWorkflowsAsync(cancellationToken).AsTask().ToList(); @@ -84,7 +84,7 @@ public class DefaultWorkflowDefinitionStorePopulator : IWorkflowDefinitionStoreP var workflowDefinition = await AddOrUpdateAsync(materializedWorkflow, cancellationToken); if (indexTriggers) - await IndexTriggersAsync(materializedWorkflow, cancellationToken); + await IndexTriggersAsync(workflowDefinition, cancellationToken); return workflowDefinition; } @@ -118,7 +118,7 @@ public class DefaultWorkflowDefinitionStorePopulator : IWorkflowDefinitionStoreP // Serialize materializer context. var materializerContext = materializedWorkflow.MaterializerContext; - var materializerContextJson = materializerContext != null ? _payloadSerializer.Serialize(materializerContext) : default; + var materializerContextJson = materializerContext != null ? _payloadSerializer.Serialize(materializerContext) : null; // Serialize the workflow root. var workflowJson = _activitySerializer.Serialize(workflow.Root); @@ -260,7 +260,7 @@ public class DefaultWorkflowDefinitionStorePopulator : IWorkflowDefinitionStoreP } } - private async Task IndexTriggersAsync(MaterializedWorkflow materializedWorkflow, CancellationToken cancellationToken) => await _triggerIndexer.IndexTriggersAsync(materializedWorkflow.Workflow, cancellationToken); + private async Task IndexTriggersAsync(WorkflowDefinition workflowDefinition, CancellationToken cancellationToken) => await _triggerIndexer.IndexTriggersAsync(workflowDefinition, cancellationToken); /// /// Syncs the items in the primary list with existing items in the secondary list, even when the object instances are not the same (but their IDs are). diff --git a/src/modules/Elsa.Workflows.Runtime/Services/EventPublisher.cs b/src/modules/Elsa.Workflows.Runtime/Services/EventPublisher.cs index 641223fa2..a760accbe 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/EventPublisher.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/EventPublisher.cs @@ -29,16 +29,17 @@ public class EventPublisher(IStimulusSender stimulusSender, IStimulusDispatcher WorkflowInstanceId = workflowInstanceId, Input = workflowInput }; + var triggerName = ActivityTypeNameHelper.GenerateTypeName(); if (asynchronous) { await stimulusDispatcher.SendAsync(new() { - ActivityTypeName = ActivityTypeNameHelper.GenerateTypeName(), + ActivityTypeName = triggerName, Stimulus = stimulus, Metadata = metadata }, cancellationToken); } else - await stimulusSender.SendAsync(stimulus, metadata, cancellationToken); + await stimulusSender.SendAsync(triggerName, stimulus, metadata, cancellationToken); } } \ No newline at end of file diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionStorePopulation/InMemoryWorkflowMaterializer.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionStorePopulation/InMemoryWorkflowMaterializer.cs new file mode 100644 index 000000000..461831eac --- /dev/null +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionStorePopulation/InMemoryWorkflowMaterializer.cs @@ -0,0 +1,20 @@ +using Elsa.Workflows.Activities; +using Elsa.Workflows.Management; +using Elsa.Workflows.Management.Entities; + +namespace Elsa.Workflows.IntegrationTests.Scenarios.WorkflowDefinitionStorePopulation; + +public class InMemoryWorkflowMaterializer(Workflow workflow) : IWorkflowMaterializer +{ + public string Name => "InMemory"; + + public ValueTask MaterializeAsync(WorkflowDefinition definition, CancellationToken cancellationToken = default) + { + var materializedWorkflow = new Workflow + { + Identity = new(definition.DefinitionId, definition.Version, definition.Id), + Root = workflow.Root + }; + return new(materializedWorkflow); + } +} \ No newline at end of file diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionStorePopulation/InMemoryWorkflowProvider.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionStorePopulation/InMemoryWorkflowProvider.cs new file mode 100644 index 000000000..1dc95db61 --- /dev/null +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionStorePopulation/InMemoryWorkflowProvider.cs @@ -0,0 +1,23 @@ +using Elsa.Workflows.Activities; +using Elsa.Workflows.Models; +using Elsa.Workflows.Runtime; +using Elsa.Workflows.Runtime.Activities; + +namespace Elsa.Workflows.IntegrationTests.Scenarios.WorkflowDefinitionStorePopulation; + +public class InMemoryWorkflowsProvider(Workflow workflow) : IWorkflowsProvider +{ + public string Name => "InMemory"; + + public ValueTask> GetWorkflowsAsync(CancellationToken cancellationToken = default) + { + var materializedWorkflow = new MaterializedWorkflow( + Workflow: workflow, + ProviderName: "InMemory", + MaterializerName: "InMemory", + MaterializerContext: null + ); + + return new([materializedWorkflow]); + } +} \ No newline at end of file diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionStorePopulation/Tests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionStorePopulation/Tests.cs new file mode 100644 index 000000000..a4cdd4859 --- /dev/null +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionStorePopulation/Tests.cs @@ -0,0 +1,73 @@ +using Elsa.Testing.Shared; +using Elsa.Workflows.Activities; +using Elsa.Workflows.Helpers; +using Elsa.Workflows.Management; +using Elsa.Workflows.Runtime; +using Elsa.Workflows.Runtime.Activities; +using Elsa.Workflows.Runtime.Stimuli; +using Microsoft.Extensions.DependencyInjection; +using Xunit.Abstractions; + +namespace Elsa.Workflows.IntegrationTests.Scenarios.WorkflowDefinitionStorePopulation; + +/// +/// Represents a test class for integration testing various scenarios related to the +/// `WorkflowDefinitionStorePopulation`. This class primarily focuses +/// on ensuring correct behavior when workflow definitions are published or updated, and their effects +/// on consuming workflows and triggers. +/// +public class Tests +{ + private readonly IServiceProvider _services; + private readonly Workflow _shiftyWorkflow; + + public Tests(ITestOutputHelper testOutputHelper) + { + _shiftyWorkflow = new() + { + Identity = new( + DefinitionId: "WorkflowWithTrigger", + Version: 1, + Id: "1", + TenantId: "default" + ), + Root = new Event("Foo") + { + CanStartWorkflow = true + } + }; + + _services = new TestApplicationBuilder(testOutputHelper) + .ConfigureServices(services => services + .AddScoped(_ => new InMemoryWorkflowsProvider(_shiftyWorkflow)) + .AddScoped(_ => new InMemoryWorkflowMaterializer(_shiftyWorkflow))) + .Build(); + } + + /// + /// When a dependency workflow is published, all consuming workflows are updated to point to the new version of the dependency. + /// + [Fact(DisplayName = "When a workflow definition from a given source has a different Id than the one in the store, the trigger should still point to the workflow definition version ID in the store.")] + public async Task Test1() + { + // Initial population of the store from workflow providers. + await _services.PopulateRegistriesAsync(); + + // Artificially change the workflow definition version ID. + _shiftyWorkflow.Identity = _shiftyWorkflow.Identity with + { + Id = ":1" + }; + + // Emulate reloading of workflow definitions. + await _services.PopulateRegistriesAsync(); + + // Triggering the workflow should still work. + var stimulusSender = _services.GetRequiredService(); + var stimulus = new EventStimulus("Foo"); + var triggerName = ActivityTypeNameHelper.GenerateTypeName(); + var result = await stimulusSender.SendAsync(triggerName, stimulus); + + Assert.NotEmpty(result.WorkflowInstanceResponses); + } +} \ No newline at end of file