diff --git a/appveyor.yml b/appveyor.yml index ec959fab0..d6ae6f5af 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -13,6 +13,9 @@ environment: version: 2.0.0-preview7.{build} +services: + - mongodb + init: - cmd: git config --global core.autocrlf true diff --git a/src/core/Elsa.Abstractions/Models/WorkflowPersistenceBehavior.cs b/src/core/Elsa.Abstractions/Models/WorkflowPersistenceBehavior.cs index e7881dea9..1bd3cae9d 100644 --- a/src/core/Elsa.Abstractions/Models/WorkflowPersistenceBehavior.cs +++ b/src/core/Elsa.Abstractions/Models/WorkflowPersistenceBehavior.cs @@ -15,7 +15,7 @@ namespace Elsa.Models /// Workflow instances are persisted after the workflow completed a burst of execution. /// WorkflowBurst, - + /// /// Workflow instances are persisted after the workflow executed scheduled activities. /// diff --git a/src/core/Elsa.Core/Builders/WorkflowBuilder.cs b/src/core/Elsa.Core/Builders/WorkflowBuilder.cs index cb2b44b69..1aadcf86d 100644 --- a/src/core/Elsa.Core/Builders/WorkflowBuilder.cs +++ b/src/core/Elsa.Core/Builders/WorkflowBuilder.cs @@ -19,6 +19,7 @@ namespace Elsa.Builders ActivityType = typeof(Workflow); WorkflowBuilder = this; PropertyValueProviders = new Dictionary(); + PersistenceBehavior = WorkflowPersistenceBehavior.WorkflowBurst; } public int Version { get; private set; } diff --git a/src/core/Elsa.Core/Handlers/PersistWorkflow.cs b/src/core/Elsa.Core/Handlers/PersistWorkflow.cs index 9cfd3ff2a..24a0dc7d6 100644 --- a/src/core/Elsa.Core/Handlers/PersistWorkflow.cs +++ b/src/core/Elsa.Core/Handlers/PersistWorkflow.cs @@ -15,7 +15,8 @@ namespace Elsa.Handlers INotificationHandler, INotificationHandler, INotificationHandler, - INotificationHandler + INotificationHandler, + INotificationHandler { private readonly IWorkflowInstanceStore _workflowInstanceStore; private readonly ILogger _logger; @@ -82,11 +83,22 @@ namespace Elsa.Handlers } } + public async Task Handle(ActivityExecuted notification, CancellationToken cancellationToken) + { + var behavior = notification.WorkflowExecutionContext.WorkflowBlueprint.PersistenceBehavior; + + if (behavior == WorkflowPersistenceBehavior.ActivityExecuted) + await SaveWorkflowAsync(notification.WorkflowExecutionContext, cancellationToken); + } + private async ValueTask SaveWorkflowAsync(WorkflowExecutionContext workflowExecutionContext, CancellationToken cancellationToken) { + _logger.LogTrace("Persisting workflow instance {instanceId}", workflowExecutionContext.WorkflowInstance.Id); + workflowExecutionContext.PruneActivityData(); var workflowInstance = workflowExecutionContext.WorkflowInstance; await _workflowInstanceStore.SaveAsync(workflowInstance, cancellationToken); + _logger.LogDebug("Committed workflow {WorkflowInstanceId} to storage", workflowInstance.Id); } } diff --git a/src/core/Elsa.Core/Services/WorkflowRunner.cs b/src/core/Elsa.Core/Services/WorkflowRunner.cs index f2016859d..2499f3d38 100644 --- a/src/core/Elsa.Core/Services/WorkflowRunner.cs +++ b/src/core/Elsa.Core/Services/WorkflowRunner.cs @@ -171,6 +171,8 @@ namespace Elsa.Services using var workflowExecutionScope = _serviceScopeFactory.CreateScope(); var workflowExecutionContext = new WorkflowExecutionContext(workflowExecutionScope.ServiceProvider, workflowBlueprint, workflowInstance, input); + _logger.LogDebug("{methodName} is running workflow instance Id {instanceId}", nameof(RunWorkflowAsync), workflowInstance.Id); + if (!string.IsNullOrWhiteSpace(workflowInstance.ContextId)) { var loadContext = new LoadWorkflowContext(workflowExecutionContext); @@ -204,7 +206,10 @@ namespace Elsa.Services { case WorkflowStatus.Idle: if (!await BeginWorkflow(workflowExecutionContext, activity, input, cancellationToken)) + { + _logger.LogDebug("Workflow {WorkflowInstanceId} cannot begin from an idle state (perhaps it needs a specific input)", workflowInstance.Id); return workflowInstance; + } break; case WorkflowStatus.Running: @@ -213,7 +218,10 @@ namespace Elsa.Services case WorkflowStatus.Suspended: if (!await ResumeWorkflowAsync(workflowExecutionContext, activity!, input, cancellationToken)) + { + _logger.LogDebug("Workflow {WorkflowInstanceId} cannot be resumed from a suspended state (perhaps it needs a specific input)", workflowInstance.Id); return workflowInstance; + } break; default: throw new ArgumentOutOfRangeException(); @@ -231,7 +239,10 @@ namespace Elsa.Services }; if (statusEvent != null) + { + _logger.LogTrace("Publishing a status event of type {eventType} for workflow {WorkflowInstanceId}", statusEvent.GetType().Name, workflowInstance.Id); await _mediator.Publish(statusEvent, cancellationToken); + } await _mediator.Publish(new WorkflowExecutionFinished(workflowExecutionContext), cancellationToken); return workflowExecutionContext.WorkflowInstance; diff --git a/src/designer/elsa-workflows-studio/src/components/designers/tree/elsa-designer-tree/elsa-designer-tree.tsx b/src/designer/elsa-workflows-studio/src/components/designers/tree/elsa-designer-tree/elsa-designer-tree.tsx index ac9e40000..bdd31a247 100644 --- a/src/designer/elsa-workflows-studio/src/components/designers/tree/elsa-designer-tree/elsa-designer-tree.tsx +++ b/src/designer/elsa-workflows-studio/src/components/designers/tree/elsa-designer-tree/elsa-designer-tree.tsx @@ -1,7 +1,7 @@ import {Component, Host, h, Prop, State, Event, EventEmitter, Listen, Watch} from '@stencil/core'; import {Map, addConnection, findActivity, getChildActivities, getInboundConnections, getOutboundConnections, removeActivity} from '../../../../utils/utils'; import {cleanup, destroy, updateConnections} from '../../../../utils/jsplumb-helper'; -import {ActivityDescriptor, ActivityDesignDisplayContext, ActivityModel, ConnectionModel, EventTypes, WorkflowModel} from "../../../../models"; +import {ActivityDescriptor, ActivityDesignDisplayContext, ActivityModel, ConnectionModel, EventTypes, WorkflowModel, WorkflowPersistenceBehavior} from "../../../../models"; import {eventBus} from '../../../../services/event-bus'; import jsPlumb from "jsplumb"; import uuid = jsPlumb.jsPlumbUtil.uuid; @@ -15,7 +15,7 @@ import {ActivityIcon} from "../../../icons/activity-icon"; }) export class ElsaWorkflowDesigner { - @Prop() model: WorkflowModel = {activities: [], connections: []}; + @Prop() model: WorkflowModel = { activities: [], connections: [], persistenceBehavior: WorkflowPersistenceBehavior.WorkflowBurst }; @Event({eventName: 'workflow-changed', bubbles: true, composed: true, cancelable: true}) workflowChanged: EventEmitter @State() workflowModel: WorkflowModel el: HTMLElement diff --git a/src/designer/elsa-workflows-studio/src/components/editors/workflow-editor/elsa-workflow-editor/elsa-workflow-editor.tsx b/src/designer/elsa-workflows-studio/src/components/editors/workflow-editor/elsa-workflow-editor/elsa-workflow-editor.tsx index cd0a4b886..2d64371eb 100644 --- a/src/designer/elsa-workflows-studio/src/components/editors/workflow-editor/elsa-workflow-editor/elsa-workflow-editor.tsx +++ b/src/designer/elsa-workflows-studio/src/components/editors/workflow-editor/elsa-workflow-editor/elsa-workflow-editor.tsx @@ -1,6 +1,6 @@ import {Component, Event, EventEmitter, h, Host, Listen, Method, Prop, State, Watch} from '@stencil/core'; import {eventBus} from '../../../../services/event-bus'; -import {ActivityDefinition, ActivityDescriptor, ActivityModel, ConnectionDefinition, ConnectionModel, EventTypes, WorkflowDefinition, WorkflowModel} from "../../../../models"; +import {ActivityDefinition, ActivityDescriptor, ActivityModel, ConnectionDefinition, ConnectionModel, EventTypes, WorkflowDefinition, WorkflowModel, WorkflowPersistenceBehavior} from "../../../../models"; import {createElsaClient, SaveWorkflowDefinitionRequest} from "../../../../services/elsa-client"; import {pluginManager} from '../../../../services/plugin-manager'; import state from '../../../../utils/store'; @@ -44,7 +44,7 @@ export class ElsaWorkflowDefinitionEditor { @Watch('workflowDefinitionId') async workflowDefinitionIdChangedHandler(newValue: string) { const workflowDefinitionId = newValue; - let workflowDefinition: WorkflowDefinition = {definitionId: this.workflowDefinitionId, version: 1, activities: [], connections: []}; + let workflowDefinition: WorkflowDefinition = this.createWorkflowDefinition(); const client = createElsaClient(this.serverUrl); if (workflowDefinitionId && workflowDefinitionId.length > 0) { @@ -340,4 +340,14 @@ export class ElsaWorkflowDefinitionEditor { onUnPublishClicked={() => this.onUnPublishClicked()} />; } + + private createWorkflowDefinition() : WorkflowDefinition { + return { + definitionId: this.workflowDefinitionId, + version: 1, + activities: [], + connections: [], + persistenceBehavior: WorkflowPersistenceBehavior.WorkflowBurst, + }; + } } diff --git a/src/designer/elsa-workflows-studio/src/models/domain.ts b/src/designer/elsa-workflows-studio/src/models/domain.ts index 3985ad695..0718ca960 100644 --- a/src/designer/elsa-workflows-studio/src/models/domain.ts +++ b/src/designer/elsa-workflows-studio/src/models/domain.ts @@ -61,7 +61,8 @@ export enum WorkflowContextFidelity { export enum WorkflowPersistenceBehavior { Suspended, - WorkflowPAssCompleted, + WorkflowBurst, + WorkflowPassCompleted, ActivityExecuted } diff --git a/src/designer/elsa-workflows-studio/src/models/view.ts b/src/designer/elsa-workflows-studio/src/models/view.ts index ac0110ee7..c8b199164 100644 --- a/src/designer/elsa-workflows-studio/src/models/view.ts +++ b/src/designer/elsa-workflows-studio/src/models/view.ts @@ -1,8 +1,9 @@ -import {ActivityDefinitionProperty} from "./domain"; +import {ActivityDefinitionProperty, WorkflowPersistenceBehavior} from "./domain"; export interface WorkflowModel { activities: Array connections: Array + persistenceBehavior?: WorkflowPersistenceBehavior, } export interface ActivityModel { diff --git a/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithElsaAndCommonTemporalActivitiesAttribute.cs b/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithElsaAndCommonTemporalActivitiesAttribute.cs new file mode 100644 index 000000000..67a714969 --- /dev/null +++ b/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithElsaAndCommonTemporalActivitiesAttribute.cs @@ -0,0 +1,21 @@ +using System.Reflection; +using AutoFixture; +using AutoFixture.Xunit2; +using Elsa.Activities.Temporal; +using Elsa.Testing.Shared.AutoFixture.Customizations; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Core.IntegrationTests.Extensions +{ + public class HostBuilderWithElsaAndCommonTemporalActivitiesAttribute : CustomizeAttribute + { + public override ICustomization GetCustomization(ParameterInfo parameter) + { + return new HostBubilderUsingServicesCustomization(services => { + services.AddElsa(elsa => { + CommonTemporalActivityServices.AddCommonTemporalActivities(elsa); + }); + }, parameter); + } + } +} \ No newline at end of file diff --git a/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithElsaAndHangfireAttribute.cs b/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithElsaAndHangfireAttribute.cs new file mode 100644 index 000000000..5ce7d0a3a --- /dev/null +++ b/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithElsaAndHangfireAttribute.cs @@ -0,0 +1,21 @@ +using System.Reflection; +using AutoFixture; +using AutoFixture.Xunit2; +using Elsa.Testing.Shared.AutoFixture.Customizations; +using Hangfire; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Core.IntegrationTests.Extensions +{ + public class HostBuilderWithElsaAndHangfireAttribute : CustomizeAttribute + { + public override ICustomization GetCustomization(ParameterInfo parameter) + { + return new HostBubilderUsingServicesCustomization(services => { + services.AddElsa(elsa => { + elsa.AddHangfireTemporalActivities(config => config.UseInMemoryStorage()); + }); + }, parameter); + } + } +} \ No newline at end of file diff --git a/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithElsaAndQuartzAttribute.cs b/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithElsaAndQuartzAttribute.cs new file mode 100644 index 000000000..81e492d0b --- /dev/null +++ b/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithElsaAndQuartzAttribute.cs @@ -0,0 +1,20 @@ +using System.Reflection; +using AutoFixture; +using AutoFixture.Xunit2; +using Elsa.Testing.Shared.AutoFixture.Customizations; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Core.IntegrationTests.Extensions +{ + public class HostBuilderWithElsaAndQuartzAttribute : CustomizeAttribute + { + public override ICustomization GetCustomization(ParameterInfo parameter) + { + return new HostBubilderUsingServicesCustomization(services => { + services.AddElsa(elsa => { + elsa.AddQuartzTemporalActivities(); + }); + }, parameter); + } + } +} \ No newline at end of file diff --git a/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithElsaSampleWorkflowAndMongoDbAttribute.cs b/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithElsaSampleWorkflowAndMongoDbAttribute.cs new file mode 100644 index 000000000..ca0734145 --- /dev/null +++ b/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithElsaSampleWorkflowAndMongoDbAttribute.cs @@ -0,0 +1,31 @@ +using System.Reflection; +using AutoFixture; +using AutoFixture.Xunit2; +using Elsa.Activities.UserTask.Activities; +using Elsa.Core.IntegrationTests.Workflows; +using Elsa.Persistence.MongoDb.Extensions; +using Elsa.Testing.Shared.AutoFixture.Customizations; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Elsa.Core.IntegrationTests.Autofixture +{ + public class HostBuilderWithElsaSampleWorkflowAndMongoDbAttribute : CustomizeAttribute + { + public override ICustomization GetCustomization(ParameterInfo parameter) + { + return new HostBubilderUsingServicesCustomization(services => { + services + .AddElsa(elsa => { + elsa.UseMongoDbPersistence(opts => { + opts.ConnectionString = "mongodb://localhost:27017"; + opts.Db = "IntegrationTests"; + }); + elsa.AddActivity(); + elsa.AddWorkflow(); + }) + ; + }, parameter); + } + } +} \ No newline at end of file diff --git a/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithElsaSampleWorkflowAttribute.cs b/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithElsaSampleWorkflowAttribute.cs new file mode 100644 index 000000000..d3048e265 --- /dev/null +++ b/test/integration/Elsa.Core.IntegrationTests/Autofixture/HostBuilderWithElsaSampleWorkflowAttribute.cs @@ -0,0 +1,25 @@ +using System.Reflection; +using AutoFixture; +using AutoFixture.Xunit2; +using Elsa.Activities.UserTask.Activities; +using Elsa.Core.IntegrationTests.Workflows; +using Elsa.Testing.Shared.AutoFixture.Customizations; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Elsa.Core.IntegrationTests.Autofixture +{ + public class HostBuilderWithElsaSampleWorkflowAttribute : CustomizeAttribute + { + public override ICustomization GetCustomization(ParameterInfo parameter) + { + return new HostBubilderUsingServicesCustomization(services => { + services + .AddElsa(elsa => { + elsa.AddActivity(); + elsa.AddWorkflow(); + }); + }, parameter); + } + } +} \ No newline at end of file diff --git a/test/integration/Elsa.Core.IntegrationTests/Elsa.Core.IntegrationTests.csproj b/test/integration/Elsa.Core.IntegrationTests/Elsa.Core.IntegrationTests.csproj index 3b1f11ec6..58e23c58f 100644 --- a/test/integration/Elsa.Core.IntegrationTests/Elsa.Core.IntegrationTests.csproj +++ b/test/integration/Elsa.Core.IntegrationTests/Elsa.Core.IntegrationTests.csproj @@ -45,6 +45,7 @@ + diff --git a/test/integration/Elsa.Core.IntegrationTests/Extensions/TemporalServiceCollectionExtensionsTests.cs b/test/integration/Elsa.Core.IntegrationTests/Extensions/TemporalServiceCollectionExtensionsTests.cs index ddf5eabf2..58bb9b76c 100644 --- a/test/integration/Elsa.Core.IntegrationTests/Extensions/TemporalServiceCollectionExtensionsTests.cs +++ b/test/integration/Elsa.Core.IntegrationTests/Extensions/TemporalServiceCollectionExtensionsTests.cs @@ -6,20 +6,15 @@ using Hangfire; using System.Threading.Tasks; using System.Threading; using Elsa.Activities.Temporal; +using Elsa.Core.IntegrationTests.Extensions; namespace Elsa.UnitTests.Extensions { public class TemporalServiceCollectionExtensionsTests { - [Fact(DisplayName = "Starting a hosted app which uses only AddCommonTemporalActivities should throw InvalidOperationException because of the missing impl")] - public void AddCommonTemporalActivitiesThrowsDuringStartupIfNoTemporalImplementationPresent() + [Theory(DisplayName = "Starting a hosted app which uses only AddCommonTemporalActivities should throw InvalidOperationException because of the missing impl"), AutoMoqData] + public void AddCommonTemporalActivitiesThrowsDuringStartupIfNoTemporalImplementationPresent([HostBuilderWithElsaAndCommonTemporalActivities] IHostBuilder hostBuilder) { - var hostBuilder = CreateHostBuilderUsingServices(services => { - services.AddElsa(elsa => { - CommonTemporalActivityServices.AddCommonTemporalActivities(elsa); - }); - }); - var cancellationSource = new CancellationTokenSource(); try { @@ -33,15 +28,9 @@ namespace Elsa.UnitTests.Extensions } } - [Fact(DisplayName = "Starting a hosted app which uses AddHangfireTemporalActivities should not throw")] - public void AddHangfireTemporalActivitiesDoesNotThrowDuringStartup() + [Theory(DisplayName = "Starting a hosted app which uses AddHangfireTemporalActivities should not throw"), AutoMoqData] + public void AddHangfireTemporalActivitiesDoesNotThrowDuringStartup([HostBuilderWithElsaAndHangfire] IHostBuilder hostBuilder) { - var hostBuilder = CreateHostBuilderUsingServices(services => { - services.AddElsa(elsa => { - elsa.AddHangfireTemporalActivities(config => config.UseInMemoryStorage()); - }); - }); - var cancellationSource = new CancellationTokenSource(); try { @@ -57,15 +46,9 @@ namespace Elsa.UnitTests.Extensions } } - [Fact(DisplayName = "Starting a hosted app which uses AddQuartzTemporalActivities should not throw")] - public void AddQuartzTemporalActivitiesDoesNotThrowDuringStartup() + [Theory(DisplayName = "Starting a hosted app which uses AddQuartzTemporalActivities should not throw"), AutoMoqData] + public void AddQuartzTemporalActivitiesDoesNotThrowDuringStartup([HostBuilderWithElsaAndQuartz] IHostBuilder hostBuilder) { - var hostBuilder = CreateHostBuilderUsingServices(services => { - services.AddElsa(elsa => { - elsa.AddQuartzTemporalActivities(); - }); - }); - var cancellationSource = new CancellationTokenSource(); try { @@ -81,15 +64,6 @@ namespace Elsa.UnitTests.Extensions } } - /// - /// Convenience method creates an using the specified services. - /// - /// Services to use in the created host builder - /// A host builder - static IHostBuilder CreateHostBuilderUsingServices(Action serviceConfig) - => Host.CreateDefaultBuilder() - .ConfigureServices((hostBuilder, services) => serviceConfig(services)); - /// /// Starts up the specified instance using console lifetime. /// The current thread is then blocked for a short while, giving the app a chance to start up and run. @@ -111,10 +85,10 @@ namespace Elsa.UnitTests.Extensions /// The cancellation source static void CancelAndSquelchExceptions(CancellationTokenSource cancellationSource) { - try { cancellationSource.Cancel(); } - catch(Exception) { - // This is only ever used as teardown code, which is why we don't care about exceptions. - } + try { cancellationSource.Cancel(); } + catch(Exception) { + // This is only ever used as teardown code, which is why we don't care about exceptions. + } } } } \ No newline at end of file diff --git a/test/integration/Elsa.Core.IntegrationTests/Persistence/InMemory/InMemoryStoreIntegrationTests.cs b/test/integration/Elsa.Core.IntegrationTests/Persistence/InMemory/InMemoryStoreIntegrationTests.cs new file mode 100644 index 000000000..20308ff38 --- /dev/null +++ b/test/integration/Elsa.Core.IntegrationTests/Persistence/InMemory/InMemoryStoreIntegrationTests.cs @@ -0,0 +1,85 @@ +using Xunit; +using System.Threading.Tasks; +using Elsa.Core.IntegrationTests.Autofixture; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.DependencyInjection; +using System.Threading; +using Elsa.Services; +using Elsa.Core.IntegrationTests.Workflows; +using Elsa.Persistence; + +namespace Elsa.Core.IntegrationTests.Persistence.InMemory +{ + public class InMemoryStoreIntegrationTests + { + [Theory(DisplayName = "A persistable workflow instance with default persistence behaviour should be persisted-to and readable-from an in-memory store after being run"), AutoMoqData] + public async Task APersistableWorkflowInstanceWithDefaultPersistanceBehaviourShouldBeRoundTrippable([HostBuilderWithElsaSampleWorkflow] IHostBuilder hostBuilder) + { + hostBuilder.ConfigureServices((ctx, services) => { + services.AddHostedService>(); + }); + var host = await hostBuilder.StartAsync(); + } + + [Theory(DisplayName = "A persistable-on-suspend workflow instance should be persisted-to and readable-from an in-memory store after being run"), AutoMoqData] + public async Task APersistableOnSuspendWorkflowInstanceShouldBeRoundTrippable([HostBuilderWithElsaSampleWorkflow] IHostBuilder hostBuilder) + { + hostBuilder.ConfigureServices((ctx, services) => { + services.AddHostedService>(); + }); + var host = await hostBuilder.StartAsync(); + } + + [Theory(DisplayName = "A persistable-on-activity-executed workflow instance should be persisted-to and readable-from an in-memory store after being run"), AutoMoqData] + public async Task APersistableOnActivityExecutedWorkflowInstanceShouldBeRoundTrippable([HostBuilderWithElsaSampleWorkflow] IHostBuilder hostBuilder) + { + hostBuilder.ConfigureServices((ctx, services) => { + services.AddHostedService>(); + }); + var host = await hostBuilder.StartAsync(); + } + + [Theory(DisplayName = "A persistable-on-workflow-burst workflow instance should be persisted-to and readable-from an in-memory store after being run"), AutoMoqData] + public async Task APersistableOnWorkflowBurstWorkflowInstanceShouldBeRoundTrippable([HostBuilderWithElsaSampleWorkflow] IHostBuilder hostBuilder) + { + hostBuilder.ConfigureServices((ctx, services) => { + services.AddHostedService>(); + }); + var host = await hostBuilder.StartAsync(); + } + + [Theory(DisplayName = "A persistable-on-workflow-pass-completed workflow instance should be persisted-to and readable-from an in-memory store after being run"), AutoMoqData] + public async Task APersistableOnWorkflowPassCompletedWorkflowInstanceShouldBeRoundTrippable([HostBuilderWithElsaSampleWorkflow] IHostBuilder hostBuilder) + { + hostBuilder.ConfigureServices((ctx, services) => { + services.AddHostedService>(); + }); + var host = await hostBuilder.StartAsync(); + } + + class HostedWorkflowRunner : IHostedService where TWorkflow : PersistableWorkflow + { + readonly IWorkflowRunner workflowRunner; + readonly IWorkflowInstanceStore instanceStore; + + public async Task StartAsync(CancellationToken cancellationToken) + { + var instance = await workflowRunner.RunWorkflowAsync(); + var retrievedInstance = await instanceStore.FindByIdAsync(instance.Id); + + // An instance should totally be retrieved from the store + Assert.NotNull(retrievedInstance); + // Because we are in-memory, it should be a reference to the same instance + Assert.Same(instance, retrievedInstance); + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public HostedWorkflowRunner(IWorkflowRunner workflowRunner, IWorkflowInstanceStore instanceStore) + { + this.workflowRunner = workflowRunner ?? throw new System.ArgumentNullException(nameof(workflowRunner)); + this.instanceStore = instanceStore ?? throw new System.ArgumentNullException(nameof(instanceStore)); + } + } + } +} \ No newline at end of file diff --git a/test/integration/Elsa.Core.IntegrationTests/Persistence/MongoDb/MongoDbIntegrationTests.cs b/test/integration/Elsa.Core.IntegrationTests/Persistence/MongoDb/MongoDbIntegrationTests.cs new file mode 100644 index 000000000..27fc34065 --- /dev/null +++ b/test/integration/Elsa.Core.IntegrationTests/Persistence/MongoDb/MongoDbIntegrationTests.cs @@ -0,0 +1,45 @@ +using Xunit; +using System.Threading.Tasks; +using Elsa.Core.IntegrationTests.Autofixture; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.DependencyInjection; +using System.Threading; +using Elsa.Services; +using Elsa.Core.IntegrationTests.Workflows; +using Elsa.Persistence; +using System; + +namespace Elsa.Core.IntegrationTests.Persistence.MongoDb +{ + public class MongoDbIntegrationTests + { + [Theory(DisplayName = "A persistable workflow instance with default persistence behaviour should be persisted-to and readable-from a MongoDb store after being run"), AutoMoqData] + public async Task APersistableWorkflowInstanceWithDefaultPersistanceBehaviourShouldBeRoundTrippable([HostBuilderWithElsaSampleWorkflowAndMongoDbAttribute] IHostBuilder hostBuilder) + { + hostBuilder.ConfigureServices((ctx, services) => services.AddHostedService()); + var host = await hostBuilder.StartAsync(); + } + + class HostedWorkflowRunner : IHostedService + { + readonly IWorkflowRunner workflowRunner; + readonly IWorkflowInstanceStore instanceStore; + + public async Task StartAsync(CancellationToken cancellationToken) + { + var instance = await workflowRunner.RunWorkflowAsync(); + var retrievedInstance = await instanceStore.FindByIdAsync(instance.Id); + + Assert.NotNull(retrievedInstance); + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public HostedWorkflowRunner(IWorkflowRunner workflowRunner, IWorkflowInstanceStore instanceStore) + { + this.workflowRunner = workflowRunner ?? throw new System.ArgumentNullException(nameof(workflowRunner)); + this.instanceStore = instanceStore ?? throw new System.ArgumentNullException(nameof(instanceStore)); + } + } + } +} \ No newline at end of file diff --git a/test/integration/Elsa.Core.IntegrationTests/Workflows/PersistableWorkflow.cs b/test/integration/Elsa.Core.IntegrationTests/Workflows/PersistableWorkflow.cs new file mode 100644 index 000000000..af7a1b36b --- /dev/null +++ b/test/integration/Elsa.Core.IntegrationTests/Workflows/PersistableWorkflow.cs @@ -0,0 +1,59 @@ +using Elsa.Activities.UserTask.Activities; +using Elsa.Builders; +using Elsa.Activities.ControlFlow; +using Elsa.Activities.Primitives; +using Elsa.Models; + +namespace Elsa.Core.IntegrationTests.Workflows +{ + public class PersistableWorkflow : IWorkflow + { + public static readonly object Result = new object(); + + public virtual void Build(IWorkflowBuilder builder) + { + builder + .StartWith(a => a.Set(x => x.VariableName, "Unused").Set(x => x.Value, "Unused")) + .Then(setup: s => { + s.Set(x => x.Actions, c => new [] { "Foo", "Bar" }); + }) + .Finish(x => x.WithOutput(Result)); + } + + public class OnSuspend : PersistableWorkflow + { + public override void Build(IWorkflowBuilder builder) + { + builder.WithPersistenceBehavior(WorkflowPersistenceBehavior.Suspended); + base.Build(builder); + } + } + + public class OnActivityExecuted : PersistableWorkflow + { + public override void Build(IWorkflowBuilder builder) + { + builder.WithPersistenceBehavior(WorkflowPersistenceBehavior.ActivityExecuted); + base.Build(builder); + } + } + + public class OnWorkflowBurst : PersistableWorkflow + { + public override void Build(IWorkflowBuilder builder) + { + builder.WithPersistenceBehavior(WorkflowPersistenceBehavior.WorkflowBurst); + base.Build(builder); + } + } + + public class OnWorkflowPassCompleted : PersistableWorkflow + { + public override void Build(IWorkflowBuilder builder) + { + builder.WithPersistenceBehavior(WorkflowPersistenceBehavior.WorkflowPassCompleted); + base.Build(builder); + } + } + } +} \ No newline at end of file diff --git a/test/shared/Elsa.Testing.Shared/AutoFixture/Customizations/HostBubilderUsingServicesCustomization.cs b/test/shared/Elsa.Testing.Shared/AutoFixture/Customizations/HostBubilderUsingServicesCustomization.cs new file mode 100644 index 000000000..7f3d3fd72 --- /dev/null +++ b/test/shared/Elsa.Testing.Shared/AutoFixture/Customizations/HostBubilderUsingServicesCustomization.cs @@ -0,0 +1,37 @@ +using System; +using System.Reflection; +using AutoFixture; +using AutoFixture.Kernel; +using Elsa.Testing.Shared.AutoFixture.SpecimenBuilders; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Testing.Shared.AutoFixture.Customizations +{ + public class HostBubilderUsingServicesCustomization : ICustomization + { + readonly Action serviceConfig; + readonly ParameterInfo? parameter; + + public void Customize(IFixture fixture) + { + fixture.Customizations.Insert(0, GetSpecimenBuilder()); + } + + ISpecimenBuilder GetSpecimenBuilder() + { + var specimenBuilder = new HostBubilderUsingServicesSpecimenBuilder(serviceConfig); + if(parameter is null) return specimenBuilder; + + var paramSpec = new ParameterSpecification(parameter.ParameterType, parameter.Name); + return new FilteringSpecimenBuilder(specimenBuilder, paramSpec); + } + + public HostBubilderUsingServicesCustomization(Action serviceConfig, + ParameterInfo? parameter = null) + { + this.parameter = parameter ?? throw new ArgumentNullException(nameof(parameter)); + this.serviceConfig = serviceConfig ?? throw new ArgumentNullException(nameof(serviceConfig)); + + } + } +} \ No newline at end of file diff --git a/test/shared/Elsa.Testing.Shared/AutoFixture/SpecimenBuilders/AutofixtureServiceProviderSpecimenBuilder.cs b/test/shared/Elsa.Testing.Shared/AutoFixture/SpecimenBuilders/AutofixtureServiceProviderSpecimenBuilder.cs index 4a124efa8..4acc74bff 100644 --- a/test/shared/Elsa.Testing.Shared/AutoFixture/SpecimenBuilders/AutofixtureServiceProviderSpecimenBuilder.cs +++ b/test/shared/Elsa.Testing.Shared/AutoFixture/SpecimenBuilders/AutofixtureServiceProviderSpecimenBuilder.cs @@ -13,9 +13,7 @@ namespace Elsa.Testing.Shared.AutoFixture.SpecimenBuilders { public object Create(object request, ISpecimenContext context) { - if(Equals(request, typeof(IServiceProvider))) - return GetServiceProvider(context); - if(request is ParameterInfo paramInfo && paramInfo.ParameterType == typeof(IServiceProvider)) + if(request.IsAnAutofixtureRequestForType()) return GetServiceProvider(context); return new NoSpecimen(); diff --git a/test/shared/Elsa.Testing.Shared/AutoFixture/SpecimenBuilders/HostBubilderUsingServicesSpecimenBuilder.cs b/test/shared/Elsa.Testing.Shared/AutoFixture/SpecimenBuilders/HostBubilderUsingServicesSpecimenBuilder.cs new file mode 100644 index 000000000..5e501da39 --- /dev/null +++ b/test/shared/Elsa.Testing.Shared/AutoFixture/SpecimenBuilders/HostBubilderUsingServicesSpecimenBuilder.cs @@ -0,0 +1,23 @@ +using System; +using AutoFixture.Kernel; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Elsa.Testing.Shared.AutoFixture.SpecimenBuilders +{ + public class HostBubilderUsingServicesSpecimenBuilder : ISpecimenBuilder + { + readonly Action serviceConfig; + + public object Create(object request, ISpecimenContext context) + => request.IsAnAutofixtureRequestForType()? GetHostBuilder() : new NoSpecimen(); + + IHostBuilder GetHostBuilder() + => Host.CreateDefaultBuilder().ConfigureServices((hostBuilder, services) => serviceConfig(services)); + + public HostBubilderUsingServicesSpecimenBuilder(Action serviceConfig) + { + this.serviceConfig = serviceConfig; + } + } +} \ No newline at end of file diff --git a/test/shared/Elsa.Testing.Shared/AutoFixture/SpecimenBuilders/RequestExtensions.cs b/test/shared/Elsa.Testing.Shared/AutoFixture/SpecimenBuilders/RequestExtensions.cs new file mode 100644 index 000000000..ab8427d75 --- /dev/null +++ b/test/shared/Elsa.Testing.Shared/AutoFixture/SpecimenBuilders/RequestExtensions.cs @@ -0,0 +1,29 @@ +using System.Reflection; + +namespace Elsa.Testing.Shared.AutoFixture.SpecimenBuilders +{ + /// + /// Convenience methods for specimen builders. You're only likey to want to use these extensions + /// within a specimen builder class. + /// + public static class AutofixtureRequestExtensions + { + /// + /// Gets a value which indicates if the specified qualifies as an Autofixture + /// request for an instance of the specified generic type. + /// + /// The request object + /// The desired specimen type + /// true if the is a request for an instance of ; false otherwise. + public static bool IsAnAutofixtureRequestForType(this object request) + { + if(Equals(request, typeof(T))) + return true; + + if(request is ParameterInfo paramInfo && paramInfo.ParameterType == typeof(T)) + return true; + + return false; + } + } +} \ No newline at end of file diff --git a/test/shared/Elsa.Testing.Shared/Elsa.Testing.Shared.csproj b/test/shared/Elsa.Testing.Shared/Elsa.Testing.Shared.csproj index 5df535cd8..18bcc5296 100644 --- a/test/shared/Elsa.Testing.Shared/Elsa.Testing.Shared.csproj +++ b/test/shared/Elsa.Testing.Shared/Elsa.Testing.Shared.csproj @@ -19,6 +19,7 @@ + diff --git a/test/unit/Elsa.UnitTests/Services/WorkflowBuilderTests.cs b/test/unit/Elsa.UnitTests/Services/WorkflowBuilderTests.cs new file mode 100644 index 000000000..e38ec7990 --- /dev/null +++ b/test/unit/Elsa.UnitTests/Services/WorkflowBuilderTests.cs @@ -0,0 +1,29 @@ +using System; +using AutoFixture.Xunit2; +using Elsa.Builders; +using Elsa.Models; +using Elsa.Testing.Shared.AutoFixture.Attributes; +using Xunit; + +namespace Elsa.Services +{ + public class WorkflowBuilderTests + { + [Theory(DisplayName = "The Build method should return a workflow blueprint with the WorkflowBurst persistence behaviour if no behaviour was specified"), AutoMoqData] + public void BuildShouldReturnWorkflowBlueprintWithWorkflowBurstPersistenceBehaviourIfNoBehaviourSpecified([AutofixtureServiceProvider, Frozen] IServiceProvider serviceProvider, + WorkflowBuilder sut, + string idPrefix) + { + var workflow = new NoOpWorkflow(); + + var blueprint = sut.Build(workflow, idPrefix); + + Assert.Equal(WorkflowPersistenceBehavior.WorkflowBurst, blueprint.PersistenceBehavior); + } + + class NoOpWorkflow : IWorkflow + { + public void Build(IWorkflowBuilder builder) { } + } + } +} \ No newline at end of file