Basic Web API series: migrate all six parts to aspnetcore-webapi/BasicWebApiSeries - #2147
Merged
vladimir-pecanac-main merged 4 commits intoSep 1, 2026
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Migrates the six-part ASP.NET Core Web API series from
CodeMazeBlog/.NET-Core-Seriesintoaspnetcore-webapi/BasicWebApiSeries/, one folder per article in reading order, following thedotnet-testing/AspNetCoreTestingSeries/precedent (PR #2118). The aggregateBasicWebApiSeries.slnsits at the two-level path CI builds, so one CI leg covers all six.Database/Part 1/init.sql(replaced, not ported)ServiceConfiguration/Part 2/AccountOwnerServer/Logging/Part 3/AccountOwnerServer/RepositoryPatternWithEfCore/Part 4/AccountOwnerServer/UsingRepositoryForGetRequests/Part 5/AccountOwnerServer/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
.mwbmodel files. It now ships ahand-written, re-runnable T-SQL script that creates
AccountOwner,Owner,Account, theforeign key (
ON UPDATE CASCADE,ON DELETE NO ACTION) and the sample data, with the same fixedGUIDs the later parts call endpoints with.
Schema.mwbandSchema.mwb.bakgo with the engine.Pomelo.EntityFrameworkCore.MySql9.0.0-preview is out;Microsoft.EntityFrameworkCore.SqlServer10.0.11 is in. Nothing calls
EnsureCreated()anywhere in the series: the script owns theschema, with the explicit
NVARCHARlengths andDATEcolumns EF Core's default model would notproduce.
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.
ConfigureIISIntegrationremoved from all five .NET folders.IISOptionsis theout-of-process hosting options type (
Microsoft.AspNetCore.Server.IISIntegration); in-processhas been the default since ASP.NET Core 3.0, and the lambda set no property.
UseForwardedHeadersfirst,UseStaticFilesdropped (nowwwrootin a controllers-only API), the explicitUseDeveloperExceptionPagedropped(
WebApplicationregisters it in Development),UseHstskept.builder.Logging.ClearProviders()plus
builder.Host.UseNLog()replacesLogManager.Setup().LoadConfigurationFromFile(...). Withoutit,
ILogger<T>messages stop reaching the file from part 4 onward whileILoggerManagerkeepsworking, 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 theILoggerManagererror both present.
nlog.configrelative paths in all four folders that carry it. The committed file pointed atd:Projects\Blog-AccountOwner\..., and NLog discards the message silently when the path isunwritable, so a reader on any machine without a
d:drive got no file and no error.NLog.Extensions.Logging5.3.15 is replaced byNLog.Web.AspNetCore6.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.three folders (
Owner.csthree times,OwnerForCreationDto,OwnerForUpdateDto).IExceptionHandlerregistered in part 6's folder as well as part 5's. Verified by runningpart 6 and issuing a failing write:
500with the handler's own JSON body, not an empty 500.Repository and controllers
IRepositoryWrapper.SaveAsync();ToListAsync()andFirstOrDefaultAsync()in the repository;async actions.
FindAll()andFindByCondition()stay synchronous and keep returningIQueryable<T>(they compose, they do not execute), andCreate/Update/Deletestaysynchronous because they only stage a change on the tracker.
RepositoryContexttakesDbContextOptions<RepositoryContext>; theDbSetproperties come fromSet<T>()instead of being declared nullable.RepositoryWrapperuses a primary constructor and??=.Owner?return types where the query can miss (the old signatures produced CS8603).IExceptionHandler.[ApiController]rejects an empty orinvalid body with a
400and aValidationProblemDetailsbefore the action runs. Confirmed onthis branch:
POST /api/ownerwith{}returns400and{"errors":{"Name":["Name is required"],"Address":["Address is required"]}}.OwnerForUpdateDtoproperties arestring?, matching the creation DTO and the entity (CS8618).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):
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-severityuncontrolled-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.Testsruns the repository against a real SQLServer started by
Testcontainers.MsSql4.14.0 and seeded with the sameDatabase/init.sqlthearticle ships: seeded reads,
Includeon the details query, create-then-SaveAsync, and theforeign key rejecting a delete with
SqlException547.The tests are gated behind a Docker probe (
DockerFactAttribute). Where Docker is unavailable theyreport 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 testreports
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:
Four real container-backed tests, zero skips, and the whole build-and-test step finished in about
half a minute.
mcr.microsoft.com/mssql/serverstarting on the runners is no longer an assumption.Local results, SDK 10.0.302
ServiceConfigurationLoggingRepositoryPatternWithEfCoreUsingRepositoryForGetRequestsUsingRepositoryForWriteRequestsBasicWebApiSeries.sln(aggregate)No C# compiler warnings anywhere. CI resolves this PR to the single folder
aspnetcore-webapi/BasicWebApiSeriesand builds the aggregate solution: one leg, all six parts.Two notes for the reviewer
ConfigureCorsstill returnsvoid. The corpus splits 34IServiceCollectionto 22voidon
this IServiceCollectionextension methods, which is a majority but not a convention, andchanging 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, andDatabase/is three levels deep insideBasicWebApiSeries, soan entry for it would never match anything. CI resolves this whole PR to the single folder
aspnetcore-webapi/BasicWebApiSeriesand builds the aggregate solution, which does not includeDatabase/because it holds no project.