Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: 6.0.x
dotnet-version: 9.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
Expand Down
8 changes: 8 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Project>
<PropertyGroup>
<!-- Enforce the latest .NET static-analyser rules as build gates. -->
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisLevel>latest</AnalysisLevel>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
</Project>
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
WORKDIR /app
EXPOSE 5000

FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
RUN pwd && ls /
WORKDIR /src
COPY ["src/WebApi/WebApi.csproj", "src/WebApi/"]
Expand Down
11 changes: 11 additions & 0 deletions parity-report/diff/flow_diff.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
flows match: False; swagger: mismatch
flow 6: mismatch: ["body.type: 'https://tools.ietf.org/html/rfc7231#section-6.5.1' != 'https://tools.ietf.org/html/rfc9110#section-15.5.1'"]
flow 7: mismatch: ['body.errors.$[0]: "JSON deserialization for type \'Application.Dtos.EducationDto\' was missing required properties, including the following: degree" != "JSON deserialization for type \'Application.Dtos.EducationDto\' was missing required properties including: \'degree\'."', "body.type: 'https://tools.ietf.org/html/rfc7231#section-6.5.1' != 'https://tools.ietf.org/html/rfc9110#section-15.5.1'"]
flow 8: mismatch: ['body.errors.$[0]: "JSON deserialization for type \'Application.Dtos.EducationDto\' was missing required properties, including the following: fieldOfStudy" != "JSON deserialization for type \'Application.Dtos.EducationDto\' was missing required properties including: \'fieldOfStudy\'."', "body.type: 'https://tools.ietf.org/html/rfc7231#section-6.5.1' != 'https://tools.ietf.org/html/rfc9110#section-15.5.1'"]
flow 9: mismatch: ['body.errors.$[0]: "JSON deserialization for type \'Application.Dtos.EducationDto\' was missing required properties, including the following: school" != "JSON deserialization for type \'Application.Dtos.EducationDto\' was missing required properties including: \'school\'."', "body.type: 'https://tools.ietf.org/html/rfc7231#section-6.5.1' != 'https://tools.ietf.org/html/rfc9110#section-15.5.1'"]
flow 10: mismatch: ["body.type: 'https://tools.ietf.org/html/rfc7231#section-6.6.1' != 'https://tools.ietf.org/html/rfc9110#section-15.6.1'"]
flow 11: mismatch: ["body.type: 'https://tools.ietf.org/html/rfc7231#section-6.6.1' != 'https://tools.ietf.org/html/rfc9110#section-15.6.1'"]
flow 12: mismatch: ["body.type: 'https://tools.ietf.org/html/rfc7231#section-6.6.1' != 'https://tools.ietf.org/html/rfc9110#section-15.6.1'"]
flow 16: mismatch: ["body.type: 'https://tools.ietf.org/html/rfc7231#section-6.5.1' != 'https://tools.ietf.org/html/rfc9110#section-15.5.1'"]
flow 17: mismatch: ["body.type: 'https://tools.ietf.org/html/rfc7231#section-6.5.1' != 'https://tools.ietf.org/html/rfc9110#section-15.5.1'"]
flow 19: mismatch: ["body.type: 'https://tools.ietf.org/html/rfc7231#section-6.5.1' != 'https://tools.ietf.org/html/rfc9110#section-15.5.1'"]
214 changes: 214 additions & 0 deletions parity-report/diff/flows.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
{
"all_flows_match": true,
"swagger": {
"diffs": [
"swagger.components.schemas.EducationDto.required: only in modern (['degree', 'fieldOfStudy', 'school'])",
"swagger.openapi: '3.0.1' != '3.0.4'",
"swagger.paths./api/Educations.get.responses.200.description: 'Success' != 'OK'",
"swagger.paths./api/Educations.post.responses.200.description: 'Success' != 'OK'",
"swagger.paths./api/Educations/{id}.delete.parameters[0].style: only in legacy ('simple')",
"swagger.paths./api/Educations/{id}.delete.responses.200.description: 'Success' != 'OK'",
"swagger.paths./api/Educations/{id}.get.parameters[0].style: only in legacy ('simple')",
"swagger.paths./api/Educations/{id}.get.responses.200.description: 'Success' != 'OK'",
"swagger.paths./api/Educations/{id}.put.parameters[0].style: only in legacy ('simple')",
"swagger.paths./api/Educations/{id}.put.responses.200.description: 'Success' != 'OK'"
],
"verdict": "justified",
"justification": "Inherent .NET 9 / Swashbuckle 9 output changes: OpenAPI patch version 3.0.1->3.0.4, response descriptions 'Success'->'OK', explicit required[] emitted for C# `required` DTO members, and default parameter style 'simple' no longer serialized. Endpoints, schemas, and semantics are unchanged."
},
"flows": [
{
"flow": 1,
"name": "GET all with seed row",
"legacy_status": 200,
"modern_status": 200,
"verdict": "match",
"diffs": []
},
{
"flow": 2,
"name": "GET seeded id -> 200",
"legacy_status": 200,
"modern_status": 200,
"verdict": "match",
"diffs": []
},
{
"flow": 3,
"name": "GET unknown id -> 204",
"legacy_status": 204,
"modern_status": 204,
"verdict": "match",
"diffs": []
},
{
"flow": 4,
"name": "GET malformed guid -> 404",
"legacy_status": 404,
"modern_status": 404,
"verdict": "match",
"diffs": []
},
{
"flow": 5,
"name": "POST valid -> 200 + dto",
"legacy_status": 200,
"modern_status": 200,
"verdict": "match",
"diffs": []
},
{
"flow": 6,
"name": "POST null body -> 400",
"legacy_status": 400,
"modern_status": 400,
"verdict": "justified",
"justification": ".NET 8+ ProblemDetails uses RFC 9110 section URLs instead of RFC 7231",
"diffs": [
"body.type: 'https://tools.ietf.org/html/rfc7231#section-6.5.1' != 'https://tools.ietf.org/html/rfc9110#section-15.5.1'"
]
},
{
"flow": 7,
"name": "POST missing degree -> 400",
"legacy_status": 400,
"modern_status": 400,
"verdict": "justified",
"justification": "System.Text.Json reworded the missing-required-properties message in .NET 9; .NET 8+ ProblemDetails uses RFC 9110 section URLs instead of RFC 7231",
"diffs": [
"body.errors.$[0]: \"JSON deserialization for type 'Application.Dtos.EducationDto' was missing required properties, including the following: degree\" != \"JSON deserialization for type 'Application.Dtos.EducationDto' was missing required properties including: 'degree'.\"",
"body.type: 'https://tools.ietf.org/html/rfc7231#section-6.5.1' != 'https://tools.ietf.org/html/rfc9110#section-15.5.1'"
]
},
{
"flow": 8,
"name": "POST missing fieldOfStudy -> 400",
"legacy_status": 400,
"modern_status": 400,
"verdict": "justified",
"justification": "System.Text.Json reworded the missing-required-properties message in .NET 9; .NET 8+ ProblemDetails uses RFC 9110 section URLs instead of RFC 7231",
"diffs": [
"body.errors.$[0]: \"JSON deserialization for type 'Application.Dtos.EducationDto' was missing required properties, including the following: fieldOfStudy\" != \"JSON deserialization for type 'Application.Dtos.EducationDto' was missing required properties including: 'fieldOfStudy'.\"",
"body.type: 'https://tools.ietf.org/html/rfc7231#section-6.5.1' != 'https://tools.ietf.org/html/rfc9110#section-15.5.1'"
]
},
{
"flow": 9,
"name": "POST missing school -> 400",
"legacy_status": 400,
"modern_status": 400,
"verdict": "justified",
"justification": "System.Text.Json reworded the missing-required-properties message in .NET 9; .NET 8+ ProblemDetails uses RFC 9110 section URLs instead of RFC 7231",
"diffs": [
"body.errors.$[0]: \"JSON deserialization for type 'Application.Dtos.EducationDto' was missing required properties, including the following: school\" != \"JSON deserialization for type 'Application.Dtos.EducationDto' was missing required properties including: 'school'.\"",
"body.type: 'https://tools.ietf.org/html/rfc7231#section-6.5.1' != 'https://tools.ietf.org/html/rfc9110#section-15.5.1'"
]
},
{
"flow": 10,
"name": "POST Degree > 50 chars",
"legacy_status": 500,
"modern_status": 500,
"verdict": "justified",
"justification": ".NET 8+ ProblemDetails uses RFC 9110 section URLs instead of RFC 7231",
"diffs": [
"body.type: 'https://tools.ietf.org/html/rfc7231#section-6.6.1' != 'https://tools.ietf.org/html/rfc9110#section-15.6.1'"
]
},
{
"flow": 11,
"name": "POST FieldOfStudy/School > 250 chars",
"legacy_status": 500,
"modern_status": 500,
"verdict": "justified",
"justification": ".NET 8+ ProblemDetails uses RFC 9110 section URLs instead of RFC 7231",
"diffs": [
"body.type: 'https://tools.ietf.org/html/rfc7231#section-6.6.1' != 'https://tools.ietf.org/html/rfc9110#section-15.6.1'"
]
},
{
"flow": 12,
"name": "POST Description > 1000 chars",
"legacy_status": 500,
"modern_status": 500,
"verdict": "justified",
"justification": ".NET 8+ ProblemDetails uses RFC 9110 section URLs instead of RFC 7231",
"diffs": [
"body.type: 'https://tools.ietf.org/html/rfc7231#section-6.6.1' != 'https://tools.ietf.org/html/rfc9110#section-15.6.1'"
]
},
{
"flow": 13,
"name": "POST null Description -> 200",
"legacy_status": 200,
"modern_status": 200,
"verdict": "match",
"diffs": []
},
{
"flow": 14,
"name": "GET after create matches",
"legacy_status": 200,
"modern_status": 200,
"verdict": "match",
"diffs": []
},
{
"flow": 15,
"name": "PUT valid -> 200; GET reflects change",
"legacy_status": 200,
"modern_status": 200,
"verdict": "match",
"diffs": []
},
{
"flow": 16,
"name": "PUT unknown id -> 400",
"legacy_status": 400,
"modern_status": 400,
"verdict": "justified",
"justification": ".NET 8+ ProblemDetails uses RFC 9110 section URLs instead of RFC 7231",
"diffs": [
"body.type: 'https://tools.ietf.org/html/rfc7231#section-6.5.1' != 'https://tools.ietf.org/html/rfc9110#section-15.5.1'"
]
},
{
"flow": 17,
"name": "PUT null body -> 400",
"legacy_status": 400,
"modern_status": 400,
"verdict": "justified",
"justification": ".NET 8+ ProblemDetails uses RFC 9110 section URLs instead of RFC 7231",
"diffs": [
"body.type: 'https://tools.ietf.org/html/rfc7231#section-6.5.1' != 'https://tools.ietf.org/html/rfc9110#section-15.5.1'"
]
},
{
"flow": 18,
"name": "DELETE valid -> 200; GET -> 204",
"legacy_status": 200,
"modern_status": 200,
"verdict": "match",
"diffs": []
},
{
"flow": 19,
"name": "DELETE unknown id -> 400",
"legacy_status": 400,
"modern_status": 400,
"verdict": "justified",
"justification": ".NET 8+ ProblemDetails uses RFC 9110 section URLs instead of RFC 7231",
"diffs": [
"body.type: 'https://tools.ietf.org/html/rfc7231#section-6.5.1' != 'https://tools.ietf.org/html/rfc9110#section-15.5.1'"
]
},
{
"flow": 20,
"name": "create -> update -> delete lifecycle",
"legacy_status": 200,
"modern_status": 200,
"verdict": "match",
"diffs": []
}
]
}
27 changes: 27 additions & 0 deletions parity-report/diff/schema.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
--- parity-report/legacy/schema.sql
+++ parity-report/modern/schema.sql
@@ -5,13 +5,12 @@
);

