VSAEF CoreAugust 2026 · 6 min read

Vertical Slice Architecture with EF Core: Where Migrations and Data Access Live

TL;DR

In a Vertical Slice Architecture app, register one shared write DbContext in the composition root, keep migrations in a dedicated Infrastructure project, and let every slice consume the context directly. A per-slice DbContext is only worth it for read models — and never put migrations in the Web project.

Vertical Slice Architecture tells you how to organize code by feature, but it does not tell you where your EF Core DbContext or your migrations should live. That gap trips up more teams than any other VSA question, so this article settles it: one shared DbContext, migrations in one dedicated project, and per-feature configuration that keeps every slice decoupled from the schema.

DbContext Placement in Vertical Slice Architecture

The simplest rule that works at every scale: register a single DbContext in your composition root and let every slice consume it as a constructor dependency. The DbContext already is a unit of work, so it gives slices a shared transactional boundary without needing a Repository abstraction on top.

// Program.cs — one context, registered once
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration
        .GetConnectionString("DefaultConnection")));

Each command slice receives the same AppDbContext through dependency injection and works with the entities it needs. Because all slices share one context, a single SaveChangesAsync can span multiple aggregates when a feature legitimately updates more than one entity — a transaction you simply cannot get with a context per slice.

Migrations in a Vertical Slice Architecture App

Migrations should live in a project that is deliberately boring: an Infrastructure project with a DbContext folder and nothing else. The Web project references it so the context is registered and migrations are applied at startup, but no feature slice ever references it. You add a migration, review the SQL it generates, and apply it in your release pipeline.

// Infrastructure/Db/Migrations — one folder, one story
Add-Migration AddOrderNumberColumn -Project Infrastructure
Update-Database

// The migration touches only the Order table, generated from
// the Order configuration registered in AppDbContext.OnModelCreating

Keeping migrations out of the Web project matters for two reasons. First, it separates schema evolution from presentation so a deployment step can run migrations explicitly. Second, it keeps the feature slices free of any reference to the migration machinery — slices depend on the context interface, not on EF Core's command-line tooling.

One Context or One Context per Slice?

The per-slice DbContext is the most common VSA trap. It sounds pure, but ten contexts mean ten sets of migrations, ten separate change trackers, and no way to run a transaction across features. Keep one context unless you have a concrete reason not to:

  • One shared write context used by every command slice — the default, and correct for 95% of applications.
  • A separate read-only context for dashboards and reports — only when projection performance genuinely demands it.
  • Split contexts only when teams own different bounded contexts — and then give each one its own migration history.
  • Never put migrations in the Web project; they belong in Infrastructure.

A read-only context is the one split that pays off in practice. Because read-model slices never write, they can disable change tracking, batch queries, and even target a different connection string for reporting — all without touching the write side of the application.

A Complete EF Core Slice

Here is a full slice that shows how configuration, the context, and the handler fit together. The entity configuration stays with the context in Infrastructure, the command is a thin message, and the handler is where the data access actually happens:

// Infrastructure/Db/OrderConfiguration.cs
public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
    public void Configure(EntityTypeBuilder<Order> builder)
    {
        builder.ToTable("Orders");
        builder.HasKey(o => o.Id);
        builder.Property(o => o.OrderNumber)
               .HasMaxLength(32).IsRequired();
        builder.HasMany(o => o.Items)
               .WithOne()
               .OnDelete(DeleteBehavior.Cascade);
    }
}

// Features/Orders/CreateOrder/CreateOrderHandler.cs
public sealed class CreateOrderHandler(AppDbContext db)
    : IRequestHandler<CreateOrderCommand, Guid>
{
    public async Task<Guid> Handle(CreateOrderCommand cmd, CancellationToken ct)
    {
        var order = new Order
        {
            Id = Guid.NewGuid(),
            OrderNumber = await OrderNumber.NextAsync(db, ct),
            CustomerId = cmd.CustomerId,
            Items = cmd.Items.Select(i => new OrderItem
            {
                ProductId = i.ProductId,
                Quantity = i.Quantity,
                UnitPrice = i.UnitPrice
            }).ToList()
        };

        db.Orders.Add(order);
        await db.SaveChangesAsync(ct);
        return order.Id;
    }
}

Nothing about this slice leaks. The handler does not know how OrderNumber is generated or how Orders are stored; it only knows the DbContext. When data access rules change — a new column, a different query shape — you change the configuration or the handler, and no other slice notices.

Conventions That Keep It Maintainable

A few conventions keep a Vertical Slice Architecture plus EF Core codebase healthy over the years. Name navigation properties explicitly in both directions so the model stays predictable. Configure delete behaviors deliberately instead of accepting cascade defaults blindly. Keep queries in the handler and schema rules in OnModelCreating. And always review generated migrations before they ship — a migration review is the cheapest schema review you will ever get.

FAQ

Should every slice have its own DbContext?

No. One shared context gives you transactions across features and a single migration history. Split contexts only for read models with extreme performance requirements.

Where do I put my migration files?

In the Infrastructure project under a Db/Migrations folder. The Web project references Infrastructure so migrations and the context are applied at startup, but feature slices never reference it.

Can I still use raw SQL for a complex report in VSA?

Yes. A read-model slice can use FromSqlRaw or a dedicated read-only context freely. VSA does not restrict how you query; it restricts where that query's code lives.

Is EF Core 10 different from EF Core 9 in a VSA app?

Not structurally. EF Core 10 adds performance improvements and better AOT support for Minimal APIs, but the slice structure and DbContext placement rules are unchanged.

Key Takeaways

  • One shared write DbContext is the right default for a Vertical Slice Architecture app
  • Migrations live in a dedicated Infrastructure project, never in the Web project
  • Per-slice contexts only pay off for read models, and only when performance demands it
  • EF Core configuration stays with the context; handlers stay thin and declarative
  • Every Indotalent product ships .NET 10 with EF Core 10 organized exactly this way — $21 each

Ready to see EF Core in a real VSA codebase?

Every Indotalent product ships as a complete .NET 10 application with EF Core 10 organized exactly as described here. Complete .NET 10 source code — $21 each.

Explore Products