From 142b8ba5231f85a5191dc6a472c4fb23882f2bf3 Mon Sep 17 00:00:00 2001
From: Nano Taboada <87288+nanotaboada@users.noreply.github.com>
Date: Sun, 18 Jan 2026 00:56:34 -0300
Subject: [PATCH 1/3] refactor: consolidate player data into single source of
truth (#210)
Eliminate duplication between PlayerData.cs and PlayerFakes.cs by
making PlayerFakes delegate to PlayerData methods.
- PlayerFakes.MakeStarting11() now calls PlayerData.MakeStarting11()
- PlayerFakes.MakeNew() now uses PlayerData.GetSubstitutes()
- All factory methods updated to delegate to PlayerData
- Enhanced XML documentation to clarify SSOT relationship
---
.../Utilities/PlayerFakes.cs | 239 ++++--------------
1 file changed, 55 insertions(+), 184 deletions(-)
diff --git a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerFakes.cs b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerFakes.cs
index 3f98710..15cfe5d 100644
--- a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerFakes.cs
+++ b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerFakes.cs
@@ -1,201 +1,67 @@
-using Dotnet.Samples.AspNetCore.WebApi.Enums;
using Dotnet.Samples.AspNetCore.WebApi.Models;
+using Dotnet.Samples.AspNetCore.WebApi.Utilities;
namespace Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities;
///
-/// A Fake is a working implementation that’s simpler than the real one.
-/// It usually has some “real” logic but is not suitable for production
+/// Test data factory for Player entities.
+/// Wraps production data from PlayerData with test-specific modifications (e.g., GUID assignment).
+/// A Fake is a working implementation that's simpler than the real one.
+/// It usually has some "real" logic but is not suitable for production
/// (e.g., an in‑memory database instead of a full SQL Server). Fakes are
-/// useful when you need behavior that’s closer to reality but still want
+/// useful when you need behavior that's closer to reality but still want
/// to avoid external dependencies.
///
public static class PlayerFakes
{
+ ///
+ /// Returns the starting 11 players with generated GUIDs for in-memory testing.
+ /// Reuses production player data from PlayerData.MakeStarting11().
+ ///
public static List MakeStarting11()
{
- return
- [
- new()
- {
- Id = Guid.NewGuid(),
- FirstName = "Damián",
- MiddleName = "Emiliano",
- LastName = "Martínez",
- DateOfBirth = new DateTime(1992, 9, 1, 0, 0, 0, DateTimeKind.Utc),
- SquadNumber = 23,
- Position = Position.Goalkeeper.Text,
- AbbrPosition = Position.Goalkeeper.Abbr,
- Team = "Aston Villa FC",
- League = "Premier League",
- Starting11 = true,
- },
- new()
- {
- Id = Guid.NewGuid(),
- FirstName = "Nahuel",
- LastName = "Molina",
- DateOfBirth = new DateTime(1998, 4, 5, 0, 0, 0, DateTimeKind.Utc),
- SquadNumber = 26,
- Position = Position.RightBack.Text,
- AbbrPosition = Position.RightBack.Abbr,
- Team = "Altético Madrid",
- League = "La Liga",
- Starting11 = true,
- },
- new()
- {
- Id = Guid.NewGuid(),
- FirstName = "Cristian",
- MiddleName = "Gabriel",
- LastName = "Romero",
- DateOfBirth = new DateTime(1998, 4, 26, 0, 0, 0, DateTimeKind.Utc),
- SquadNumber = 13,
- Position = Position.CentreBack.Text,
- AbbrPosition = Position.CentreBack.Abbr,
- Team = "Tottenham Hotspur",
- League = "Premier League",
- Starting11 = true,
- },
- new()
- {
- Id = Guid.NewGuid(),
- FirstName = "Nicolás",
- MiddleName = "Hernán Gonzalo",
- LastName = "Otamendi",
- DateOfBirth = new DateTime(1988, 2, 11, 0, 0, 0, DateTimeKind.Utc),
- SquadNumber = 19,
- Position = Position.CentreBack.Text,
- AbbrPosition = Position.CentreBack.Abbr,
- Team = "SL Benfica",
- League = "Liga Portugal",
- Starting11 = true,
- },
- new()
- {
- Id = Guid.NewGuid(),
- FirstName = "Nicolás",
- MiddleName = "Alejandro",
- LastName = "Tagliafico",
- DateOfBirth = new DateTime(1992, 8, 30, 0, 0, 0, DateTimeKind.Utc),
- SquadNumber = 3,
- Position = Position.LeftBack.Text,
- AbbrPosition = Position.LeftBack.Abbr,
- Team = "Olympique Lyon",
- League = "Ligue 1",
- Starting11 = true,
- },
- new()
- {
- Id = Guid.NewGuid(),
- FirstName = "Ángel",
- MiddleName = "Fabián",
- LastName = "Di María",
- DateOfBirth = new DateTime(1988, 2, 13, 0, 0, 0, DateTimeKind.Utc),
- SquadNumber = 11,
- Position = Position.RightWinger.Text,
- AbbrPosition = Position.RightWinger.Abbr,
- Team = "SL Benfica",
- League = "Liga Portugal",
- Starting11 = true,
- },
- new()
- {
- Id = Guid.NewGuid(),
- FirstName = "Rodrigo",
- MiddleName = "Javier",
- LastName = "de Paul",
- DateOfBirth = new DateTime(1994, 5, 23, 0, 0, 0, DateTimeKind.Utc),
- SquadNumber = 7,
- Position = Position.CentralMidfield.Text,
- AbbrPosition = Position.CentralMidfield.Abbr,
- Team = "Altético Madrid",
- League = "La Liga",
- Starting11 = true,
- },
- new()
- {
- Id = Guid.NewGuid(),
- FirstName = "Enzo",
- MiddleName = "Jeremías",
- LastName = "Fernández",
- DateOfBirth = new DateTime(2001, 1, 16, 0, 0, 0, DateTimeKind.Utc),
- SquadNumber = 24,
- Position = Position.CentralMidfield.Text,
- AbbrPosition = Position.CentralMidfield.Abbr,
- Team = "Chelsea FC",
- League = "Premier League",
- Starting11 = true,
- },
- new()
- {
- Id = Guid.NewGuid(),
- 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",
- League = "Premier League",
- Starting11 = true,
- },
- new()
- {
- Id = Guid.NewGuid(),
- FirstName = "Lionel",
- MiddleName = "Andrés",
- LastName = "Messi",
- DateOfBirth = new DateTime(1987, 6, 23, 0, 0, 0, DateTimeKind.Utc),
- SquadNumber = 10,
- Position = Position.RightWinger.Text,
- AbbrPosition = Position.RightWinger.Abbr,
- Team = "Inter Miami CF",
- League = "Major League Soccer",
- Starting11 = true,
- },
- new()
- {
- Id = Guid.NewGuid(),
- FirstName = "Julián",
- LastName = "Álvarez",
- DateOfBirth = new DateTime(2000, 1, 30, 0, 0, 0, DateTimeKind.Utc),
- SquadNumber = 9,
- Position = Position.CentreForward.Text,
- AbbrPosition = Position.CentreForward.Abbr,
- Team = "Manchester City",
- League = "Premier League",
- Starting11 = true,
- }
- ];
+ var players = PlayerData.MakeStarting11();
+
+ // Assign GUIDs for in-memory database testing
+ foreach (var player in players)
+ {
+ player.Id = Guid.NewGuid();
+ }
+
+ return players;
}
+ ///
+ /// Returns a specific player from the starting 11 by squad number.
+ /// Reuses production player data from PlayerData.MakeStarting11().
+ ///
public static Player MakeFromStarting11(int squadNumber)
{
var player =
- MakeStarting11().SingleOrDefault(player => player.SquadNumber == squadNumber)
+ PlayerData.MakeStarting11().SingleOrDefault(p => p.SquadNumber == squadNumber)
?? throw new ArgumentNullException(
$"Player with Squad Number {squadNumber} not found."
);
+ player.Id = Guid.NewGuid();
return player;
}
+ ///
+ /// Returns a new player (substitute) for testing create operations.
+ /// Reuses production player data from PlayerData.GetSubstitutes().
+ ///
public static Player MakeNew()
{
- return new()
- {
- FirstName = "Leandro",
- MiddleName = "Daniel",
- LastName = "Paredes",
- DateOfBirth = new DateTime(1994, 06, 29, 0, 0, 0, DateTimeKind.Utc),
- SquadNumber = 5,
- Position = Position.DefensiveMidfield.Text,
- AbbrPosition = Position.DefensiveMidfield.Abbr,
- Team = "AS Roma",
- League = "Serie A",
- Starting11 = false
- };
+ // Get Leandro Paredes (squad number 5) from substitutes
+ var player =
+ PlayerData.GetSubstitutes().SingleOrDefault(player => player.SquadNumber == 5)
+ ?? throw new InvalidOperationException(
+ "Substitute player with squad number 5 not found."
+ );
+
+ player.Id = Guid.NewGuid();
+ return player;
}
/* -------------------------------------------------------------------------
@@ -243,7 +109,7 @@ public static PlayerResponseModel MakeResponseModelForCreate()
public static PlayerRequestModel MakeRequestModelForRetrieve(int squadNumber)
{
var player =
- MakeStarting11().SingleOrDefault(player => player.SquadNumber == squadNumber)
+ PlayerData.MakeStarting11().SingleOrDefault(p => p.SquadNumber == squadNumber)
?? throw new ArgumentNullException(
$"Player with Squad Number {squadNumber} not found."
);
@@ -264,7 +130,7 @@ public static PlayerRequestModel MakeRequestModelForRetrieve(int squadNumber)
public static PlayerResponseModel MakeResponseModelForRetrieve(int squadNumber)
{
var player =
- MakeStarting11().SingleOrDefault(player => player.SquadNumber == squadNumber)
+ PlayerData.MakeStarting11().SingleOrDefault(p => p.SquadNumber == squadNumber)
?? throw new ArgumentNullException(
$"Player with Squad Number {squadNumber} not found."
);
@@ -284,17 +150,22 @@ public static PlayerResponseModel MakeResponseModelForRetrieve(int squadNumber)
public static List MakeResponseModelsForRetrieve() =>
[
- .. MakeStarting11()
- .Select(player => new PlayerResponseModel
+ .. PlayerData
+ .MakeStarting11()
+ .Select(player =>
{
- FullName =
- $"{player.FirstName} {(string.IsNullOrWhiteSpace(player.MiddleName) ? "" : player.MiddleName + " ")}{player.LastName}".Trim(),
- Birth = $"{player.DateOfBirth:MMMM d, yyyy}",
- Dorsal = player.SquadNumber,
- Position = player.Position,
- Club = player.Team,
- League = player.League,
- Starting11 = player.Starting11 ? "Yes" : "No"
+ player.Id = Guid.NewGuid();
+ return new PlayerResponseModel
+ {
+ FullName =
+ $"{player.FirstName} {(string.IsNullOrWhiteSpace(player.MiddleName) ? "" : player.MiddleName + " ")}{player.LastName}".Trim(),
+ Birth = $"{player.DateOfBirth:MMMM d, yyyy}",
+ Dorsal = player.SquadNumber,
+ Position = player.Position,
+ Club = player.Team,
+ League = player.League,
+ Starting11 = player.Starting11 ? "Yes" : "No"
+ };
})
];
From ac62c9a432a5cd7e0f96e0b6b59c5e7527330bed Mon Sep 17 00:00:00 2001
From: Nano Taboada <87288+nanotaboada@users.noreply.github.com>
Date: Sun, 18 Jan 2026 01:15:21 -0300
Subject: [PATCH 2/3] chore: optimize CI triggers to reduce pipeline costs
Remove feature/* from pipeline triggers to avoid running CI on every
push to development branches. CI now only runs on:
- Pushes to master (production)
- Pull requests to master (validation before merge)
Development branches using conventional commit naming (refactor/*,
fix/*, feat/*, etc.) will only trigger CI when a PR is opened,
not on every commit. This maintains code quality validation while
reducing Azure DevOps pipeline execution costs.
---
azure-pipelines.yml | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index cb0d074..7e760eb 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -2,10 +2,13 @@
# Build and test ASP.NET Core projects targeting .NET 8 on Linux.
# https://learn.microsoft.com/en-us/azure/devops/pipelines/ecosystems/dotnet-core
-# Trigger pipeline on commits to master and pull requests (equivalent to GitHub Actions 'on: [push, pull_request]')
+# Pipeline triggers:
+# - Runs on pushes to master (production deployments)
+# - Runs on all pull requests to master (validation before merge)
+# - Does NOT run on pushes to development branches (cost optimization)
+# Development branches (refactor/*, fix/*, feat/*, etc.) are validated only when a PR is opened
trigger:
- master
- - feature/*
pr:
- master
From 0d34a180237a8a03fe0759c8a9608ef9c5adcc75 Mon Sep 17 00:00:00 2001
From: Nano Taboada <87288+nanotaboada@users.noreply.github.com>
Date: Sun, 18 Jan 2026 01:22:21 -0300
Subject: [PATCH 3/3] docs: add XML documentation to utilities and
configurations
---
.../AuthorizeCheckOperationFilter.cs | 5 +++
.../Utilities/PlayerData.cs | 9 ++++++
.../Utilities/DatabaseFakes.cs | 20 ++++++++++++
.../Utilities/PlayerFakes.cs | 32 +++++++++++++++++++
.../Utilities/PlayerMocks.cs | 20 ++++++++++++
.../Utilities/PlayerStubs.cs | 7 ++++
6 files changed, 93 insertions(+)
diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Configurations/AuthorizeCheckOperationFilter.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Configurations/AuthorizeCheckOperationFilter.cs
index 31c710d..4d8895d 100644
--- a/src/Dotnet.Samples.AspNetCore.WebApi/Configurations/AuthorizeCheckOperationFilter.cs
+++ b/src/Dotnet.Samples.AspNetCore.WebApi/Configurations/AuthorizeCheckOperationFilter.cs
@@ -10,6 +10,11 @@ namespace Dotnet.Samples.AspNetCore.WebApi.Configurations
///
public class AuthorizeCheckOperationFilter : IOperationFilter
{
+ ///
+ /// Applies the Bearer security requirement to Swagger operations with [Authorize] attributes.
+ ///
+ /// The OpenAPI operation to modify.
+ /// The operation filter context containing method metadata.
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
// Check if [Authorize] is applied at the method or class level
diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Utilities/PlayerData.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Utilities/PlayerData.cs
index 1b986e6..6facad1 100644
--- a/src/Dotnet.Samples.AspNetCore.WebApi/Utilities/PlayerData.cs
+++ b/src/Dotnet.Samples.AspNetCore.WebApi/Utilities/PlayerData.cs
@@ -4,8 +4,17 @@
namespace Dotnet.Samples.AspNetCore.WebApi.Utilities;
+///
+/// Provides static player data for database seeding and testing.
+/// Single source of truth for all player definitions.
+///
public static class PlayerData
{
+ ///
+ /// Returns the starting 11 players without IDs (for EF Core auto-increment).
+ /// Used for database migrations and seeding.
+ ///
+ /// List of 11 Player entities representing the starting lineup.
public static List MakeStarting11()
{
return
diff --git a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/DatabaseFakes.cs b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/DatabaseFakes.cs
index 470b27e..5c1af3f 100644
--- a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/DatabaseFakes.cs
+++ b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/DatabaseFakes.cs
@@ -14,6 +14,11 @@ namespace Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities
///
public static class DatabaseFakes
{
+ ///
+ /// Creates an in-memory SQLite connection and DbContext options for testing.
+ /// The connection remains open for the lifetime of the test.
+ ///
+ /// A tuple containing the SQLite connection and DbContext options.
public static (DbConnection, DbContextOptions) CreateSqliteConnection()
{
var dbConnection = new SqliteConnection("Filename=:memory:");
@@ -26,6 +31,11 @@ public static (DbConnection, DbContextOptions) CreateSqliteConn
return (dbConnection, dbContextOptions);
}
+ ///
+ /// Creates a PlayerDbContext instance with the specified options.
+ ///
+ /// The DbContext options to use.
+ /// A new PlayerDbContext instance.
public static PlayerDbContext CreateDbContext(
DbContextOptions dbContextOptions
)
@@ -33,6 +43,11 @@ DbContextOptions dbContextOptions
return new PlayerDbContext(dbContextOptions);
}
+ ///
+ /// Creates the database schema for the test database.
+ /// Extension method for PlayerDbContext.
+ ///
+ /// The PlayerDbContext instance.
public static void CreateTable(this PlayerDbContext context)
{
using var cmd = context.Database.GetDbConnection().CreateCommand();
@@ -46,6 +61,11 @@ CREATE TABLE players (
cmd.ExecuteNonQuery();
}
+ ///
+ /// Seeds the test database with the starting 11 players.
+ /// Extension method for PlayerDbContext.
+ ///
+ /// The PlayerDbContext instance.
public static void Seed(this PlayerDbContext context)
{
context.Players.AddRange(PlayerFakes.MakeStarting11());
diff --git a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerFakes.cs b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerFakes.cs
index 15cfe5d..7c118b3 100644
--- a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerFakes.cs
+++ b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerFakes.cs
@@ -68,6 +68,10 @@ public static Player MakeNew()
* Create
* ---------------------------------------------------------------------- */
+ ///
+ /// Creates a PlayerRequestModel for testing create operations.
+ /// Uses data from a substitute player (Leandro Paredes).
+ ///
public static PlayerRequestModel MakeRequestModelForCreate()
{
var player = MakeNew();
@@ -85,6 +89,10 @@ public static PlayerRequestModel MakeRequestModelForCreate()
};
}
+ ///
+ /// Creates a PlayerResponseModel for testing create operation responses.
+ /// Uses data from a substitute player (Leandro Paredes).
+ ///
public static PlayerResponseModel MakeResponseModelForCreate()
{
var player = MakeNew();
@@ -106,6 +114,11 @@ public static PlayerResponseModel MakeResponseModelForCreate()
* Retrieve
* ---------------------------------------------------------------------- */
+ ///
+ /// Creates a PlayerRequestModel for testing retrieve operations.
+ /// Uses data from the starting 11 based on the specified squad number.
+ ///
+ /// The squad number of the player to retrieve.
public static PlayerRequestModel MakeRequestModelForRetrieve(int squadNumber)
{
var player =
@@ -127,6 +140,11 @@ public static PlayerRequestModel MakeRequestModelForRetrieve(int squadNumber)
};
}
+ ///
+ /// Creates a PlayerResponseModel for testing retrieve operation responses.
+ /// Uses data from the starting 11 based on the specified squad number.
+ ///
+ /// The squad number of the player to retrieve.
public static PlayerResponseModel MakeResponseModelForRetrieve(int squadNumber)
{
var player =
@@ -148,6 +166,10 @@ public static PlayerResponseModel MakeResponseModelForRetrieve(int squadNumber)
};
}
+ ///
+ /// Creates a list of PlayerResponseModel for testing retrieve all operations.
+ /// Uses all players from the starting 11.
+ ///
public static List MakeResponseModelsForRetrieve() =>
[
.. PlayerData
@@ -173,11 +195,21 @@ .. PlayerData
* Update
* ---------------------------------------------------------------------- */
+ ///
+ /// Creates a PlayerRequestModel for testing update operations.
+ /// Delegates to MakeRequestModelForRetrieve.
+ ///
+ /// The squad number of the player to update.
public static PlayerRequestModel MakeRequestModelForUpdate(int squadNumber)
{
return MakeRequestModelForRetrieve(squadNumber);
}
+ ///
+ /// Creates a PlayerResponseModel for testing update operation responses.
+ /// Delegates to MakeResponseModelForRetrieve.
+ ///
+ /// The squad number of the player to update.
public static PlayerResponseModel MakeResponseModelForUpdate(int squadNumber)
{
return MakeResponseModelForRetrieve(squadNumber);
diff --git a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerMocks.cs b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerMocks.cs
index 1c07405..4b52fde 100644
--- a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerMocks.cs
+++ b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerMocks.cs
@@ -22,6 +22,10 @@ namespace Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities
///
public static class PlayerMocks
{
+ ///
+ /// Initializes mocks for PlayerController dependencies.
+ ///
+ /// A tuple containing mocked service, logger, and validator.
public static (
Mock service,
Mock> logger,
@@ -35,6 +39,10 @@ Mock> validator
return (service, logger, validator);
}
+ ///
+ /// Creates a mock IUrlHelper configured to return any string for Action calls.
+ ///
+ /// A configured Mock of IUrlHelper.
public static Mock SetupUrlHelperMock()
{
var mock = new Mock();
@@ -43,6 +51,12 @@ public static Mock SetupUrlHelperMock()
return mock;
}
+ ///
+ /// Initializes mocks for PlayerService dependencies.
+ ///
+ /// Optional cache value to return from memory cache.
+ /// The environment name (defaults to "Development").
+ /// A tuple containing mocked repository, logger, cache, mapper, and environment.
public static (
Mock repository,
Mock> logger,
@@ -60,6 +74,12 @@ Mock environment
return (repository, logger, memoryCache, mapper, environment);
}
+ ///
+ /// Creates a mock IMemoryCache with configurable behavior.
+ /// First TryGetValue call returns false, subsequent calls return true with the specified value.
+ ///
+ /// The value to return from cache after the first call.
+ /// A configured Mock of IMemoryCache.
public static Mock SetupMemoryCacheMock(object? value)
{
var cachedValue = false;
diff --git a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerStubs.cs b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerStubs.cs
index 282eb17..18bdb4d 100644
--- a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerStubs.cs
+++ b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerStubs.cs
@@ -14,6 +14,13 @@ namespace Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities
///
public static class PlayerStubs
{
+ ///
+ /// Creates a ModelStateDictionary with a single validation error.
+ /// Used for testing validation failure scenarios.
+ ///
+ /// The property name or key for the error.
+ /// The error message.
+ /// A ModelStateDictionary containing the specified error.
public static ModelStateDictionary CreateModelError(string key, string errorMessage)
{
var modelStateDictionary = new ModelStateDictionary();