START TRANSACTION;
-
CREATE TABLE "Educations" (
"Id" uuid NOT NULL,
"Degree" character varying(50) NOT NULL,
"FieldOfStudy" character varying(250) NOT NULL,
"School" character varying(250) NOT NULL,
- "Description" character varying(1000) NULL,
+ "Description" character varying(1000),
CONSTRAINT "PK_Educations" PRIMARY KEY ("Id")
);

@@ -19,7 +18,7 @@
VALUES ('c92ea179-dd5c-46ca-b7b5-b44a191b974c', 'Bachelor''s degree', NULL, 'Software engineering', 'Sample university');

INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
-VALUES ('20240113141226_Initialize', '7.0.10');
+VALUES ('20240113141226_Initialize', '9.0.19');

COMMIT;

DIFFERS: DDL is semantically equivalent; EF9 omits explicit NULL and a blank line, while the migration history ProductVersion is emitted as 9.0.19 by the EF9 tool/runtime rather than the snapshot annotation 7.0.10.
45 changes: 45 additions & 0 deletions parity-report/fixes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"summary": "Migration of the WebAPI-Sample solution from .NET 7 to .NET 9: all six projects retargeted to net9.0, NuGet packages upgraded to .NET 9-compatible stable versions (EF Core 9.0.19, Npgsql 9.x, Swashbuckle 9.0.6, Mapster 10, xunit 2.9.3, FakeItEasy 9), Docker images and GitHub Actions workflow updated to 9.0, and a Directory.Build.props added enabling .NET analysers with warnings-as-errors as the static-analyser gate. Legacy baselines (build, tests, smoke, 20 golden flows, swagger, EF DDL) were captured on net7 before any migration edit; all gates were re-run on net9 and diffed against those baselines.",
"fixes": [
{
"area": "Target frameworks",
"description": "All six csproj files retargeted net7.0 -> net9.0 (WebApi, Application, Infrastructure, Domain, Application.Tests, WebApi.Tests)."
},
{
"area": "NuGet packages",
"description": "EF Core + Design/SqlServer/Tools 7.0.10 -> 9.0.19; Npgsql 7.0.4 -> 9.0.5; Npgsql.EntityFrameworkCore.PostgreSQL(+NodaTime) 7.0.4 -> 9.0.4; AspNetCore.HealthChecks.NpgSql 7.0.0 -> 9.0.0; Microsoft.AspNetCore.OpenApi 7.0.9 -> 9.0.19; Mapster 7.3.0 -> 10.0.12; Microsoft.NET.Test.Sdk 17.5.0 -> 18.9.0; xunit 2.4.2 -> 2.9.3; xunit.runner.visualstudio 2.4.5 -> 4.0.0; FakeItEasy 7.4.0 -> 9.0.1; coverlet.collector 3.2.0 -> 10.0.1."
},
{
"area": "Swashbuckle compatibility",
"description": "Swashbuckle.AspNetCore pinned to 9.0.6 instead of 10.x: version 10 pulls Microsoft.OpenApi 2.x which throws TypeLoadException at runtime alongside Microsoft.AspNetCore.OpenApi 9.0.x."
},
{
"area": "src/WebApi/Program.cs",
"description": "Connection string is now resolved once and null-guarded with an InvalidOperationException, fixing nullable warnings (CS8604) that became errors under the analyser gate."
},
{
"area": "src/Infrastructure/DbContexts/SampleDbContext.cs",
"description": "HasData seed changed from Guid.NewGuid() to the stable GUID already used by the committed migration; EF Core 9 rejects nondeterministic seed data with PendingModelChangesWarning."
},
{
"area": "tests/WebApi.Tests/EducationsControllerTests.cs",
"description": "Two null-literal arguments changed to null! so the null-body test cases compile under nullable warnings-as-errors; controller signatures and runtime behavior are unchanged (assertions untouched)."
},
{
"area": "Static analyser gate",
"description": "New Directory.Build.props enables EnableNETAnalyzers, AnalysisLevel=latest, and TreatWarningsAsErrors solution-wide; the net9 build passes with 0 warnings / 0 errors (legacy net7 build had 3 warnings)."
},
{
"area": "Dockerfile",
"description": "Base images bumped from mcr.microsoft.com/dotnet/aspnet:7.0 and sdk:7.0 to 9.0."
},
{
"area": ".github/workflows/dotnet.yml",
"description": "dotnet-version fixed from 6.0.x to 9.0.x; actions/checkout and actions/setup-dotnet bumped to v4."
},
{
"area": "Behavioral differences (justified)",
"description": "10 of 20 golden flows differ only in framework-generated error metadata inherent to .NET 9: ProblemDetails type URLs moved from RFC 7231 to RFC 9110 sections, and System.Text.Json reworded its missing-required-properties message. All HTTP status codes and response semantics are identical. Swagger output differs only in OpenAPI patch version, response description wording, explicit required[] for C# required members, and omission of default parameter style. EF DDL is semantically identical (EF9 omits redundant explicit NULL; migration-history ProductVersion reflects the EF9 tool)."
}
]
}
Loading
Loading