From 7028e73d58e58daf4b0e77ffb355c14fb7567a5f Mon Sep 17 00:00:00 2001 From: Nano Taboada <87288+nanotaboada@users.noreply.github.com> Date: Sat, 21 Mar 2026 17:03:42 -0300 Subject: [PATCH 1/4] fix(validators): scope BeUniqueSquadNumber to \"Create\" rule set (#424) Organizes all PlayerRequestModelValidator rules into explicit CRUD-named rule sets: \"Create\" (POST, includes BeUniqueSquadNumber) and \"Update\" (PUT, omits it). The controller passes IncludeRuleSets(\"Create\") or IncludeRuleSets(\"Update\") so only the appropriate rules run per operation. Adds ValidateAsync_SquadNumber_BelongsToPlayerBeingUpdated_ReturnsNoErrors as a regression test for the PUT 400 bug. Updates all controller test mocks to match on IValidationContext (the overload the rule-set extension method routes through). Co-authored-by: Claude --- .../Controllers/PlayerController.cs | 14 ++- .../Validators/PlayerRequestModelValidator.cs | 119 ++++++++++++++---- .../Unit/PlayerControllerTests.cs | 39 +++--- .../Unit/PlayerValidatorTests.cs | 88 +++++++++++-- 4 files changed, 207 insertions(+), 53 deletions(-) diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs index ada6515..1bf26ca 100644 --- a/src/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs @@ -36,7 +36,11 @@ IValidator validator [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task PostAsync([FromBody] PlayerRequestModel player) { - var validation = await validator.ValidateAsync(player); + // Use the "Create" rule set, which includes BeUniqueSquadNumber. + var validation = await validator.ValidateAsync( + player, + options => options.IncludeRuleSets("Create") + ); if (!validation.IsValid) { @@ -199,7 +203,13 @@ public async Task PutAsync( [FromBody] PlayerRequestModel player ) { - var validation = await validator.ValidateAsync(player); + // Use the "Update" rule set, which omits BeUniqueSquadNumber. + // The player being updated already exists in the database, so a + // uniqueness check on its own squad number would always fail. + var validation = await validator.ValidateAsync( + player, + options => options.IncludeRuleSets("Update") + ); if (!validation.IsValid) { var errors = validation diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Validators/PlayerRequestModelValidator.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Validators/PlayerRequestModelValidator.cs index bfb42e5..0d36726 100644 --- a/src/Dotnet.Samples.AspNetCore.WebApi/Validators/PlayerRequestModelValidator.cs +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Validators/PlayerRequestModelValidator.cs @@ -11,8 +11,24 @@ namespace Dotnet.Samples.AspNetCore.WebApi.Validators; /// PlayerRequestModel. /// /// -/// This class is part of the FluentValidation library, which provides a fluent -/// interface for building validation rules. +/// Rules are organized into CRUD-named rule sets to make their intent explicit. +/// This prevents BeUniqueSquadNumber from running on PUT requests, where +/// the player's squad number already exists in the database by definition. +/// +/// +/// +/// "Create" — used by POST /players; includes all rules plus +/// the uniqueness check for SquadNumber. +/// +/// +/// "Update" — used by PUT /players/squadNumber/{n}; same +/// rules, but BeUniqueSquadNumber is intentionally omitted. +/// +/// +/// +/// Controllers pass opts.IncludeRuleSets("Create") or +/// opts.IncludeRuleSets("Update") so that only the appropriate rule +/// set runs for each operation. /// public class PlayerRequestModelValidator : AbstractValidator { @@ -26,35 +42,88 @@ public PlayerRequestModelValidator( _playerRepository = playerRepository; var clock = timeProvider ?? TimeProvider.System; - RuleFor(player => player.FirstName).NotEmpty().WithMessage("FirstName is required."); + // "Create" rule set — POST /players + // Includes BeUniqueSquadNumber to prevent duplicate squad numbers on insert. + RuleSet( + "Create", + () => + { + RuleFor(player => player.FirstName) + .NotEmpty() + .WithMessage("FirstName is required."); + + RuleFor(player => player.LastName).NotEmpty().WithMessage("LastName is required."); - RuleFor(player => player.LastName).NotEmpty().WithMessage("LastName is required."); + RuleFor(player => player.SquadNumber) + .NotEmpty() + .WithMessage("SquadNumber is required.") + .GreaterThan(0) + .WithMessage("SquadNumber must be greater than 0.") + .MustAsync(BeUniqueSquadNumber) + .WithMessage("SquadNumber must be unique."); - RuleFor(player => player.SquadNumber) - .NotEmpty() - .WithMessage("SquadNumber is required.") - .GreaterThan(0) - .WithMessage("SquadNumber must be greater than 0.") - .MustAsync(BeUniqueSquadNumber) - .WithMessage("SquadNumber must be unique."); + RuleFor(player => player.AbbrPosition) + .NotEmpty() + .WithMessage("AbbrPosition is required.") + .Must(Position.IsValidAbbr) + .WithMessage("AbbrPosition is invalid."); - RuleFor(player => player.AbbrPosition) - .NotEmpty() - .WithMessage("AbbrPosition is required.") - .Must(Position.IsValidAbbr) - .WithMessage("AbbrPosition is invalid."); + When( + player => player.DateOfBirth.HasValue, + () => + { + RuleFor(player => player.DateOfBirth) + .Must(date => date!.Value.Date < clock.GetUtcNow().Date) + .WithMessage("DateOfBirth must be a date in the past.") + .Must(date => + date!.Value.Date + >= new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc) + ) + .WithMessage("DateOfBirth must be on or after January 1, 1900."); + } + ); + } + ); - When( - player => player.DateOfBirth.HasValue, + // "Update" rule set — PUT /players/squadNumber/{n} + // BeUniqueSquadNumber is intentionally omitted: on PUT the player being + // updated already exists in the database, so the check would always fail. + RuleSet( + "Update", () => { - RuleFor(player => player.DateOfBirth) - .Must(date => date!.Value.Date < clock.GetUtcNow().Date) - .WithMessage("DateOfBirth must be a date in the past.") - .Must(date => - date!.Value.Date >= new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc) - ) - .WithMessage("DateOfBirth must be on or after January 1, 1900."); + RuleFor(player => player.FirstName) + .NotEmpty() + .WithMessage("FirstName is required."); + + RuleFor(player => player.LastName).NotEmpty().WithMessage("LastName is required."); + + RuleFor(player => player.SquadNumber) + .NotEmpty() + .WithMessage("SquadNumber is required.") + .GreaterThan(0) + .WithMessage("SquadNumber must be greater than 0."); + + RuleFor(player => player.AbbrPosition) + .NotEmpty() + .WithMessage("AbbrPosition is required.") + .Must(Position.IsValidAbbr) + .WithMessage("AbbrPosition is invalid."); + + When( + player => player.DateOfBirth.HasValue, + () => + { + RuleFor(player => player.DateOfBirth) + .Must(date => date!.Value.Date < clock.GetUtcNow().Date) + .WithMessage("DateOfBirth must be a date in the past.") + .Must(date => + date!.Value.Date + >= new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc) + ) + .WithMessage("DateOfBirth must be on or after January 1, 1900."); + } + ); } ); } diff --git a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerControllerTests.cs b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerControllerTests.cs index 38af79b..5936824 100644 --- a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerControllerTests.cs +++ b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerControllerTests.cs @@ -2,6 +2,7 @@ using Dotnet.Samples.AspNetCore.WebApi.Models; using Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities; using FluentAssertions; +using FluentValidation; using FluentValidation.Results; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; @@ -31,12 +32,17 @@ public async Task Post_Players_ValidationError_Returns400BadRequest() var request = PlayerFakes.MakeRequestModelForCreate(); var (service, logger, validator) = PlayerMocks.InitControllerMocks(); validator - .Setup(validator => validator.ValidateAsync(request, It.IsAny())) + .Setup(validator => + validator.ValidateAsync( + It.IsAny(), + It.IsAny() + ) + ) .ReturnsAsync( new ValidationResult( new List { - new("SquadNumber", "SquadNumber must be greater than 0.") + new("SquadNumber", "SquadNumber must be greater than 0."), } ) ); @@ -52,7 +58,7 @@ public async Task Post_Players_ValidationError_Returns400BadRequest() validator.Verify( validator => validator.ValidateAsync( - It.IsAny(), + It.IsAny(), It.IsAny() ), Times.Once @@ -75,7 +81,7 @@ public async Task Post_Players_Existing_Returns409Conflict() validator .Setup(validator => validator.ValidateAsync( - It.IsAny(), + It.IsAny(), It.IsAny() ) ) @@ -92,7 +98,7 @@ public async Task Post_Players_Existing_Returns409Conflict() validator.Verify( validator => validator.ValidateAsync( - It.IsAny(), + It.IsAny(), It.IsAny() ), Times.Once @@ -116,7 +122,7 @@ public async Task Post_Players_NonExisting_Returns201Created() validator .Setup(validator => validator.ValidateAsync( - It.IsAny(), + It.IsAny(), It.IsAny() ) ) @@ -136,7 +142,7 @@ public async Task Post_Players_NonExisting_Returns201Created() validator.Verify( validator => validator.ValidateAsync( - It.IsAny(), + It.IsAny(), It.IsAny() ), Times.Once @@ -303,12 +309,17 @@ public async Task Put_PlayerBySquadNumber_ValidationError_Returns400BadRequest() var controller = new PlayerController(service.Object, logger.Object, validator.Object); validator - .Setup(validator => validator.ValidateAsync(request, It.IsAny())) + .Setup(validator => + validator.ValidateAsync( + It.IsAny(), + It.IsAny() + ) + ) .ReturnsAsync( new ValidationResult( new List { - new("SquadNumber", "SquadNumber must be greater than 0.") + new("SquadNumber", "SquadNumber must be greater than 0."), } ) ); @@ -322,7 +333,7 @@ public async Task Put_PlayerBySquadNumber_ValidationError_Returns400BadRequest() validator.Verify( validator => validator.ValidateAsync( - It.IsAny(), + It.IsAny(), It.IsAny() ), Times.Once @@ -344,7 +355,7 @@ public async Task Put_PlayerBySquadNumber_NonExisting_Returns404NotFound() validator .Setup(validator => validator.ValidateAsync( - It.IsAny(), + It.IsAny(), It.IsAny() ) ) @@ -364,7 +375,7 @@ public async Task Put_PlayerBySquadNumber_NonExisting_Returns404NotFound() validator.Verify( validator => validator.ValidateAsync( - It.IsAny(), + It.IsAny(), It.IsAny() ), Times.Once @@ -389,7 +400,7 @@ public async Task Put_PlayerBySquadNumber_SquadNumberMismatch_Returns400BadReque validator .Setup(validator => validator.ValidateAsync( - It.IsAny(), + It.IsAny(), It.IsAny() ) ) @@ -425,7 +436,7 @@ public async Task Put_PlayerBySquadNumber_Existing_Returns204NoContent() validator .Setup(validator => validator.ValidateAsync( - It.IsAny(), + It.IsAny(), It.IsAny() ) ) diff --git a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerValidatorTests.cs b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerValidatorTests.cs index baa2632..a9337ec 100644 --- a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerValidatorTests.cs +++ b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerValidatorTests.cs @@ -3,6 +3,7 @@ using Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities; using Dotnet.Samples.AspNetCore.WebApi.Validators; using FluentAssertions; +using FluentValidation; using Moq; namespace Dotnet.Samples.AspNetCore.WebApi.Tests.Unit; @@ -40,7 +41,10 @@ public async Task ValidateAsync_ValidRequest_ReturnsNoErrors() var validator = CreateValidator(repositoryMock); // Act - var result = await validator.ValidateAsync(request); + var result = await validator.ValidateAsync( + request, + options => options.IncludeRuleSets("Create") + ); // Assert result.IsValid.Should().BeTrue(); @@ -61,7 +65,10 @@ public async Task ValidateAsync_FirstNameEmpty_ReturnsValidationError() var validator = CreateValidator(); // Act - var result = await validator.ValidateAsync(request); + var result = await validator.ValidateAsync( + request, + options => options.IncludeRuleSets("Create") + ); // Assert result.IsValid.Should().BeFalse(); @@ -82,7 +89,10 @@ public async Task ValidateAsync_LastNameEmpty_ReturnsValidationError() var validator = CreateValidator(); // Act - var result = await validator.ValidateAsync(request); + var result = await validator.ValidateAsync( + request, + options => options.IncludeRuleSets("Create") + ); // Assert result.IsValid.Should().BeFalse(); @@ -103,7 +113,10 @@ public async Task ValidateAsync_SquadNumberNotGreaterThanZero_ReturnsValidationE var validator = CreateValidator(); // Act - var result = await validator.ValidateAsync(request); + var result = await validator.ValidateAsync( + request, + options => options.IncludeRuleSets("Create") + ); // Assert result.IsValid.Should().BeFalse(); @@ -124,7 +137,10 @@ public async Task ValidateAsync_SquadNumberNotUnique_ReturnsValidationError() var validator = CreateValidator(repositoryMock); // Act - var result = await validator.ValidateAsync(request); + var result = await validator.ValidateAsync( + request, + options => options.IncludeRuleSets("Create") + ); // Assert result.IsValid.Should().BeFalse(); @@ -135,6 +151,33 @@ public async Task ValidateAsync_SquadNumberNotUnique_ReturnsValidationError() ); } + [Fact] + [Trait("Category", "Unit")] + public async Task ValidateAsync_SquadNumber_BelongsToPlayerBeingUpdated_ReturnsNoErrors() + { + // Arrange + // Simulate a PUT request for an existing player: the squad number in the + // body matches the one already in the database. The "Update" rule set must + // not run BeUniqueSquadNumber, otherwise this valid request would be rejected. + var request = PlayerFakes.MakeRequestModelForUpdate(10); + var existingPlayer = PlayerFakes.MakeFromStarting11(10); + var repositoryMock = new Mock(); + repositoryMock + .Setup(repository => repository.FindBySquadNumberAsync(request.SquadNumber)) + .ReturnsAsync(existingPlayer); + var validator = CreateValidator(repositoryMock); + + // Act + var result = await validator.ValidateAsync( + request, + options => options.IncludeRuleSets("Update") + ); + + // Assert + result.IsValid.Should().BeTrue(); + result.Errors.Should().BeEmpty(); + } + /* ------------------------------------------------------------------------- * AbbrPosition * ---------------------------------------------------------------------- */ @@ -149,7 +192,10 @@ public async Task ValidateAsync_AbbrPositionEmpty_ReturnsValidationError() var validator = CreateValidator(); // Act - var result = await validator.ValidateAsync(request); + var result = await validator.ValidateAsync( + request, + options => options.IncludeRuleSets("Create") + ); // Assert result.IsValid.Should().BeFalse(); @@ -166,7 +212,10 @@ public async Task ValidateAsync_AbbrPositionInvalid_ReturnsValidationError() var validator = CreateValidator(); // Act - var result = await validator.ValidateAsync(request); + var result = await validator.ValidateAsync( + request, + options => options.IncludeRuleSets("Create") + ); // Assert result.IsValid.Should().BeFalse(); @@ -191,7 +240,10 @@ public async Task ValidateAsync_DateOfBirthNull_ReturnsNoErrors() var validator = CreateValidator(repositoryMock); // Act - var result = await validator.ValidateAsync(request); + var result = await validator.ValidateAsync( + request, + options => options.IncludeRuleSets("Create") + ); // Assert result.IsValid.Should().BeTrue(); @@ -207,7 +259,10 @@ public async Task ValidateAsync_DateOfBirthInFuture_ReturnsValidationError() var validator = CreateValidator(); // Act - var result = await validator.ValidateAsync(request); + var result = await validator.ValidateAsync( + request, + options => options.IncludeRuleSets("Create") + ); // Assert result.IsValid.Should().BeFalse(); @@ -226,7 +281,10 @@ public async Task ValidateAsync_DateOfBirthToday_ReturnsValidationError() var validator = CreateValidator(timeProvider: timeProvider); // Act - var result = await validator.ValidateAsync(request); + var result = await validator.ValidateAsync( + request, + options => options.IncludeRuleSets("Create") + ); // Assert result.IsValid.Should().BeFalse(); @@ -243,7 +301,10 @@ public async Task ValidateAsync_DateOfBirthBeforeYear1900_ReturnsValidationError var validator = CreateValidator(); // Act - var result = await validator.ValidateAsync(request); + var result = await validator.ValidateAsync( + request, + options => options.IncludeRuleSets("Create") + ); // Assert result.IsValid.Should().BeFalse(); @@ -264,7 +325,10 @@ public async Task ValidateAsync_DateOfBirthOnJanuary1st1900_ReturnsNoErrors() var validator = CreateValidator(repositoryMock); // Act - var result = await validator.ValidateAsync(request); + var result = await validator.ValidateAsync( + request, + options => options.IncludeRuleSets("Create") + ); // Assert result.IsValid.Should().BeTrue(); From 2f3bc6030bfcf67fc4737c0702badb38d713eb94 Mon Sep 17 00:00:00 2001 From: Nano Taboada <87288+nanotaboada@users.noreply.github.com> Date: Sat, 21 Mar 2026 17:05:31 -0300 Subject: [PATCH 2/4] ci(github): add bug report issue template Introduced alongside the first reported bug (#424) to standardize how future issues are filed. The template mirrors the structure used in #424: Description, Steps to Reproduce, Expected / Actual Behavior, Environment, and an optional Possible Solution section. Co-authored-by: Claude --- .github/ISSUE_TEMPLATE/bug_report.md | 40 ++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..4b732f6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,40 @@ +--- +name: Bug report +about: Report a bug or unexpected behavior +title: "[BUG]" +labels: bug, .NET +assignees: '' + +--- + +## Description + +A clear and concise description of what the bug is. + +## Steps to Reproduce + +1. Step 1 +2. Step 2 +3. Step 3 + +## Expected Behavior + +What you expected to happen. + +## Actual Behavior + +What actually happened. + +## Environment + +- **.NET SDK version:** (output of `dotnet --version`) +- **ASP.NET Core version:** (from `*.csproj`) +- **OS:** (e.g., macOS 15.0, Ubuntu 24.04, Windows 11) + +## Additional Context + +Add any other context about the problem here (logs, screenshots, etc.). + +## Possible Solution + +(Optional) Suggest a fix or workaround if you have one. From b32c56d20aa8eaf1270f190ffe74aff2755d08a7 Mon Sep 17 00:00:00 2001 From: Nano Taboada <87288+nanotaboada@users.noreply.github.com> Date: Sat, 21 Mar 2026 17:14:31 -0300 Subject: [PATCH 3/4] ci(sonar): add sonar-project.properties MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Configures SonarCloud analysis for the project: - Scopes sources and tests to their respective directories - Excludes build artifacts, generated files, and EF Core migrations from analysis and coverage metrics - Excludes the test project from duplicate code (CPD) detection — Fakes, Mocks, and Stubs are intentionally repetitive by design Co-authored-by: Claude --- sonar-project.properties | 51 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 sonar-project.properties diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..431c2bf --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,51 @@ +# ============================================================================= +# 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/** + +# ============================================================================= +# 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 + +# ============================================================================= +# Duplicate code (CPD) exclusions +# Test utilities (Fakes, Mocks, Stubs) are intentionally repetitive by design: +# each operation gets its own factory method and the similarity between +# arrange/act/assert blocks across tests is expected and harmless. +# ============================================================================= + +sonar.cpd.exclusions=test/**/*.cs From 90383a509877c2ba57c9fdd968dfe237a21e8fd1 Mon Sep 17 00:00:00 2001 From: Nano Taboada <87288+nanotaboada@users.noreply.github.com> Date: Sat, 21 Mar 2026 17:19:49 -0300 Subject: [PATCH 4/4] ci(sonar): exclude validator from CPD to suppress intentional duplication The "Create" and "Update" rule sets in PlayerRequestModelValidator share common rules by design; each operation's validation is intentionally kept self-contained for readability. // NOSONAR only suppresses rule-based issues and has no effect on CPD metrics, so the file is listed under sonar.cpd.exclusions instead. Co-authored-by: Claude --- sonar-project.properties | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/sonar-project.properties b/sonar-project.properties index 431c2bf..b019777 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -43,9 +43,19 @@ sonar.coverage.exclusions=\ # ============================================================================= # Duplicate code (CPD) exclusions -# Test utilities (Fakes, Mocks, Stubs) are intentionally repetitive by design: -# each operation gets its own factory method and the similarity between -# arrange/act/assert blocks across tests is expected and harmless. +# 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. +# +# test/**/*.cs — Fakes, Mocks, and Stubs are intentionally repetitive by +# design; similarity between arrange/act/assert blocks across tests is +# expected and harmless. +# +# **/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=test/**/*.cs +sonar.cpd.exclusions=\ + test/**/*.cs,\ + **/Validators/PlayerRequestModelValidator.cs