Skip to content

fix(validators): scope BeUniqueSquadNumber to "Create" rule set (#424)#426

Merged
nanotaboada merged 4 commits intomasterfrom
fix/squad-number-uniqueness-rule-set
Mar 21, 2026
Merged

fix(validators): scope BeUniqueSquadNumber to "Create" rule set (#424)#426
nanotaboada merged 4 commits intomasterfrom
fix/squad-number-uniqueness-rule-set

Conversation

@nanotaboada
Copy link
Copy Markdown
Owner

@nanotaboada nanotaboada commented Mar 21, 2026

Summary

  • Organizes all PlayerRequestModelValidator rules into explicit CRUD-named rule sets: "Create" (POST) and "Update" (PUT)
  • BeUniqueSquadNumber lives only in "Create" — on PUT the player already exists, so the uniqueness check would always fail
  • Controllers pass opts.IncludeRuleSets("Create") or opts.IncludeRuleSets("Update"), making the intent self-documenting
  • Adds regression test ValidateAsync_SquadNumber_BelongsToPlayerBeingUpdated_ReturnsNoErrors to prevent the bug from silently returning
  • Updates all controller test mocks to match on IValidationContext (the overload the rule-set extension method routes through internally)

Closes #424

Test plan

  • dotnet build --configuration Release passes
  • dotnet test --settings .runsettings — all 39 tests pass (32 pre-existing + 1 new regression test + 6 controller tests unblocked)
  • Manual: PUT /players/squadNumber/10 with "squadNumber": 10 returns 204 No Content
  • Manual: POST /players with a duplicate squad number still returns 400 Bad Request with "SquadNumber must be unique."

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Documentation

    • Added GitHub issue template for submitting bug reports with structured sections and metadata configuration.
  • Bug Fixes

    • Player squad number validation now differs between operations: uniqueness checks apply when creating players, but not when updating them.
  • Tests

    • Updated unit tests to validate creation and update operations using appropriate context-specific validation rule configurations and assertions.

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 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 21, 2026

Warning

Rate limit exceeded

@nanotaboada has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 14 minutes and 40 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: dfa1763a-72f4-408a-aedb-bea0cd08ebaa

📥 Commits

Reviewing files that changed from the base of the PR and between 2f3bc60 and 90383a5.

📒 Files selected for processing (1)
  • sonar-project.properties

Walkthrough

This PR implements FluentValidation rule sets to fix a bug where the BeUniqueSquadNumber validator incorrectly rejects PUT requests for existing players. The "Create" rule set includes the uniqueness check; the "Update" rule set omits it. Controller methods now invoke validation with appropriate rule sets.

Changes

Cohort / File(s) Summary
GitHub Issue Template
.github/ISSUE_TEMPLATE/bug_report.md
New structured bug report template with metadata, description, reproduction steps, expected/actual behavior, environment details, and optional solution sections.
Controller Validation Logic
src/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs
Modified PostAsync and PutAsync to invoke validator with explicit rule sets (IncludeRuleSets("Create") and IncludeRuleSets("Update") respectively) instead of default validation.
Validator Rule Sets
src/Dotnet.Samples.AspNetCore.WebApi/Validators/PlayerRequestModelValidator.cs
Refactored rules into two named rule sets: "Create" (includes BeUniqueSquadNumber uniqueness check) and "Update" (omits uniqueness check but retains other validations like squad number > 0 and date constraints).
Controller Unit Tests
test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerControllerTests.cs
Updated mock validator setup to accept IValidationContext parameter instead of PlayerRequestModel; added explicit using FluentValidation; directive.
Validator Unit Tests
test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerValidatorTests.cs
Updated existing POST/create tests to include IncludeRuleSets("Create"); added new test ValidateAsync_SquadNumber_BelongsToPlayerBeingUpdated_ReturnsNoErrors to verify update scenario passes when squad number belongs to the player being updated.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Assessment against linked issues

Objective Addressed Explanation
Fix BeUniqueSquadNumber validation to not reject PUT requests for existing players [#424]
Implement FluentValidation rule sets ("Create" and "Update") in PlayerRequestModelValidator [#424]
Update PlayerController POST and PUT methods to use appropriate rule sets [#424]
Add test ValidateAsync_SquadNumber_BelongsToPlayerBeingUpdated_ReturnsNoErrors to prevent regression [#424]

Out-of-scope changes

Code Change Explanation
New bug report issue template (.github/ISSUE_TEMPLATE/bug_report.md) Not required by issue #424; appears to be a repository housekeeping addition unrelated to the core bug fix and validation rule set refactoring.

Possibly related PRs

🚥 Pre-merge checks | ✅ 1 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 43.48% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title uses Conventional Commits format (fix:), is 70 characters (under 80 limit), and specifically describes the main change: scoping BeUniqueSquadNumber to the Create rule set.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

📋 Issue Planner

Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).

View plan for ticket: #424

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/squad-number-uniqueness-rule-set
  • 🛠️ sync documentation: Commit on current branch
  • 🛠️ sync documentation: Create PR
  • 🛠️ enforce http error handling: Commit on current branch
  • 🛠️ enforce http error handling: Create PR
  • 🛠️ idiomatic review: Commit on current branch
  • 🛠️ idiomatic review: Create PR
  • 🛠️ verify api contract: Commit on current branch
  • 🛠️ verify api contract: Create PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

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 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerValidatorTests.cs (1)

154-179: Consider adding coverage for "Update" rule set field validation.

The new test verifies that BeUniqueSquadNumber is not triggered for updates, but there's no test confirming that the "Update" rule set still validates structural fields (e.g., empty FirstName). This would ensure the rule set isn't accidentally empty.

🧪 Suggested additional test
[Fact]
[Trait("Category", "Unit")]
public async Task ValidateAsync_UpdateRuleSet_FirstNameEmpty_ReturnsValidationError()
{
    // Arrange
    var request = PlayerFakes.MakeRequestModelForUpdate(10);
    request.FirstName = string.Empty;
    var validator = CreateValidator();

    // Act
    var result = await validator.ValidateAsync(
        request,
        options => options.IncludeRuleSets("Update")
    );

    // Assert
    result.IsValid.Should().BeFalse();
    result.Errors.Should().Contain(error => error.PropertyName == "FirstName");
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerValidatorTests.cs`
around lines 154 - 179, Add a unit test to ensure the "Update" rule set still
validates structural fields by creating a request via
PlayerFakes.MakeRequestModelForUpdate(10), setting request.FirstName =
string.Empty, calling validator.ValidateAsync(request, options =>
options.IncludeRuleSets("Update")) with a validator from CreateValidator(), and
asserting the result is invalid and contains an error with PropertyName
"FirstName"; place this alongside the existing
ValidateAsync_SquadNumber_BelongsToPlayerBeingUpdated_ReturnsNoErrors test in
PlayerValidatorTests to guard against the "Update" rule set becoming empty.
src/Dotnet.Samples.AspNetCore.WebApi/Validators/PlayerRequestModelValidator.cs (2)

45-86: Consider extracting shared rules to reduce duplication.

Both "Create" and "Update" rule sets define identical rules for FirstName, LastName, SquadNumber (basic checks), AbbrPosition, and DateOfBirth. This duplication increases maintenance risk—if a rule changes, it must be updated in both places.

FluentValidation supports Include() to share common rules or you could define common rules outside the rule sets and use RuleSet only for the unique BeUniqueSquadNumber check.

♻️ Proposed refactor to reduce duplication
 public PlayerRequestModelValidator(
     IPlayerRepository playerRepository,
     TimeProvider? timeProvider = null
 )
 {
     _playerRepository = playerRepository;
     var clock = timeProvider ?? TimeProvider.System;

+    // Common structural rules (run for all rule sets)
+    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.");
+        }
+    );
+
     // "Create" rule set — POST /players
-    // Includes BeUniqueSquadNumber to prevent duplicate squad numbers on insert.
+    // Adds BeUniqueSquadNumber to prevent duplicate squad numbers on insert.
     RuleSet(
         "Create",
         () =>
         {
-            RuleFor(player => player.FirstName)
-                .NotEmpty()
-                .WithMessage("FirstName is required.");
-            // ... (remove all duplicated rules)
             RuleFor(player => player.SquadNumber)
                 .MustAsync(BeUniqueSquadNumber)
                 .WithMessage("SquadNumber must be unique.");
         }
     );

-    // "Update" rule set — PUT /players/squadNumber/{n}
-    // BeUniqueSquadNumber is intentionally omitted...
-    RuleSet(
-        "Update",
-        () =>
-        {
-            // ... (remove entire Update rule set - common rules apply automatically)
-        }
-    );
+    // "Update" rule set — empty; common rules apply automatically.
+    // BeUniqueSquadNumber is intentionally omitted.
+    RuleSet("Update", () => { });
 }

Note: When using IncludeRuleSets("Create") or IncludeRuleSets("Update"), you'll also need to include the default rule set: options.IncludeRuleSets("Create", "default") to run both the shared rules and the rule-set-specific rules.

Also applies to: 88-128

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Dotnet.Samples.AspNetCore.WebApi/Validators/PlayerRequestModelValidator.cs`
around lines 45 - 86, Extract the shared validation rules (RuleFor for
FirstName, LastName, SquadNumber basic checks without MustAsync
BeUniqueSquadNumber, AbbrPosition with Position.IsValidAbbr, and the
When(DateOfBirth.HasValue) block using clock.GetUtcNow and the 1900 lower-bound)
into a reusable rule set or private method (e.g., CreateSharedRules or a default
ruleset) and then call Include(...) or IncludeRuleSets(...) inside both
RuleSet("Create", ...) and RuleSet("Update", ...); keep the BeUniqueSquadNumber
MustAsync only in the "Create" RuleSet and ensure the Update RuleSet references
the shared rules so duplication in RuleFor and the DateOfBirth checks is
removed.

62-63: BeUniqueSquadNumber performs business validation via repository lookup.

Per coding guidelines, validators should handle structural validation only, while business validation rules belong in services. The BeUniqueSquadNumber check queries the repository to enforce a uniqueness constraint, which is business logic.

Consider moving this check to PlayerService.CreateAsync() where the duplicate check already exists (controller line 59), or document this as an intentional exception to the guideline for fail-fast validation.

Based on learnings: "Implement business validation rules in services, not in validators; validators should handle structural validation only"

Also applies to: 131-134

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/Dotnet.Samples.AspNetCore.WebApi/Validators/PlayerRequestModelValidator.cs`
around lines 62 - 63, The validator PlayerRequestModelValidator currently calls
the async repository check BeUniqueSquadNumber (structural vs business rule);
remove the MustAsync(BeUniqueSquadNumber) calls (including the duplicate at
lines ~131-134) from PlayerRequestModelValidator and instead enforce uniqueness
inside the service layer by adding the duplicate-squad-number check in
PlayerService.CreateAsync (or ensure the existing check there runs and returns a
validation/error result). Update any controller flow that relied on the
validator to handle the error path returned by PlayerService.CreateAsync and
adjust tests to assert the duplicate-squad-number behavior through the service
instead of the validator.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@src/Dotnet.Samples.AspNetCore.WebApi/Validators/PlayerRequestModelValidator.cs`:
- Around line 45-86: Extract the shared validation rules (RuleFor for FirstName,
LastName, SquadNumber basic checks without MustAsync BeUniqueSquadNumber,
AbbrPosition with Position.IsValidAbbr, and the When(DateOfBirth.HasValue) block
using clock.GetUtcNow and the 1900 lower-bound) into a reusable rule set or
private method (e.g., CreateSharedRules or a default ruleset) and then call
Include(...) or IncludeRuleSets(...) inside both RuleSet("Create", ...) and
RuleSet("Update", ...); keep the BeUniqueSquadNumber MustAsync only in the
"Create" RuleSet and ensure the Update RuleSet references the shared rules so
duplication in RuleFor and the DateOfBirth checks is removed.
- Around line 62-63: The validator PlayerRequestModelValidator currently calls
the async repository check BeUniqueSquadNumber (structural vs business rule);
remove the MustAsync(BeUniqueSquadNumber) calls (including the duplicate at
lines ~131-134) from PlayerRequestModelValidator and instead enforce uniqueness
inside the service layer by adding the duplicate-squad-number check in
PlayerService.CreateAsync (or ensure the existing check there runs and returns a
validation/error result). Update any controller flow that relied on the
validator to handle the error path returned by PlayerService.CreateAsync and
adjust tests to assert the duplicate-squad-number behavior through the service
instead of the validator.

In `@test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerValidatorTests.cs`:
- Around line 154-179: Add a unit test to ensure the "Update" rule set still
validates structural fields by creating a request via
PlayerFakes.MakeRequestModelForUpdate(10), setting request.FirstName =
string.Empty, calling validator.ValidateAsync(request, options =>
options.IncludeRuleSets("Update")) with a validator from CreateValidator(), and
asserting the result is invalid and contains an error with PropertyName
"FirstName"; place this alongside the existing
ValidateAsync_SquadNumber_BelongsToPlayerBeingUpdated_ReturnsNoErrors test in
PlayerValidatorTests to guard against the "Update" rule set becoming empty.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: dbda2b24-0cd3-4296-809c-3bbc301cd14a

📥 Commits

Reviewing files that changed from the base of the PR and between e7af849 and 2f3bc60.

📒 Files selected for processing (5)
  • .github/ISSUE_TEMPLATE/bug_report.md
  • src/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.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/PlayerValidatorTests.cs

nanotaboada and others added 2 commits March 21, 2026 17:14
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 <noreply@anthropic.com>
…tion

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 <noreply@anthropic.com>
@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
14.5% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] BeUniqueSquadNumber validator rejects all PUT requests for existing players

1 participant