Modernizing an ASP.NET Core API to .NET 10: What Changed in the Zero to Production v2 Refresh

Modernizing an ASP.NET Core API to .NET 10 is rarely about the target framework moniker. Our production sample API had said net10.0 for months, yet the code around it still carried habits from from releases before: Swashbuckle for docs, the deprecated FluentValidation auto-validation package, Moq in the test project, and EnsureCreated standing in for real migrations. When we rebuilt every chapter of the ASP.NET Core Web API: Zero to Production course for its v2 release, we did not bump a version number - we retired the stale pieces one by one. This post is the changelog for that refresh, and every item is a change you can apply to your own API today.
If you want the complete, rebuilt-and-verified source for all 15 chapters - each one running clean on .NET 10 with the new tooling wired together - the ASP.NET Core Web API: Zero to Production course just shipped this exact v2 refresh, and existing readers get it free. And because a lot of the "why" behind these decisions is easier to show than tell, I keep the annotated deep-dive versions - the edge cases, the failures, the trade-offs that did not fit here - on Patreon, where the working code lives alongside the reasoning.
Why Modernize a .NET 10 API That Already Compiles?
Because "it compiles" and "it reflects current .NET practice" are different bars. Between .NET 8 and .NET 10, three of the defaults every Web API tutorial leaned on quietly changed: Swagger UI left the box, FluentValidation's ASP.NET integration package was deprecated by its own maintainers, and the community's default mocking library lost trust. An API that ignores those shifts is not broken, but it teaches new hires patterns they will have to unlearn. The v2 refresh was a chance to align the whole codebase with what a team starting fresh in 2026 would actually pick.
What Actually Changed: The v2 Refresh at a Glance
| Area | v1 (before) | v2 (.NET 10 refresh) |
|---|---|---|
| API docs | Swashbuckle.AspNetCore 7.3.1 |
Microsoft.AspNetCore.OpenApi 10 + Scalar.AspNetCore |
| Validation | FluentValidation.AspNetCore 11 (auto-validation) |
FluentValidation 12 + explicit filter |
| Mocking | Moq 4.20 | NSubstitute 5.3 |
| API versioning | Asp.Versioning.Mvc 8.1.1 |
Asp.Versioning.Mvc 10 + ApiExplorer |
| DB schema at startup | EnsureCreatedAsync() |
MigrateAsync() |
| Log export | Console + File sinks | added Serilog.Sinks.OpenTelemetry |
| Docker user | custom adduser |
built-in non-root app user |
| Servicing | 10.0.5 / .6 / .7 | patched to 10.0.9 across the board |
The rest of this post walks the changes that matter most and shows the before-and-after shape of each.
Swagger and Swashbuckle Are Out: Built-In OpenAPI Plus Scalar
This is the headline change. Since .NET 9, the ASP.NET Core Web API template no longer ships Swashbuckle. The framework generates the OpenAPI document itself through Microsoft.AspNetCore.OpenApi, and you bring your own UI. We picked Scalar. The wiring drops from a SwaggerGen configuration block to two calls:
// v1
builder.Services.AddSwaggerGen();
app.UseSwagger();
app.UseSwaggerUI();
// v2 - built-in generator, OpenAPI 3.1 document at /openapi/v1.json
builder.Services.AddOpenApi();
app.MapOpenApi();
app.MapScalarApiReference(); // interactive UI at /scalar
Requires .NET 9 or later for AddOpenApi. The practical wins: one fewer third-party dependency to track for security advisories, an OpenAPI 3.1 document by default, and a UI that is genuinely pleasant to hand to a frontend team. If you are weighing the UI options rather than defaulting to one, I broke them down in Scalar vs Swashbuckle vs NSwag in ASP.NET Core. The official rundown of the generator lives in the Microsoft Learn OpenAPI docs.
FluentValidation 12: The Auto-Validation Package Is Gone
FluentValidation.AspNetCore - the package that auto-ran validators before your action executed - has been deprecated by the FluentValidation team for a while, and v2 finally stops using it. In v1 the setup was a single magic line; in v2 validation is explicit. You register the validators and invoke them through a small filter (or middleware) you control:
// v1 - deprecated auto-validation
builder.Services.AddFluentValidationAutoValidation();
builder.Services.AddValidatorsFromAssemblyContaining<CreateProductRequestValidator>();
// v2 - register validators, validate explicitly via your own filter
builder.Services.AddValidatorsFromAssemblyContaining<CreateProductRequestValidator>();
Note the version jump to FluentValidation 12 and the FluentValidation.DependencyInjectionExtensions package for the DI helpers. The explicit approach is more code, but it removes hidden model-binding behavior and makes the validation step something you can read, test, and place deliberately in the pipeline. For the common traps here, see 7 Common FluentValidation Mistakes in ASP.NET Core.
Should You Still Use Moq in 2026?
Short answer: for new projects, I no longer reach for it first, and v2 moves the test suite to NSubstitute. The switch is not about features - it is about trust after the SponsorLink episode shook confidence in Moq for a lot of teams. NSubstitute's API is also just cleaner to read, with no .Object unwrapping:
// v1 - Moq
var repo = new Mock<IProductRepository>();
repo.Setup(r => r.GetByIdAsync(1)).ReturnsAsync(product);
var sut = new Handler(repo.Object);
// v2 - NSubstitute
var repo = Substitute.For<IProductRepository>();
repo.GetByIdAsync(1).Returns(product);
var sut = new Handler(repo);
The full comparison, including where FakeItEasy fits, is in Moq vs NSubstitute vs FakeItEasy in .NET. If your suite is large, migrate one test class at a time rather than in a single sweep.
EF Core: EnsureCreated Out, MigrateAsync In
v1 used EnsureCreatedAsync() at startup for demo simplicity, with a comment admitting production needs migrations. v2 makes the code match the advice it gives:
// v1
await db.Database.EnsureCreatedAsync();
// v2 - applies pending migration files; evolves an existing schema
await db.Database.MigrateAsync();
This matters more than it looks. EnsureCreated bypasses the migrations system entirely and cannot evolve a schema that already exists - the two do not mix. Shipping MigrateAsync from chapter one means the sample never teaches a pattern you have to tear out later. Applying migrations automatically at startup has its own production caveats around concurrency and rollback, which I cover in the EF Core Migration Checklist for Production .NET Teams and in the official EF Core applying-migrations guide.
Versioning, Observability, and the Servicing Patches
Three quieter but worthwhile changes rounded out the refresh:
API Versioning 10.
Asp.Versioning.Mvcmoved from 8.1.1 to 10, and we addedAsp.Versioning.Mvc.ApiExplorerso versioned endpoints surface correctly in the new OpenAPI document.Serilog through OpenTelemetry. The observability chapter added
Serilog.Sinks.OpenTelemetry, so structured logs now export over OTLP alongside traces and metrics instead of living only in console and file sinks. That unifies the three signals into one pipeline.Security servicing. Every runtime package moved to the latest 10.0.9 servicing release - EF Core, JWT bearer, the MVC testing host, Redis caching, and the EF health checks - closing known advisories in one pass.
Deployment: The Non-Root Docker User That Broke on .NET 10
A small but real gotcha: v1's Dockerfile created a non-root user with adduser. On the .NET 10 Ubuntu-based runtime image that command is not present, so the build fails. The fix is to use the non-root app user the runtime images already ship (exposed via $APP_UID):
# v2 - reuse the built-in non-root user instead of creating one
RUN chown -R app:app /app
USER app
Cleaner, one fewer layer, and it does not break on the current base image. If deployment strategy is on your mind, Blue-Green vs Canary vs Rolling Deployment in .NET pairs well with this.
What to Adopt Now vs Later
If you are refreshing your own API, here is the order I would tackle it in:
Now, low risk: apply the 10.0.9 servicing patches and move off Swashbuckle to the built-in OpenAPI generator plus a UI. These are mostly mechanical and close real security and tooling gaps.
Now, small effort: fix the Docker non-root user if you build on the .NET 10 base image, and switch
EnsureCreatedtoMigrateAsyncbefore you have real data to lose.Plan it in: the FluentValidation 12 move and the Moq-to-NSubstitute migration touch more files. Do them per-module, with the tests green at each step.
None of these require a framework upgrade - they are all about matching .NET 10 as it actually ships in 2026.
Frequently Asked Questions
Do I need to upgrade the target framework to modernize an ASP.NET Core API?
No. Every change in this refresh happened on net10.0 in both the before and after code. Modernizing an ASP.NET Core API to .NET 10 is mostly about retiring deprecated packages and patterns - Swashbuckle, FluentValidation auto-validation, EnsureCreated, Moq - not about the TargetFramework value. Bump the moniker separately if you are still on .NET 8 or 9.
Is Swashbuckle deprecated in .NET 10?
Swashbuckle is not removed from NuGet, but ASP.NET Core stopped including it in the Web API template as of .NET 9. The framework now generates the OpenAPI document through Microsoft.AspNetCore.OpenApi, and you add a UI such as Scalar or the Swagger UI package separately. For new APIs, the built-in generator is the default path.
Why move from FluentValidation.AspNetCore to FluentValidation 12?
The FluentValidation.AspNetCore auto-validation package was deprecated by the FluentValidation maintainers, who recommend validating explicitly instead of hooking into MVC model binding. v2 registers validators with AddValidatorsFromAssemblyContaining and runs them through an explicit filter, which is clearer and easier to test than the hidden auto-validation behavior.
Why did the course switch from Moq to NSubstitute?
The move was driven by trust rather than features, following the SponsorLink controversy that pushed many .NET teams off Moq. NSubstitute offers an equivalent capability set with a cleaner syntax and no .Object indirection. Existing Moq test suites still work, so migrate incrementally if you choose to switch.
What is the difference between EnsureCreated and MigrateAsync in EF Core?
EnsureCreated() builds the database directly from the model and skips the migrations system, so it cannot update a schema that already exists. MigrateAsync() applies your migration files in order and can evolve a live schema over time. Use migrations for anything beyond a throwaway demo, and do not mix the two on the same database.
About the Author
I'm Celin Daniel, Co-founder of Coding Droplets. I've been building .NET and ASP.NET Core systems in production for 13+ years - APIs, distributed backends, enterprise platforms. Everything I write here comes from real shipping experience: patterns that held up, trade-offs that bit us, and lessons learned the hard way.
GitHub: codingdroplets
YouTube: Coding Droplets
Website: codingdroplets.com






