Skip to content

Advanced REST Concepts series: migrate the samples to CodeMazeGuides on .NET 10 - #2148

Merged
vladimir-pecanac-main merged 3 commits into
CodeMazeBlog:mainfrom
vladimir-pecanac-main:seo/batch6-advanced-series
Sep 2, 2026
Merged

Advanced REST Concepts series: migrate the samples to CodeMazeGuides on .NET 10#2148
vladimir-pecanac-main merged 3 commits into
CodeMazeBlog:mainfrom
vladimir-pecanac-main:seo/batch6-advanced-series

Conversation

@vladimir-pecanac-main

Copy link
Copy Markdown
Collaborator

Migrates the six-part advanced REST concepts series from CodeMazeBlog/advanced-rest-concepts-aspnetcore into aspnetcore-webapi/AdvancedRestConceptsSeries/, one folder per article plus the starting project, following the dotnet-testing/AspNetCoreTestingSeries/ layout. The aggregate AdvancedRestConceptsSeries.sln sits at the two-level path CI builds.

Folder Source branch Article
StartingProject paging-start the state before paging
Paging paging-end Paging
Filtering filtering-end Filtering
Searching searching-end Searching
Sorting sorting-end Sorting
DataShaping datashaping-end Data shaping
Hateoas hateoas-end HATEOAS

Why this is a rewrite and not a copy

All twelve source branches predate .NET 5, and ten of them share one commit dated 2020-10-02: netcoreapp3.0, EF Core 2.2, Pomelo.EntityFrameworkCore.MySql 2.2, System.Linq.Dynamic.Core 1.0.19, Startup.cs and WebHost.CreateDefaultBuilder.

  • net10.0 on every project, Nullable enabled, file-scoped namespaces.
  • Startup.cs folded into a minimal-hosting Program.cs and deleted.
  • Microsoft.EntityFrameworkCore.SqlServer 10.0.11 replaces Pomelo. Pomelo.EntityFrameworkCore.MySql tops out at 9.0.0 with a hard [9.0.0, 9.0.999] bound on EF Core Relational, so there is no EF Core 10 MySQL path. The MySQL dump is replaced by _SqlServer_Init_Script/init.sql, and appsettings.json points at LocalDB.
  • System.Text.Json everywhere. Microsoft.AspNetCore.Mvc.NewtonsoftJson is gone, and AddCustomMediaTypes() registers the HATEOAS media types against SystemTextJsonOutputFormatter. System.Text.Json serialises ExpandoObject correctly on .NET 10, so nothing needs Newtonsoft any more.
  • System.Linq.Dynamic.Core 1.7.4, NLog.Extensions.Logging 6.2.0.

The folders accumulate rather than revert

Five of the six source branches carry the earlier parts' defects verbatim, so a faithful copy produces a chain in which part N+1 undoes part N four times over. Every folder here carries every fix the earlier articles make.

  • One paging entry point. PagedList<T>.ToPagedListAsync(IQueryable<T>, ...) built on CountAsync()/ToListAsync(), in all six folders. The IEnumerable<T> overload that datashaping-end introduced is not carried over: DataShaping and Hateoas page the sorted IQueryable first and shape the returned page, so paging happens in the database in every folder rather than only the first four, and the X-Pagination values come from the paged query and never from the shaped list.
  • QueryStringParameters is abstract in all six folders (filtering-end silently dropped the keyword and searching-end restored it), and it clamps both ends: Math.Max(value, 1) on the page number and Math.Clamp(value, 1, 50) on the page size. ?pageNumber=0 used to become Skip(-10), which SQL Server rejects outright with a 500.
  • Response.Headers["X-Pagination"] = ... in place of .Add(...), which throws ArgumentException on a second write, plus .WithExposedHeaders("X-Pagination") in ConfigureCors so a browser can actually read the header the articles tell readers to consume.
  • The year filter is nullable int? with [Range(1900, 2100)] and a composed half-open date range, instead of uint, a DateTime.Now.Year property initializer and o.DateOfBirth.Year >= ... && <= ..., which translates to DATEPART(year, ...) on both sides and defeats any index on the column. ValidYearRange compares with >=, so a single-year filter is no longer a 400, and the controller returns ProblemDetails rather than a bare string.
  • The search rewrite reaches every folder that has search. SearchByName(ref IQueryable<Owner>, string) becomes a Search extension method. That removes the !owners.Any() guard, which fires a SELECT CASE WHEN EXISTS ... round trip purely to decide whether to add a WHERE to a query that has not run, the duplicated dead IsNullOrEmpty check, and the ToLowerInvariant() that EF Core 10 cannot translate. On sorting-end, datashaping-end and hateoas-end, any request carrying a search term returned 500.
  • SortHelper<T> drops !entities.Any(), restores the empty-query guard the generic refactor lost (OrderBy("") throws ArgumentException in Dynamic LINQ, so ?orderBy=age was an unhandled 500), and compares " desc" with StringComparison.OrdinalIgnoreCase so ?orderBy=name DESC sorts descending. Because the restored guard hands the default ordering back to the caller, the repository keeps one, so an explicitly empty orderBy still pages an ordered query instead of letting EF Core emit ORDER BY (SELECT 1).
  • DataShaper<T> holds its PropertyInfo[] private readonly and is registered as a singleton rather than scoped, so typeof(T).GetProperties() stops running on every request; property-name matching is OrdinalIgnoreCase; FetchData's accumulating loop becomes a Select.
  • Cosmetic normalisation done once across the folders: tabs versus spaces in RepositoryBase and RepositoryWrapper, using blocks, and SortHelper's sortingOrder variable, which hateoas-end renamed for no behavioural reason.

