Skip to content

The search box in the website knows all the secrets—try it!

For any queries, join our Discord Channel to reach us faster.

JasperFx Logo

JasperFx provides formal support for Wolverine and other JasperFx libraries. Please check our Support Plans for more details.

Storage Operations with EF Core ​

Just know that Wolverine completely supports the concept of Storage Operations for EF Core.

Assuming you have an EF Core DbContext type like this registered in your system:

cs
public class TodoDbContext : DbContext
{
    public TodoDbContext(DbContextOptions<TodoDbContext> options) : base(options)
    {
    }

    public DbSet<Todo> Todos { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Todo>(map =>
        {
            map.ToTable("todos", "todo_app");
            map.HasKey(x => x.Id);
            map.Property(x => x.Name);
            map.Property(x => x.IsComplete).HasColumnName("is_complete");
        });
    }
}

snippet source | anchor

You can use storage operations in Wolverine message handlers or HTTP endpoints like these samples from the Wolverine test suite:

cs
public static class TodoHandler
{
    public static Insert<Todo> Handle(CreateTodo command) => Storage.Insert(new Todo
    {
        Id = command.Id,
        Name = command.Name
    });
    
    public static Store<Todo> Handle(CreateTodo2 command) => Storage.Store(new Todo
    {
        Id = command.Id,
        Name = command.Name
    });
    
    // Use "Id" as the default member
    public static Update<Todo> Handle(
        // The first argument is always the incoming message
        RenameTodo command, 
        
        // By using this attribute, we're telling Wolverine
        // to load the Todo entity from the configured
        // persistence of the app using a member on the
        // incoming message type
        [Entity] Todo todo)
    {
        // Do your actual business logic
        todo.Name = command.Name;
        
        // Tell Wolverine that you want this entity
        // updated in persistence
        return Storage.Update(todo);
    }
    
    // Use "TodoId" as the default member
    public static Update<Todo> Handle(RenameTodo2 command, [Entity] Todo todo)
    {
        todo.Name = command.Name;
        return Storage.Update(todo);
    }
    
    // Use the explicit member
    public static Update<Todo> Handle(RenameTodo3 command, [Entity("Identity")] Todo todo)
    {
        todo.Name = command.Name;
        return Storage.Update(todo);
    }

    public static Delete<Todo> Handle(DeleteTodo command, [Entity("Identity")] Todo todo)
    {
        return Storage.Delete(todo);
    }
    
    public static IStorageAction<Todo> Handle(AlterTodo command, [Entity("Identity")] Todo todo)
    {
        switch (command.Action)
        {
            case StorageAction.Delete:
                return Storage.Delete(todo);
            case StorageAction.Update:
                todo.Name = command.Name;
                return Storage.Update(todo);
            case StorageAction.Store:
                todo.Name = command.Name;
                return Storage.Store(todo);
            default:
                return Storage.Nothing<Todo>();
        }
    }

    public static IStorageAction<Todo> Handle(MaybeInsertTodo command)
    {
        if (command.ShouldInsert)
        {
            return Storage.Insert(new Todo { Id = command.Id, Name = command.Name });
        }

        return Storage.Nothing<Todo>();
    }

    public static Insert<Todo>? Handle(ReturnNullInsert command) => null;

    public static IStorageAction<Todo>? Handle(ReturnNullStorageAction command) => null;

    public static IStorageAction<Todo> Handle(CompleteTodo command, [Entity] Todo todo)
    {
        if (todo == null) throw new ArgumentNullException(nameof(todo));
        todo.IsComplete = true;
        return Storage.Update(todo);
    }
    
    public static IStorageAction<Todo> Handle(MaybeCompleteTodo command, [Entity(Required = false)] Todo? todo)
    {
        if (todo == null) return Storage.Nothing<Todo>();
        todo.IsComplete = true;
        return Storage.Update(todo);
    }
}

snippet source | anchor

WARNING

When a handler returns an IStorageAction, Wolverine automatically applies transactional middleware for that handler — even if the handler is not explicitly decorated with [Transactional] or AutoApplyTransactions() is not configured.

This behavior is required because Wolverine needs to automatically call SaveChangesAsync() on the EF Core DbContext to persist the storage operation, which should be done within a single transaction together with publication of messages to the outbox/inbox.

TIP

That single-transaction promise holds in TransactionMiddlewareMode.Lightweight too, as of 6.41. There is no explicit transaction there, but Wolverine enrolls the DbContext in the outbox, so your storage operation and the rows for every message the handler cascades are written by one SaveChangesAsync(). Before 6.41 a message handler in Lightweight mode wrote the messages in a separate call afterwards, and a failure in between left the row without them. See Lightweight Mode and the Outbox.

INFO

It does not matter whether the entity you hand to Storage.Update() or Storage.Store() came from a [Entity] parameter, an AsNoTracking() query, an HTTP request body, or a new up off the message itself -- Wolverine attaches an entity the DbContext isn't already tracking, so SaveChangesAsync() writes it either way. Storage.Store() is a real upsert: it reads the row by its primary key first and then inserts or updates. That read is unavoidable, because EF Core has no upsert statement of its own -- so reach for Storage.Insert() or Storage.Update() when you already know which one you want.

Before 6.41, declaring the return type as Update<T> or Store<T> (rather than IStorageAction<T>) silently saved nothing at all for an entity the DbContext wasn't tracking.

[Entity] ​

Wolverine also supports the usage of the [Entity] attribute to load entity data by its identity with EF Core. As you'd expect, Wolverine can "find" the right EF Core DbContext type for the entity type through IoC service registrations. The loaded EF core entity does not included related entities.

For more information on the usage of this attribute see Automatically loading entities to method parameters.

Released under the MIT License.