Jasper as a Mediator
Note! All of the code on this page is from the InMemoryMediator sample project.
Recently there's been some renewed interest in the old Gof Mediator pattern as a way to isolate the actual functionality of web services and applications from the mechanics of HTTP request handling. In more concrete terms for .Net developers, a mediator tool allows you to keep MVC Core code ceremony out of your application business logic and service layer. It wasn't the original motivation of the project, but Jasper can be used as a full-featured mediator.
Let's jump into a sample project. Let's say that your system creates and tracks Items of some sort. One of the API requirements is to expose an HTTP
endpoint that can accept an input that will create and persist a new Item
, while also publishing an ItemCreated
event message to any other system
(or internal listener within the same system). For the technology stack, let's use:
- MVC Core as the Web API framework
- Jasper as our mediator of course!
- Sql Server as the backing database store, using Jasper's Sql Server message persistence
- EF Core as the persistence mechanism
First off, let's start a new project with the dotnet new webapi
template. Next, let's put all the Jasper configuration into a
custom JasperOptions class like this:
public class JasperConfig : JasperOptions
{
public override void Configure(IHostEnvironment hosting, IConfiguration config)
{
if (hosting.IsDevelopment())
{
// In development mode, we're just going to have the message persistence
// schema objects dropped and rebuilt on app startup so you're
// always starting from a clean slate
Advanced.StorageProvisioning = StorageProvisioning.Rebuild;
}
// Just the normal work to get the connection string out of
// application configuration
var connectionString = config.GetConnectionString("sqlserver");
// Setting up Sql Server-backed message persistence
// This requires a reference to Jasper.Persistence.SqlServer
Extensions.PersistMessagesWithSqlServer(connectionString);
// Set up Entity Framework Core as the support
// for Jasper's transactional middleware
Extensions.UseEntityFrameworkCorePersistence();
// Register the EF Core DbContext
Services.AddDbContext<ItemsDbContext>(
x => x.UseSqlServer(connectionString),
// This is important! Using Singleton scoping
// of the options allows Jasper + Lamar to significantly
// optimize the runtime pipeline of the handlers that
// use this DbContext type
optionsLifetime:ServiceLifetime.Singleton);
}
}
From there, we'll slightly modify the Program
file generated by the webapi
template to add Jasper and opt
into Jasper's extended command line support:
public class Program
{
// Change the return type to Task<int> to communicate
// success/failure codes
public static Task<int> Main(string[] args)
{
return CreateHostBuilder(args)
// This replaces Build().Start() from the default
// dotnet new templates
.RunJasper(args);
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
// You can do the Jasper configuration inline with a
// Lambda, but here I've centralized the Jasper
// configuration into a separate class
.UseJasper<JasperConfig>()
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
Now, let's add a Jasper message handler that will:
- Handle a new
CreateItemCommand
message - Create a new
Item
entity and persist that with a newItemsDbContext
custom EF CoreDbContext
- Create and publish a new
ItemCreated
event message reflecting the newItem
Using idiomatic Jasper, that handler looks like this:
public class ItemHandler
{
// This attribute applies Jasper's EF Core transactional
// middleware
[Transactional]
public static ItemCreated Handle(
// This would be the message
CreateItemCommand command,
// Any other arguments are assumed
// to be service dependencies
ItemsDbContext db)
{
// Create a new Item entity
var item = new Item
{
Name = command.Name
};
// Add the item to the current
// DbContext unit of work
db.Items.Add(item);
// This event being returned
// by the handler will be automatically sent
// out as a "cascading" message
return new ItemCreated
{
Id = item.Id
};
}
}
Note, as long as this handler class is public and in the main application assembly, Jasper is going to find it and wire it up inside its execution pipeline. There's no explicit code or funky IoC registration necessary.
Now, moving up to the controller layer, we can add a controller like this:
public class UseJasperAsMediatorController : ControllerBase
{
private readonly ICommandBus _bus;
public UseJasperAsMediatorController(ICommandBus bus)
{
_bus = bus;
}
[HttpPost("/items/create")]
public Task Create([FromBody] CreateItemCommand command)
{
// Using Jasper as a Mediator
return _bus.Invoke(command);
}
}
There isn't much to this code -- and that's the entire point! When Jasper registers itself into
a .Net Core application, it adds the ICommandBus
service to the underlying system IoC container
so it can be injected into controller classes as a constructor argument or as a method argument
if you prefer to use the [FromServices]
attribute and method injection. The ICommandBus.Invoke(message)
method takes the message passed in, finds the correct execution path for the message type, and
executes the correct Jasper handler(s) as well as any of the registered Jasper Middleware and Policies.
Note! This execution happens inline, and does not involve any of Jasper's Error Handling functionality that would apply to enqueued messages.
See also:
- Cascading Messages for a better explanation of how the
ItemCreated
event message is automatically published if the handler success. - Execution Pipeline for the details of how to write Jasper message handlers and how they are discovered
As a contrast, here's what the same functionality looks like if you write all the functionality out explicitly in a controller action:
// This controller does all the transactional work and business
// logic all by itself
public class DoItAllMyselfItemController : ControllerBase
{
private readonly IMessageContext _messaging;
private readonly ItemsDbContext _db;
public DoItAllMyselfItemController(IMessageContext messaging, ItemsDbContext db)
{
_messaging = messaging;
_db = db;
}
[HttpPost("/items/create3")]
public async Task Create([FromBody] CreateItemCommand command)
{
// Start the "Outbox" transaction
await _messaging.EnlistInTransaction(_db);
// Create a new Item entity
var item = new Item
{
Name = command.Name
};
// Add the item to the current
// DbContext unit of work
_db.Items.Add(item);
// Publish an event to anyone
// who cares that a new Item has
// been created
var @event = new ItemCreated
{
Id = item.Id
};
// Because the message context is enlisted in an
// "outbox" transaction, these outgoing messages are
// held until the ongoing transaction completes
await _messaging.Send(@event);
// Commit the unit of work. This will persist
// both the Item entity we created above, and
// also a Jasper Envelope for the outgoing
// ItemCreated message
await _db.SaveChangesAsync();
// After the DbContext transaction succeeds, kick out
// the persisted messages in the context "outbox"
await _messaging.SendAllQueuedOutgoingMessages();
}
}
So one, there's just more going on in the Create()
method above because you're needing to do a little bit of
additional work that Jasper can do for you inside of its execution pipeline (the outbox mechanics, the cascading message getting published, transaction management).
Also though, you're now mixing up MVC controller stuff like the [HttpPost]
attribute to control the
Url for the endpoint and the service application code that exercises the data and domain model layers.
Getting a Response
The controller methods above would both return an empty response body and the default 200 OK
status code.
But what if you want to return some kind of response body that gave the client of the web service some
kind of contextual information about the newly created Item
.
To that end, let's write a different controller action that will relay the body of the ItemCreated
output of the message handler to the HTTP response body (and assume we'll use JSON because that makes the
example code simpler):
public class WithResponseController : ControllerBase
{
private readonly ICommandBus _bus;
public WithResponseController(ICommandBus bus)
{
_bus = bus;
}
[HttpPost("/items/create2")]
public Task<ItemCreated> Create([FromBody] CreateItemCommand command)
{
// Using Jasper as a Mediator, and receive the
// expected response from Jasper
return _bus.Invoke<ItemCreated>(command);
}
}
Using the ICommandBus.Invoke<T>(message)
overload, the returned ItemCreated
response
of the message handler is returned from the Invoke()
message. To be perfectly clear, this only
works if the message handler method returns a cascading message of the exact same type of the
designated T
parameter.