Tests

Every article folder gets an AccountOwnerServer.Tests xUnit project. The in-memory tests need nothing but the assembly under test and always run: the page clamp, the year-range validation, the search extension, the sort helper (including the unknown-field and upper-case DESC cases), the data shaper, and the HATEOAS media-type filter. On the installed SDK (10.0.302) that is 117 passing tests across the six folders, none of which touch a database.

The repository tests that need a real database start a SQL Server through Testcontainers. Those 28 tests are skipped unless CODEMAZE_SQLSERVER_TESTS=1 is set, and they are deliberately gated rather than left to run on CI: the mcr.microsoft.com/mssql/server image has never been shown to start on these runners, and a suite that silently fails to reach a database is worse than one that says it did not try. Run them locally with:

CODEMAZE_SQLSERVER_TESTS=1 dotnet test AdvancedRestConceptsSeries.sln

They have not been executed in this branch: no Docker daemon was available on the machine that prepared it. They compile, they are skipped by name in the run output, and the container path is unproven. If someone with Docker confirms them, the gate can be relaxed to run by default on CI.

Verification

dotnet build and dotnet test are green on .NET SDK 10.0.302 for each of the seven per-article solutions and for the aggregate AdvancedRestConceptsSeries.sln, with no warnings.

…on .NET 10

Brings the six-part advanced REST concepts series (paging, filtering, searching,
sorting, data shaping, HATEOAS) into aspnetcore-webapi/AdvancedRestConceptsSeries,
one folder per article plus the starting project, following the
dotnet-testing/AspNetCoreTestingSeries layout.

The source branches were last touched in 2020 and target netcoreapp3.0 with EF Core
2.2, Pomelo 2.2 and a Startup.cs, so this is a rewrite rather than a copy:

- net10.0 across all projects, nullable enabled, file-scoped namespaces.
- Minimal hosting in a single Program.cs; Startup.cs is gone.
- EF Core 10 on Microsoft.EntityFrameworkCore.SqlServer in place of Pomelo/MySQL,
  with a SQL Server init script and LocalDB defaults.
- System.Text.Json throughout; the Newtonsoft package reference is gone, and the
  HATEOAS custom media types register against SystemTextJsonOutputFormatter.
- System.Linq.Dynamic.Core 1.7.4, NLog.Extensions.Logging 6.2.0.

The folders are built to accumulate, so each one carries every fix the earlier
articles make rather than reverting them:

- One paging entry point in every folder, PagedList<T>.ToPagedListAsync over an
  IQueryable<T>, so paging happens in the database everywhere. DataShaping and
  Hateoas page first and shape the returned page; the X-Pagination metadata comes
  from the paged query.
- QueryStringParameters is abstract everywhere and clamps both page number and page
  size; Response.Headers uses the indexer; CORS exposes X-Pagination.
- The year filter is nullable int with [Range] and a composed half-open date range,
  so no index-defeating DATEPART wrapper and no clock read in a property initializer.
- SearchByName(ref ...) is replaced by a Search extension method with no
  ToLowerInvariant (which EF Core 10 cannot translate) and no !Any() round trip.
- SortHelper drops !entities.Any(), restores the empty-query guard and compares
  " desc" case-insensitively; the repository keeps a default ordering, so an
  explicitly empty orderBy still pages an ordered query.
- DataShaper holds its PropertyInfo array private readonly and is registered as a
  singleton; property matching is ordinal.

Each article folder ships an xUnit project. The in-memory tests always run; the
repository tests that need a real SQL Server start one through Testcontainers and
are skipped unless CODEMAZE_SQLSERVER_TESTS=1 is set.
@vladimir-pecanac-main
vladimir-pecanac-main merged commit b61adf2 into CodeMazeBlog:main Sep 2, 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