Skip to content

Message Handler Discovery

WARNING

The handler type scanning and discovery is done against an allow list of assemblies rather than running through your entire application's dependency tree. Watch for this if handlers are missing.

Wolverine has built in mechanisms for automatically finding message handler methods in your application based on a set of naming conventions or using explicit interface or attribute markers. If you really wanted to, you could also explicitly add handler types programmatically.

Troubleshooting Handler Discovery

It's an imperfect world and sometimes Wolverine isn't finding handler methods for some reason or another -- or seems to be using types and methods you'd rather it didn't. Not to fear, there are some diagnostic tools to help Wolverine explain what's going on.

Directly on WolverineOptions itself is a diagnostic method named DescribeHandlerMatch that will give you a full textual report on why or why not Wolverine is identifying that type as a handler type, then if it is found by Wolverine to be a handler type, also giving you a full report on each public method about why or why not Wolverine considers it to be a valid handler method.

cs
using var host = await Host.CreateDefaultBuilder()
    .UseWolverine(opts =>
    {
        // Surely plenty of other configuration for Wolverine...

        // This *temporary* line of code will write out a full report about why or
        // why not Wolverine is finding this handler and its candidate handler messages
        Console.WriteLine(opts.DescribeHandlerMatch(typeof(MyMissingMessageHandler)));
    }).StartAsync();

snippet source | anchor

Even if the report itself isn't exactly clear to you, using this textual report in a Wolverine issue or within the Critter Stack Discord group will help the Wolverine team be able to assist you much quicker.

Assembly Discovery

TIP

The handler discovery uses the type scanning functionality into JasperFx.Core library for type scanning that is shared with several other JasperFx projects.

The first issue is which assemblies will Wolverine look through to find candidate handlers? By default, Wolverine is looking through what it calls the application assembly. When you call IHostBuilder.UseWolverine() to add Wolverine to an application, Wolverine looks up the call stack to find where the call to that method came from, and uses that to determine the application assembly. If you're using an idiomatic approach to bootstrap your application through Program.Main(args), the application assembly is going to be the application's main assembly that holds the Program.Main() entrypoint.

TIP

We highly recommend you use WebApplicationFactory or Alba (which uses WebApplicationFactory behind the covers) to bootstrap your application in integration tests to avoid any problems around Wolverine's application assembly determination.

In testing scenarios, if you're bootstrapping the application independently somehow of the application's "official" configuration, you may have to help Wolverine out a little bit and explicitly tell it what the application assembly is:

cs
using var host = Host.CreateDefaultBuilder()
    .UseWolverine(opts =>
    {
        // Override the application assembly to help
        // Wolverine find its handlers
        // Should not be necessary in most cases
        opts.ApplicationAssembly = typeof(Program).Assembly;
    }).StartAsync();

snippet source | anchor

To pull in handlers from other assemblies, you can either decorate an assembly with this attribute:

cs
using Wolverine.Attributes;

[assembly: WolverineModule]

snippet source | anchor

Or you can programmatically add additional assemblies to the handler discovery with this syntax:

cs
using var host = Host.CreateDefaultBuilder()
    .UseWolverine(opts =>
    {
        // Add as many other assemblies as you need
        opts.Discovery.IncludeAssembly(typeof(MessageFromOtherAssembly).Assembly);
    }).StartAsync();

snippet source | anchor

Handler Type Discovery

WARNING

Wolverine does not support any kind of open generic types for message handlers and has no intentions of ever doing so.

By default, Wolverine is looking for public, concrete classes that follow any of these rules:

  • Implements the Wolverine.IWolverineHandler interface
  • Is decorated with the [Wolverine.WolverineHandler] attribute
  • Type name ends with "Handler"
  • Type name ends with "Consumer"

The original intention was to strictly use naming conventions to locate message handlers, but if you prefer a more explicit approach for discovery, feel free to utilize the IWolverineHandler interface or [WolverineHandler] (you'll have to use the attribute approach for static classes).

From the types, by default, Wolverine looks for any public instance method that is:

  1. Is named Handle, Handles, Consume, Consumes or one of the names from Wolverine's saga support
  2. Is decorated by the [WolverineHandler] attribute if you want to use a different, descriptive name

In all cases, Wolverine assumes that the first argument is the incoming message.

To make that concrete, here are some valid handler method signatures:

cs
[WolverineHandler]
public class ValidMessageHandlers
{
    // There's only one argument, so we'll assume that
    // argument is the message
    public void Handle(Message1 something)
    {
    }

    // The parameter named "message" is assumed to be the message type
    public Task ConsumeAsync(Message1 message, IDocumentSession session)
    {
        return session.SaveChangesAsync();
    }

    // In this usage, we're "cascading" a new message of type
    // Message2
    public Task<Message2> HandleAsync(Message1 message, IDocumentSession session)
    {
        return Task.FromResult(new Message2());
    }

    // In this usage we're "cascading" 0 to many additional
    // messages from the return value
    public IEnumerable<object> Handle(Message3 message)
    {
        yield return new Message1();
        yield return new Message2();
    }

    // It's perfectly valid to have multiple handler methods
    // for a given message type. Each will be called in sequence
    // they were discovered
    public void Consume(Message1 input, IEmailService emails)
    {
    }

    // You can inject additional services directly into the handler
    // method
    public ValueTask ConsumeAsync(Message3 weirdName, IEmailService service)
    {
        return ValueTask.CompletedTask;
    }

    public interface IEvent
    {
        string CustomerId { get; }
        Guid Id { get; }
    }
}

snippet source | anchor

The valid method names are:

  1. Handle / HandleAsync
  2. Handles / HandlesAsync
  3. Consume / ConsumeAsync
  4. Consumes / ConsumesAsync

And also specific to sagas:

  1. Start / StartAsync
  2. Starts / StartAsync
  3. Orchestrate / OrchestrateAsync
  4. Orchestrates / OrchestratesAsync
  5. StartOrHandle / StartOrHandleAsync
  6. StartsOrHandles / StartsOrHandlesAsync
  7. NotFound / NotFoundAsync

See Stateful Sagas for more information.

Disabling Conventional Discovery

WARNING

Note that disabling conventional discovery will also disable any customizations you may have made to the conventional handler discovery

You can completely turn off any automatic discovery of message handlers through type scanning by using this syntax in your WolverineOptions:

cs
using var host = await Host.CreateDefaultBuilder()
    .UseWolverine(opts =>
    {
        // No automatic discovery of handlers
        opts.DisableConventionalDiscovery();
    }).StartAsync();

snippet source | anchor

Explicitly Ignoring Methods

You can force Wolverine to disregard a candidate message handler action at either the class or method level by using the [WolverineIgnore] attribute like this:

cs
public class NetflixHandler : IMovieSink
{
    public void Listen(MovieAdded added)
    {
    }

    public void Handles(IMovieEvent @event)
    {
    }

    public void Handles(MovieEvent @event)
    {
    }

    public void Consume(MovieAdded added)
    {
    }

    // Only this method will be ignored as
    // a handler method
    [WolverineIgnore]
    public void Handles(MovieAdded added)
    {
    }

    public void Handle(MovieAdded message, IMessageContext context)
    {
    }

    public static Task Handle(MovieRemoved removed)
    {
        return Task.CompletedTask;
    }
}

// All methods on this class will be ignored
// as handler methods even though the class
// name matches the discovery naming conventions
[WolverineIgnore]
public class BlockbusterHandler
{
    public void Handle(MovieAdded added)
    {
    }
}

snippet source | anchor

Customizing Conventional Discovery

WARNING

Do note that handler finding conventions are additive, meaning that adding additional criteria does not disable the built in handler discovery

The easiest way to use the Wolverine messaging functionality is to just code against the default conventions. However, if you wish to deviate from those naming conventions you can either supplement the handler discovery or replace it completely with your own conventions.

At a minimum, you can disable the built in discovery, add additional type filtering criteria, or register specific handler classes with the code below:

cs
using var host = await Host.CreateDefaultBuilder()
    .UseWolverine(opts =>
    {
        opts.Discovery

            // Turn off the default handler conventions
            // altogether
            .DisableConventionalDiscovery()

            // Include candidate actions by a user supplied
            // type filter
            .CustomizeHandlerDiscovery(x =>
            {
                x.Includes.WithNameSuffix("Worker");
                x.Includes.WithNameSuffix("Listener");
            })

            // Include a specific handler class with a generic argument
            .IncludeType<SimpleHandler>();
    }).StartAsync();

snippet source | anchor

Released under the MIT License.