diff --git a/src/Stateless/StateMachine.cs b/src/Stateless/StateMachine.cs
index 121d6ede..bb9fa877 100644
--- a/src/Stateless/StateMachine.cs
+++ b/src/Stateless/StateMachine.cs
@@ -570,6 +570,12 @@ public bool IsInState(TState state)
/// Returns true if can be fired
/// in the current state.
///
+ ///
+ /// When the trigger is configured with parameters, the default value of each of the trigger parameter's types will be used
+ /// to evaluate whether it can fire, which may not be the desired behavior; to check if a trigger can be fired with specific arguments,
+ /// use the overload of CanFire<TArg1[, TArg2[ ,TArg3]]>(TriggerWithParameters<TArg1[, TArg2[ ,TArg3]]>, ...) that
+ /// matches the type arguments of your trigger.
+ ///
/// Trigger to test.
/// True if the trigger can be fired, false otherwise.
public bool CanFire(TTrigger trigger)
@@ -579,8 +585,64 @@ public bool CanFire(TTrigger trigger)
///
/// Returns true if can be fired
- /// in the current state.
+ /// in the current state using the supplied trigger argument.
+ ///
+ /// Type of the first trigger argument.
+ /// Trigger to test.
+ /// The first argument.
+ /// True if the trigger can be fired, false otherwise.
+ public bool CanFire(TriggerWithParameters trigger, TArg0 arg0)
+ {
+ if (trigger == null) throw new ArgumentNullException(nameof(trigger));
+
+ return CurrentRepresentation.CanHandle(trigger.Trigger, arg0);
+ }
+
+ ///
+ /// Returns true if can be fired
+ /// in the current state using the supplied trigger arguments.
+ ///
+ /// Type of the first trigger argument.
+ /// Type of the second trigger argument.
+ /// Trigger to test.
+ /// The first argument.
+ /// The second argument.
+ /// True if the trigger can be fired, false otherwise.
+ public bool CanFire(TriggerWithParameters trigger, TArg0 arg0, TArg1 arg1)
+ {
+ if (trigger == null) throw new ArgumentNullException(nameof(trigger));
+
+ return CurrentRepresentation.CanHandle(trigger.Trigger, arg0, arg1);
+ }
+
+ ///
+ /// Returns true if can be fired
+ /// in the current state using the supplied trigger arguments.
+ ///
+ /// Type of the first trigger argument.
+ /// Type of the second trigger argument.
+ /// Type of the third trigger argument.
+ /// Trigger to test.
+ /// The first argument.
+ /// The second argument.
+ /// The third argument.
+ /// True if the trigger can be fired, false otherwise.
+ public bool CanFire(TriggerWithParameters trigger, TArg0 arg0, TArg1 arg1, TArg2 arg2)
+ {
+ if (trigger == null) throw new ArgumentNullException(nameof(trigger));
+
+ return CurrentRepresentation.CanHandle(trigger.Trigger, arg0, arg1, arg2);
+ }
+
+ ///
+ /// Returns true if can be fired in the current state.
///
+ ///
+ /// When the trigger is configured with parameters, the default value of each of the trigger parameter's types will be used
+ /// to evaluate whether it can fire, which may not be the desired behavior; to check if a trigger can be fired with specific arguments,
+ /// use the overload of CanFire<TArg1[, TArg2[ ,TArg3]]>(TriggerWithParameters<TArg1[, TArg2[ ,TArg3]]>, ...) that
+ /// matches the type arguments of your trigger.
+ ///
/// Trigger to test.
/// Guard descriptions of unmet guards. If given trigger is not configured for current state, this will be null.
/// True if the trigger can be fired, false otherwise.
@@ -589,6 +651,60 @@ public bool CanFire(TTrigger trigger, out ICollection unmetGuards)
return CurrentRepresentation.CanHandle(trigger, new object[] { }, out unmetGuards);
}
+ ///
+ /// Returns true if can be fired
+ /// in the current state using the supplied trigger argument.
+ ///
+ /// Type of the first trigger argument.
+ /// Trigger to test.
+ /// The first argument.
+ /// Guard descriptions of unmet guards. If given trigger is not configured for current state, this will be null.
+ /// True if the trigger can be fired, false otherwise.
+ public bool CanFire(TriggerWithParameters trigger, TArg0 arg0, out ICollection unmetGuards)
+ {
+ if (trigger == null) throw new ArgumentNullException(nameof(trigger));
+
+ return CurrentRepresentation.CanHandle(trigger.Trigger, new object[] { arg0 }, out unmetGuards);
+ }
+
+ ///
+ /// Returns true if can be fired
+ /// in the current state using the supplied trigger arguments.
+ ///
+ /// Type of the first trigger argument.
+ /// Type of the second trigger argument.
+ /// Trigger to test.
+ /// The first argument.
+ /// The second argument.
+ /// Guard descriptions of unmet guards. If given trigger is not configured for current state, this will be null.
+ /// True if the trigger can be fired, false otherwise.
+ public bool CanFire(TriggerWithParameters trigger, TArg0 arg0, TArg1 arg1, out ICollection unmetGuards)
+ {
+ if (trigger == null) throw new ArgumentNullException(nameof(trigger));
+
+ return CurrentRepresentation.CanHandle(trigger.Trigger, new object[] { arg0, arg1 }, out unmetGuards);
+ }
+
+ ///
+ /// Returns true if can be fired
+ /// in the current state using the supplied trigger arguments.
+ ///
+ /// Type of the first trigger argument.
+ /// Type of the second trigger argument.
+ /// Type of the third trigger argument.
+ /// Trigger to test.
+ /// The first argument.
+ /// The second argument.
+ /// The third argument.
+ /// Guard descriptions of unmet guards. If given trigger is not configured for current state, this will be null.
+ /// True if the trigger can be fired, false otherwise.
+ public bool CanFire(TriggerWithParameters trigger, TArg0 arg0, TArg1 arg1, TArg2 arg2, out ICollection unmetGuards)
+ {
+ if (trigger == null) throw new ArgumentNullException(nameof(trigger));
+
+ return CurrentRepresentation.CanHandle(trigger.Trigger, new object[] { arg0, arg1, arg2 }, out unmetGuards);
+ }
+
///
/// A human-readable representation of the state machine.
///
diff --git a/test/Stateless.Tests/StateMachineFixture.cs b/test/Stateless.Tests/StateMachineFixture.cs
index f89b8560..91cb42ac 100644
--- a/test/Stateless.Tests/StateMachineFixture.cs
+++ b/test/Stateless.Tests/StateMachineFixture.cs
@@ -115,7 +115,7 @@ public void WhenInSubstate_TriggerSuperStateTwiceToSameSubstate_DoesNotReenterSu
var eCount = 0;
sm.Configure(State.B)
- .OnEntry(() => { eCount++;})
+ .OnEntry(() => { eCount++; })
.SubstateOf(State.C);
sm.Configure(State.A)
@@ -721,6 +721,185 @@ public void ExceptionWhenPermitIfHasMultipleNonExclusiveGuards()
Assert.Throws(() => sm.Fire(x, 2));
}
+ [Fact]
+ public void CanFire_When_Transition_Is_Conditional_On_Default_Value_And_Trigger_Parameters_Are_Omitted_Returns_True()
+ {
+ // This test verifies behavior in CanFire that may be considered a bug.
+ // When a PermitIf transition is configured with a parameterized trigger and the trigger is fired without parameters,
+ // CanFire will test the transition with the default values of the omitted trigger parameters' types.
+ var sm = new StateMachine(State.A);
+ var valueTrigger = sm.SetTriggerParameters(Trigger.X);
+ sm.Configure(State.A).PermitIf(valueTrigger, State.B, i => i == default);
+
+ Assert.True(sm.CanFire(Trigger.X));
+ }
+
+ [Fact]
+ public void CanFire_When_Transition_Is_Conditional_On_Non_Default_Value_And_Trigger_Parameters_Are_Omitted_Returns_False()
+ {
+ // This test verifies behavior in CanFire that may be considered a bug.
+ // When a PermitIf transition is configured with a parameterized trigger and the trigger is fired without parameters,
+ // CanFire will test the transition with the default values of the omitted trigger parameters' types.
+ var sm = new StateMachine(State.A);
+ var valueTrigger = sm.SetTriggerParameters(Trigger.X);
+ sm.Configure(State.A).PermitIf(valueTrigger, State.B, i => i == 1);
+
+ Assert.False(sm.CanFire(Trigger.X));
+ }
+
+ [Fact]
+ public void CanFire_When_Transition_Is_Conditional_And_One_Trigger_Parameter_Is_Used_And_Condition_Is_Met_Returns_True()
+ {
+ var sm = new StateMachine(State.A);
+ var valueTrigger = sm.SetTriggerParameters(Trigger.X);
+ sm.Configure(State.A).PermitIf(valueTrigger, State.B, i => i == 1);
+
+ Assert.True(sm.CanFire(valueTrigger, 1));
+ }
+
+ [Fact]
+ public void CanFire_When_Transition_Is_Conditional_And_One_Trigger_Parameter_Is_Used_And_Condition_Is_NotMet_Returns_False()
+ {
+ var sm = new StateMachine(State.A);
+ var valueTrigger = sm.SetTriggerParameters(Trigger.X);
+ sm.Configure(State.A).PermitIf(valueTrigger, State.B, i => i != 1);
+
+ Assert.False(sm.CanFire(valueTrigger, 1));
+ }
+
+ [Fact]
+ public void CanFire_When_Transition_Is_Conditional_And_Two_Trigger_Parameters_Are_Used_And_Condition_Is_Met_Returns_True()
+ {
+ var sm = new StateMachine(State.A);
+ var valueTrigger = sm.SetTriggerParameters(Trigger.X);
+ sm.Configure(State.A).PermitIf(valueTrigger, State.B, (i, s) => i == 1 && s.Equals("X", StringComparison.Ordinal));
+
+ Assert.True(sm.CanFire(valueTrigger, 1, "X"));
+ }
+
+ [Fact]
+ public void CanFire_When_Transition_Is_Conditional_And_Two_Trigger_Parameters_Are_Used_And_Condition_Is_Not_Met_Returns_False()
+ {
+ var sm = new StateMachine(State.A);
+ var valueTrigger = sm.SetTriggerParameters(Trigger.X);
+ sm.Configure(State.A).PermitIf(valueTrigger, State.B, (i, s) => i != 1 && s.Equals("Y", StringComparison.Ordinal));
+
+ Assert.False(sm.CanFire(valueTrigger, 1, "X"));
+ }
+
+ [Fact]
+ public void CanFire_When_Transition_Is_Conditional_And_Three_Trigger_Parameters_Are_Used_And_Condition_Is_Met_Returns_True()
+ {
+ var sm = new StateMachine(State.A);
+ var valueTrigger = sm.SetTriggerParameters(Trigger.X);
+ sm.Configure(State.A).PermitIf(valueTrigger, State.B, (i, s, b) => i == 1 && s.Equals("X", StringComparison.Ordinal) && b);
+
+ Assert.True(sm.CanFire(valueTrigger, 1, "X", true));
+ }
+
+ [Fact]
+ public void CanFire_When_Transition_Is_Conditional_And_Three_Trigger_Parameters_Are_Used_And_Condition_Is_Not_Met_Returns_False()
+ {
+ var sm = new StateMachine(State.A);
+ var valueTrigger = sm.SetTriggerParameters(Trigger.X);
+ sm.Configure(State.A).PermitIf(valueTrigger, State.B, (i, s, b) => i != 1 && s.Equals("Y", StringComparison.Ordinal) && !b);
+
+ Assert.False(sm.CanFire(valueTrigger, 1, "X", true));
+ }
+
+ [Fact]
+ public void CanFire_When_Transition_Is_Contidional_And_Has_One_Trigger_Parameter_And_A_Guard_Condition_Is_Met_Returns_Empty_Collection()
+ {
+ var sm = new StateMachine(State.A);
+ var valueTrigger = sm.SetTriggerParameters(Trigger.X);
+ sm.Configure(State.A)
+ .PermitIf(valueTrigger, State.B, i => i == 1, "i equal to 1")
+ .PermitIf(valueTrigger, State.B, i => i == 2, "i equal to 2");
+
+ bool result = sm.CanFire(valueTrigger, 1, out ICollection unmetGuards);
+
+ Assert.True(result);
+ Assert.False(unmetGuards.Any());
+ }
+
+ [Fact]
+ public void CanFire_When_Transition_Is_Contidional_And_Has_One_Trigger_Parameter_And_No_Guard_Conditions_Are_Met_Returns_Guard_Conditions()
+ {
+ var sm = new StateMachine(State.A);
+ var valueTrigger = sm.SetTriggerParameters(Trigger.X);
+ sm.Configure(State.A)
+ .PermitIf(valueTrigger, State.B, i => i == 1, "i equal to 1")
+ .PermitIf(valueTrigger, State.B, i => i == 2, "i equal to 2");
+
+ bool result = sm.CanFire(valueTrigger, 3, out ICollection unmetGuards);
+
+ Assert.Collection(unmetGuards,
+ item => Assert.Equal("i equal to 1", item),
+ item => Assert.Equal("i equal to 2", item));
+ }
+
+ [Fact]
+ public void CanFire_When_Transition_Is_Contidional_And_Has_Two_Trigger_Parameters_And_A_Guard_Condition_Is_Met_Returns_Empty_Collection()
+ {
+ var sm = new StateMachine(State.A);
+ var valueTrigger = sm.SetTriggerParameters(Trigger.X);
+ sm.Configure(State.A)
+ .PermitIf(valueTrigger, State.B, (i, s) => i == 1 && s == "X", "i equal to 1 and s equal to 'X'")
+ .PermitIf(valueTrigger, State.B, (i, s) => i == 2 && s == "X", "i equal to 2 and s equal to 'Y'");
+
+ bool result = sm.CanFire(valueTrigger, 1, "X", out ICollection unmetGuards);
+
+ Assert.True(result);
+ Assert.False(unmetGuards.Any());
+ }
+
+ [Fact]
+ public void CanFire_When_Transition_Is_Contidional_And_Has_Two_Trigger_Parameters_And_No_Guard_Conditions_Are_Met_Returns_Guard_Conditions()
+ {
+ var sm = new StateMachine(State.A);
+ var valueTrigger = sm.SetTriggerParameters(Trigger.X);
+ sm.Configure(State.A)
+ .PermitIf(valueTrigger, State.B, (i, s) => i == 1 && s == "X", "i equal to 1 and s equal to 'X'")
+ .PermitIf(valueTrigger, State.B, (i, s) => i == 2 && s == "X", "i equal to 2 and s equal to 'Y'");
+
+ bool result = sm.CanFire(valueTrigger, 3, "Z", out ICollection unmetGuards);
+
+ Assert.Collection(unmetGuards,
+ item => Assert.Equal("i equal to 1 and s equal to 'X'", item),
+ item => Assert.Equal("i equal to 2 and s equal to 'Y'", item));
+ }
+
+ [Fact]
+ public void CanFire_When_Transition_Is_Contidional_And_Has_Three_Trigger_Parameters_And_A_Guard_Condition_Is_Met_Returns_Empty_Collection()
+ {
+ var sm = new StateMachine(State.A);
+ var valueTrigger = sm.SetTriggerParameters(Trigger.X);
+ sm.Configure(State.A)
+ .PermitIf(valueTrigger, State.B, (i, s, b) => i == 1 && s == "X", "i equal to 1 and s equal to 'X' and boolean is true")
+ .PermitIf(valueTrigger, State.B, (i, s, b) => i == 2 && s == "X", "i equal to 2 and s equal to 'Y' and boolean is true");
+
+ bool result = sm.CanFire(valueTrigger, 1, "X", true, out ICollection unmetGuards);
+
+ Assert.True(result);
+ Assert.False(unmetGuards.Any());
+ }
+
+ [Fact]
+ public void CanFire_When_Transition_Is_Contidional_And_Has_Three_Trigger_Parameters_And_No_Guard_Conditions_Are_Met_Returns_Guard_Conditions()
+ {
+ var sm = new StateMachine(State.A);
+ var valueTrigger = sm.SetTriggerParameters(Trigger.X);
+ sm.Configure(State.A)
+ .PermitIf(valueTrigger, State.B, (i, s, b) => i == 1 && s == "X", "i equal to 1 and s equal to 'X' and boolean is true")
+ .PermitIf(valueTrigger, State.B, (i, s, b) => i == 2 && s == "X", "i equal to 2 and s equal to 'Y' and boolean is true");
+
+ bool result = sm.CanFire(valueTrigger, 3, "Z", false, out ICollection unmetGuards);
+
+ Assert.Collection(unmetGuards,
+ item => Assert.Equal("i equal to 1 and s equal to 'X' and boolean is true", item),
+ item => Assert.Equal("i equal to 2 and s equal to 'Y' and boolean is true", item));
+ }
+
[Fact]
public void TransitionWhenPermitDyanmicIfHasMultipleExclusiveGuards()
{
@@ -1048,7 +1227,7 @@ public void CanFire_GetUnmetGuardDescriptionsIfGuardFails()
const string guardDescription = "Guard failed";
var sm = new StateMachine(State.A);
sm.Configure(State.A)
- .PermitIf(Trigger.X, State.B, ()=> false, guardDescription);
+ .PermitIf(Trigger.X, State.B, () => false, guardDescription);
bool result = sm.CanFire(Trigger.X, out ICollection unmetGuards);