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:

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 mark optionsLifetime as singleton scoped because Jasper will be able to generate more efficient handler pipeline code for message handlers that use your EF Core DbContext.

Transactional Support and Outbox Usage

First, let's look at using the EF Core-backed outbox usage in the following MVC controller method that:

  1. Starts an outbox transaction with ItemsDbContext and IMessageContext (Jasper's main entrypoint for messaging)
  2. Accepts a CreateItemCommand command input
  3. Creates and saves a new Item entity with ItemsDbContext
  4. Also creates and publishes a matching ItemCreated event for any interested subscribers
  5. Commits the unit of work
  6. 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:

  1. The primary key / identity for the state document has to be either an int, long, string, or System.Guid
  2. Jasper analyzes the dependency tree of your StatefulSagaOf<TState> handler class for a Lamar dependency that inherits from DbContext, and if it finds exactly 1 dependency, that is assumed to be used for persisting the state
  3. All saga message handlers are automatically wrapped with the transactional middleware
  4. You will have to have EF Core mapping for the .Net state type of your saga handler