Skip to content

Basic Web API series: migrate all six parts to aspnetcore-webapi/BasicWebApiSeries - #2147

Merged
vladimir-pecanac-main merged 4 commits into
CodeMazeBlog:mainfrom
vladimir-pecanac-main:seo/batch6-basic-series
Sep 1, 2026
Merged

Basic Web API series: migrate all six parts to aspnetcore-webapi/BasicWebApiSeries#2147
vladimir-pecanac-main merged 4 commits into
CodeMazeBlog:mainfrom
vladimir-pecanac-main:seo/batch6-basic-series

Conversation

@vladimir-pecanac-main

@vladimir-pecanac-main vladimir-pecanac-main commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

Migrates the six-part ASP.NET Core Web API series from CodeMazeBlog/.NET-Core-Series into
aspnetcore-webapi/BasicWebApiSeries/, one folder per article in reading order, following the
dotnet-testing/AspNetCoreTestingSeries/ precedent (PR #2118). The aggregate
BasicWebApiSeries.sln sits at the two-level path CI builds, so one CI leg covers all six.

# Folder Source
1 Database/ Part 1/init.sql (replaced, not ported)
2 ServiceConfiguration/ Part 2/AccountOwnerServer/
3 Logging/ Part 3/AccountOwnerServer/
4 RepositoryPatternWithEfCore/ Part 4/AccountOwnerServer/
5 UsingRepositoryForGetRequests/ Part 5/AccountOwnerServer/
6 UsingRepositoryForWriteRequests/ Part 6/AccountOwnerServer/

Folder N+1 is folder N plus that article's work. Each folder also keeps its own solution so a
reader can work through one part alone.

Database moves to SQL Server

Part 1 shipped MySQL Workbench forward-engineered DDL and two .mwb model files. It now ships a
hand-written, re-runnable T-SQL script that creates AccountOwner, Owner, Account, the
foreign key (ON UPDATE CASCADE, ON DELETE NO ACTION) and the sample data, with the same fixed
GUIDs the later parts call endpoints with. Schema.mwb and Schema.mwb.bak go with the engine.

Pomelo.EntityFrameworkCore.MySql 9.0.0-preview is out; Microsoft.EntityFrameworkCore.SqlServer
10.0.11 is in. Nothing calls EnsureCreated() anywhere in the series: the script owns the
schema, with the explicit NVARCHAR lengths and DATE columns EF Core's default model would not
produce.

Every project targets net10.0.

Fixes applied to every folder that carries the code, not just the one article that documents it

Five of these were scoped to one or three folders in the drafts. They are applied across the chain
here, because a reader working part N+1 on top of part N finds the difference by diffing.

  • ConfigureIISIntegration removed from all five .NET folders. IISOptions is the
    out-of-process hosting options type (Microsoft.AspNetCore.Server.IISIntegration); in-process
    has been the default since ASP.NET Core 3.0, and the lambda set no property.
  • Pipeline reordered in all five: UseForwardedHeaders first, UseStaticFiles dropped (no
    wwwroot in a controllers-only API), the explicit UseDeveloperExceptionPage dropped
    (WebApplication registers it in Development), UseHsts kept.
  • NLog host wiring carried into parts 4, 5 and 6, not only part 3. builder.Logging.ClearProviders()
    plus builder.Host.UseNLog() replaces LogManager.Setup().LoadConfigurationFromFile(...). Without
    it, ILogger<T> messages stop reaching the file from part 4 onward while ILoggerManager keeps
    working, so the sample runs and the lesson silently disappears. Verified by running part 6 and
    reading the produced file: 201 lines, framework ILogger<T> entries and the ILoggerManager
    error both present.
  • nlog.config relative paths in all four folders that carry it. The committed file pointed at
    d:Projects\Blog-AccountOwner\..., and NLog discards the message silently when the path is
    unwritable, so a reader on any machine without a d: drive got no file and no error.
    NLog.Extensions.Logging 5.3.15 is replaced by NLog.Web.AspNetCore 6.2.0.
  • [Table(...)] casing normalised across parts 4 to 6 to the identifiers the script creates
    ([Owner], [Account]). SQL Server does not care; this is a consistency fix.
  • The "Address cannot be loner then 100 characters" typo fixed in all five occurrences across
    three folders (Owner.cs three times, OwnerForCreationDto, OwnerForUpdateDto).
  • IExceptionHandler registered in part 6's folder as well as part 5's. Verified by running
    part 6 and issuing a failing write: 500 with the handler's own JSON body, not an empty 500.

Repository and controllers

  • IRepositoryWrapper.SaveAsync(); ToListAsync() and FirstOrDefaultAsync() in the repository;
    async actions. FindAll() and FindByCondition() stay synchronous and keep returning
    IQueryable<T> (they compose, they do not execute), and Create / Update / Delete stay
    synchronous because they only stage a change on the tracker.
  • RepositoryContext takes DbContextOptions<RepositoryContext>; the DbSet properties come from
    Set<T>() instead of being declared nullable.
  • RepositoryWrapper uses a primary constructor and ??=.
  • Owner? return types where the query can miss (the old signatures produced CS8603).
  • try/catch removed from every action, replaced by one IExceptionHandler.
  • The two model-state guards removed from the write actions: [ApiController] rejects an empty or
    invalid body with a 400 and a ValidationProblemDetails before the action runs. Confirmed on
    this branch: POST /api/owner with {} returns 400 and
    {"errors":{"Name":["Name is required"],"Address":["Address is required"]}}.
  • OwnerForUpdateDto properties are string?, matching the creation DTO and the entity (CS8618).
  • The CreatedAtRoute("OwnerById", ...) route name is untouched by the async rename; only the C#
    method names on the repository gained Async.

AutoMapper: pinned 14.0.0, and the preservation claim re-measured on it

Pinned to 14.0.0 in both folders that reference it. services.AddAutoMapper(typeof(Program))
compiles on 14.0.0 and is a hard CS1503 error on 16.x, so the pin keeps the registration line as
well as the licence.

Part 6's central claim was measured on 16.2.0. Re-run on 14.0.0 (net10.0, Map(dto, entity)
onto a loaded entity):

