Message Handlers

Jasper purposely eschews the typical IHandler<T> approach that most .Net messaging frameworks take in favor of a more flexible model that relies on naming conventions. This might throw some users that are used to being guided by implementing an expected interface or base class, but it allows Jasper to be much more flexible and reduces code noise.

As an example, here's about the simplest possible handler you could create:

public class MyMessageHandler
{
    public void Handle(MyMessage message)
    {
        // do stuff with the message
    }
}

snippet source | anchor

Like most frameworks, Jasper follows the Hollywood Principle where the framework acts as an intermediary between the rest of the world and your application code. When a Jasper application receives a MyMessage message through one of its transports, Jasper will call your method and pass in the message that it received.

How Jasper Consumes Your Message Handlers

If you're worried about the performance implications of Jasper calling into your code without any interfaces or base classes, nothing to worry about because Jasper does not use Reflection at runtime to call your actions. Instead, Jasper uses runtime code generation with Roslyn to write the "glue" code around your actions. Internally, Jasper is generating a subclass of MessageHandler for each known message type:

public abstract class MessageHandler
{
    public HandlerChain? Chain { get; set; }

    // This method actually processes the incoming Envelope
    public abstract Task HandleAsync(IExecutionContext context, CancellationToken cancellation);
}

snippet source | anchor

See <[linkto:documentation/execution/handlers]> for information on how Jasper generates the MessageHandler code and how to customize that code.

Naming Conventions

Out of the box, message handlers need to follow these naming conventions and rules:

  • Classes must be public, concrete classes suffixed with either "Handler" or "Consumer"
  • Message handling methods must have be public and have a deterministic message type
  • The message type has to be a public type

If a candidate method has a single argument, that argument type is assumed to be the message type. Otherwise, Jasper looks for any argument named either "message", "input", or "@event" to be the message type.

See <[linkto:documentation/execution/discovery]> for more information.

Instance Handler Methods

Handler methods can be instance methods on handler classes if it's desirable to scope the handler object to the message:

public class ExampleHandler
{
    public void Handle(Message1 message)
    {
        // Do work synchronously
    }

    public Task Handle(Message2 message)
    {
        // Do work asynchronously
        return Task.CompletedTask;
    }
}

snippet source | anchor

Note that you can use either synchronous or asynchronous methods depending on your needs, so you're not constantly being forced to return Task.CompletedTask over and over again for operations that are purely CPU-bound (but Jasper itself might be doing that for you in its generated MessageHandler code).

Static Handler Methods

Note! Using a static method as your message handler can be a small performance improvement by avoiding the need to create and garbage collect new objects at runtime.

As an alternative, you can also use static methods as message handlers:

public static class ExampleHandler
{
    public static void Handle(Message1 message)
    {
        // Do work synchronously
    }

    public static Task Handle(Message2 message)
    {
        // Do work asynchronously
        return Task.CompletedTask;
    }
}

snippet source | anchor

The handler classes can be static classes as well. This technique gets much more useful when combined with Jasper's support for method injection in a following section.

Constructor Injection

Jasper can create your message handler objects by using an IoC container (or in the future just use straight up dependency injection without any IoC container overhead). In that case, you can happily inject dependencies into your message handler classes through the constructor like this example that takes in a dependency on an IDocumentSession from Marten:

public class ServiceUsingHandler
{
    private readonly IDocumentSession _session;

    public ServiceUsingHandler(IDocumentSession session)
    {
        _session = session;
    }

    public Task Handle(InvoiceCreated created)
    {
        var invoice = new Invoice {Id = created.InvoiceId};
        _session.Store(invoice);

        return _session.SaveChangesAsync();
    }
}

snippet source | anchor

See <[linkto:documentation/ioc]> for more information about how Jasper integrates the application's IoC container.

Method Injection

Similar to ASP.Net MVC Core, Jasper supports the concept of method injection in handler methods where you can just accept additional arguments that will be passed into your method by Jasper when a new message is being handled.

Below is an example action method that takes in a dependency on an IDocumentSession from Marten:

public static class MethodInjectionHandler
{
    public static Task Handle(InvoiceCreated message, IDocumentSession session)
    {
        var invoice = new Invoice {Id = message.InvoiceId};
        session.Store(invoice);

        return session.SaveChangesAsync();
    }
}

snippet source | anchor

So, what can be injected as an argument to your message handler?

  1. Any service that is registered in your application's IoC container
  2. Envelope
  3. The current time in UTC if you have a parameter like DateTime now or DateTimeOffset now
  4. Services or variables that match a registered code generation strategy. See <[linkto:documentation/execution/middleware_and_codegen]> for more information on this mechanism.

Cascading Messages from Actions

To have additional messages queued up to be sent out when the current message has been successfully completed, you can return the outgoing messages from your handler methods with <[linkto:documentation/execution/cascading]>.

Using the Message Envelope

To access the Envelope for the current message being handled in your message handler, just accept Envelope as a method argument like this:

public class EnvelopeUsingHandler
{
    public void Handle(InvoiceCreated message, Envelope envelope)
    {
        var howOldIsThisMessage =
            DateTimeOffset.Now.Subtract(envelope.SentAt);
    }
}

snippet source | anchor

See <[linkto:documentation/integration/customizing_envelopes]> for more information on interacting with Envelope objects.

Using the Current IExecutionContext

If you want to access or use the current IExecutionContext for the message being handled to send response messages or maybe to enqueue local commands within the current outbox scope, just take in IExecutionContext as a method argument like in this example:

using Jasper;
using Messages;
using Microsoft.Extensions.Logging;

namespace Ponger;

public class PingHandler
{
    public ValueTask Handle(Ping ping, ILogger<PingHandler> logger, IExecutionContext context)
    {
        logger.LogInformation("Got Ping #{Number}", ping.Number);
        return context.RespondToSenderAsync(new Pong { Number = ping.Number });
    }
}

snippet source | anchor

public static class PingHandler
{
    // Simple message handler for the PingMessage message type
    public static ValueTask Handle(
        // The first argument is assumed to be the message type
        PingMessage message,

        // Jasper supports method injection similar to ASP.Net Core MVC
        // In this case though, IMessageContext is scoped to the message
        // being handled
        IExecutionContext context)
    {
        ConsoleWriter.Write(ConsoleColor.Blue, $"Got ping #{message.Number}");

        var response = new PongMessage
        {
            Number = message.Number
        };

        // This usage will send the response message
        // back to the original sender. Jasper uses message
        // headers to embed the reply address for exactly
        // this use case
        return context.RespondToSenderAsync(response);
    }
}

snippet source | anchor