private void ConfigureStateMachine() { // Use builder pattern to configure each state _stateMachine.ConfigureState(SaleState.Open) // Add possible transitions .AddTransition(SaleEvent.Cancel, SaleState.Cancelled, condition: sale => sale.IsCancellable()) .AddTransition(SaleEvent.SetItemQuantity, SaleState.Overpaid, condition: sale => sale.HasPositiveBalance()) .AddTransition(SaleEvent.SetItemQuantity, SaleState.Paid, condition: sale => sale.IsPaid) .AddTransition(SaleEvent.Pay, SaleState.Overpaid, condition: sale => sale.HasPositiveBalance()) .AddTransition(SaleEvent.Pay, SaleState.Paid, condition: sale => sale.IsPaid) // Add trigger actions -- how to process requests passed from Sale // - Include parameter type where required .AddTriggerAction <SaleItem>(SaleEvent.AddItem, (sale, item) => sale.AddItemInternal(item)) .AddTriggerAction <SaleItem>(SaleEvent.SetItemQuantity, (sale, item) => sale.SetItemQuantityInternal(item)) .AddTriggerAction <SaleItem>(SaleEvent.DeleteItem, (sale, item) => sale.DeleteItemInternal(item)) .AddTriggerAction <Payment>(SaleEvent.Pay, (sale, payment) => sale.AddPaymentInternal(payment)) .AddTriggerAction <Change>(SaleEvent.GiveChange, (sale, change) => sale.AddChangeInternal(change)) .AddTriggerAction(SaleEvent.Cancel, sale => sale.CancelInternal()); _stateMachine.ConfigureState(SaleState.Overpaid) .MakeSubstateOf(_stateMachine.ConfigureState(SaleState.Open)) // This inherits all of Open's configuration unless overridden // Overpaid can go back to Open if the balance goes negative .AddTransition(SaleEvent.GiveChange, SaleState.Open, condition: sale => sale.HasNegativeBalance()) .AddTransition(SaleEvent.AddItem, SaleState.Open, condition: sale => sale.HasNegativeBalance()) .AddTransition(SaleEvent.SetItemQuantity, SaleState.Open, condition: sale => sale.HasNegativeBalance()) // Can't add payments when customer over paid .AddTriggerAction <Payment>(SaleEvent.Pay, (sale, payment) => throw new InvalidOperationException("Cannot pay on an overpaid sale")); _stateMachine.ConfigureState(SaleState.Finalized) // This is the parent class for any finalized state -- Paid and Cancelled // -- No actions or transitions allowed .AddTriggerAction <SaleItem>(SaleEvent.AddItem, (_, item) => throw new InvalidOperationException("Cannot add item to a finalized sale")) .AddTriggerAction <SaleItem>(SaleEvent.SetItemQuantity, (_, item) => throw new InvalidOperationException("Cannot change item quantities a finalized sale")) .AddTriggerAction <SaleItem>(SaleEvent.DeleteItem, (_, item) => throw new InvalidOperationException("Cannot delete item on a finalized sale")) .AddTriggerAction <Payment>(SaleEvent.Pay, (_, payment) => throw new InvalidOperationException("Cannot pay on a finalized sale")) .AddTriggerAction <Change>(SaleEvent.GiveChange, (_, change) => throw new InvalidOperationException("Cannot give change on a finalized sale")) .AddTriggerAction(SaleEvent.Cancel, sale => throw new InvalidOperationException("Cannot cancel a finalized sale")); _stateMachine.ConfigureState(SaleState.Paid) .MakeSubstateOf(_stateMachine.ConfigureState(SaleState.Finalized)); _stateMachine.ConfigureState(SaleState.Cancelled) .MakeSubstateOf(_stateMachine.ConfigureState(SaleState.Finalized)); #if DEBUG // This is how to export state configuration to Csv and/or DotGraph var configSummary = _stateMachine.GetSummary(); Debug.WriteLine(CsvExporter <SaleState, SaleEvent> .Export(configSummary)); Debug.WriteLine(DotGraphExporter <SaleState, SaleEvent> .Export(configSummary)); // Plug the output of the DotGraphExporter into https://dreampuf.github.io/GraphvizOnline to see graph of current configuration #endif }
public void SubStates_summarized_correctly() { var stateMachine = new StateMachine <TestSale, TestSaleState, TestSaleEvent>( stateAccessor: sale1 => sale1.State, stateMutator: (sale1, state) => sale1.State = state); stateMachine.ConfigureState(TestSaleState.Open) .AddTransition(TestSaleEvent.Cancel, TestSaleState.Cancelled, condition: sale => sale.IsCancellable()) .AddTransition(TestSaleEvent.SetItemQuantity, TestSaleState.Overpaid, condition: sale => sale.HasPositiveBalance()) .AddTransition(TestSaleEvent.SetItemQuantity, TestSaleState.Paid, condition: sale => sale.IsPaid) .AddTransition(TestSaleEvent.Pay, TestSaleState.Overpaid, condition: sale => sale.HasPositiveBalance()) .AddTransition(TestSaleEvent.Pay, TestSaleState.Paid, condition: sale => sale.IsPaid); stateMachine.ConfigureState(TestSaleState.Overpaid) .MakeSubStateOf(stateMachine.ConfigureState(TestSaleState.Open)) // This inherits all of Open's configuration unless overridden // Overpaid can go back to Open if the balance goes negative .AddTransition(TestSaleEvent.GiveChange, TestSaleState.Open, condition: sale => sale.HasNegativeBalance()) .AddTransition(TestSaleEvent.GiveChange, TestSaleState.Paid, condition: sale => sale.IsPaid) .AddTransition(TestSaleEvent.AddItem, TestSaleState.Open, condition: sale => sale.HasNegativeBalance()) .AddTransition(TestSaleEvent.SetItemQuantity, TestSaleState.Open, condition: sale => sale.HasNegativeBalance()) .AddTransition(TestSaleEvent.SetItemQuantity, TestSaleState.Paid, condition: sale => sale.IsPaid); stateMachine.ConfigureState(TestSaleState.Finalized); // This is the parent class for any finalized state -- Paid and Cancelled // -- No actions or transitions allowed stateMachine.ConfigureState(TestSaleState.Paid) .MakeSubStateOf(stateMachine.ConfigureState(TestSaleState.Finalized)); stateMachine.ConfigureState(TestSaleState.Cancelled) .MakeSubStateOf(stateMachine.ConfigureState(TestSaleState.Finalized)); var configSummary = stateMachine.GetSummary(); _testOutputHelper.WriteLine(DotGraphExporter <TestSaleState, TestSaleEvent> .Export(configSummary)); Assert.Equal(0, configSummary.StartingStates.Count); Assert.Contains(configSummary.FinalStates, s => s.State == TestSaleState.Cancelled); Assert.Contains(configSummary.FinalStates, s => s.State == TestSaleState.Paid); Assert.Contains(configSummary.Transitions, t => t.FromState.State == TestSaleState.Open && t.ToState.State == TestSaleState.Cancelled && t.Trigger == TestSaleEvent.Cancel); Assert.Contains(configSummary.Transitions, t => t.FromState.State == TestSaleState.Open && t.ToState.State == TestSaleState.Overpaid && t.Trigger == TestSaleEvent.SetItemQuantity); Assert.Contains(configSummary.Transitions, t => t.FromState.State == TestSaleState.Open && t.ToState.State == TestSaleState.Paid && t.Trigger == TestSaleEvent.SetItemQuantity); Assert.Contains(configSummary.Transitions, t => t.FromState.State == TestSaleState.Open && t.ToState.State == TestSaleState.Overpaid && t.Trigger == TestSaleEvent.Pay); Assert.Contains(configSummary.Transitions, t => t.FromState.State == TestSaleState.Open && t.ToState.State == TestSaleState.Paid && t.Trigger == TestSaleEvent.Pay); Assert.Contains(configSummary.Transitions, t => t.FromState.State == TestSaleState.Overpaid && t.ToState.State == TestSaleState.Open && t.Trigger == TestSaleEvent.GiveChange); Assert.Contains(configSummary.Transitions, t => t.FromState.State == TestSaleState.Overpaid && t.ToState.State == TestSaleState.Open && t.Trigger == TestSaleEvent.AddItem); Assert.Contains(configSummary.Transitions, t => t.FromState.State == TestSaleState.Overpaid && t.ToState.State == TestSaleState.Open && t.Trigger == TestSaleEvent.SetItemQuantity); Assert.Contains(configSummary.Transitions, t => t.FromState.State == TestSaleState.Overpaid && t.ToState.State == TestSaleState.Paid && t.Trigger == TestSaleEvent.GiveChange); Assert.Contains(configSummary.Transitions, t => t.FromState.State == TestSaleState.Overpaid && t.ToState.State == TestSaleState.Paid && t.Trigger == TestSaleEvent.SetItemQuantity); }
public void Export_exports_sale_graph() { var stateMachine = getSaleStateMachine(); var summary = stateMachine.GetSummary(); var dotGraphLines = DotGraphExporter <SaleState, SaleEvent> .Export(summary)?.Split("\r\n"); Assert.Equal("digraph NStateManagerGraph {", dotGraphLines.First()); Assert.Equal("\trankdir=LR;", dotGraphLines[1]); Assert.Equal(string.Empty, dotGraphLines[2]); Assert.Equal(string.Empty, dotGraphLines[3]); Assert.Equal("\tOpen;", dotGraphLines[4]); Assert.Equal("\tChangeDue;", dotGraphLines[5]); Assert.Equal("\tComplete;", dotGraphLines[6]); Assert.Equal(string.Empty, dotGraphLines[7]); Assert.Equal("\tOpen -> Complete [label=\"Pay(*)\"];", dotGraphLines[8]); Assert.Equal("\tOpen -> ChangeDue [label=\"Pay(*)\"];", dotGraphLines[9]); }
public void Export_exports_graph_with_substates() { var stateMachine = getComplexSaleStateMachine(); var summary = stateMachine.GetSummary(); var dotGraphLines = DotGraphExporter <SaleComplexState, SaleComplexEvent> .Export(summary)?.Split("\r\n"); Assert.Equal("digraph NStateManagerGraph {", dotGraphLines[0]); Assert.Equal("\tcompound=true;", dotGraphLines[1]); Assert.Equal("\trankdir=LR;", dotGraphLines[2]); Assert.Equal(string.Empty, dotGraphLines[3]); Assert.Equal(string.Empty, dotGraphLines[4]); Assert.Equal("\tsubgraph cluster_Open {", dotGraphLines[5]); Assert.Equal("\t\tlabel=Open;", dotGraphLines[6]); Assert.Equal("\t\tOpen [shape=circle label=\"\" style=filled color=black]", dotGraphLines[7]); Assert.Equal(string.Empty, dotGraphLines[8]); Assert.Equal("\t\tOverpaid;", dotGraphLines[9]); Assert.Equal("\t}", dotGraphLines[10]); Assert.Equal(string.Empty, dotGraphLines[11]); Assert.Equal("\tsubgraph cluster_Finalized {", dotGraphLines[12]); Assert.Equal("\t\tlabel=Finalized;", dotGraphLines[13]); Assert.Equal("\t\tFinalized [shape=point color=white];", dotGraphLines[14]); Assert.Equal(string.Empty, dotGraphLines[15]); Assert.Equal("\t\tPaid;", dotGraphLines[16]); Assert.Equal("\t\tCancelled;", dotGraphLines[17]); Assert.Equal("\t}", dotGraphLines[18]); Assert.Equal(string.Empty, dotGraphLines[19]); Assert.Equal("\tOpen -> Cancelled [label=\"Cancel(*)\" ltail=cluster_Open];", dotGraphLines[20]); Assert.Equal("\tOpen -> Overpaid [label=\"SetItemQuantity(*)\" ltail=cluster_Open];", dotGraphLines[21]); Assert.Equal("\tOpen -> Paid [label=\"SetItemQuantity(*)\" ltail=cluster_Open];", dotGraphLines[22]); Assert.Equal("\tOpen -> Overpaid [label=\"Pay(*)\" ltail=cluster_Open];", dotGraphLines[23]); Assert.Equal("\tOpen -> Paid [label=\"Pay(*)\" ltail=cluster_Open];", dotGraphLines[24]); Assert.Equal("\tOverpaid -> Open [label=\"GiveChange(*)\" lhead=cluster_Open];", dotGraphLines[25]); Assert.Equal("\tOverpaid -> Open [label=\"AddItem(*)\" lhead=cluster_Open];", dotGraphLines[26]); Assert.Equal("\tOverpaid -> Open [label=\"SetItemQuantity(*)\" lhead=cluster_Open];", dotGraphLines[27]); Assert.Equal("}", dotGraphLines[28]); }
public void Export_exports_sale_graph() { var stateMachine = GetSaleStateMachine(); var summary = stateMachine.GetSummary(); var dotGraph = DotGraphExporter <SaleState, SaleEvent> .Export(summary); }
public void Export_exports_phone_graph() { var stateMachine = new PhoneCall("Scott"); var summary = stateMachine.GetSummary(); var dotGraph = DotGraphExporter <State, Trigger> .Export(summary); }
public static string GetDotGraph() { return(DotGraphExporter <PhoneState, PhoneEvent> .Export(_stateMachine.GetSummary())); }