AutoMapper version : 14.0.0.0
Id preserved       : True   (24fd81f8-d58a-4bcc-9f35-dc6cd5641906)
Accounts preserved : True   (count=1)
Name mapped        : John Keen Updated
Address mapped     : 62 Wellfield Road

The behaviour is identical: properties the DTO does not carry keep their values. Part 6's update
section is correct as written.

One thing the pin brings with it, flagged rather than decided here. AutoMapper 14.0.0 raises
NU1903: GHSA-rvv3-g6hj-g44x, a high-severity
uncontrolled-recursion denial of service affecting < 15.1.1. 14.0.0 is the last MIT release
(15.1.1 and later ship a file licence), so there is no patched version that keeps the MIT terms. It
is a build warning, not an error, and CI is unaffected. Raised here because the version was chosen
on licence grounds before this advisory was part of the picture.

Tests

UsingRepositoryForWriteRequests/AccountOwnerServer.Tests runs the repository against a real SQL
Server started by Testcontainers.MsSql 4.14.0 and seeded with the same Database/init.sql the
article ships: seeded reads, Include on the details query, create-then-SaveAsync, and the
foreign key rejecting a delete with SqlException 547.

The tests are gated behind a Docker probe (DockerFactAttribute). Where Docker is unavailable they
report as skipped with a reason and never as passed, so a machine that cannot run them says so
instead of going green on nothing. On the authoring machine, which has no Docker, dotnet test
reports Failed: 0, Passed: 0, Skipped: 4.

The open question is now answered: the image does start on a CI runner. This PR's own run
(33316207526) reports:

Passed!  - Failed: 0, Passed: 4, Skipped: 0, Total: 4, Duration: 1 s - AccountOwnerServer.Tests.dll (net10.0)

Four real container-backed tests, zero skips, and the whole build-and-test step finished in about
half a minute. mcr.microsoft.com/mssql/server starting on the runners is no longer an assumption.

Local results, SDK 10.0.302

Folder Build Test
ServiceConfiguration succeeded, 0 warnings no test project
Logging succeeded, 0 warnings no test project
RepositoryPatternWithEfCore succeeded, 0 warnings no test project
UsingRepositoryForGetRequests succeeded, NU1903 only no test project
UsingRepositoryForWriteRequests succeeded, NU1903 only 4 skipped (no Docker locally)
BasicWebApiSeries.sln (aggregate) succeeded, 0 errors, 4 warnings, all NU1903 4 skipped locally, 4 passed on CI

