From 14efe16a21cdb5e5547fb4441498866fbe70db77 Mon Sep 17 00:00:00 2001 From: Nano Taboada <87288+nanotaboada@users.noreply.github.com> Date: Sun, 29 Mar 2026 16:54:25 -0300 Subject: [PATCH 1/7] feat(data): normalize 2022 World Cup Argentina squad dataset (#435) Co-authored-by: Claude Sonnet 4.6 --- CHANGELOG.md | 2 + ...9000000_NormalizePlayerDataset.Designer.cs | 69 ++++++ .../20260329000000_NormalizePlayerDataset.cs | 207 ++++++++++++++++++ .../Utilities/PlayerData.cs | 115 ++++++---- .../storage/players-sqlite3.db | Bin 28672 -> 28672 bytes .../Unit/PlayerControllerTests.cs | 4 +- .../Utilities/PlayerFakes.cs | 22 +- 7 files changed, 362 insertions(+), 57 deletions(-) create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260329000000_NormalizePlayerDataset.Designer.cs create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260329000000_NormalizePlayerDataset.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index f3c48b7..63a0dbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,8 @@ This project uses famous football stadiums (A-Z) that hosted FIFA World Cup matc ### Changed +- Normalize player dataset: add Lo Celso (squad 27), correct Fernández/Mac Allister/Messi team data, replace random UUIDs with deterministic UUID v5 values (#435) +- Align CRUD test fixtures: Lo Celso (squad 27) for Create and Delete, Messi (squad 10) for Retrieve, Damián Martínez (squad 23) for Update (#435) - Bump `codecov/codecov-action` from 5.5.2 to 5.5.3 (#423) ### Fixed diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260329000000_NormalizePlayerDataset.Designer.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260329000000_NormalizePlayerDataset.Designer.cs new file mode 100644 index 0000000..33c29f9 --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260329000000_NormalizePlayerDataset.Designer.cs @@ -0,0 +1,69 @@ +// +using System; +using Dotnet.Samples.AspNetCore.WebApi.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Dotnet.Samples.AspNetCore.WebApi.Migrations +{ + [DbContext(typeof(PlayerDbContext))] + [Migration("20260329000000_NormalizePlayerDataset")] + partial class NormalizePlayerDataset + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.11"); + + modelBuilder.Entity("Dotnet.Samples.AspNetCore.WebApi.Models.Player", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AbbrPosition") + .HasColumnType("TEXT"); + + b.Property("DateOfBirth") + .HasColumnType("TEXT"); + + b.Property("FirstName") + .HasColumnType("TEXT"); + + b.Property("LastName") + .HasColumnType("TEXT"); + + b.Property("League") + .HasColumnType("TEXT"); + + b.Property("MiddleName") + .HasColumnType("TEXT"); + + b.Property("Position") + .HasColumnType("TEXT"); + + b.Property("SquadNumber") + .HasColumnType("INTEGER"); + + b.Property("Starting11") + .HasColumnType("INTEGER"); + + b.Property("Team") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SquadNumber") + .IsUnique(); + + b.ToTable("Players"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260329000000_NormalizePlayerDataset.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260329000000_NormalizePlayerDataset.cs new file mode 100644 index 0000000..201488b --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260329000000_NormalizePlayerDataset.cs @@ -0,0 +1,207 @@ +using Dotnet.Samples.AspNetCore.WebApi.Utilities; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Dotnet.Samples.AspNetCore.WebApi.Migrations +{ + /// + public partial class NormalizePlayerDataset : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // Update UUIDs to canonical UUID v5 values for starting 11 + migrationBuilder.Sql( + "UPDATE Players SET Id = '01772c59-43f0-5d85-b913-c78e4e281452' WHERE SquadNumber = 23" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = 'da31293b-4c7e-5e0f-a168-469ee29ecbc4' WHERE SquadNumber = 26" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = 'c096c69e-762b-5281-9290-bb9c167a24a0' WHERE SquadNumber = 13" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = 'd5f7dd7a-1dcb-5960-ba27-e34865b63358' WHERE SquadNumber = 19" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = '2f6f90a0-9b9d-5023-96d2-a2aaf03143a6' WHERE SquadNumber = 3" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = 'b5b46e79-929e-5ed2-949d-0d167109c022' WHERE SquadNumber = 11" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = '0293b282-1da8-562e-998e-83849b417a42' WHERE SquadNumber = 7" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = 'd3ba552a-dac3-588a-b961-1ea7224017fd', Team = 'SL Benfica', League = 'Liga Portugal' WHERE SquadNumber = 24" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = '9613cae9-16ab-5b54-937e-3135123b9e0d', Team = 'Brighton & Hove Albion' WHERE SquadNumber = 20" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = 'acc433bf-d505-51fe-831e-45eb44c4d43c', Team = 'Paris Saint-Germain', League = 'Ligue 1' WHERE SquadNumber = 10" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = '38bae91d-8519-55a2-b30a-b9fe38849bfb' WHERE SquadNumber = 9" + ); + + // Update UUIDs to canonical UUID v5 values for substitutes + migrationBuilder.Sql( + "UPDATE Players SET Id = '5a9cd988-95e6-54c1-bc34-9aa08acca8d0' WHERE SquadNumber = 1" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = 'c62f2ac1-41e8-5d34-b073-2ba0913d0e31' WHERE SquadNumber = 12" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = '5fdb10e8-38c0-5084-9a3f-b369a960b9c2' WHERE SquadNumber = 2" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = 'bbd441f7-fcfb-5834-8468-2a9004b64c8c' WHERE SquadNumber = 4" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = 'd8bfea25-f189-5d5e-b3a5-ed89329b9f7c' WHERE SquadNumber = 6" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = 'dca343a8-12e5-53d6-89a8-916b120a5ee4' WHERE SquadNumber = 8" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = '98306555-a466-5d18-804e-dc82175e697b' WHERE SquadNumber = 25" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = '9d140400-196f-55d8-86e1-e0b96a375c83' WHERE SquadNumber = 5" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = 'd3b0e8e8-2c34-531a-b608-b24fed0ef986' WHERE SquadNumber = 14" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = '7cc8d527-56a2-58bd-9528-2618fc139d30' WHERE SquadNumber = 17" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = '191c82af-0c51-526a-b903-c3600b61b506' WHERE SquadNumber = 18" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = 'b1306b7b-a3a4-5f7c-90fd-dd5bdbed57ba' WHERE SquadNumber = 15" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = 'ecec27e8-487b-5622-b116-0855020477ed' WHERE SquadNumber = 16" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = '7941cd7c-4df1-5952-97e8-1e7f5d08e8aa' WHERE SquadNumber = 21" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = '79c96f29-c59f-5f98-96b8-3a5946246624' WHERE SquadNumber = 22" + ); + + // Insert Lo Celso (squad 27) — OR IGNORE is safe on a fresh DB where SeedSubstitutes + // already seeded him (because GetSubstitutesWithId() now includes him), and required + // for an existing DB that was seeded before this migration was added. + migrationBuilder.Sql( + "INSERT OR IGNORE INTO \"Players\" (\"Id\", \"FirstName\", \"MiddleName\", \"LastName\", \"DateOfBirth\", \"SquadNumber\", \"Position\", \"AbbrPosition\", \"Team\", \"League\", \"Starting11\") " + + "VALUES ('F8D13028-0D22-5513-8774-08A2332B5814', 'Giovani', NULL, 'Lo Celso', '1996-04-09 00:00:00', 27, 'Central Midfield', 'CM', 'Real Betis Balompié', 'La Liga', 0)" + ); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + // Remove Lo Celso (squad 27) + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: Guid.Parse("f8d13028-0d22-5513-8774-08a2332b5814") + ); + + // Revert team/league corrections + migrationBuilder.Sql( + "UPDATE Players SET Team = 'Chelsea FC', League = 'Premier League' WHERE SquadNumber = 24" + ); + migrationBuilder.Sql("UPDATE Players SET Team = 'Liverpool FC' WHERE SquadNumber = 20"); + migrationBuilder.Sql( + "UPDATE Players SET Team = 'Inter Miami CF', League = 'Major League Soccer' WHERE SquadNumber = 10" + ); + + // Revert UUIDs for starting 11 + migrationBuilder.Sql( + "UPDATE Players SET Id = 'f91b9671-cfd1-48d7-afb9-34e93568c9ee' WHERE SquadNumber = 23" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = '51ec988a-0d8b-42d9-84e4-5e17c93d8d50' WHERE SquadNumber = 26" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = '0969be24-086c-4c51-9c29-0280683b8133' WHERE SquadNumber = 13" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = 'ac532709-4682-49db-acc2-395f61f405ab' WHERE SquadNumber = 19" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = 'dc052ee4-c69d-49da-a256-b8ec727f4d59' WHERE SquadNumber = 3" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = '6def9bb7-23c2-42b5-b37b-2e9b6fec31cd' WHERE SquadNumber = 11" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = '8ca911d9-ab32-4366-b2b1-cad5eb6f4bcc' WHERE SquadNumber = 7" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = '198c4774-9607-4e76-8475-ec2528af69d2' WHERE SquadNumber = 24" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = '06971ada-1b3d-4d4a-88f5-e2f35311b5aa' WHERE SquadNumber = 20" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = 'df6f6bab-5efd-4518-80bb-09ef54435636' WHERE SquadNumber = 10" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = '27cf4e29-67d5-4c3b-9cf8-7d3fa3942fcb' WHERE SquadNumber = 9" + ); + + // Revert UUIDs for substitutes + migrationBuilder.Sql( + "UPDATE Players SET Id = 'b1f8a5d3-2c4e-4a6b-8d9f-1e3c5a7b9d2f' WHERE SquadNumber = 1" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = 'c2e9b6f4-3d5f-4b7c-9e0a-2f4d6b8c0e3a' WHERE SquadNumber = 12" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = 'd3f0c7e5-4e6a-5c8d-0f1b-3a5e7c9d1f4b' WHERE SquadNumber = 2" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = 'e4a1d8f6-5f7b-6d9e-1a2c-4b6d8e0a2c5d' WHERE SquadNumber = 4" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = 'f5b2e9a7-6a8c-7e0f-2b3d-5c7e9a1b3d6e' WHERE SquadNumber = 6" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = 'a6c3f0b8-7b9d-8f1a-3c4e-6d8f0b2c4e7f' WHERE SquadNumber = 8" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = 'b7d4a1c9-8c0e-9a2b-4d5f-7e9a1c3d5f8a' WHERE SquadNumber = 25" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = 'c8e5b2d0-9d1f-0b3c-5e6a-8f0b2d4e6a9b' WHERE SquadNumber = 5" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = 'd9f6c3e1-0e2a-1c4d-6f7b-9a1c3e5f7b0c' WHERE SquadNumber = 14" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = 'e0a7d4f2-1f3b-2d5e-7a8c-0b2d4f6a8c1d' WHERE SquadNumber = 17" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = 'f1b8e5a3-2a4c-3e6f-8b9d-1c3e5a7b9d2e' WHERE SquadNumber = 18" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = 'a2c9f6b4-3b5d-4f7a-9c0e-2d4f6b8c0e3f' WHERE SquadNumber = 15" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = 'b3d0a7c5-4c6e-5a8b-0d1f-3e5a7c9d1f4a' WHERE SquadNumber = 16" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = 'c4e1b8d6-5d7f-6b9c-1e2a-4f6d8e0a2c5b' WHERE SquadNumber = 21" + ); + migrationBuilder.Sql( + "UPDATE Players SET Id = 'd5f2c9e7-6e8a-7c0d-2f3b-5a7e9a1b3d6c' WHERE SquadNumber = 22" + ); + } + } +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Utilities/PlayerData.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Utilities/PlayerData.cs index 6facad1..b3ef354 100644 --- a/src/Dotnet.Samples.AspNetCore.WebApi/Utilities/PlayerData.cs +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Utilities/PlayerData.cs @@ -118,8 +118,8 @@ public static List MakeStarting11() SquadNumber = 24, Position = Position.CentralMidfield.Text, AbbrPosition = Position.CentralMidfield.Abbr, - Team = "Chelsea FC", - League = "Premier League", + Team = "SL Benfica", + League = "Liga Portugal", Starting11 = true, }, new() @@ -130,7 +130,7 @@ public static List MakeStarting11() SquadNumber = 20, Position = Position.CentralMidfield.Text, AbbrPosition = Position.CentralMidfield.Abbr, - Team = "Liverpool FC", + Team = "Brighton & Hove Albion", League = "Premier League", Starting11 = true, }, @@ -143,8 +143,8 @@ public static List MakeStarting11() SquadNumber = 10, Position = Position.RightWinger.Text, AbbrPosition = Position.RightWinger.Abbr, - Team = "Inter Miami CF", - League = "Major League Soccer", + Team = "Paris Saint-Germain", + League = "Ligue 1", Starting11 = true, }, new() @@ -158,7 +158,7 @@ public static List MakeStarting11() Team = "Manchester City", League = "Premier League", Starting11 = true, - } + }, ]; } @@ -172,7 +172,7 @@ public static List MakeStarting11WithId() [ new() { - Id = Guid.Parse("f91b9671-cfd1-48d7-afb9-34e93568c9ee"), + Id = Guid.Parse("01772c59-43f0-5d85-b913-c78e4e281452"), FirstName = "Damián", MiddleName = "Emiliano", LastName = "Martínez", @@ -186,7 +186,7 @@ public static List MakeStarting11WithId() }, new() { - Id = Guid.Parse("51ec988a-0d8b-42d9-84e4-5e17c93d8d50"), + Id = Guid.Parse("da31293b-4c7e-5e0f-a168-469ee29ecbc4"), FirstName = "Nahuel", LastName = "Molina", DateOfBirth = new DateTime(1998, 4, 5, 0, 0, 0, DateTimeKind.Utc), @@ -199,7 +199,7 @@ public static List MakeStarting11WithId() }, new() { - Id = Guid.Parse("0969be24-086c-4c51-9c29-0280683b8133"), + Id = Guid.Parse("c096c69e-762b-5281-9290-bb9c167a24a0"), FirstName = "Cristian", MiddleName = "Gabriel", LastName = "Romero", @@ -213,7 +213,7 @@ public static List MakeStarting11WithId() }, new() { - Id = Guid.Parse("ac532709-4682-49db-acc2-395f61f405ab"), + Id = Guid.Parse("d5f7dd7a-1dcb-5960-ba27-e34865b63358"), FirstName = "Nicolás", MiddleName = "Hernán Gonzalo", LastName = "Otamendi", @@ -227,7 +227,7 @@ public static List MakeStarting11WithId() }, new() { - Id = Guid.Parse("dc052ee4-c69d-49da-a256-b8ec727f4d59"), + Id = Guid.Parse("2f6f90a0-9b9d-5023-96d2-a2aaf03143a6"), FirstName = "Nicolás", MiddleName = "Alejandro", LastName = "Tagliafico", @@ -241,7 +241,7 @@ public static List MakeStarting11WithId() }, new() { - Id = Guid.Parse("6def9bb7-23c2-42b5-b37b-2e9b6fec31cd"), + Id = Guid.Parse("b5b46e79-929e-5ed2-949d-0d167109c022"), FirstName = "Ángel", MiddleName = "Fabián", LastName = "Di María", @@ -255,7 +255,7 @@ public static List MakeStarting11WithId() }, new() { - Id = Guid.Parse("8ca911d9-ab32-4366-b2b1-cad5eb6f4bcc"), + Id = Guid.Parse("0293b282-1da8-562e-998e-83849b417a42"), FirstName = "Rodrigo", MiddleName = "Javier", LastName = "de Paul", @@ -269,7 +269,7 @@ public static List MakeStarting11WithId() }, new() { - Id = Guid.Parse("198c4774-9607-4e76-8475-ec2528af69d2"), + Id = Guid.Parse("d3ba552a-dac3-588a-b961-1ea7224017fd"), FirstName = "Enzo", MiddleName = "Jeremías", LastName = "Fernández", @@ -277,26 +277,26 @@ public static List MakeStarting11WithId() SquadNumber = 24, Position = Position.CentralMidfield.Text, AbbrPosition = Position.CentralMidfield.Abbr, - Team = "Chelsea FC", - League = "Premier League", + Team = "SL Benfica", + League = "Liga Portugal", Starting11 = true, }, new() { - Id = Guid.Parse("06971ada-1b3d-4d4a-88f5-e2f35311b5aa"), + Id = Guid.Parse("9613cae9-16ab-5b54-937e-3135123b9e0d"), FirstName = "Alexis", LastName = "Mac Allister", DateOfBirth = new DateTime(1998, 12, 23, 0, 0, 0, DateTimeKind.Utc), SquadNumber = 20, Position = Position.CentralMidfield.Text, AbbrPosition = Position.CentralMidfield.Abbr, - Team = "Liverpool FC", + Team = "Brighton & Hove Albion", League = "Premier League", Starting11 = true, }, new() { - Id = Guid.Parse("df6f6bab-5efd-4518-80bb-09ef54435636"), + Id = Guid.Parse("acc433bf-d505-51fe-831e-45eb44c4d43c"), FirstName = "Lionel", MiddleName = "Andrés", LastName = "Messi", @@ -304,13 +304,13 @@ public static List MakeStarting11WithId() SquadNumber = 10, Position = Position.RightWinger.Text, AbbrPosition = Position.RightWinger.Abbr, - Team = "Inter Miami CF", - League = "Major League Soccer", + Team = "Paris Saint-Germain", + League = "Ligue 1", Starting11 = true, }, new() { - Id = Guid.Parse("27cf4e29-67d5-4c3b-9cf8-7d3fa3942fcb"), + Id = Guid.Parse("38bae91d-8519-55a2-b30a-b9fe38849bfb"), FirstName = "Julián", LastName = "Álvarez", DateOfBirth = new DateTime(2000, 1, 30, 0, 0, 0, DateTimeKind.Utc), @@ -320,7 +320,7 @@ public static List MakeStarting11WithId() Team = "Manchester City", League = "Premier League", Starting11 = true, - } + }, ]; } @@ -523,21 +523,33 @@ public static List GetSubstitutes() Team = "Inter Milan", League = "Serie A", Starting11 = false, - } + }, + new() + { + FirstName = "Giovani", + LastName = "Lo Celso", + DateOfBirth = new DateTime(1996, 4, 9, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 27, + Position = Position.CentralMidfield.Text, + AbbrPosition = Position.CentralMidfield.Abbr, + Team = "Real Betis Balompié", + League = "La Liga", + Starting11 = false, + }, ]; } /// - /// Create a predefined list of 15 substitute players where each player has a fixed GUID identifier and full profile data. + /// Create a predefined list of 16 substitute players where each player has a fixed GUID identifier and full profile data. /// - /// A list of 15 Player instances representing substitute players; each entry includes a predefined Id (Guid) and populated fields such as name, date of birth, squad number, position (and abbreviation), team, league, and Starting11 set to false. + /// A list of 16 Player instances representing substitute players; each entry includes a predefined Id (Guid) and populated fields such as name, date of birth, squad number, position (and abbreviation), team, league, and Starting11 set to false. public static List GetSubstitutesWithId() { return [ new() { - Id = Guid.Parse("b1f8a5d3-2c4e-4a6b-8d9f-1e3c5a7b9d2f"), + Id = Guid.Parse("5a9cd988-95e6-54c1-bc34-9aa08acca8d0"), FirstName = "Franco", MiddleName = "Daniel", LastName = "Armani", @@ -551,7 +563,7 @@ public static List GetSubstitutesWithId() }, new() { - Id = Guid.Parse("c2e9b6f4-3d5f-4b7c-9e0a-2f4d6b8c0e3a"), + Id = Guid.Parse("c62f2ac1-41e8-5d34-b073-2ba0913d0e31"), FirstName = "Gerónimo", LastName = "Rulli", DateOfBirth = new DateTime(1992, 5, 20, 0, 0, 0, DateTimeKind.Utc), @@ -564,7 +576,7 @@ public static List GetSubstitutesWithId() }, new() { - Id = Guid.Parse("d3f0c7e5-4e6a-5c8d-0f1b-3a5e7c9d1f4b"), + Id = Guid.Parse("5fdb10e8-38c0-5084-9a3f-b369a960b9c2"), FirstName = "Juan", MiddleName = "Marcos", LastName = "Foyth", @@ -578,7 +590,7 @@ public static List GetSubstitutesWithId() }, new() { - Id = Guid.Parse("e4a1d8f6-5f7b-6d9e-1a2c-4b6d8e0a2c5d"), + Id = Guid.Parse("bbd441f7-fcfb-5834-8468-2a9004b64c8c"), FirstName = "Gonzalo", MiddleName = "Ariel", LastName = "Montiel", @@ -592,7 +604,7 @@ public static List GetSubstitutesWithId() }, new() { - Id = Guid.Parse("f5b2e9a7-6a8c-7e0f-2b3d-5c7e9a1b3d6e"), + Id = Guid.Parse("d8bfea25-f189-5d5e-b3a5-ed89329b9f7c"), FirstName = "Germán", MiddleName = "Alejo", LastName = "Pezzella", @@ -606,7 +618,7 @@ public static List GetSubstitutesWithId() }, new() { - Id = Guid.Parse("a6c3f0b8-7b9d-8f1a-3c4e-6d8f0b2c4e7f"), + Id = Guid.Parse("dca343a8-12e5-53d6-89a8-916b120a5ee4"), FirstName = "Marcos", MiddleName = "Javier", LastName = "Acuña", @@ -620,7 +632,7 @@ public static List GetSubstitutesWithId() }, new() { - Id = Guid.Parse("b7d4a1c9-8c0e-9a2b-4d5f-7e9a1c3d5f8a"), + Id = Guid.Parse("98306555-a466-5d18-804e-dc82175e697b"), FirstName = "Lisandro", LastName = "Martínez", DateOfBirth = new DateTime(1998, 1, 18, 0, 0, 0, DateTimeKind.Utc), @@ -633,7 +645,7 @@ public static List GetSubstitutesWithId() }, new() { - Id = Guid.Parse("c8e5b2d0-9d1f-0b3c-5e6a-8f0b2d4e6a9b"), + Id = Guid.Parse("9d140400-196f-55d8-86e1-e0b96a375c83"), FirstName = "Leandro", MiddleName = "Daniel", LastName = "Paredes", @@ -647,7 +659,7 @@ public static List GetSubstitutesWithId() }, new() { - Id = Guid.Parse("d9f6c3e1-0e2a-1c4d-6f7b-9a1c3e5f7b0c"), + Id = Guid.Parse("d3b0e8e8-2c34-531a-b608-b24fed0ef986"), FirstName = "Exequiel", MiddleName = "Alejandro", LastName = "Palacios", @@ -661,7 +673,7 @@ public static List GetSubstitutesWithId() }, new() { - Id = Guid.Parse("e0a7d4f2-1f3b-2d5e-7a8c-0b2d4f6a8c1d"), + Id = Guid.Parse("7cc8d527-56a2-58bd-9528-2618fc139d30"), FirstName = "Alejandro", MiddleName = "Darío", LastName = "Gómez", @@ -675,7 +687,7 @@ public static List GetSubstitutesWithId() }, new() { - Id = Guid.Parse("f1b8e5a3-2a4c-3e6f-8b9d-1c3e5a7b9d2e"), + Id = Guid.Parse("191c82af-0c51-526a-b903-c3600b61b506"), FirstName = "Guido", LastName = "Rodríguez", DateOfBirth = new DateTime(1994, 4, 12, 0, 0, 0, DateTimeKind.Utc), @@ -688,7 +700,7 @@ public static List GetSubstitutesWithId() }, new() { - Id = Guid.Parse("a2c9f6b4-3b5d-4f7a-9c0e-2d4f6b8c0e3f"), + Id = Guid.Parse("b1306b7b-a3a4-5f7c-90fd-dd5bdbed57ba"), FirstName = "Ángel", MiddleName = "Martín", LastName = "Correa", @@ -702,7 +714,7 @@ public static List GetSubstitutesWithId() }, new() { - Id = Guid.Parse("b3d0a7c5-4c6e-5a8b-0d1f-3e5a7c9d1f4a"), + Id = Guid.Parse("ecec27e8-487b-5622-b116-0855020477ed"), FirstName = "Thiago", MiddleName = "Ezequiel", LastName = "Almada", @@ -716,7 +728,7 @@ public static List GetSubstitutesWithId() }, new() { - Id = Guid.Parse("c4e1b8d6-5d7f-6b9c-1e2a-4f6d8e0a2c5b"), + Id = Guid.Parse("7941cd7c-4df1-5952-97e8-1e7f5d08e8aa"), FirstName = "Paulo", MiddleName = "Exequiel", LastName = "Dybala", @@ -730,7 +742,7 @@ public static List GetSubstitutesWithId() }, new() { - Id = Guid.Parse("d5f2c9e7-6e8a-7c0d-2f3b-5a7e9a1b3d6c"), + Id = Guid.Parse("79c96f29-c59f-5f98-96b8-3a5946246624"), FirstName = "Lautaro", MiddleName = "Javier", LastName = "Martínez", @@ -741,7 +753,20 @@ public static List GetSubstitutesWithId() Team = "Inter Milan", League = "Serie A", Starting11 = false, - } + }, + new() + { + Id = Guid.Parse("f8d13028-0d22-5513-8774-08a2332b5814"), + FirstName = "Giovani", + LastName = "Lo Celso", + DateOfBirth = new DateTime(1996, 4, 9, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 27, + Position = Position.CentralMidfield.Text, + AbbrPosition = Position.CentralMidfield.Abbr, + Team = "Real Betis Balompié", + League = "La Liga", + Starting11 = false, + }, ]; } @@ -900,6 +925,8 @@ public static List MakeStarting11FromDeserializedJson() return players; } - private static readonly JsonSerializerOptions options = - new() { PropertyNameCaseInsensitive = true }; + private static readonly JsonSerializerOptions options = new() + { + PropertyNameCaseInsensitive = true, + }; } diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/storage/players-sqlite3.db b/src/Dotnet.Samples.AspNetCore.WebApi/storage/players-sqlite3.db index 9d38e8c55a3a3b07291d425b677aa209e56889eb..b48b4e5ab0328c26585d80a2551c101ec70f863b 100644 GIT binary patch delta 2903 zcma);OKe=#8OLWle$3c2b1^goa{-Ujn+6vh=e+MZmH;Jo5+^u`A#sxeAvv$>4jMek z#EC>f4Bi0Cz}+RPvPhQ*6tQB%rt9tylvUHJ?6Ro3=(^vXv=T);?nooeo%!GI@ju`9 zyYue7>bv)XoJU7VrAuJdy}AM0JPgWrCL(U!qD0oj!x{GRCb+r7|9! zuSDNOccTxY^PkN=v=)h_@!GjLimAkmYCL*fJk=k(VS3%)hmD?jGYnSEj=36k2wuWV z;y|r-d@jHJ{8N(;uJgMG_x3z|CqWwd4KZq#NQ@f zAOCiItM+B>!q{KOUcI~f)Y#T;C2nrUl_yyh&5F2wRIL(*sX};_QX~XnNTn%9SRJB_ z(ZfO!eyrQSZnnGY{mx40UF{RC!BLRt&d5P9F zNJul`u?otV{Km@JB)H&c@e5a}MM6RcmJldT34tl*X&QW9U)@eD^e8Q83^N>3Lu`#R zkGvPw+u%iNO@4Frm4s;G6g3&bPC#lZ4YC@h$1#bqmBb1x^SL9JlGHin1x+EV0pFDM z2ieB@yy}?B_rw>q-F#nO01BjJPRHNutDZ~j=r8~PdMfnBLXhL zNqB{n3U zH)hp^*@KI9Ydz;AOHt-BuwOxcic9F!z{+qdxl=B`wRUJFT;16igg3fj&{+>=>sq+n zZf@?G+grq0i0dNrmVil!DTk53iX&}U1~o`+w1oa0%{#}%vx8uI>!!2b-7?o(&CU7B zD8k}G-AS5J;|Sse!UzwQSe(MrTZ1)WU^656hsVz*!f5BU0uR6^Bskn+i*wP&U}c;$ z%HzCo;^l-XYl0>oDM7#z0f%fEE~X(1OewByX7l$?Y|wY6o11en6taJo#SI9-8M7Jk z0)yow3rH~%5EsF6?zrd7<YUwZoL=`%Yd+XeKj_4Ms479wvh_KnAD{H73K1z;w&}&dJvj4?FFB zY9O@(mtbg+HL#t5aV3S7j0u(RKb0j8Yw09pO(g{(Rir2a)G%a_+)!?C{^==~c*981 z58@8o76E6-5EyO_HwP%&S;upE`lSS1N+}=>ahAdKdL_U_z^F@A-~keFLGzDKpI?q= zKB+{1Y5%?XR>a%SwfD6C)U3>W-TY(wMr*$L@0m~9Tg{iEnbytdz1IEapWB~D7g|@M zPg`rvMR-nJo*6z}+pLzsbzBI`!1nP6!k>rjTp=8mq3vUZvJ7qa7Q!;LeRNbL(02H9 zxZP7I%i#7Gg|G~69~lY%i`(H(@%G_S?h?G69SKYD_Mt-fUBV8346#w6EW^-tAuNN+ z)+pllP&xb|RyGS|8CK2|!ZLK6E`&b>z2Wz%*C>=_5LPdQW#BYb2!9Mt!|&2*vQU6+MVm0E` delta 2648 zcma);O>9(E6vwBX4xMRd=1~Qqt)VTx1}iz`-TUr+?_Q#kcfa^*`I07zfzsdrMWD7I ziAXZAA-FJNUScAckRVGF(ztTt#>>8r3)EF18J-2)WQSj|xY(ydNHEo};k6&a*DGC*CdfM=McGQ;%{o`L%7CS!kU(yJw+wYPPj7Jkgpt_I_)wS)W?n zzs4I4;bvf&_!Q>o8|GRVh%gC=Gj1Q`sPc8&`Kzs&lM^$;6SK!=&Nb>&L#1Vdv5~6A z@>Sw6Uq1+@QVkLl!u?|ALB3gjbvteh!QpU&`T9YyEKwjZA>6N6nm5la{#Wmv++^7t<+B)1cP6UWZ z;a*DXIAbwS;v}!`Wc#&vl)J4?#PkW!llj{8ka`n4(J;l zt3`=SNoH_`d@>AI0EBQHEPg{gCPeWp%RkyFCMRakwHDr+n>#t|^goAwsZzzM4~=-o zk+B+)LbEhwAd@J?(WeOrmZGDkoQljPIE;Ml#A`eC@z%*Rt(kxOb#(jh8`lB)V6Y~1 zCJ8YTG8I!`agu+tC)t@9fb(_vz)lS-vPV~s+pIH(vm=zfS+$3&Za{p7*W z=!jEm8EC8pn8fZg%F$L>ozfJ;wo!A!b%$+5(AZ^RITlDF%A)l$uwA%gt|^uLrk--sZjf zzQ(rTU~_eStTEL1wO*?K)%dRWPW}6UHILPAHLle^Y|IBgB8#v0wqGt!brb>UfEN}4 zXulU00q9yUY!@Nu8m}xu&=$9`_$2 service.RetrieveBySquadNumberAsync(squadNumber)) diff --git a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerFakes.cs b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerFakes.cs index 0059ba1..3d628af 100644 --- a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerFakes.cs +++ b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerFakes.cs @@ -56,11 +56,11 @@ public static Player MakeFromStarting11(int squadNumber) /// public static Player MakeNew() { - // Get Leandro Paredes (squad number 5) from substitutes + // Get Giovani Lo Celso (squad number 27) from substitutes var player = - PlayerData.GetSubstitutes().SingleOrDefault(player => player.SquadNumber == 5) + PlayerData.GetSubstitutes().SingleOrDefault(player => player.SquadNumber == 27) ?? throw new InvalidOperationException( - "Substitute player with squad number 5 not found." + "Substitute player with squad number 27 not found." ); player.Id = Guid.NewGuid(); @@ -73,7 +73,7 @@ public static Player MakeNew() /// /// Creates a PlayerRequestModel for testing create operations. - /// Uses data from a substitute player (Leandro Paredes). + /// Uses data from a substitute player (Giovani Lo Celso). /// public static PlayerRequestModel MakeRequestModelForCreate() { @@ -88,13 +88,13 @@ public static PlayerRequestModel MakeRequestModelForCreate() SquadNumber = player.SquadNumber, AbbrPosition = player.AbbrPosition, Team = player.Team, - League = player.League + League = player.League, }; } /// /// Creates a PlayerResponseModel for testing create operation responses. - /// Uses data from a substitute player (Leandro Paredes). + /// Uses data from a substitute player (Giovani Lo Celso). /// public static PlayerResponseModel MakeResponseModelForCreate() { @@ -109,7 +109,7 @@ public static PlayerResponseModel MakeResponseModelForCreate() Position = player.Position, Club = player.Team, League = player.League, - Starting11 = player.Starting11 ? "Yes" : "No" + Starting11 = player.Starting11 ? "Yes" : "No", }; } @@ -139,7 +139,7 @@ public static PlayerRequestModel MakeRequestModelForRetrieve(int squadNumber) SquadNumber = player.SquadNumber, AbbrPosition = player.AbbrPosition, Team = player.Team, - League = player.League + League = player.League, }; } @@ -165,7 +165,7 @@ public static PlayerResponseModel MakeResponseModelForRetrieve(int squadNumber) Position = player.Position, Club = player.Team, League = player.League, - Starting11 = player.Starting11 ? "Yes" : "No" + Starting11 = player.Starting11 ? "Yes" : "No", }; } @@ -189,9 +189,9 @@ .. PlayerData Position = player.Position, Club = player.Team, League = player.League, - Starting11 = player.Starting11 ? "Yes" : "No" + Starting11 = player.Starting11 ? "Yes" : "No", }; - }) + }), ]; /* ------------------------------------------------------------------------- From 690e8f6e7a0f56367b30b0c2aadd9905a13ce362 Mon Sep 17 00:00:00 2001 From: Nano Taboada <87288+nanotaboada@users.noreply.github.com> Date: Sun, 29 Mar 2026 17:11:21 -0300 Subject: [PATCH 2/7] chore(ci): harden SonarCloud exclusions and CodeRabbit paths (#435) Co-authored-by: Claude Sonnet 4.6 --- .coderabbit.yaml | 22 ++++++++++++++++++++++ sonar-project.properties | 21 +++++++++++++++++++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 7b780e1..8f0cb20 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -77,6 +77,28 @@ reviews: - Ensure error messages are descriptive - Verify validators are registered in DI container + - path: "src/**/Mappings/**/*.cs" + instructions: | + - Profiles should inherit from AutoMapper Profile + - Verify CreateMap() calls cover all DTOs in use + - Check reverse mappings are defined where bidirectional mapping is needed + - Ensure profiles are registered via AddAutoMapper() in the DI setup + - Flag any manual property mappings that could be replaced by convention + + - path: "src/**/Middlewares/**/*.cs" + instructions: | + - Middleware must implement InvokeAsync(HttpContext context) signature + - Verify _next(context) is called unless the middleware intentionally short-circuits + - Ensure exceptions are not swallowed — rethrow or translate to Problem Details + - Middleware should be stateless; dependencies requiring request scope belong in services + + - path: "src/**/Extensions/**/*.cs" + instructions: | + - Extension methods should target IServiceCollection and return IServiceCollection + - Each method should encapsulate a single registration concern + - Verify service lifetimes (Singleton/Scoped/Transient) match the dependency's usage + - Follow the Add{Feature} naming convention (e.g. AddPlayerServices, AddRateLimiting) + - path: "test/**/*.cs" instructions: | - Tests should use xUnit, Moq, and FluentAssertions diff --git a/sonar-project.properties b/sonar-project.properties index b019777..4c450ac 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -28,7 +28,8 @@ sonar.test.inclusions=test/**/*.cs sonar.exclusions=\ **/obj/**,\ **/bin/**,\ - **/Migrations/** + **/Migrations/**,\ + **/*.Designer.cs # ============================================================================= # Coverage exclusions @@ -39,7 +40,8 @@ sonar.exclusions=\ sonar.coverage.exclusions=\ test/**/*.cs,\ **/Migrations/**,\ - **/Data/PlayerDbContext.cs + **/Data/PlayerDbContext.cs,\ + **/Program.cs # ============================================================================= # Duplicate code (CPD) exclusions @@ -47,15 +49,30 @@ sonar.coverage.exclusions=\ # hotspots) but has no effect on CPD metrics. Files must be listed here to be # excluded from duplicate code detection. # +# **/Migrations/** — EF Core migration files are intentionally repetitive +# (sequential InsertData/UpdateData/Sql calls); must be listed explicitly +# here even though already present in sonar.exclusions, as CPD is evaluated +# independently of the global exclusion list. +# +# **/Data/PlayerDbContext.cs — scaffolded EF Core infrastructure; excluded +# from coverage metrics above for the same reason. +# # test/**/*.cs — Fakes, Mocks, and Stubs are intentionally repetitive by # design; similarity between arrange/act/assert blocks across tests is # expected and harmless. # +# **/Utilities/PlayerData.cs — parallel WithId/without-Id method pairs +# (MakeStarting11/MakeStarting11WithId, GetSubstitutes/GetSubstitutesWithId) +# mirror each other by design; duplication is structural, not accidental. +# # **/Validators/PlayerRequestModelValidator.cs — the "Create" and "Update" # rule sets share common rules by design; the duplication is intentional to # keep each operation's validation self-contained and readable. # ============================================================================= sonar.cpd.exclusions=\ + **/Migrations/**,\ + **/Data/PlayerDbContext.cs,\ test/**/*.cs,\ + **/Utilities/PlayerData.cs,\ **/Validators/PlayerRequestModelValidator.cs From 9586e357ec6ee878eedc4c4b3ca525478c80796a Mon Sep 17 00:00:00 2001 From: Nano Taboada <87288+nanotaboada@users.noreply.github.com> Date: Sun, 29 Mar 2026 17:49:03 -0300 Subject: [PATCH 3/7] fix(data): sync JSON literal and derive Delete test squadNumber (#435) Co-authored-by: Claude Sonnet 4.6 --- .../Utilities/PlayerData.cs | 10 +++++----- .../Unit/PlayerControllerTests.cs | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Utilities/PlayerData.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Utilities/PlayerData.cs index b3ef354..6bb2b9f 100644 --- a/src/Dotnet.Samples.AspNetCore.WebApi/Utilities/PlayerData.cs +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Utilities/PlayerData.cs @@ -873,8 +873,8 @@ public static List MakeStarting11FromDeserializedJson() "squadNumber": 24, "position": "Central Midfield", "abbrPosition": "CM", - "team": "Chelsea FC", - "league": "Premier League", + "team": "SL Benfica", + "league": "Liga Portugal", "starting11": true }, { @@ -885,7 +885,7 @@ public static List MakeStarting11FromDeserializedJson() "squadNumber": 20, "position": "Central Midfield", "abbrPosition": "CM", - "team": "Liverpool FC", + "team": "Brighton & Hove Albion", "league": "Premier League", "starting11": true }, @@ -897,8 +897,8 @@ public static List MakeStarting11FromDeserializedJson() "squadNumber": 10, "position": "Right Winger", "abbrPosition": "RW", - "team": "Inter Miami CF", - "league": "Major League Soccer", + "team": "Paris Saint-Germain", + "league": "Ligue 1", "starting11": true }, { diff --git a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerControllerTests.cs b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerControllerTests.cs index 6cb055f..72deea7 100644 --- a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerControllerTests.cs +++ b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerControllerTests.cs @@ -486,8 +486,8 @@ public async Task Delete_PlayerBySquadNumber_NonExisting_Returns404NotFound() public async Task Delete_PlayerBySquadNumber_Existing_Returns204NoContent() { // Arrange - var squadNumber = 27; var response = PlayerFakes.MakeResponseModelForCreate(); + var squadNumber = response.Dorsal; var (service, logger, validator) = PlayerMocks.InitControllerMocks(); service .Setup(service => service.RetrieveBySquadNumberAsync(squadNumber)) From f260b06c14e98ba9b430ef68f69f5ea02d424391 Mon Sep 17 00:00:00 2001 From: Nano Taboada <87288+nanotaboada@users.noreply.github.com> Date: Sun, 29 Mar 2026 18:01:50 -0300 Subject: [PATCH 4/7] chore(ci): add .sonarcloud.properties for automatic analysis (#435) Co-authored-by: Claude Sonnet 4.6 --- .sonarcloud.properties | 98 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 .sonarcloud.properties diff --git a/.sonarcloud.properties b/.sonarcloud.properties new file mode 100644 index 0000000..8f39e8d --- /dev/null +++ b/.sonarcloud.properties @@ -0,0 +1,98 @@ +# ============================================================================= +# SonarCloud Automatic Analysis configuration +# https://docs.sonarsource.com/sonarqube-cloud/advanced-setup/automatic-analysis/ +# +# SonarCloud's automatic analysis (GitHub App, no CI workflow) reads THIS file, +# not sonar-project.properties. Wildcard patterns are NOT supported here, so +# all exclusion paths must be listed explicitly. +# +# sonar-project.properties is kept for documentation and as a fallback if +# CI-based analysis is ever introduced. +# ============================================================================= + +sonar.sources=src/ +sonar.tests=test/ +sonar.sourceEncoding=UTF-8 + +# ============================================================================= +# Global exclusions +# ============================================================================= + +sonar.exclusions=\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20250414191223_InitialCreate.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20250414191223_InitialCreate.Designer.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20250414195445_SeedStarting11.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20250414195445_SeedStarting11.Designer.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20251221220614_SeedSubstitutes.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20251221220614_SeedSubstitutes.Designer.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260329000000_NormalizePlayerDataset.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260329000000_NormalizePlayerDataset.Designer.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/PlayerDbContextModelSnapshot.cs + +# ============================================================================= +# Coverage exclusions +# Test files and generated/infrastructure code should not count against +# production code coverage metrics. +# ============================================================================= + +sonar.coverage.exclusions=\ + test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerControllerTests.cs,\ + test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerServiceTests.cs,\ + test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerValidatorTests.cs,\ + test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/DatabaseFakes.cs,\ + test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerFakes.cs,\ + test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerMocks.cs,\ + test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerStubs.cs,\ + test/Dotnet.Samples.AspNetCore.WebApi.Tests/Usings.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20250414191223_InitialCreate.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20250414191223_InitialCreate.Designer.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20250414195445_SeedStarting11.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20250414195445_SeedStarting11.Designer.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20251221220614_SeedSubstitutes.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20251221220614_SeedSubstitutes.Designer.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260329000000_NormalizePlayerDataset.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260329000000_NormalizePlayerDataset.Designer.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/PlayerDbContextModelSnapshot.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Data/PlayerDbContext.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Program.cs + +# ============================================================================= +# Duplicate code (CPD) exclusions +# Automatic analysis does not support wildcard patterns, so each file is +# listed explicitly. +# +# Migrations — EF Core migration files are intentionally repetitive +# (sequential InsertData/UpdateData/Sql calls). +# +# PlayerDbContext.cs — scaffolded EF Core infrastructure. +# +# PlayerData.cs — parallel WithId/without-Id method pairs mirror each other +# by design; duplication is structural, not accidental. +# +# PlayerRequestModelValidator.cs — the "Create" and "Update" rule sets share +# common rules by design; duplication is intentional. +# +# Test files — Fakes, Mocks, and Stubs are intentionally repetitive by design. +# ============================================================================= + +sonar.cpd.exclusions=\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20250414191223_InitialCreate.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20250414191223_InitialCreate.Designer.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20250414195445_SeedStarting11.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20250414195445_SeedStarting11.Designer.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20251221220614_SeedSubstitutes.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20251221220614_SeedSubstitutes.Designer.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260329000000_NormalizePlayerDataset.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260329000000_NormalizePlayerDataset.Designer.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/PlayerDbContextModelSnapshot.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Data/PlayerDbContext.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Utilities/PlayerData.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Validators/PlayerRequestModelValidator.cs,\ + test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerControllerTests.cs,\ + test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerServiceTests.cs,\ + test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerValidatorTests.cs,\ + test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/DatabaseFakes.cs,\ + test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerFakes.cs,\ + test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerMocks.cs,\ + test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerStubs.cs,\ + test/Dotnet.Samples.AspNetCore.WebApi.Tests/Usings.cs From 9a04c829eea535d7836f4ff18572711fb5c5f260 Mon Sep 17 00:00:00 2001 From: Nano Taboada <87288+nanotaboada@users.noreply.github.com> Date: Sun, 29 Mar 2026 18:05:44 -0300 Subject: [PATCH 5/7] chore(ci): drop sonar-project.properties for .sonarcloud.properties (#435) Co-authored-by: Claude Sonnet 4.6 --- sonar-project.properties | 78 ---------------------------------------- 1 file changed, 78 deletions(-) delete mode 100644 sonar-project.properties diff --git a/sonar-project.properties b/sonar-project.properties deleted file mode 100644 index 4c450ac..0000000 --- a/sonar-project.properties +++ /dev/null @@ -1,78 +0,0 @@ -# ============================================================================= -# SonarCloud configuration -# https://docs.sonarsource.com/sonarcloud/advanced-setup/analysis-parameters/ -# -# Project key and organization can be verified in SonarCloud under: -# Administration → General Settings -# ============================================================================= - -sonar.projectKey=nanotaboada_Dotnet.Samples.AspNetCore.WebApi -sonar.organization=nanotaboada - -# Source encoding -sonar.sourceEncoding=UTF-8 - -# ============================================================================= -# Sources and tests -# ============================================================================= - -sonar.sources=src/ -sonar.tests=test/ -sonar.test.inclusions=test/**/*.cs - -# ============================================================================= -# Global exclusions -# Keeps analysis focused on hand-written production code. -# ============================================================================= - -sonar.exclusions=\ - **/obj/**,\ - **/bin/**,\ - **/Migrations/**,\ - **/*.Designer.cs - -# ============================================================================= -# Coverage exclusions -# Test files and generated/infrastructure code should not count against -# production code coverage metrics. -# ============================================================================= - -sonar.coverage.exclusions=\ - test/**/*.cs,\ - **/Migrations/**,\ - **/Data/PlayerDbContext.cs,\ - **/Program.cs - -# ============================================================================= -# Duplicate code (CPD) exclusions -# NOTE: // NOSONAR suppresses rule-based issues (bugs, code smells, security -# hotspots) but has no effect on CPD metrics. Files must be listed here to be -# excluded from duplicate code detection. -# -# **/Migrations/** — EF Core migration files are intentionally repetitive -# (sequential InsertData/UpdateData/Sql calls); must be listed explicitly -# here even though already present in sonar.exclusions, as CPD is evaluated -# independently of the global exclusion list. -# -# **/Data/PlayerDbContext.cs — scaffolded EF Core infrastructure; excluded -# from coverage metrics above for the same reason. -# -# test/**/*.cs — Fakes, Mocks, and Stubs are intentionally repetitive by -# design; similarity between arrange/act/assert blocks across tests is -# expected and harmless. -# -# **/Utilities/PlayerData.cs — parallel WithId/without-Id method pairs -# (MakeStarting11/MakeStarting11WithId, GetSubstitutes/GetSubstitutesWithId) -# mirror each other by design; duplication is structural, not accidental. -# -# **/Validators/PlayerRequestModelValidator.cs — the "Create" and "Update" -# rule sets share common rules by design; the duplication is intentional to -# keep each operation's validation self-contained and readable. -# ============================================================================= - -sonar.cpd.exclusions=\ - **/Migrations/**,\ - **/Data/PlayerDbContext.cs,\ - test/**/*.cs,\ - **/Utilities/PlayerData.cs,\ - **/Validators/PlayerRequestModelValidator.cs From be21405d2f98c4245f0d53b66c0860878709e5dc Mon Sep 17 00:00:00 2001 From: Nano Taboada <87288+nanotaboada@users.noreply.github.com> Date: Sun, 29 Mar 2026 18:36:07 -0300 Subject: [PATCH 6/7] fix(data): exclude Lo Celso from seed data, keep as Create/Delete fixture (#435) Co-authored-by: Claude Sonnet 4.6 --- .../20260329000000_NormalizePlayerDataset.cs | 14 -------------- .../Utilities/PlayerData.cs | 17 ++--------------- .../storage/players-sqlite3.db | Bin 28672 -> 28672 bytes 3 files changed, 2 insertions(+), 29 deletions(-) diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260329000000_NormalizePlayerDataset.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260329000000_NormalizePlayerDataset.cs index 201488b..5ab5b08 100644 --- a/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260329000000_NormalizePlayerDataset.cs +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260329000000_NormalizePlayerDataset.cs @@ -93,25 +93,11 @@ protected override void Up(MigrationBuilder migrationBuilder) "UPDATE Players SET Id = '79c96f29-c59f-5f98-96b8-3a5946246624' WHERE SquadNumber = 22" ); - // Insert Lo Celso (squad 27) — OR IGNORE is safe on a fresh DB where SeedSubstitutes - // already seeded him (because GetSubstitutesWithId() now includes him), and required - // for an existing DB that was seeded before this migration was added. - migrationBuilder.Sql( - "INSERT OR IGNORE INTO \"Players\" (\"Id\", \"FirstName\", \"MiddleName\", \"LastName\", \"DateOfBirth\", \"SquadNumber\", \"Position\", \"AbbrPosition\", \"Team\", \"League\", \"Starting11\") " - + "VALUES ('F8D13028-0D22-5513-8774-08A2332B5814', 'Giovani', NULL, 'Lo Celso', '1996-04-09 00:00:00', 27, 'Central Midfield', 'CM', 'Real Betis Balompié', 'La Liga', 0)" - ); } /// protected override void Down(MigrationBuilder migrationBuilder) { - // Remove Lo Celso (squad 27) - migrationBuilder.DeleteData( - table: "Players", - keyColumn: "Id", - keyValue: Guid.Parse("f8d13028-0d22-5513-8774-08a2332b5814") - ); - // Revert team/league corrections migrationBuilder.Sql( "UPDATE Players SET Team = 'Chelsea FC', League = 'Premier League' WHERE SquadNumber = 24" diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Utilities/PlayerData.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Utilities/PlayerData.cs index 6bb2b9f..500a394 100644 --- a/src/Dotnet.Samples.AspNetCore.WebApi/Utilities/PlayerData.cs +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Utilities/PlayerData.cs @@ -540,9 +540,9 @@ public static List GetSubstitutes() } /// - /// Create a predefined list of 16 substitute players where each player has a fixed GUID identifier and full profile data. + /// Create a predefined list of 15 substitute players where each player has a fixed GUID identifier and full profile data. /// - /// A list of 16 Player instances representing substitute players; each entry includes a predefined Id (Guid) and populated fields such as name, date of birth, squad number, position (and abbreviation), team, league, and Starting11 set to false. + /// A list of 15 Player instances representing substitute players (squads 1–26); each entry includes a predefined Id (Guid) and populated fields such as name, date of birth, squad number, position (and abbreviation), team, league, and Starting11 set to false. Lo Celso (squad 27) is intentionally excluded — his squad number falls outside the seeded range so he can serve as the canonical Create/Delete fixture without conflicting with seeded data. public static List GetSubstitutesWithId() { return @@ -754,19 +754,6 @@ public static List GetSubstitutesWithId() League = "Serie A", Starting11 = false, }, - new() - { - Id = Guid.Parse("f8d13028-0d22-5513-8774-08a2332b5814"), - FirstName = "Giovani", - LastName = "Lo Celso", - DateOfBirth = new DateTime(1996, 4, 9, 0, 0, 0, DateTimeKind.Utc), - SquadNumber = 27, - Position = Position.CentralMidfield.Text, - AbbrPosition = Position.CentralMidfield.Abbr, - Team = "Real Betis Balompié", - League = "La Liga", - Starting11 = false, - }, ]; } diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/storage/players-sqlite3.db b/src/Dotnet.Samples.AspNetCore.WebApi/storage/players-sqlite3.db index b48b4e5ab0328c26585d80a2551c101ec70f863b..b8899ac8d36de5f23e03d65761b01bf5c751a8c9 100644 GIT binary patch delta 157 zcmV;O0Al}u-~oW(0gxL33XvQ`0Sd8TsSg3%vk?%=50Ov@A0r|G0{}}hFfukRFf%PO zIUq7IIx{soFf}eWH#Rjhv-%$jAOji#-;)PGHL-!C0<+&jB@G@L3)cV-*bd?hdk-`Y zJ`NQP!wdim*9@c%kPROU_Om2ViVKmDD+DM5RRNRjLL{^QTLlCI8V_cZ2Vga`5fCa5 LlVEQIlWK34n7u7k delta 314 zcmZp8z}WDBae_1>$3z)tMvjdMEBP63Zx$3d#Xm8CU0#`ynZeh{z{pJ3z*yJFQo+#D z%Fw{d*hJ6B!ot+jeDg1PE(K<3rkKg>4yH^@F&i7@n9>^=q(l;CsYV&2PeI z$0y2rn1_M;I?ocm3B2+=-?-OpRto6i*41DRWhBNe(jbMC-#V#m{vXZC!Ys|7Fqu8U Yl!-q97${2oY^= Date: Sun, 29 Mar 2026 18:41:23 -0300 Subject: [PATCH 7/7] chore(ci): remove stale sonar-project.properties reference in comment (#435) Co-authored-by: Claude Sonnet 4.6 --- .sonarcloud.properties | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.sonarcloud.properties b/.sonarcloud.properties index 8f39e8d..3010202 100644 --- a/.sonarcloud.properties +++ b/.sonarcloud.properties @@ -2,12 +2,9 @@ # SonarCloud Automatic Analysis configuration # https://docs.sonarsource.com/sonarqube-cloud/advanced-setup/automatic-analysis/ # -# SonarCloud's automatic analysis (GitHub App, no CI workflow) reads THIS file, -# not sonar-project.properties. Wildcard patterns are NOT supported here, so -# all exclusion paths must be listed explicitly. -# -# sonar-project.properties is kept for documentation and as a fallback if -# CI-based analysis is ever introduced. +# SonarCloud's automatic analysis (GitHub App, no CI workflow) reads THIS file. +# Wildcard patterns are NOT supported here, so all exclusion paths must be +# listed explicitly. # ============================================================================= sonar.sources=src/