Merge pull request #735 from craigfowler/feature/728-WorkflowInstances-not-persisting

Resolve #728 workflow instances weren't persisting
This commit is contained in:
Craig Fowler 2021-03-11 09:01:07 +00:00 committed by GitHub
commit 6f7d512a9f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 486 additions and 48 deletions

View file

@ -13,6 +13,9 @@ environment:
version: 2.0.0-preview7.{build}
services:
- mongodb
init:
- cmd: git config --global core.autocrlf true

View file

@ -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>

View file

@ -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; }

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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

View file

@ -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,
};
}
}

View file

@ -61,7 +61,8 @@ export enum WorkflowContextFidelity {
export enum WorkflowPersistenceBehavior {
Suspended,
WorkflowPAssCompleted,
WorkflowBurst,
WorkflowPassCompleted,
ActivityExecuted
}

View file

@ -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 {

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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>

View file

@ -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.
}
}
}
}

View file

@ -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));
}
}
}
}

View file

@ -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));
}
}
}
}

View file

@ -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);
}
}
}
}

View file

@ -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));
}
}
}

View file

@ -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();

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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>

View 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) { }
}
}
}