Merge pull request #735 from craigfowler/feature/728-WorkflowInstances-not-persisting
Resolve #728 workflow instances weren't persisting
This commit is contained in:
commit
6f7d512a9f
|
|
@ -13,6 +13,9 @@ environment:
|
|||
|
||||
version: 2.0.0-preview7.{build}
|
||||
|
||||
services:
|
||||
- mongodb
|
||||
|
||||
init:
|
||||
- cmd: git config --global core.autocrlf true
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ namespace Elsa.Models
|
|||
/// Workflow instances are persisted after the workflow completed a burst of execution.
|
||||
/// </summary>
|
||||
WorkflowBurst,
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Workflow instances are persisted after the workflow executed scheduled activities.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ namespace Elsa.Builders
|
|||
ActivityType = typeof(Workflow);
|
||||
WorkflowBuilder = this;
|
||||
PropertyValueProviders = new Dictionary<string, IActivityPropertyValueProvider>();
|
||||
PersistenceBehavior = WorkflowPersistenceBehavior.WorkflowBurst;
|
||||
}
|
||||
|
||||
public int Version { get; private set; }
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ namespace Elsa.Handlers
|
|||
INotificationHandler<WorkflowExecutionPassCompleted>,
|
||||
INotificationHandler<WorkflowExecutionBurstStarting>,
|
||||
INotificationHandler<WorkflowExecutionBurstCompleted>,
|
||||
INotificationHandler<WorkflowExecutionFinished>
|
||||
INotificationHandler<WorkflowExecutionFinished>,
|
||||
INotificationHandler<ActivityExecuted>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<WorkflowModel>
|
||||
@State() workflowModel: WorkflowModel
|
||||
el: HTMLElement
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,8 @@ export enum WorkflowContextFidelity {
|
|||
|
||||
export enum WorkflowPersistenceBehavior {
|
||||
Suspended,
|
||||
WorkflowPAssCompleted,
|
||||
WorkflowBurst,
|
||||
WorkflowPassCompleted,
|
||||
ActivityExecuted
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import {ActivityDefinitionProperty} from "./domain";
|
||||
import {ActivityDefinitionProperty, WorkflowPersistenceBehavior} from "./domain";
|
||||
|
||||
export interface WorkflowModel {
|
||||
activities: Array<ActivityModel>
|
||||
connections: Array<ConnectionModel>
|
||||
persistenceBehavior?: WorkflowPersistenceBehavior,
|
||||
}
|
||||
|
||||
export interface ActivityModel {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<UserTask>();
|
||||
elsa.AddWorkflow<PersistableWorkflow>();
|
||||
})
|
||||
;
|
||||
}, parameter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<UserTask>();
|
||||
elsa.AddWorkflow<PersistableWorkflow>();
|
||||
});
|
||||
}, parameter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -45,6 +45,7 @@
|
|||
<ProjectReference Include="..\..\..\src\activities\Elsa.Activities.Temporal.Common\Elsa.Activities.Temporal.Common.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\activities\Elsa.Activities.Temporal.Hangfire\Elsa.Activities.Temporal.Hangfire.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\activities\Elsa.Activities.Temporal.Quartz\Elsa.Activities.Temporal.Quartz.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\persistence\Elsa.Persistence.MongoDb\Elsa.Persistence.MongoDb.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -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
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convenience method creates an <see cref="IHostBuilder"/> using the specified services.
|
||||
/// </summary>
|
||||
/// <param name="serviceConfig">Services to use in the created host builder</param>
|
||||
/// <returns>A host builder</returns>
|
||||
static IHostBuilder CreateHostBuilderUsingServices(Action<IServiceCollection> serviceConfig)
|
||||
=> Host.CreateDefaultBuilder()
|
||||
.ConfigureServices((hostBuilder, services) => serviceConfig(services));
|
||||
|
||||
/// <summary>
|
||||
/// Starts up the specified <paramref name="hostBuilder"/> 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
|
|||
/// <param name="cancellationSource">The cancellation source</param>
|
||||
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.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<HostedWorkflowRunner<PersistableWorkflow>>();
|
||||
});
|
||||
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<HostedWorkflowRunner<PersistableWorkflow.OnSuspend>>();
|
||||
});
|
||||
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<HostedWorkflowRunner<PersistableWorkflow.OnActivityExecuted>>();
|
||||
});
|
||||
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<HostedWorkflowRunner<PersistableWorkflow.OnWorkflowBurst>>();
|
||||
});
|
||||
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<HostedWorkflowRunner<PersistableWorkflow.OnWorkflowPassCompleted>>();
|
||||
});
|
||||
var host = await hostBuilder.StartAsync();
|
||||
}
|
||||
|
||||
class HostedWorkflowRunner<TWorkflow> : IHostedService where TWorkflow : PersistableWorkflow
|
||||
{
|
||||
readonly IWorkflowRunner workflowRunner;
|
||||
readonly IWorkflowInstanceStore instanceStore;
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var instance = await workflowRunner.RunWorkflowAsync<TWorkflow>();
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<HostedWorkflowRunner>());
|
||||
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<PersistableWorkflow>();
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<SetVariable>(a => a.Set(x => x.VariableName, "Unused").Set(x => x.Value, "Unused"))
|
||||
.Then<UserTask>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<IServiceCollection> 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<IServiceCollection> serviceConfig,
|
||||
ParameterInfo? parameter = null)
|
||||
{
|
||||
this.parameter = parameter ?? throw new ArgumentNullException(nameof(parameter));
|
||||
this.serviceConfig = serviceConfig ?? throw new ArgumentNullException(nameof(serviceConfig));
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<IServiceProvider>())
|
||||
return GetServiceProvider(context);
|
||||
|
||||
return new NoSpecimen();
|
||||
|
|
|
|||
|
|
@ -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<IServiceCollection> serviceConfig;
|
||||
|
||||
public object Create(object request, ISpecimenContext context)
|
||||
=> request.IsAnAutofixtureRequestForType<IHostBuilder>()? GetHostBuilder() : new NoSpecimen();
|
||||
|
||||
IHostBuilder GetHostBuilder()
|
||||
=> Host.CreateDefaultBuilder().ConfigureServices((hostBuilder, services) => serviceConfig(services));
|
||||
|
||||
public HostBubilderUsingServicesSpecimenBuilder(Action<IServiceCollection> serviceConfig)
|
||||
{
|
||||
this.serviceConfig = serviceConfig;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
using System.Reflection;
|
||||
|
||||
namespace Elsa.Testing.Shared.AutoFixture.SpecimenBuilders
|
||||
{
|
||||
/// <summary>
|
||||
/// Convenience methods for specimen builders. You're only likey to want to use these extensions
|
||||
/// within a specimen builder class.
|
||||
/// </summary>
|
||||
public static class AutofixtureRequestExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a value which indicates if the specified <see cref="object"/> qualifies as an Autofixture
|
||||
/// request for an instance of the specified generic type.
|
||||
/// </summary>
|
||||
/// <param name="request">The request object</param>
|
||||
/// <typeparam name="T">The desired specimen type</typeparam>
|
||||
/// <returns><c>true</c> if the <paramref name="request"/> is a request for an instance of <typeparamref name="T"/>; <c>false</c> otherwise.</returns>
|
||||
public static bool IsAnAutofixtureRequestForType<T>(this object request)
|
||||
{
|
||||
if(Equals(request, typeof(T)))
|
||||
return true;
|
||||
|
||||
if(request is ParameterInfo paramInfo && paramInfo.ParameterType == typeof(T))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@
|
|||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
|
||||
<PackageReference Include="NodaTime" Version="3.0.3" />
|
||||
<PackageReference Include="xunit.extensibility.core" Version="2.4.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
29
test/unit/Elsa.UnitTests/Services/WorkflowBuilderTests.cs
Normal file
29
test/unit/Elsa.UnitTests/Services/WorkflowBuilderTests.cs
Normal file
|
|
@ -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) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue