Message Handler Discovery
Jasper has built in mechanisms for automatically finding message handler methods in your application or the ability to explicitly add handler types. The conventional discovery can be disabled or customized as well.
Default Conventional Discovery
Jasper uses Lamar's type scanning (based on StructureMap 4.0's type scanning support) to find handler classes and candidate methods from known assemblies based on naming conventions.
By default, Jasper is looking for public classes in the main application assembly with names matching these rules:
- Type name ends with "Handler"
- Type name ends with "Consumer"
From the types, Jasper looks for any public instance method that either accepts a single parameter that is assumed to be the message type, or one parameter with one of these names: message, input, command, or @event. In addition, Jasper will also pick the first parameter as the input type regardless of parameter name if it is concrete, not a "simple" type like a string, date, or number, and not a "Settings" type.
To make that concrete, here are some valid handler method signatures:
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 void Consume(Message1 message, IDocumentSession session)
{
}
// It's perfectly valid to have multiple handler methods
// for a given message type. Each will be called in sequence
public void SendEmail(Message1 input, IEmailService emails)
{
}
// It's also legal to handle a message by an abstract
// base class or an implemented interface.
public void PostProcessEvent(IEvent @event)
{
}
// In this case, we assume that the first type is the message type
// because it's concrete, not "simple", and isn't suffixed with
// "Settings"
public void Consume(Message3 weirdName, IEmailService service)
{
}
public interface IEvent
{
string CustomerId { get; }
Guid Id { get; }
}
}
The valid method names are:
- Handle
- Handles
- Consume
- Consumes
- Start
- Starts
- Orchestrate
- Orchestrates
With the last two options being horrendously awkward to type, but backwards compatible with the naming conventions in the older FubuMVC messaging that Jasper replaces.
Disabling Conventional Discovery
You can completely turn off any automatic discovery of message handlers through type scanning by
using this syntax in your JasperRegistry
:
public class ExplicitHandlerDiscovery : JasperOptions
{
public ExplicitHandlerDiscovery()
{
// No automatic discovery of handlers
Handlers.DisableConventionalDiscovery();
}
}
Explicitly Ignoring Methods
You can force Jasper to disregard a candidate message handler action at either the class or method
level by using the [JasperIgnore]
attribute like this:
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
[JasperIgnore]
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
[JasperIgnore]
public class BlockbusterHandler
{
public void Handle(MovieAdded added)
{
}
}
Customizing Conventional Discovery
The easiest way to use the Jasper 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:
public class CustomHandlerApp : JasperOptions
{
public CustomHandlerApp()
{
Handlers.Discovery(x =>
{
// Turn off the default handler conventions
// altogether
x.DisableConventionalDiscovery();
// Include candidate actions by a user supplied
// type filter
x.IncludeTypes(t => t.IsInNamespace("MyApp.Handlers"));
// Include candidate classes by suffix
x.IncludeClassesSuffixedWith("Listener");
// Include a specific handler class with a generic argument
x.IncludeType<SimpleHandler>();
});
}
}
Subclass or Interface Handlers
Jasper will allow you to use handler methods that work against interfaces or abstract types to apply or reuse
generic functionality across messages. Let's say that some subset of your messages implement some kind of
IMessage
interface like this one and an implentation of it below:
public interface IMessage
{
}
public class MessageOne : IMessage
{
}
You can handle the MessageOne
specifically with a handler action like this:
public class SpecificMessageHandler
{
public void Consume(MessageOne message)
{
}
}
You can also create a handler for IMessage
like this one:
public class GenericMessageHandler
{
public void Consume(IMessage messagem, Envelope envelope)
{
Console.WriteLine($"Got a message from {envelope.Source}");
}
}
When Jasper handles the MessageOne
message, it first calls all the specific handlers for that message type,
then will call any handlers that handle a more generic message type (interface or abstract class most likely) where
the specific type can be cast to the generic type.