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