I’ve spit out quite a bit of blogging content the past several weeks on both Marten and Jasper:
- Marten just got better for CQRS architectures about some new functionality dreamed up by Oskar Dudycz and I to smooth out some repetitive friction when using Marten with a CQRS style architecture
- (Re) Introducing Jasper as a Command Bus about the ongoing Jasper 2.0 reboot
- A Vision for Low Ceremony CQRS with Event Sourcing that combined some new functionality in Marten together with Jasper’s execution pipeline and middleware strategy to show how CQRS command handlers can be built with very minimal code ceremony
- Building a More Useful Outbox for Reliable Messaging about Jasper’s transactional outbox implementation
For this post though, I just want to show off a very small sample “Ping/Pong” example of using Jasper to do messaging between different .NET processes. All of the code is drawn from a brand new sample on GitHub. I’m just using Jasper’s little built in TCP transport that’s probably just going to be useful for local development, but it happily serves for this sample.
First off, I’m going to build out a very small shared library just to hold the messages we’re going to exchange:
public class Ping
{
public int Number { get; set; }
}
public class Pong
{
public int Number { get; set; }
}
And next, I’ll start a small Pinger service with the dotnet new worker
template. There’s just three pieces of code, starting with the boostrapping code:
using Jasper;
using Jasper.Transports.Tcp;
using Messages;
using Oakton;
using Pinger;
return await Host.CreateDefaultBuilder(args)
.UseJasper(opts =>
{
// Using Jasper's built in TCP transport
// listen to incoming messages at port 5580
opts.ListenAtPort(5580);
// route all Ping messages to port 5581
opts.PublishMessage<Ping>().ToPort(5581);
// Registering the hosted service here, but could do
// that with a separate call to IHostBuilder.ConfigureServices()
opts.Services.AddHostedService<Worker>();
})
.RunOaktonCommands(args);
and the Worker
class that’s just going to publish a new Ping
message once a second:
using Jasper;
using Messages;
namespace Pinger;
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
private readonly IMessagePublisher _publisher;
public Worker(ILogger<Worker> logger, IMessagePublisher publisher)
{
_logger = logger;
_publisher = publisher;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var pingNumber = 1;
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(1000, stoppingToken);
_logger.LogInformation("Sending Ping #{Number}", pingNumber);
await _publisher.PublishAsync(new Ping { Number = pingNumber });
pingNumber++;
}
}
}
and lastly a message handler for any Pong
messages coming back from the Ponger
we’ll build next:
using Messages;
namespace Pinger;
public class PongHandler
{
public void Handle(Pong pong, ILogger<PongHandler> logger)
{
logger.LogInformation("Received Pong #{Number}", pong.Number);
}
}
Okay then, next let’s move on to building the Ponger
application. This time I’ll use dotnet new console
to start the new project, then add references to our Messages library and Jasper itself. For the bootstrapping, add this code:
using Jasper;
using Jasper.Transports.Tcp;
using Microsoft.Extensions.Hosting;
using Oakton;
return await Host.CreateDefaultBuilder(args)
.UseJasper(opts =>
{
// Using Jasper's built in TCP transport
opts.ListenAtPort(5581);
})
.RunOaktonCommands(args);
And a message handler for the Ping
messages that will turn right around and shoot a Pong
response right back to the original sender:
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 });
}
}
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);
}
}
If I start up first the Ponger service, then the Pinger service, I’ll see console output like this from Pinger:
info: Pinger.Worker[0]
Sending Ping #11
info: Pinger.PongHandler[0]
Received Pong #1
info: Jasper.Runtime.JasperRuntime[104]
Successfully processed message Pong#01817277-f692-42d5-a3e4-35d9b7d119fb from tcp://localhost:5581/
info: Pinger.PongHandler[0]
Received Pong #2
info: Jasper.Runtime.JasperRuntime[104]
Successfully processed message Pong#01817277-f699-4340-a59d-9616aee61cb8 from tcp://localhost:5581/
info: Pinger.PongHandler[0]
Received Pong #3
info: Jasper.Runtime.JasperRuntime[104]
Successfully processed message Pong#01817277-f699-48ea-988b-9e835bc53020 from tcp://localhost:5581/
info: Pinger.PongHandler[0]
and output like this in the Ponger process:
info: Ponger.PingHandler[0]
Got Ping #1
info: Jasper.Runtime.JasperRuntime[104]
Successfully processed message Ping#01817277-d673-4357-84e3-834c36f3446c from tcp://localhost:5580/
info: Ponger.PingHandler[0]
Got Ping #2
info: Jasper.Runtime.JasperRuntime[104]
Successfully processed message Ping#01817277-da61-4c9d-b381-6cda92038d41 from tcp://localhost:5580/
info: Ponger.PingHandler[0]
Got Ping #3