No C# compiler warnings anywhere. CI resolves this PR to the single folder
aspnetcore-webapi/BasicWebApiSeries and builds the aggregate solution: one leg, all six parts.

Two notes for the reviewer

  • ConfigureCors still returns void. The corpus splits 34 IServiceCollection to 22 void
    on this IServiceCollection extension methods, which is a majority but not a convention, and
    changing the signature would put the repo out of step with what the articles print. Left alone
    deliberately.
  • Database/ is not added to .github/ci-skip-folders.txt. That file matches two-level
    <category>/<article> paths, and Database/ is three levels deep inside BasicWebApiSeries, so
    an entry for it would never match anything. CI resolves this whole PR to the single folder
    aspnetcore-webapi/BasicWebApiSeries and builds the aggregate solution, which does not include
    Database/ because it holds no project.

…cWebApiSeries

Brings the six-part ASP.NET Core Web API series over from CodeMazeBlog/.NET-Core-Series
into one folder per article, in reading order, with an aggregate solution at the folder
root. Folder N+1 is folder N plus that article's work.

Database/                        (part 1, was Part 1/init.sql)
ServiceConfiguration/            (part 2, was Part 2/AccountOwnerServer)
Logging/                         (part 3)
RepositoryPatternWithEfCore/     (part 4)
UsingRepositoryForGetRequests/   (part 5)
UsingRepositoryForWriteRequests/ (part 6)

Database and platform

- Part 1's MySQL Workbench script is replaced by a hand-written, re-runnable T-SQL script
  that creates AccountOwner, both tables, the foreign key and the sample data with the
  same fixed ids the later parts call endpoints with. Schema.mwb and Schema.mwb.bak are
  gone with the engine.
- Pomelo.EntityFrameworkCore.MySql out, Microsoft.EntityFrameworkCore.SqlServer 10.0.11 in.
  Nothing calls EnsureCreated(): the script owns the schema, including the explicit column
  lengths and DATE types EF Core's default model would not produce.
- Every project targets net10.0.

Applied across all five .NET folders, not just the one article that documents each

- ConfigureIISIntegration and its call site removed everywhere. IISOptions is the
  out-of-process hosting options type and the lambda set nothing.
- Pipeline reordered so UseForwardedHeaders runs first; UseStaticFiles dropped (no wwwroot)
  and the explicit UseDeveloperExceptionPage dropped (WebApplication registers it in
  Development), keeping UseHsts.
- launchSettings.json regenerated to the net10 template's two Project profiles.
- NLog host wiring (ClearProviders + UseNLog) carried into parts 4, 5 and 6 as well as 3,
  so ILogger<T> keeps reaching the file. Verified by running part 6 and reading the log.
- nlog.config relative paths in all four folders that carry the file. The committed file
  pointed at one developer's d: drive and NLog discards silently when the path is unwritable.
- NLog.Extensions.Logging 5.3.15 replaced by NLog.Web.AspNetCore 6.2.0.
- [Table] casing normalised to the script's identifiers across parts 4 to 6.
- "Address cannot be loner then 100 characters" fixed in all five places.

Repository and controllers

- IRepositoryWrapper.SaveAsync(), async repository reads with ToListAsync and
  FirstOrDefaultAsync, async actions. FindAll and FindByCondition stay synchronous and keep
  returning IQueryable<T>; Create, Update and Delete only stage a change.
- RepositoryContext takes DbContextOptions<RepositoryContext>; DbSet properties come from
  Set<T>() instead of being declared nullable.
- RepositoryWrapper uses a primary constructor and ??=.
- Owner? return types where the query can miss.
- try/catch removed from every action; one IExceptionHandler registered in parts 5 and 6.
- The unreachable model-state guards removed from the write actions: [ApiController]
  short-circuits an empty or invalid body with a 400 before the action runs.
- OwnerForUpdateDto properties made string?, matching the creation DTO and the entity.
- AutoMapper pinned to 14.0.0 in both folders that use it.

Tests

- UsingRepositoryForWriteRequests/AccountOwnerServer.Tests runs the repository against a
  real SQL Server started by Testcontainers and seeded with the same init.sql. Where Docker
  is unavailable the tests report as skipped with a reason rather than passing silently.
@vladimir-pecanac-main
vladimir-pecanac-main merged commit 1ff2760 into CodeMazeBlog:main Sep 1, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant