A compact, recordable .NET 10 sample that shows what a practical legacy migration looks like — without pretending the entire legacy system disappears overnight.
Built as a companion to the video ".NET 10 for real projects: what I would actually adopt in a legacy migration".
Most migration demos start from a clean greenfield. This one starts from something boring: a small order-management module with hidden config, a blocking import, inconsistent errors, and static database helpers.
The sample walks through four concrete migration seams — the same four covered in the video — and shows each one as a before/after pair you can read side by side in the code.
| # | Before (samples/legacy-snapshot) |
After (src/) |
|---|---|---|
| 1 | IConfiguration injected directly, magic string keys |
Typed DatabaseOptions / ImportOptions, validated at startup |
| 2 | Ad-hoc JSON errors, raw exception messages leaked | AddProblemDetails + AddValidation + /openapi/v1.yaml |
| 3 | Blocking import runs on the request thread | Channel<ImportJob> + BackgroundService, HTTP returns 202 immediately |
| 4 | Raw SQL via static StaticDbHelper, DataTable returned as JSON |
EF Core write path + explicit raw SQL read path, side by side |
| Requirement | Notes |
|---|---|
| .NET 10 SDK | 10.0.102 or later |
| SQL Server (local instance) | Windows Authentication — no password needed |
dotnet ef tools |
dotnet tool install --global dotnet-ef |
The app connects to Server=. with Windows Authentication. If your instance name differs, update appsettings.json before running.
# 1. Clone
git clone <repo-url>
cd NET10LegacyMigration/Northwind.MigrationLab
# 2. Create the database
dotnet ef database update \
--project src/Northwind.Infrastructure \
--startup-project src/Northwind.Api
# 3. Run
dotnet run --project src/Northwind.ApiThe API starts on https://localhost:5001 (or whatever port is shown in the terminal).
| Method | Route | What it does |
|---|---|---|
GET |
/api/orders |
Paginated order list (EF Core path) |
GET |
/api/orders/{id} |
Single order by GUID |
POST |
/api/orders |
Create order — validated, returns 201 + Location |
POST |
/api/import/orders |
Enqueue import job — returns 202 Accepted immediately |
GET |
/healthz |
SQL Server health check as JSON |
GET |
/openapi/v1.yaml |
Generated OpenAPI 3.x document |
Demo 2 — validation + ProblemDetails
# Bad request → structured 400
curl -X POST https://localhost:5001/api/orders \
-H "Content-Type: application/json" \
-d '{"customerId": "", "items": []}'
# Good request → 201 + Location header
curl -X POST https://localhost:5001/api/orders \
-H "Content-Type: application/json" \
-d '{"customerId": "CUST-1", "items": []}'Demo 3 — async import
# Returns 202 immediately; watch the terminal for worker logs
curl -X POST https://localhost:5001/api/import/orderssrc/
Northwind.Api/ Minimal API — routes, Program.cs, middleware
Northwind.Application/ Use cases, interfaces, DTOs (no EF, no HTTP)
Northwind.Domain/ Entities and enums (pure .NET BCL only)
Northwind.Infrastructure/ EF Core, workers, raw SQL queries, typed options
tests/
Northwind.UnitTests/ Domain + Application + ImportChannel (no DB, no HTTP)
Northwind.IntegrationTests/ WebApplicationFactory against a real SQL Server test DB
samples/
legacy-snapshot/ The "before" code — MVC controller, static helper,
synchronous import. Reference only, not in solution.
# All tests (requires local SQL Server for integration tests)
dotnet test Northwind.MigrationLab.slnx
# Unit tests only (no SQL Server required — same as CI)
dotnet test Northwind.MigrationLab.slnx --filter "Category!=Integration"Integration tests spin up a fresh SQL Server database per test class (named NorthwindMigrationLab_Test_<guid>) and drop it when done.
# Build
dotnet build Northwind.MigrationLab.slnx
# Run API
dotnet run --project src/Northwind.Api
# Apply migrations
dotnet ef database update \
--project src/Northwind.Infrastructure \
--startup-project src/Northwind.Api
# Add a new migration
dotnet ef migrations add <MigrationName> \
--project src/Northwind.Infrastructure \
--startup-project src/Northwind.Api \
--output-dir Migrations
# Format check
dotnet format --verify-no-changesGitHub Actions runs on every push and pull request to main.
- Builds the solution in Release configuration
- Runs unit tests only (no SQL Server in CI)
- Uploads TRX test results as a build artifact
Integration tests are tagged [Trait("Category", "Integration")] and excluded from the CI filter.
These topics are deferred — each one is a good follow-up video on its own:
- Authentication and authorization
- EF Core query tuning (indexes, projections, pagination)
- OpenTelemetry and structured logging
- CI/CD deployment pipeline
- WCF or legacy SOAP service behind the adapter seam
- AI-assisted reporting on top of the clean API surface
| Layer | Choice |
|---|---|
| Runtime | .NET 10 |
| API | ASP.NET Core Minimal API |
| Validation | Microsoft.Extensions.Validation (.NET 10 built-in) |
| API docs | Microsoft.AspNetCore.OpenApi |
| ORM | EF Core 10 with SQL Server |
| Raw reads | DbConnection + DbCommand (no extra package) |
| Import queue | System.Threading.Channels (no broker) |
| Health checks | AspNetCore.HealthChecks.SqlServer |
| Tests | xUnit + Microsoft.AspNetCore.Mvc.Testing |