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:
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");
});
}
}You can use storage operations in Wolverine message handlers or HTTP endpoints like these samples from the Wolverine test suite:
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);
}
}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.

