← All cases

02 · Case study

.NET monolith to microservices

ASP.NET Core monolith.NET 10 · Aspire · YARP · EF Core · Wolverine

The system

Read from code
everything
Questions asked
32
Target
.NET 10 / Aspire
Model writes
the seams

The problem

One solution, one deployment, one database. Every feature touches several modules, checkout cannot be scaled without scaling the catalog, and the build is slow enough that people batch their changes. The team knows it wants services. What it does not have is a defensible answer to where the boundaries go — and the two usual answers are an eighteen-month rewrite, or a model that invents boundaries and hands back code that does not compile.

What ran

  1. Read the monolith

    Projects and namespaces, EF Core entities and their relationships, DbContext boundaries, ASP.NET routes, the call graph, and every cross-context write. None of that is a question you answer — it is read from the code.

  2. Check the machine first

    Whether this machine can build and test the code at all, before any model call, so a machine that cannot build stops early and costs nothing.

  3. Map the services

    Deterministically, from data ownership and the call graph. Two runs over the same code produce the same map.

  4. Approve

    Thirty-two questions, all with a recommended default, each one changing real files. Then the plan. Stop here and nothing has been added to the repository except working notes.

  5. Move the code

    Your existing code is moved into the new service projects rather than regenerated, and the places where the move breaks are marked. The model writes those seams — the typed clients, and the sagas for writes that used to be one transaction.

What it did

What lands

services/<ctx>/ one project per service with its Dockerfile and manifests · gateway/ YARP with edge auth and rate limiting · AppHost/ Aspire, so dotnet run starts the whole fleet locally · ServiceDefaults/ telemetry, health, resilience, discovery

Tests, generated not suggested

Integration tests per aggregate against a real database in a Testcontainer · health smoke test · architecture fitness test that fails the build if a domain type reaches for EF Core · contract boundary tests · equivalence replay of old path against new · schema guard · saga tests with compensation

Documents

ARCHITECTURE.md · STATE-FLOW.md · SEQUENCE-DIAGRAMS.md · RUNBOOK.md · docs/adr/ · MIGRATION-SCORECARD.md · one brief per service. Written after you approve the plan, not before.

Where it ended

A compiling solution, one commit per verified task, and a strangler route on the gateway so anything no service has taken yet still goes to the monolith. A test project that compiles but discovers zero tests is reported as a failure rather than a pass — dotnet test exits 0 when it finds nothing, and that used to read as green.

What happens next

  • Cut over service by service on the strangler route, rather than on a flag day.
  • Run the equivalence replay on each extraction — it is how you tell an extraction from a folder move.
  • Deploy from what was generated: GitHub Actions, Azure DevOps or GitLab CI, onto Kubernetes, Container Apps or ECS.

The heavy structural work — where the boundaries are, what owns which data, what order to extract in — is deterministic and derived from the code. The model is pointed at the small genuinely hard part: reconnecting the seams and writing the sagas. That is why the result compiles.