Skip to content

Commit

Permalink
fix: make choice about choosing the next step from the workflow data
Browse files Browse the repository at this point in the history
  • Loading branch information
iancooper committed Nov 11, 2024
1 parent 56f577e commit 3cb3c4b
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 31 deletions.
35 changes: 11 additions & 24 deletions src/Paramore.Brighter.MediatorWorkflow/Workflows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ namespace Paramore.Brighter.MediatorWorkflow;
/// <param name="Action">The action to be taken with the step.</param>
/// <param name="OnCompletion">The action to be taken upon completion of the step.</param>
/// <param name="Next">The next step in the sequence.</param>
public record Step<TData>(string Name, IWorkflowAction<TData> Action, Action OnCompletion, Step<TData>? Next);
public record Step<TData>(string Name, IWorkflowAction<TData> Action, Action OnCompletion, Step<TData>? Next, Action? OnFaulted = null, Step<TData>? FaultNext = null);

/// <summary>
/// Defines an interface for workflow actions.
Expand All @@ -53,37 +53,24 @@ public interface IWorkflowAction<TData>
/// <summary>
/// Represents a workflow based on evaluating a specification to determine which one to send
/// </summary>
/// <param name="trueRequestFactory">The type of the true branch</param>
/// <param name="falseRequestFactory">The type of the false branch</param>
/// <param name="predicate">The rule that decides between the command issued by each branch</param>
/// <typeparam name="TTrueRequest"></typeparam>
/// <typeparam name="TFalseRequest"></typeparam>
/// <typeparam name="TData"></typeparam>
public class Choice<TTrueRequest, TFalseRequest, TData>(
Func<TTrueRequest> trueRequestFactory,
Func<TFalseRequest> falseRequestFactory,
/// <typeparam name="TData">The workflow data, used to make the choice</typeparam>
public class Choice<TData>(
Func<TData, Step<TData>> OnTrue,
Func<TData, Step<TData>> OnFalse,
ISpecification<TData> predicate
)
: IWorkflowAction<TData>
where TTrueRequest : class, IRequest
where TFalseRequest : class, IRequest
{
public void Handle(Workflow<TData> state, IAmACommandProcessor commandProcessor)
{
//NOTE: we chose the command handler by parameterized type from the argument to Send() so the type needs to be explicit here
// do not try to optimize this branch condition via a base type, it will not work
if (predicate.IsSatisfiedBy(state.Data))
{
TTrueRequest command = trueRequestFactory();
command.CorrelationId = state.Id;
commandProcessor.Send(command);
}
else
if (state.CurrentStep is null)
throw new InvalidOperationException("The workflow has not been initialized.");

state.CurrentStep = state.CurrentStep with
{
TFalseRequest command = falseRequestFactory();
command.CorrelationId = state.Id;
commandProcessor.Send(command);
}
Next = (predicate.IsSatisfiedBy(state.Data) ? OnTrue(state.Data) : OnFalse(state.Data))
};
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#region Licence
/* The MIT License (MIT)
Copyright © 2014 Ian Cooper <[email protected]>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the “Software”), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. */

#endregion

using System;

namespace Paramore.Brighter.Core.Tests.Workflows.TestDoubles
{
internal class MyFault(string? value) : Event(Guid.NewGuid().ToString())
{
public string Value { get; set; } = value ?? string.Empty;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ public class MediatorFailingChoiceFlowTests
private readonly Mediator<WorkflowTestData>? _mediator;
private readonly Workflow<WorkflowTestData> _flow;
private bool _stepCompletedOne;
private bool _stepCompletedTwo;
private bool _stepCompletedThree;

public MediatorFailingChoiceFlowTests()
{
Expand All @@ -36,10 +38,20 @@ public MediatorFailingChoiceFlowTests()
var workflowData= new WorkflowTestData();
workflowData.Bag.Add("MyValue", "Fail");

var stepOne = new Step<WorkflowTestData>("Test of Workflow Step One",
new Choice<MyCommand, MyOtherCommand, WorkflowTestData>(
() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! },
() => new MyOtherCommand { Value = (workflowData.Bag["MyValue"] as string)! },
var stepThree = new Step<WorkflowTestData>("Test of Workflow Step Three",
new FireAndForget<MyOtherCommand, WorkflowTestData>(() => new MyOtherCommand { Value = (workflowData.Bag["MyValue"] as string)! }),
() => { _stepCompletedThree = true; },
null);

var stepTwo = new Step<WorkflowTestData>("Test of Workflow Step Two",
new FireAndForget<MyCommand, WorkflowTestData>(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }),
() => { _stepCompletedTwo = true; },
null);

var stepOne = new Step<WorkflowTestData>("Test of Workflow Step One",
new Choice<WorkflowTestData>(
(_) => stepTwo,
(_) => stepThree,
new Specification<WorkflowTestData>(x => x.Bag["MyValue"] as string == "Pass")),
() => { _stepCompletedOne = true; },
null);
Expand All @@ -61,6 +73,8 @@ public void When_running_a_choice_workflow_step()
_mediator?.RunWorkFlow(_flow);

_stepCompletedOne.Should().BeTrue();
_stepCompletedTwo.Should().BeFalse();
_stepCompletedThree.Should().BeTrue();
MyOtherCommandHandler.ReceivedCommands.Any(c => c.Value == "Fail").Should().BeTrue();
MyCommandHandler.ReceivedCommands.Any().Should().BeFalse();
_stepCompletedOne.Should().BeTrue();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ public class MediatorPassingChoiceFlowTests
private readonly Mediator<WorkflowTestData>? _mediator;
private readonly Workflow<WorkflowTestData> _flow;
private bool _stepCompletedOne;
private bool _stepCompletedTwo;
private bool _stepCompletedThree;

public MediatorPassingChoiceFlowTests()
{
Expand All @@ -35,11 +37,21 @@ public MediatorPassingChoiceFlowTests()

var workflowData= new WorkflowTestData();
workflowData.Bag.Add("MyValue", "Pass");

var stepThree = new Step<WorkflowTestData>("Test of Workflow Step Three",
new FireAndForget<MyOtherCommand, WorkflowTestData>(() => new MyOtherCommand { Value = (workflowData.Bag["MyValue"] as string)! }),
() => { _stepCompletedThree = true; },
null);

var stepTwo = new Step<WorkflowTestData>("Test of Workflow Step Two",
new FireAndForget<MyCommand, WorkflowTestData>(() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! }),
() => { _stepCompletedTwo = true; },
null);

var stepOne = new Step<WorkflowTestData>("Test of Workflow Step One",
new Choice<MyCommand, MyOtherCommand, WorkflowTestData>(
() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! },
() => new MyOtherCommand { Value = (workflowData.Bag["MyValue"] as string)! },
new Choice<WorkflowTestData>(
(_) => stepTwo,
(_) => stepThree,
new Specification<WorkflowTestData>(x => x.Bag["MyValue"] as string == "Pass")),
() => { _stepCompletedOne = true; },
null);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System;
using System.Linq;
using Amazon.Runtime.Internal.Transform;
using FluentAssertions;
using Paramore.Brighter.Core.Tests.Workflows.TestDoubles;
using Paramore.Brighter.MediatorWorkflow;
using Polly.Registry;
using Xunit;

namespace Paramore.Brighter.Core.Tests.Workflows;

public class MediatorRobustReplyNoFaultStepFlowTests
{
private readonly Mediator<WorkflowTestData> _mediator;
private bool _stepCompleted;
private bool _stepFaulted;
private readonly Workflow<WorkflowTestData> _flow;

public MediatorRobustReplyNoFaultStepFlowTests()
{
var registry = new SubscriberRegistry();
registry.Register<MyCommand, MyCommandHandler>();
registry.Register<MyEvent, MyEventHandler>();

IAmACommandProcessor commandProcessor = null;
var handlerFactory = new SimpleHandlerFactorySync((handlerType) =>
handlerType switch
{
_ when handlerType == typeof(MyCommandHandler) => new MyCommandHandler(commandProcessor),
_ when handlerType == typeof(MyEventHandler) => new MyEventHandler(_mediator),
_ => throw new InvalidOperationException($"The handler type {handlerType} is not supported")
});

commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry());
PipelineBuilder<MyCommand>.ClearPipelineCache();

var workflowData= new WorkflowTestData();
workflowData.Bag.Add("MyValue", "Test");

var firstStep = new Step<WorkflowTestData>("Test of Workflow",
new RobustRequestAndReaction<MyCommand, MyEvent, MyFault, WorkflowTestData>(
() => new MyCommand { Value = (workflowData.Bag["MyValue"] as string)! },
(reply) => workflowData.Bag.Add("MyReply", ((MyEvent)reply).Value),
(fault) => workflowData.Bag.Add("MyFault", ((MyFault)fault).Value)),
() => { _stepCompleted = true; },
null,
() => { _stepFaulted = true; },
null);

_flow = new Workflow<WorkflowTestData>(firstStep, workflowData) ;

_mediator = new Mediator<WorkflowTestData>(
commandProcessor,
new InMemoryWorkflowStore()
);
}

[Fact]
public void When_running_a_workflow_with_reply()
{
MyCommandHandler.ReceivedCommands.Clear();
MyEventHandler.ReceivedEvents.Clear();

_mediator.RunWorkFlow(_flow);

_stepCompleted.Should().BeTrue();
_stepFaulted.Should().BeFalse();

MyCommandHandler.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue();
MyEventHandler.ReceivedEvents.Any(e => e.Value == "Test").Should().BeTrue();
_flow.State.Should().Be(WorkflowState.Done);
}
}

0 comments on commit 3cb3c4b

Please sign in to comment.