MassTransit / Automatonymous

A state machine library for .Net - 100% code - No doodleware
Apache License 2.0
736 stars 117 forks source link

generic payload for raising an event throws exception #94

Open kuldeepGDI opened 3 months ago

kuldeepGDI commented 3 months ago

Hi, I know that this repo is not maintained anymore but nevertheless I have a use case where I use Automatonymous without MT.

I have a sample code here that works but if I use a payload that is coming using generics, i get an exception

Automatonymous.AutomatonymousException: This activity requires a body with the event, but no body was specified.

The working code is from a unit test in the repo.

using Automatonymous;

namespace TestAutomato;

class Instance
{
    public State CurrentState { get; set; }
    public DateTime? Initialized { get; set; }
}

class InstanceStateMachine :
    AutomatonymousStateMachine<Instance>
{
    public InstanceStateMachine()
    {
        During(Initial,
            When(Initialize)
                .TransitionTo(Draft));

        During(Draft,
            When(Thing, context => context.Data.PreviousState.Equals(nameof(Draft)))
                .TransitionTo(True)
                .Then(context => context.Raise(SecInitialize)),
            When(Thing, context => !context.Data.PreviousState.Equals(nameof(Draft)))
                .TransitionTo(False));

        DuringAny(
            When(SecInitialize)
                .Then(context => context.Instance.Initialized = DateTime.Now));
    }

    public State Draft { get; private set; }
    public State True { get; private set; }
    public State False { get; private set; }

    public Event<SomeEventFailedContextData> Thing { get; private set; }
    public Event Initialize { get; private set; }

    public Event SecInitialize { get; private set; }
}

public record SomeEventFailedContextData(string PreviousState);

then i use this as following

using Automatonymous;
using TestAutomato;

var instance = new Instance();
var machine = new InstanceStateMachine();

await machine.RaiseEvent(instance, machine.Initialize);

await machine.RaiseEvent(instance, machine.Thing, new SomeEventFailedContextData(nameof(InstanceStateMachine.Draft)), CancellationToken.None);

Console.WriteLine(instance.CurrentState);
if (instance.Initialized != null)
{
    Console.WriteLine(instance.Initialized.Value);
}

And this works as expected. however, i want to use the ability to pass payload as generics because i might have many events with their own payload types, so I wanted to use it as following

namespace TestAutomato;

class Instance
{
    public State CurrentState { get; set; }
    public DateTime? Initialized { get; set; }
}

class InstanceStateMachine :
    AutomatonymousStateMachine<Instance>
{
    public InstanceStateMachine()
    {
        During(Initial,
            When(Initialize)
                .TransitionTo(Draft));

        During(Draft,
            When(Thing, context => context.Data.PreviousState.Equals(nameof(Draft)))
                .TransitionTo(True)
                .Then(context => context.Raise(SecInitialize)),
            When(Thing, context => !context.Data.PreviousState.Equals(nameof(Draft)))
                .TransitionTo(False));

        DuringAny(
            When(SecInitialize)
                .Then(context => context.Instance.Initialized = DateTime.Now));
    }

    public State Draft { get; private set; }
    public State True { get; private set; }
    public State False { get; private set; }

    public Event<SomeEventFailedContextData> Thing { get; private set; }
    public Event Initialize { get; private set; }

    public Event SecInitialize { get; private set; }
}

public record SomeEventFailedContextData(string PreviousState);

public record GenericSpec<T>(T Payload) where T: class;

and then call it as in

await SomeMethod(new GenericSpec<SomeEventFailedContextData>(new SomeEventFailedContextData(nameof(InstanceStateMachine.Draft))));

async Task SomeMethod<T>(GenericSpec<T> data) where T: class
{
    var instance = new Instance();
    var machine = new InstanceStateMachine();

    await machine.RaiseEvent(instance, machine.Initialize);

    await machine.RaiseEvent(instance, machine.Thing, data.Payload, CancellationToken.None);

    Console.WriteLine(instance.CurrentState);
    if (instance.Initialized != null)
    {
        Console.WriteLine(instance.Initialized.Value);
    }
}

which results into the above mentioned error ! Is there no way to do this or achieve this somehow or am I missing some configuration ?

Additionally, I also notice that if i do following

using Automatonymous;
using TestAutomato;

var instance = new Instance();
var machine = new InstanceStateMachine();

await machine.RaiseEvent(instance, machine.Initialize);
var nextEvents = await machine.NextEvents(instance);

var allowedEvent = nextEvents.FirstOrDefault(x => x.Name.Equals(nameof(machine.Thing)));

await machine.RaiseEvent(instance, allowedEvent, new SomeEventFailedContextData(nameof(InstanceStateMachine.Draft)), CancellationToken.None);

Console.WriteLine(instance.CurrentState);
if (instance.Initialized != null)
{
    Console.WriteLine(instance.Initialized.Value);
}

Then as well RaiseEvent throws the same error. So when

  1. I use generic payload i receive this error
  2. If I use Event which is coming from nextEvents FirstOrDefault then I receive same error. However, call to RaiseEvent works fine If the other override of RaiseEvent as following is used.

await machine.RaiseEvent(instance, allowedEvent, CancellationToken.None); //assuming there is no context arguments required for this event in state machine

Thanks