diff --git a/Core/Stateflows/StateMachines/Engine/Executor.cs b/Core/Stateflows/StateMachines/Engine/Executor.cs index 7ecfd9f2..8994b41d 100644 --- a/Core/Stateflows/StateMachines/Engine/Executor.cs +++ b/Core/Stateflows/StateMachines/Engine/Executor.cs @@ -25,14 +25,15 @@ internal sealed class Executor : IDisposable public StateMachinesRegister Register { get; set; } - public IServiceProvider ServiceProvider => Scope.ServiceProvider; + public IServiceProvider ServiceProvider => ScopesStack.Peek().ServiceProvider; - private readonly IServiceScope Scope; + //private readonly IServiceScope Scope; + private readonly Stack ScopesStack = new Stack(); public Executor(StateMachinesRegister register, Graph graph, IServiceProvider serviceProvider, StateflowsContext stateflowsContext, Event @event) { Register = register; - Scope = serviceProvider.CreateScope(); + ScopesStack.Push(serviceProvider.CreateScope()); Graph = graph; Context = new RootContext(stateflowsContext, this, @event); var logger = ServiceProvider.GetService>(); @@ -41,7 +42,18 @@ public Executor(StateMachinesRegister register, Graph graph, IServiceProvider se public void Dispose() { - Scope.Dispose(); + //Scope.Dispose(); + } + + public void BeginScope() + { + ScopesStack.Push(ServiceProvider.CreateScope()); + } + + public void EndScope() + { + var scope = ScopesStack.Pop(); + scope.Dispose(); } public readonly RootContext Context; @@ -332,6 +344,8 @@ private async Task DoProcessAsync(TEvent @event) private async Task DoGuardAsync(Edge edge) where TEvent : Event, new() { + BeginScope(); + var context = new GuardContext(Context, edge); await Inspector.BeforeTransitionGuardAsync(context); @@ -340,12 +354,15 @@ private async Task DoGuardAsync(Edge edge) await Inspector.AfterGuardAsync(context, result); + return result; } private async Task DoEffectAsync(Edge edge) where TEvent : Event, new() { + BeginScope(); + var context = new TransitionContext(Context, edge); await Inspector.BeforeEffectAsync(context); @@ -353,10 +370,14 @@ private async Task DoEffectAsync(Edge edge) await edge.Effects.WhenAll(Context); await Inspector.AfterEffectAsync(context); + + EndScope(); } public async Task DoInitializeStateMachineAsync(InitializationRequest @event) { + BeginScope(); + var result = false; if ( @@ -384,57 +405,79 @@ public async Task DoInitializeStateMachineAsync(InitializationRequest @eve await Inspector.AfterStateMachineInitializeAsync(context); } + EndScope(); + return result; } public async Task DoFinalizeStateMachineAsync() { + BeginScope(); + var context = new StateMachineActionContext(Context); await Inspector.BeforeStateMachineFinalizeAsync(context); await Graph.Finalize.WhenAll(Context); await Inspector.AfterStateMachineFinalizeAsync(context); + + EndScope(); } public async Task DoInitializeStateAsync(Vertex vertex) { + BeginScope(); + var context = new StateActionContext(Context, vertex, Constants.Initialize); await Inspector.BeforeStateInitializeAsync(context); await vertex.Initialize.WhenAll(Context); await Inspector.AfterStateInitializeAsync(context); + + EndScope(); } public async Task DoFinalizeStateAsync(Vertex vertex) { + BeginScope(); + var context = new StateActionContext(Context, vertex, Constants.Finalize); await Inspector.BeforeStateFinalizeAsync(context); await vertex.Finalize.WhenAll(Context); await Inspector.AfterStateFinalizeAsync(context); + + EndScope(); } public async Task DoEntryAsync(Vertex vertex) { + BeginScope(); + var context = new StateActionContext(Context, vertex, Constants.Entry); await Inspector.BeforeStateEntryAsync(context); await vertex.Entry.WhenAll(Context); await Inspector.AfterStateEntryAsync(context); + + EndScope(); } private async Task DoExitAsync(Vertex vertex) { + BeginScope(); + var context = new StateActionContext(Context, vertex, Constants.Exit); await Inspector.BeforeStateExitAsync(context); await vertex.Exit.WhenAll(Context); await Inspector.AfterStateExitAsync(context); + + EndScope(); } private async Task DoConsumeAsync(Edge edge) diff --git a/Core/Stateflows/StateMachines/Engine/Processor.cs b/Core/Stateflows/StateMachines/Engine/Processor.cs index 152e4075..177a317e 100644 --- a/Core/Stateflows/StateMachines/Engine/Processor.cs +++ b/Core/Stateflows/StateMachines/Engine/Processor.cs @@ -76,11 +76,19 @@ public async Task ProcessEventAsync(BehaviorId id, TEvent @ executor.Context.SetEvent(ev); - var status = await ExecuteBehaviorAsync(ev, result, stateflowsContext, graph, executor); - - results.Add(new RequestResult(ev, ev.GetResponse(), status, new EventValidation(true, new List()))); - - executor.Context.ClearEvent(); + executor.BeginScope(); + try + { + var status = await ExecuteBehaviorAsync(ev, result, stateflowsContext, graph, executor); + + results.Add(new RequestResult(ev, ev.GetResponse(), status, new EventValidation(true, new List()))); + } + finally + { + executor.EndScope(); + + executor.Context.ClearEvent(); + } } compoundRequest.Respond(new CompoundResponse() @@ -90,7 +98,15 @@ public async Task ProcessEventAsync(BehaviorId id, TEvent @ } else { - result = await ExecuteBehaviorAsync(@event, result, stateflowsContext, graph, executor); + executor.BeginScope(); + try + { + result = await ExecuteBehaviorAsync(@event, result, stateflowsContext, graph, executor); + } + finally + { + executor.EndScope(); + } } await executor.DehydrateAsync(); @@ -116,9 +132,17 @@ public async Task ProcessEventAsync(BehaviorId id, TEvent @ { executor.Context.SetEvent(initializationRequest); - await executor.InitializeAsync(initializationRequest); - - executor.Context.ClearEvent(); + executor.BeginScope(); + try + { + await executor.InitializeAsync(initializationRequest); + } + finally + { + executor.EndScope(); + + executor.Context.ClearEvent(); + } } } diff --git a/Examples/Examples.Common/SomeEvent.cs b/Examples/Examples.Common/SomeEvent.cs index fffdc9e5..9513ccb5 100644 --- a/Examples/Examples.Common/SomeEvent.cs +++ b/Examples/Examples.Common/SomeEvent.cs @@ -5,5 +5,6 @@ namespace Examples.Common public class SomeEvent : Event { public string TheresSomethingHappeningHere { get; set; } = "What it is ain't exactly clear"; + public int DelaySize { get; set; } } } \ No newline at end of file diff --git a/Tests/Activity.IntegrationTests/Tests/ServiceScopes.cs b/Tests/Activity.IntegrationTests/Tests/ServiceScopes.cs new file mode 100644 index 00000000..0c197203 --- /dev/null +++ b/Tests/Activity.IntegrationTests/Tests/ServiceScopes.cs @@ -0,0 +1,96 @@ +using Activity.IntegrationTests.Classes.Tokens; +using Microsoft.Extensions.DependencyInjection; +using Stateflows.Activities.Typed; +using Stateflows.Common; +using StateMachine.IntegrationTests.Utils; +using System.Runtime.CompilerServices; + +namespace Activity.IntegrationTests.Tests +{ + public class Service + { + public Service() + { + Value = Random.Shared.Next().ToString(); + } + + public readonly string Value; + } + + public class ScopeAction1 : ActionNode + { + private readonly Service service; + public ScopeAction1(Service service) + { + this.service = service; + } + + public override Task ExecuteAsync() + { + ServiceScopes.Value1 = service.Value; + + return Task.CompletedTask; + } + } + + public class ScopeAction2 : ActionNode + { + private readonly Service service; + public ScopeAction2(Service service) + { + this.service = service; + } + + public override Task ExecuteAsync() + { + ServiceScopes.Value1 = service.Value; + + return Task.CompletedTask; + } + } + + [TestClass] + public class ServiceScopes : StateflowsTestClass + { + public static string Value1 = string.Empty; + public static string Value2 = string.Empty; + + [TestInitialize] + public override void Initialize() + => base.Initialize(); + + [TestCleanup] + public override void Cleanup() + => base.Cleanup(); + + protected override void InitializeStateflows(IStateflowsBuilder builder) + { + builder + .AddActivities(b => b + .AddActivity("scopes", b => b + .AddInitial(b => b + .AddControlFlow() + ) + .AddAction(b => b + .AddControlFlow() + ) + .AddAction() + ) + ) + + .ServiceCollection.AddScoped() + ; + } + + [TestMethod] + public async Task SeparateScopesOnActions() + { + if (ActivityLocator.TryLocateActivity(new ActivityId("scopes", "x"), out var a)) + { + await a.InitializeAsync(); + } + + Assert.AreNotEqual(Value1, Value2); + } + } +} \ No newline at end of file diff --git a/Tests/StateMachine/StateMachine.IntegrationTests/Tests/Concurrency.cs b/Tests/StateMachine/StateMachine.IntegrationTests/Tests/Concurrency.cs new file mode 100644 index 00000000..e2637717 --- /dev/null +++ b/Tests/StateMachine/StateMachine.IntegrationTests/Tests/Concurrency.cs @@ -0,0 +1,110 @@ +using Stateflows.Common; +using StateMachine.IntegrationTests.Utils; + +namespace StateMachine.IntegrationTests.Tests +{ + [TestClass] + public class Concurrency : StateflowsTestClass + { + public bool eventConsumed = false; + + [TestInitialize] + public override void Initialize() + => base.Initialize(); + + [TestCleanup] + public override void Cleanup() + => base.Cleanup(); + + protected override void InitializeStateflows(IStateflowsBuilder builder) + { + builder + + .AddStateMachines(b => b + .AddStateMachine("a", b => b + .AddExecutionSequenceObserver() + .AddInitialState("a_state1", b => b + .AddTransition("a_state2", b => b + .AddEffect(async c => await Task.Delay(100)) + ) + ) + .AddState("a_state2") + ) + + .AddStateMachine("b", b => b + .AddExecutionSequenceObserver() + .AddInitialState("b_state1", b => b + .AddTransition("b_state2") + ) + .AddState("b_state2") + ) + + .AddStateMachine("instance", b => b + .AddExecutionSequenceObserver() + .AddInitialState("state1", b => b + .AddTransition("state2", b => b + .AddGuard(async c => c.Event.DelaySize > 0) + .AddEffect(async c => await Task.Delay(c.Event.DelaySize)) + ) + .AddElseTransition("state3") + ) + .AddState("state2") + .AddState("state3") + ) + ) + ; + } + + [TestMethod] + public async Task TwoConcurrentBehaviors() + { + var initialized = false; + string currentState1 = ""; + var someStatus1 = EventStatus.Rejected; + + if (StateMachineLocator.TryLocateStateMachine(new StateMachineId("a", "x"), out var a) && + StateMachineLocator.TryLocateStateMachine(new StateMachineId("b", "x"), out var b)) + { + _ = await a.InitializeAsync(); + _ = await b.InitializeAsync(); + await Task.WhenAll( + a.SendAsync(new SomeEvent()), + b.SendAsync(new SomeEvent()) + ); + } + + ExecutionSequence.Verify(b => b + .StateEntry("a_state1") + .StateEntry("b_state1") + .StateEntry("b_state2") + .StateEntry("a_state2") + ); + } + + [TestMethod] + public async Task TwoConcurrentInstances() + { + var initialized = false; + string currentState1 = ""; + var someStatus1 = EventStatus.Rejected; + + if (StateMachineLocator.TryLocateStateMachine(new StateMachineId("instance", "a"), out var a) && + StateMachineLocator.TryLocateStateMachine(new StateMachineId("instance", "b"), out var b)) + { + _ = await a.InitializeAsync(); + _ = await b.InitializeAsync(); + await Task.WhenAll( + a.SendAsync(new SomeEvent() { DelaySize = 100 }), + b.SendAsync(new SomeEvent() { DelaySize = 0 }) + ); + } + + ExecutionSequence.Verify(b => b + .StateEntry("state1") + .StateEntry("state1") + .StateEntry("state3") + .StateEntry("state2") + ); + } + } +} \ No newline at end of file diff --git a/Tests/StateMachine/StateMachine.IntegrationTests/Tests/ServiceScopes.cs b/Tests/StateMachine/StateMachine.IntegrationTests/Tests/ServiceScopes.cs new file mode 100644 index 00000000..a6669853 --- /dev/null +++ b/Tests/StateMachine/StateMachine.IntegrationTests/Tests/ServiceScopes.cs @@ -0,0 +1,144 @@ +using StateMachine.IntegrationTests.Classes.StateMachines; +using StateMachine.IntegrationTests.Classes.States; +using StateMachine.IntegrationTests.Utils; +using Stateflows.StateMachines.Typed; +using Stateflows.Common; +using Microsoft.Extensions.DependencyInjection; + +namespace StateMachine.IntegrationTests.Tests +{ + public class Service + { + public Service() + { + Value = Random.Shared.Next().ToString(); + } + + public readonly string Value; + } + + public class ScopeState : State + { + private readonly Service service; + public ScopeState(Service service) + { + this.service = service; + } + + public override Task OnEntryAsync() + { + ServiceScopes.EntryValue = service.Value; + + return Task.CompletedTask; + } + + public override Task OnExitAsync() + { + ServiceScopes.ExitValue = service.Value; + + return Task.CompletedTask; + } + } + + public class Some : Transition + { + private readonly Service service; + public Some(Service service) + { + this.service = service; + } + + public override Task EffectAsync() + { + ServiceScopes.SomeValue = service.Value; + + return Task.CompletedTask; + } + } + + public class Other : Transition + { + private readonly Service service; + public Other(Service service) + { + this.service = service; + } + + public override Task EffectAsync() + { + ServiceScopes.OtherValue = service.Value; + + return Task.CompletedTask; + } + } + + [TestClass] + public class ServiceScopes : StateflowsTestClass + { + public static string SomeValue = string.Empty; + public static string OtherValue = string.Empty; + public static string EntryValue = string.Empty; + public static string ExitValue = string.Empty; + + [TestInitialize] + public override void Initialize() + => base.Initialize(); + + [TestCleanup] + public override void Cleanup() + => base.Cleanup(); + + protected override void InitializeStateflows(IStateflowsBuilder builder) + { + builder + .AddStateMachines(b => b + .AddStateMachine("compound", b => b + .AddInitialState("initial", b => b + .AddInternalTransition() + .AddInternalTransition() + ) + ) + + .AddStateMachine("state", b => b + .AddInitialState(b => b + .AddDefaultTransition("state2") + ) + .AddState("state2") + ) + ) + + .ServiceCollection.AddScoped() + ; + } + + [TestMethod] + public async Task SeparateScopesOnCompoundEvents() + { + if (StateMachineLocator.TryLocateStateMachine(new StateMachineId("compound", "x"), out var sm)) + { + await sm.InitializeAsync(); + await sm.SendAsync(new CompoundRequest() + { + Events = new List() + { + new SomeEvent(), + new OtherEvent(), + } + }); + } + + Assert.AreNotEqual(ServiceScopes.SomeValue, ServiceScopes.OtherValue); + } + + [TestMethod] + public async Task SeparateScopesOnStateEvents() + { + if (StateMachineLocator.TryLocateStateMachine(new StateMachineId("state", "x"), out var sm)) + { + await sm.InitializeAsync(); + } + + Assert.AreNotEqual(ServiceScopes.SomeValue, ServiceScopes.OtherValue); + } + } +} \ No newline at end of file