A couple months ago I blogged a little bit about a yet another OSS service bus project my shop is building out for messaging in .Net Core systems called Jasper that services as a wire compatible successor to the tooling we use in older .Net applications. While it’s already in production systems at work and doing fine, I have no clue if it’ll have any success as an OSS project. At the very least I’m going to squeeze some blog posts out of the process of building it and here we are.
Service bus frameworks are definitely an example of the Hollywood Principle where a framework handles much of the event handling and workflow while delegating to your application specific code through some kind of interface or idiom. In most of the cases I’ve seen over the years in .Net, you’ll see some kind of interface like the one below that allows you to plug your message handlers into your service bus infrastructure:
public interface IHandler { Task Handle(T message); }
I’ve certainly used this approach in a handful of cases, and there’s even some direct support for auto-registering this kind of service strategy inside of StructureMap if you want to roll your own framework. It’s easy to understand, adds some level of discoverability, and might help guide users. It’s also somewhat limiting in flexibility and the copious usage of generics can easily lead users into some bad places — and I say that partially based on a decade of helping folks with generics on the StructureMap user lists.
Jasper takes a different approach that relies much more on naming conventions and method signatures. To make that concrete, here’s the very simplest form of message handlers you can use in Jasper and if you don’t mind, let me leave how could this possibly work efficiently for a followup post (spoiler alert: Roslyn is awesome):
public class ExampleHandler { public void Handle(Message1 message) { // Do work synchronously } public Task Handle(Message2 message) { // Do work asynchronously return Task.CompletedTask; } }
Out of the box, Jasper finds and uses message handling methods by searching for concrete classes whose names are suffixed with either “Handler” or “Consumer” (there’s some historical reasons for having both) and then discovers message handling actions by analyzing the public methods on those classes for message handling candidates.
Right off the bat, you can see that Jasper allows you to write either synchronous or asynchronous methods to handle messages, so no more phantom “return Task.CompletedTask;” lines cluttering up your code.
Moreover, you can wring out a little more performance in your system by using static methods instead:
public static class StaticHandler { public static Task Handle(Message3 message) { return Task.CompletedTask; } }
It might be advantageous to use this approach to reduce memory allocations at run time and should give you slightly more efficient IL. Of course, any handler method, static or otherwise, isn’t terribly helpful unless you can get at the services within your application that you’ll need to invoke to process the message.
To that end Jasper gives you a couple possibilities. First, you can do the idiomatic, constructor injection approach like this:
public class ServiceUsingHandler { private readonly IService _service; public ServiceUsingHandler(IService service) { _service = service; } public void Handle(Message1 message) { // do something with _service to handle this thing } }
At the moment, Jasper would revert to spinning up a StructureMap nested container and uses that to build out the ServiceUsingHandler objects something like this:
// _root is a reference to the application's root // container using (var nested = _root.GetNestedContainer()) { var serviceUsingHandler = nested.GetInstance(); var message1 = (Message1)context.Envelope.Message; serviceUsingHandler.Handle(message1, widget); }
Using that approach enables Jasper to build objects of your handler classes with whatever dependencies you would need. Alternatively, you can also use “method injection” in your handlers like this:
public void Handle( Message2 message, IService service, Envelope envelope ) { // handle the message }
In the example above, Jasper “knows” how to resolve both the IService and Envelope dependencies before calling into the Handle() method. The Envelope object is Jasper’s version of an envelope wrapper that gives you more metadata about the current message. Instead of pushing everything through the constructor function, you can opt for potentially simpler and cleaner code by opting for method injection instead. In the case of the Envelope, that is not even available through the IoC container.
More about this in a later post, but ironically as the author of literally the oldest IoC container in .the .Net ecosystem, I’m trying hard to reduce Jasper’s usage of IoC containers at runtime.
The last thing I wanted to show here was Jasper’s concept of cascading messages that we used with some success in the earlier FubuMVC service bus. It’s very common for the handling of the original message to trigger additional “cascading” messages. In most service bus frameworks, that’d probably be something like this:
public class MessageHandler { // Successfully handling Message1 will generate // a Message2 going out public Message2 Handle(Message1 message, IServiceBus bus) { bus.Send(new Message2()); } }
You can absolutely do that in Jasper as well, but we also support a policy where object(s) returned from a handler method are considered to be outgoing messages that are sent out as part of considering the message request complete. In its simplest usage, that may look like this:
public class MessageHandler { // Successfully handling Message1 will generate // a Message2 going out public Message2 Handle(Message1 message) { return new Message2(); } // Same thing, but async public Task Handle(Message1 message) { } }
To get a little more complex, let’s say that your neck deep in CQRS jargon and when your service receives a “Command1” you raise one or more domain events that are handled separately. With cascading messages, that can look like this:
public IEnumerable<object> Handle(Command1 command) { yield return new Event1(); yield return new Event2(); }
In this case, each object returned is an outgoing message. I like the cascading message approach because it makes your message handlers easier to test with pure state-based testing.
There’s plenty more going on with this feature, but my wife really needs me to get out the office to go help with the little ones, so there’s going to have to more later;)
One thought on “Message Handlers in the new Jasper Service Bus”