Side Effects vs Cascading Messages in Wolverine

Hey, did you know that JasperFx Software is ready for formal support plans for Marten and Wolverine? Not only are we making the “Critter Stack” tools be viable long term options for your shop, we’re also interested in hearing your opinions about the tools and how they should change. We’re also certainly open to help you succeed with your software development projects on a consulting basis whether you’re using any part of the Critter Stack or some completely different .NET server side tooling.

Wolverine has a pair of features called cascading messages and side effects that allow users to designate “work” that happens after your main message handler or HTTP endpoint method. In our Discord room this week, there was a little bit of user confusion over what the difference was and when you should use one or the other. To that end, let’s do a little dive into what both of these features are (both are unique to Wolverine and don’t have analogues to the best of my knowledge in any other messaging or command processing framework in the .NET space).

First, let’s take a look at “side effects.” Here’s the simple example of a custom “side effect” from the Wolverine documentation to write string data to a file on the file system:

public class WriteFile : ISideEffect
{
    public string Path { get; }
    public string Contents { get; }

    public WriteFile(string path, string contents)
    {
        Path = path;
        Contents = contents;
    }

    // Wolverine will call this method. 
    public Task ExecuteAsync(PathSettings settings)
    {
        if (!Directory.Exists(settings.Directory))
        {
            Directory.CreateDirectory(settings.Directory);
        }
        
        return File.WriteAllTextAsync(Path, Contents);
    }
}

And a message handler that uses this custom side effect:

public class RecordTextHandler
{
    public WriteFile Handle(RecordText command)
    {
        return new WriteFile(command.Id + ".txt", command.Text);
    }
}

A Wolverine “side effect” really just designates some work that should happen inline with your message or HTTP request handling, so we could eschew the “side effect” and rewrite our message handler as:

    public Task Handle(RecordText command, PathSettings settings)
    {
        if (!Directory.Exists(settings.Directory))
        {
            Directory.CreateDirectory(settings.Directory);
        }
        
        return File.WriteAllTextAsync(command.Id + ".txt", command.Text);
    }

The value of the “side effect” usage within Wolverine is to allow you to make a message or HTTP endpoint method be responsible for deciding what to do, without coupling that method and its logic to some kind of pesky infrastructure like the file system that becomes a pain to deal with in unit tests. The “side effect” object returned from a message handler or HTTP endpoint is running inline within the same transaction (if there is one) and retry loop for the message itself.

On the other hand, a cascading message is really just sending a subsequent message after the successful completion of the original message. Here’s an example from the “Building a Critter Stack Application” blog series:

    [WolverinePost("/api/incidents/categorise"), AggregateHandler]
    // Any object in the OutgoingMessages collection will
    // be treated as a "cascading message" to be published by
    // Wolverine after the original CategoriseIncident command
    // is successfully completed
    public static (Events, OutgoingMessages) Post(
        CategoriseIncident command, 
        IncidentDetails existing, 
        User user)
    {
        var events = new Events();
        var messages = new OutgoingMessages();
        
        if (existing.Category != command.Category)
        {
            events += new IncidentCategorised
            {
                Category = command.Category,
                UserId = user.Id
            };

            // Send a command message to try to assign the priority
            messages.Add(new TryAssignPriority
            {
                IncidentId = existing.Id
            });
        }

        return (events, messages);
    }
}

In the example above, the TryAssignPriority message will be published by Wolverine to whatever subscribes to that message type (local queues, external transports, nowhere because nothing actually cares?). The “cascading messages” are really the equivalent to calling IMessageBus.PublishAsync() on each cascaded message. It’s important to note that cascaded messages are not executed inline with your original message. Instead, they are only published after the original message is completely handled, and will run in completely different contexts, retry loops, and database transactions.

To sum up, you would:

  1. Use a “side effect” to select actions that need to happen within the current message context as part of an atomic transaction and as something that should succeed or fail (and be retried) along with the message handler itself and any other middleware or post processors for that message type. In other words, “side effects” are for actions that should absolutely happen right there and now!
  2. Use a “cascaded message” for a subsequent action that should happen after the current message, should be executed within a separate transaction, or could be retried in its own retry loop after the original message handling has succeeded.

I’d urge users to consider the proper transaction boundaries and retry boundaries to decide which approach to use. And remember that in both cases, there is value in trying to use pure functions for any kind of business or workflow logic — and both side effects and cascaded messages help you do exactly that for easily unit testable code.

Leave a comment