Using Entity Framework Core with Jasper
The Jasper.Persistence.EntityFrameworkCore
Nuget can be used with a Jasper application to add support for
using Entity Framework Core in the Jasper:
[Transactional]
middleware- Outbox support
- Saga persistence
Note that you will also need to use one of the database backed message persistence mechanisms like Jasper.Persistence.SqlServer or Jasper.Persistence.Postgresql in conjunction with the EF Core integration.
As an example of using the EF Core integration with Sql Server inside a Jasper application, see the the InMemoryMediator sample project.
Assuming that Jasper.Persistence.EntityFrameworkCore
is referenced by your application, here's a custom
DbContext type from the sample project:
public class ItemsDbContext : DbContext
{
public ItemsDbContext(DbContextOptions<ItemsDbContext> options) : base(options)
{
}
public DbSet<Item> Items { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// This adds the Jasper Envelope storage mapping to
// this DbContext type. This enables the EF Core "outbox"
// support with Jasper
modelBuilder.MapEnvelopeStorage();
// Your normal EF Core mapping
modelBuilder.Entity<Item>(map =>
{
map.ToTable("items");
map.HasKey(x => x.Id);
map.Property(x => x.Name);
});
}
}
Most of this is just standard EF Core. The only Jasper specific thing is the call
to modelBuilder.MapEnvelopeStorage()
in the OnModelCreating()
method. This adds mappings
to Jasper's message persistence and allowing
the ItemsDbContext
objects to enroll in Jasper outbox transactions.
Note! You will have to explicitly opt into a specific database persistence for the messaging and also explicitly add in the EF Core transactional support.
Now, to wire up EF Core into our Jasper application and add Sql Server-backed message persistence, use this JasperOptions class:
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);
}
}
There's a couple things to note in the code above:
- The call to
Extensions.PersistMessagesWithSqlServer()
sets up the Sql Server backed message persistence - The
AddDbContext<ItemsDbContext>()
call is just the normal EF Core set up, with one difference. It's a possibly significant performance optimization to markoptionsLifetime
as singleton scoped because Jasper will be able to generate more efficient handler pipeline code for message handlers that use your EF CoreDbContext
.
Transactional Support and Outbox Usage
First, let's look at using the EF Core-backed outbox usage in the following MVC controller method that:
- Starts an outbox transaction with
ItemsDbContext
andIMessageContext
(Jasper's main entrypoint for messaging) - Accepts a
CreateItemCommand
command input - Creates and saves a new
Item
entity withItemsDbContext
- Also creates and publishes a matching
ItemCreated
event for any interested subscribers - Commits the unit of work
- Flushes out the newly persisted
ItemCreated
outgoing message to Jasper's sending agents
// 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();
}
}
Outside of a Jasper message handler, you will have to explicitly
enlist a Jasper IMessageContext
in a DbContext
unit of work through the EnlistInTransaction(DbContext)
extension method. Secondly, after calling DbContext.SaveChangesAsync()
, you'll need to manually call
IMessageContext.SendAllQueuedOutgoingMessages()
to actually release the newly persisted ItemCreated
event message to be be sent. If your application somehow manages to crash between the successful call
to SaveChangesAsync()
and the ItemCreated
message actually being delivered by Jasper to wherever it
was supposed to go, not to worry. The outgoing message is persisted and will be sent either by restarting
the application or by failing over to another running node of your application.
Alright, that was some busy code, so let's see how this can be cleaner running inside a Jasper message
handler that takes advantage of the [Transactional]
middleware:
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
};
}
}
The code above effectively does the same thing as the DoItAllMyselfItemController
shown earlier,
but Jasper is generating some middleware code around the ItemHandler.Handle()
method to enlist
the scoped IMessageContext
object into the scoped ItemsDbContext
unit of work. That same middleware is
coming behind the call to Item.Handler()
and both saving the changes in ItemsDbContext
and pushing out
any newly persisted cascading messages.
Saga Persistence
If the Jasper.Persistence.EntityFrameworkCore
Nuget is referenced, your Jasper application can use custom DbContext
types
as the saga persistence mechanism. There's just a couple things to know:
- The primary key / identity for the state document has to be either an
int
,long
,string
, orSystem.Guid
- Jasper analyzes the dependency tree of your
StatefulSagaOf<TState>
handler class for a Lamar dependency that inherits fromDbContext
, and if it finds exactly 1 dependency, that is assumed to be used for persisting the state - All saga message handlers are automatically wrapped with the transactional middleware
- You will have to have EF Core mapping for the .Net state type of your saga handler