diff --git a/src/WorkflowCore/Interface/IScopeProvider.cs b/src/WorkflowCore/Interface/IScopeProvider.cs new file mode 100644 index 000000000..c69351731 --- /dev/null +++ b/src/WorkflowCore/Interface/IScopeProvider.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace WorkflowCore.Interface +{ + /// + /// The implemention of this interface will be responsible for + /// providing a new service scope for a DI container + /// + public interface IScopeProvider + { + /// + /// Create a new service scope + /// + /// + IServiceScope CreateScope(); + } +} \ No newline at end of file diff --git a/src/WorkflowCore/ServiceCollectionExtensions.cs b/src/WorkflowCore/ServiceCollectionExtensions.cs index aebae27a1..1d10c5742 100644 --- a/src/WorkflowCore/ServiceCollectionExtensions.cs +++ b/src/WorkflowCore/ServiceCollectionExtensions.cs @@ -46,6 +46,7 @@ public static IServiceCollection AddWorkflow(this IServiceCollection services, A services.AddSingleton(); services.AddSingleton(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/src/WorkflowCore/Services/ScopeProvider.cs b/src/WorkflowCore/Services/ScopeProvider.cs new file mode 100644 index 000000000..ad1ae98d2 --- /dev/null +++ b/src/WorkflowCore/Services/ScopeProvider.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.DependencyInjection; +using System; +using WorkflowCore.Interface; + +namespace WorkflowCore.Services +{ + /// + /// A concrete implementation for the IScopeProvider interface + /// Largely to get around the problems of unit testing an extension method (CreateScope()) + /// + public class ScopeProvider : IScopeProvider + { + private readonly IServiceProvider provider; + + public ScopeProvider(IServiceProvider provider) + { + this.provider = provider; + } + + public IServiceScope CreateScope() + { + return provider.CreateScope(); + } + } +} diff --git a/src/WorkflowCore/Services/WorkflowExecutor.cs b/src/WorkflowCore/Services/WorkflowExecutor.cs index a3e5c0c46..45716b83d 100755 --- a/src/WorkflowCore/Services/WorkflowExecutor.cs +++ b/src/WorkflowCore/Services/WorkflowExecutor.cs @@ -14,6 +14,7 @@ public class WorkflowExecutor : IWorkflowExecutor { protected readonly IWorkflowRegistry _registry; protected readonly IServiceProvider _serviceProvider; + protected readonly IScopeProvider _scopeProvider; protected readonly IDateTimeProvider _datetimeProvider; protected readonly ILogger _logger; private readonly IExecutionResultProcessor _executionResultProcessor; @@ -23,9 +24,10 @@ public class WorkflowExecutor : IWorkflowExecutor private IWorkflowHost Host => _serviceProvider.GetService(); - public WorkflowExecutor(IWorkflowRegistry registry, IServiceProvider serviceProvider, IDateTimeProvider datetimeProvider, IExecutionResultProcessor executionResultProcessor, ILifeCycleEventPublisher publisher, ICancellationProcessor cancellationProcessor, WorkflowOptions options, ILoggerFactory loggerFactory) + public WorkflowExecutor(IWorkflowRegistry registry, IServiceProvider serviceProvider, IScopeProvider scopeProvider, IDateTimeProvider datetimeProvider, IExecutionResultProcessor executionResultProcessor, ILifeCycleEventPublisher publisher, ICancellationProcessor cancellationProcessor, WorkflowOptions options, ILoggerFactory loggerFactory) { _serviceProvider = serviceProvider; + _scopeProvider = scopeProvider; _registry = registry; _datetimeProvider = datetimeProvider; _publisher = publisher; @@ -87,57 +89,60 @@ public async Task Execute(WorkflowInstance workflow) pointer.StartTime = _datetimeProvider.Now.ToUniversalTime(); } - _logger.LogDebug("Starting step {0} on workflow {1}", step.Name, workflow.Id); + using (var scope = _scopeProvider.CreateScope()) + { + _logger.LogDebug("Starting step {0} on workflow {1}", step.Name, workflow.Id); - IStepBody body = step.ConstructBody(_serviceProvider); + IStepBody body = step.ConstructBody(scope.ServiceProvider); - if (body == null) - { - _logger.LogError("Unable to construct step body {0}", step.BodyType.ToString()); - pointer.SleepUntil = _datetimeProvider.Now.ToUniversalTime().Add(_options.ErrorRetryInterval); - wfResult.Errors.Add(new ExecutionError() + if (body == null) { - WorkflowId = workflow.Id, - ExecutionPointerId = pointer.Id, - ErrorTime = _datetimeProvider.Now.ToUniversalTime(), - Message = String.Format("Unable to construct step body {0}", step.BodyType.ToString()) - }); - continue; - } + _logger.LogError("Unable to construct step body {0}", step.BodyType.ToString()); + pointer.SleepUntil = _datetimeProvider.Now.ToUniversalTime().Add(_options.ErrorRetryInterval); + wfResult.Errors.Add(new ExecutionError() + { + WorkflowId = workflow.Id, + ExecutionPointerId = pointer.Id, + ErrorTime = _datetimeProvider.Now.ToUniversalTime(), + Message = String.Format("Unable to construct step body {0}", step.BodyType.ToString()) + }); + continue; + } - IStepExecutionContext context = new StepExecutionContext() - { - Workflow = workflow, - Step = step, - PersistenceData = pointer.PersistenceData, - ExecutionPointer = pointer, - Item = pointer.ContextItem - }; + IStepExecutionContext context = new StepExecutionContext() + { + Workflow = workflow, + Step = step, + PersistenceData = pointer.PersistenceData, + ExecutionPointer = pointer, + Item = pointer.ContextItem + }; - foreach (var input in step.Inputs) - input.AssignInput(workflow.Data, body, context); + foreach (var input in step.Inputs) + input.AssignInput(workflow.Data, body, context); - switch (step.BeforeExecute(wfResult, context, pointer, body)) - { - case ExecutionPipelineDirective.Defer: - continue; - case ExecutionPipelineDirective.EndWorkflow: - workflow.Status = WorkflowStatus.Complete; - workflow.CompleteTime = _datetimeProvider.Now.ToUniversalTime(); - continue; - } + switch (step.BeforeExecute(wfResult, context, pointer, body)) + { + case ExecutionPipelineDirective.Defer: + continue; + case ExecutionPipelineDirective.EndWorkflow: + workflow.Status = WorkflowStatus.Complete; + workflow.CompleteTime = _datetimeProvider.Now.ToUniversalTime(); + continue; + } - var result = await body.RunAsync(context); + var result = await body.RunAsync(context); - if (result.Proceed) - { - foreach (var output in step.Outputs) - output.AssignOutput(workflow.Data, body, context); - } + if (result.Proceed) + { + foreach (var output in step.Outputs) + output.AssignOutput(workflow.Data, body, context); + } - _executionResultProcessor.ProcessExecutionResult(workflow, def, pointer, step, result, wfResult); - step.AfterExecute(wfResult, context, result, pointer); + _executionResultProcessor.ProcessExecutionResult(workflow, def, pointer, step, result, wfResult); + step.AfterExecute(wfResult, context, result, pointer); + } } catch (Exception ex) { diff --git a/test/WorkflowCore.IntegrationTests/Scenarios/DiScenario.cs b/test/WorkflowCore.IntegrationTests/Scenarios/DiScenario.cs new file mode 100644 index 000000000..81a782264 --- /dev/null +++ b/test/WorkflowCore.IntegrationTests/Scenarios/DiScenario.cs @@ -0,0 +1,255 @@ +using System; +using WorkflowCore.Interface; +using WorkflowCore.Models; +using Xunit; +using FluentAssertions; +using WorkflowCore.Testing; +using Microsoft.Extensions.DependencyInjection; +using Autofac.Extensions.DependencyInjection; +using Autofac; +using System.Diagnostics; + +namespace WorkflowCore.IntegrationTests.Scenarios +{ + public class DiWorkflow : IWorkflow + { + public string Id => "DiWorkflow"; + public int Version => 1; + + public void Build(IWorkflowBuilder builder) + { + builder + .StartWith() + .Output(_ => _.instance1, _ => _.dependency1.Instance) + .Output(_ => _.instance2, _ => _.dependency2.dependency1.Instance) + .Then(context => + { + return ExecutionResult.Next(); + }); + } + } + + public class DiData + { + public int instance1 { get; set; } = -1; + public int instance2 { get; set; } = -1; + } + + public class Dependency1 + { + private static int InstanceCounter = 0; + + public int Instance { get; set; } = ++InstanceCounter; + } + + public class Dependency2 + { + public Dependency1 dependency1 { get; private set; } + + public Dependency2(Dependency1 dependency1) + { + this.dependency1 = dependency1; + } + } + + public class DiStep1 : StepBody + { + public Dependency1 dependency1 { get; private set; } + public Dependency2 dependency2 { get; private set; } + + public DiStep1(Dependency1 dependency1, Dependency2 dependency2) + { + this.dependency1 = dependency1; + this.dependency2 = dependency2; + } + + public override ExecutionResult Run(IStepExecutionContext context) + { + return ExecutionResult.Next(); + } + } + + /// + /// The DI scenarios are design to test whether the scoped / transient dependecies are honoured with + /// various IoC container implementations. The basic premise is that a step has a dependency on + /// two services, one of which has a dependency on the other. + /// + /// We then use the instance numbers of the services to determine whether the container has created a + /// transient instance or a scoped instance + /// + /// if step.dependency2.dependency1.instance == step.dependency1.instance then + /// we can be assured that dependency1 was created in the same scope as dependency 2 + /// + /// otherwise if the instances are different, they were created as transient + /// + /// + public abstract class DiScenario : WorkflowTest + { + protected void ConfigureHost(IServiceProvider serviceProvider) + { + PersistenceProvider = serviceProvider.GetService(); + Host = serviceProvider.GetService(); + Host.RegisterWorkflow(); + Host.OnStepError += Host_OnStepError; + Host.Start(); + } + } + + /// + /// Because of the static InMemory Persistence provider, this test must run in issolation + /// to prevent other hosts from picking up steps intended for this host and incorrectly + /// cross-referencing the scoped / transient IoC container for step constrcution + /// + [CollectionDefinition("DiMsTransientScenario", DisableParallelization = true)] + [Collection("DiMsTransientScenario")] + public class DiMsTransientScenario : DiScenario + { + public DiMsTransientScenario() + { + //setup dependency injection + IServiceCollection services = new ServiceCollection(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddLogging(); + ConfigureServices(services); + + var serviceProvider = services.BuildServiceProvider(); + ConfigureHost(serviceProvider); + } + + [Fact] + public void Scenario() + { + var workflowId = StartWorkflow(new DiData()); + WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(5)); + var data = GetData(workflowId); + + // DI provider should have created two transient instances, with different instance ids + data.instance1.Should().NotBe(-1); + data.instance2.Should().NotBe(-1); + data.instance1.Should().NotBe(data.instance2); + } + } + + /// + /// Because of the static InMemory Persistence provider, this test must run in issolation + /// to prevent other hosts from picking up steps intended for this host and incorrectly + /// cross-referencing the scoped / transient IoC container for step constrcution + /// + [CollectionDefinition("DiMsScopedScenario", DisableParallelization = true)] + [Collection("DiMsScopedScenario")] + public class DiMsScopedScenario : DiScenario + { + public DiMsScopedScenario() + { + //setup dependency injection + IServiceCollection services = new ServiceCollection(); + services.AddScoped(); + services.AddScoped(); + services.AddTransient(); + services.AddLogging(); + ConfigureServices(services); + + var serviceProvider = services.BuildServiceProvider(); + ConfigureHost(serviceProvider); + } + + [Fact] + public void Scenario() + { + var workflowId = StartWorkflow(new DiData()); + WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(5)); + var data = GetData(workflowId); + + // scope provider should have created one scoped instance, with the same instance ids + data.instance1.Should().NotBe(-1); + data.instance2.Should().NotBe(-1); + data.instance1.Should().Be(data.instance2); + } + } + + /// + /// Because of the static InMemory Persistence provider, this test must run in issolation + /// to prevent other hosts from picking up steps intended for this host and incorrectly + /// cross-referencing the scoped / transient IoC container for step constrcution + /// + [CollectionDefinition("DiAutoFacTransientScenario", DisableParallelization = true)] + [Collection("DiAutoFacTransientScenario")] + public class DiAutoFacTransientScenario : DiScenario + { + public DiAutoFacTransientScenario() + { + //setup dependency injection + IServiceCollection services = new ServiceCollection(); + services.AddLogging(); + ConfigureServices(services); + + //setup dependency injection + var builder = new ContainerBuilder(); + builder.Populate(services); + builder.RegisterType().InstancePerDependency(); + builder.RegisterType().InstancePerDependency(); + builder.RegisterType().InstancePerDependency(); + var container = builder.Build(); + + var serviceProvider = new AutofacServiceProvider(container); + ConfigureHost(serviceProvider); + } + + [Fact] + public void Scenario() + { + var workflowId = StartWorkflow(new DiData()); + WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(5)); + var data = GetData(workflowId); + + // scope provider should have created one scoped instance, with the same instance ids + data.instance1.Should().NotBe(-1); + data.instance2.Should().NotBe(-1); + data.instance1.Should().NotBe(data.instance2); + } + } + + /// + /// Because of the static InMemory Persistence provider, this test must run in issolation + /// to prevent other hosts from picking up steps intended for this host and incorrectly + /// cross-referencing the scoped / transient IoC container for step constrcution + /// + [CollectionDefinition("DiAutoFacScopedScenario", DisableParallelization = true)] + [Collection("DiAutoFacScopedScenario")] + public class DiAutoFacScopedScenario : DiScenario + { + public DiAutoFacScopedScenario() + { + //setup dependency injection + IServiceCollection services = new ServiceCollection(); + services.AddLogging(); + ConfigureServices(services); + + //setup dependency injection + var builder = new ContainerBuilder(); + builder.Populate(services); + builder.RegisterType().InstancePerLifetimeScope(); + builder.RegisterType().InstancePerLifetimeScope(); + builder.RegisterType().InstancePerLifetimeScope(); + var container = builder.Build(); + + var serviceProvider = new AutofacServiceProvider(container); + ConfigureHost(serviceProvider); + } + + [Fact] + public void Scenario() + { + var workflowId = StartWorkflow(null); + WaitForWorkflowToComplete(workflowId, TimeSpan.FromSeconds(5)); + var data = GetData(workflowId); + + // scope provider should have created one scoped instance, with the same instance ids + data.instance1.Should().NotBe(-1); + data.instance2.Should().NotBe(-1); + data.instance1.Should().Be(data.instance2); + } + } +} diff --git a/test/WorkflowCore.IntegrationTests/WorkflowCore.IntegrationTests.csproj b/test/WorkflowCore.IntegrationTests/WorkflowCore.IntegrationTests.csproj index ddde2f87a..a2a88fb71 100644 --- a/test/WorkflowCore.IntegrationTests/WorkflowCore.IntegrationTests.csproj +++ b/test/WorkflowCore.IntegrationTests/WorkflowCore.IntegrationTests.csproj @@ -18,6 +18,7 @@ + diff --git a/test/WorkflowCore.Testing/WorkflowTest.cs b/test/WorkflowCore.Testing/WorkflowTest.cs index 3ab2ec91b..9c7d0dacc 100644 --- a/test/WorkflowCore.Testing/WorkflowTest.cs +++ b/test/WorkflowCore.Testing/WorkflowTest.cs @@ -39,7 +39,7 @@ protected virtual void Setup() Host.Start(); } - private void Host_OnStepError(WorkflowInstance workflow, WorkflowStep step, Exception exception) + protected void Host_OnStepError(WorkflowInstance workflow, WorkflowStep step, Exception exception) { UnhandledStepErrors.Add(new StepError() { diff --git a/test/WorkflowCore.UnitTests/Services/WorkflowExecutorFixture.cs b/test/WorkflowCore.UnitTests/Services/WorkflowExecutorFixture.cs index d94ed3416..7597532d5 100644 --- a/test/WorkflowCore.UnitTests/Services/WorkflowExecutorFixture.cs +++ b/test/WorkflowCore.UnitTests/Services/WorkflowExecutorFixture.cs @@ -25,6 +25,7 @@ public class WorkflowExecutorFixture protected ILifeCycleEventPublisher EventHub; protected ICancellationProcessor CancellationProcessor; protected IServiceProvider ServiceProvider; + protected IScopeProvider ScopeProvider; protected IDateTimeProvider DateTimeProvider; protected WorkflowOptions Options; @@ -33,6 +34,7 @@ public WorkflowExecutorFixture() Host = A.Fake(); PersistenceProvider = A.Fake(); ServiceProvider = A.Fake(); + ScopeProvider = A.Fake(); Registry = A.Fake(); ResultProcesser = A.Fake(); EventHub = A.Fake(); @@ -41,13 +43,17 @@ public WorkflowExecutorFixture() Options = new WorkflowOptions(A.Fake()); + var scope = A.Fake(); + A.CallTo(() => ScopeProvider.CreateScope()).Returns(scope); + A.CallTo(() => scope.ServiceProvider).Returns(ServiceProvider); + A.CallTo(() => DateTimeProvider.Now).Returns(DateTime.Now); //config logging var loggerFactory = new LoggerFactory(); loggerFactory.AddConsole(LogLevel.Debug); - Subject = new WorkflowExecutor(Registry, ServiceProvider, DateTimeProvider, ResultProcesser, EventHub, CancellationProcessor, Options, loggerFactory); + Subject = new WorkflowExecutor(Registry, ServiceProvider, ScopeProvider, DateTimeProvider, ResultProcesser, EventHub, CancellationProcessor, Options, loggerFactory); } [Fact(DisplayName = "Should execute active step")]