
Heads up, you will need at least Wolverine 0.9.7 for these samples!
I’ve mostly been writing about Wolverine samples that involve its “critter stack” compatriot Marten as the persistence tooling. I’m obviously deeply invested in making that “critter stack” the highest productivity combination for server side development basically anywhere.
Today though, let’s go meet potential Wolverine users where they actually live and finally talk about how to integrate Entity Framework Core (EF Core) and SQL Server into Wolverine applications.
All of the samples in this post are from the EFCoreSample project in the Wolverine codebase. There’s also some newly published documentation about integrating EF Core with Wolverine now too.
Alright, let’s say that we’re building a simplistic web service to capture information about Item
entities (so original) and we’ve decided to use SQL Server as the backing database and use EF Core as our ORM for persistence — and also use Wolverine as an in memory mediator because why not?
I’m going to start by creating a brand new project with the dotnet new webapi
template. Next I’m going to add some Nuget references for:
- Microsoft.EntityFrameworkCore.SqlServer
- WolverineFx.SqlServer
- WolverineFx.EntityFrameworkCore
Now, let’s say that I have a simplistic DbContext
class to define my EF Core mappings like so:
public class ItemsDbContext : DbContext
{
public ItemsDbContext(DbContextOptions<ItemsDbContext> options) : base(options)
{
}
public DbSet<Item> Items { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Your normal EF Core mapping
modelBuilder.Entity<Item>(map =>
{
map.ToTable("items");
map.HasKey(x => x.Id);
map.Property(x => x.Name);
});
}
}
Now let’s switch to the Program
file that holds all our application bootstrapping and configuration:
using ItemService;
using Microsoft.EntityFrameworkCore;
using Oakton;
using Oakton.Resources;
using Wolverine;
using Wolverine.EntityFrameworkCore;
using Wolverine.SqlServer;
var builder = WebApplication.CreateBuilder(args);
// Just the normal work to get the connection string out of
// application configuration
var connectionString = builder.Configuration.GetConnectionString("sqlserver");
#region sample_optimized_efcore_registration
// If you're okay with this, this will register the DbContext as normally,
// but make some Wolverine specific optimizations at the same time
builder.Services.AddDbContextWithWolverineIntegration<ItemsDbContext>(
x => x.UseSqlServer(connectionString));
builder.Host.UseWolverine(opts =>
{
// Setting up Sql Server-backed message storage
// This requires a reference to Wolverine.SqlServer
opts.PersistMessagesWithSqlServer(connectionString);
// Enrolling all local queues into the
// durable inbox/outbox processing
opts.Policies.UseDurableLocalQueues();
});
// This is rebuilding the persistent storage database schema on startup
builder.Host.UseResourceSetupOnStartup();
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Make sure the EF Core db is set up
await app.Services.GetRequiredService<ItemsDbContext>().Database.EnsureCreatedAsync();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.MapControllers();
app.MapPost("/items/create", (CreateItemCommand command, IMessageBus bus) => bus.InvokeAsync(command));
// Opt into using Oakton for command parsing
await app.RunOaktonCommands(args);
In the code above, I’ve:
- Added a service registration for the new
ItemsDbContext
EF Core class, but I did so with a special Wolverine wrapper that adds some optimizations for us, quietly adds some mapping to theItemsDbContext
at runtime for the Wolverine message storage, and also enables Wolverine’s transactional middleware and stateful saga support for EF Core. - I added Wolverine to the application, and used the
PersistMessagesWithSqlServer()
extension method to tell Wolverine to add message storage for SQL Server in the defaultdbo
schema (that can be overridden). This also adds Wolverine’s durable agent for its transactional outbox and inbox running as a background service in anIHostedService
- I directed the application to build out any missing database schema objects on application startup through the call to
builder.Host.UseResourceSetupOnStartup();
If you’re curious, this is using Oakton’s stateful resource model. - For the sake of testing this little bugger, I’m having the application build the implied database schema from the
ItemsDbContext
as well
Moving on, let’s build a simple message handler that creates a new Item
, persists that with EF Core, and raises a new ItemCreated
event message:
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
};
}
Simple enough, but a couple notes about that code:
- I didn’t explicitly call the
SaveChangesAsync()
method on ourItemsDbContext
to commit the changes, and that’s because Wolverine sees that the handler has a dependency on an EF CoreDbContext
type, so it automatically wraps its EF Core transactional middleware around the handler - The
ItemCreated
object returned from the message handler is a Wolverine cascaded message, and will be sent out upon successful completion of the originalCreateItemCommand
message — including the transactional middleware that wraps the handler. - And oh, by the way, we want the
ItemCreated
message to be persisted in the underlying Sql Server database as part of the transaction being committed so that Wolverine’s transactional outbox functionality makes sure that message gets processed (eventually) even if the process somehow fails between publishing the new message and that message being successfully completed.
I should also note that as a potentially significant performance optimization, Wolverine is able to persist the ItemCreated
message when ItemsDbContext.SaveChangesAsync()
is called to enroll in EF Core’s ability to batch changes to the database rather than incurring the cost of extra network hops if we’d used raw SQL.
Hopefully that’s all pretty easy to follow, even though there’s some “magic” there. If you’re curious, here’s the actual code that Wolverine is generating to handle the CreateItemCommand
message (just remember that auto-generated code tends to be ugly as sin):
// <auto-generated/>
#pragma warning disable
using Microsoft.EntityFrameworkCore;
namespace Internal.Generated.WolverineHandlers
{
// START: CreateItemCommandHandler1452615242
public class CreateItemCommandHandler1452615242 : Wolverine.Runtime.Handlers.MessageHandler
{
private readonly Microsoft.EntityFrameworkCore.DbContextOptions<ItemService.ItemsDbContext> _dbContextOptions;
public CreateItemCommandHandler1452615242(Microsoft.EntityFrameworkCore.DbContextOptions<ItemService.ItemsDbContext> dbContextOptions)
{
_dbContextOptions = dbContextOptions;
}
public override async System.Threading.Tasks.Task HandleAsync(Wolverine.Runtime.MessageContext context, System.Threading.CancellationToken cancellation)
{
await using var itemsDbContext = new ItemService.ItemsDbContext(_dbContextOptions);
var createItemCommand = (ItemService.CreateItemCommand)context.Envelope.Message;
var outgoing1 = ItemService.CreateItemCommandHandler.Handle(createItemCommand, itemsDbContext);
// Outgoing, cascaded message
await context.EnqueueCascadingAsync(outgoing1).ConfigureAwait(false);
}
}
So that’s EF Core within a Wolverine handler, and using SQL Server as the backing message store. One of the weaknesses of some of the older messaging tools in .NET is that they’ve long lacked a usable outbox feature outside of the context of their message handlers (both NServiceBus and MassTransit have just barely released “real” outbox features), but that’s a frequent need in the applications at my own shop and we’ve had to work around these limitations. Fortunately though, Wolverine’s outbox functionality is usable outside of message handlers.
As an example, let’s implement basically the same functionality we did in the message handler, but this time in an ASP.Net Core Controller
method:
[HttpPost("/items/create2")]
public async Task Post(
[FromBody] CreateItemCommand command,
[FromServices] IDbContextOutbox<ItemsDbContext> outbox)
{
// Create a new Item entity
var item = new Item
{
Name = command.Name
};
// Add the item to the current
// DbContext unit of work
outbox.DbContext.Items.Add(item);
// Publish a message to take action on the new item
// in a background thread
await outbox.PublishAsync(new ItemCreated
{
Id = item.Id
});
// Commit all changes and flush persisted messages
// to the persistent outbox
// in the correct order
await outbox.SaveChangesAndFlushMessagesAsync();
}
In the sample above I’m using the Wolverine IDbContextOutbox<T>
service to wrap the ItemsDbContext
and automatically enroll the EF Core service in Wolverine’s outbox. This service exposes all the possible ways to publish messages through Wolverine’s normal IMessageBus
entrypoint.
Here’s a slightly different possible usage where I directly inject ItemsDbContext
, but also a Wolverine IDbContextOutbox
service:
[HttpPost("/items/create3")]
public async Task Post3(
[FromBody] CreateItemCommand command,
[FromServices] ItemsDbContext dbContext,
[FromServices] IDbContextOutbox outbox)
{
// Create a new Item entity
var item = new Item
{
Name = command.Name
};
// Add the item to the current
// DbContext unit of work
dbContext.Items.Add(item);
// Gotta attach the DbContext to the outbox
// BEFORE sending any messages
outbox.Enroll(dbContext);
// Publish a message to take action on the new item
// in a background thread
await outbox.PublishAsync(new ItemCreated
{
Id = item.Id
});
// Commit all changes and flush persisted messages
// to the persistent outbox
// in the correct order
await outbox.SaveChangesAndFlushMessagesAsync();
}
That’s about all there is, but to sum it up:
- Wolverine is able to use SQL Server as its persistent message store for durable messaging
- There’s a ton of functionality around managing the database schema for you so you can focus on just getting stuff done
- Wolverine has transactional middleware that can be applied automatically around your handlers as a way to simplify your message handlers while also getting the durable outbox messaging
- EF Core is absolutely something that’s supported by Wolverine
This looks cool, I’m always looking out for source generator magic to replace boilerplate.
Would you be able to send another command in the handler and tie the two operations in a transaction? (i.e. upload/add asset to assets table -> add asset to product-assets table).
Also, in the generated source, is constructing the dbcontext directly an intentional choice over using an IoC factory (like AddDbContextPool) ?
Thanks!
So there’s no current support for batching commands in a single transaction (yet). The precursor tool to Wolverine (FubuTransportation) could do that, but not in a very elegant or efficient way.
“is constructing the dbcontext directly an intentional choice over using an IoC factory” — that’s a yes. Wolverine is using Lamar as its IoC tool, and it’s digging into Lamar’s configuration model to “know” how to generate the code in the most efficient way possible when it can, or falls back to using IoC construction when it can’t. My argument is that Wolverine’s approach is potentially much more efficient than the typical .NET framework approach.