Description
The BeUniqueSquadNumber validation rule in PlayerRequestModelValidator is applied to both POST and PUT requests without discrimination. On PUT, the player being updated already exists in the database, so the uniqueness check always fails — causing every update to return 400 Bad Request with "SquadNumber must be unique." before any business logic executes.
Steps to Reproduce
- Start the API with a seeded database (any existing player, e.g. squad number 10)
- Send
PUT /players/squadNumber/10 with a valid request body containing "squadNumber": 10
- Observe
400 Bad Request — "SquadNumber must be unique."
- The player is never updated
Expected Behavior
PUT /players/squadNumber/10 with a matching body should update the player and return 204 No Content.
Actual Behavior
400 Bad Request is returned with FluentValidation error "SquadNumber must be unique." because BeUniqueSquadNumber finds the existing player in the DB and returns false.
Execution order in PlayerController.PutAsync (lines 197–257):
validator.ValidateAsync(player) is called at line 202 — before the route/body mismatch check
BeUniqueSquadNumber calls FindBySquadNumberAsync(squadNumber) → finds the existing player → returns false
validation.IsValid is false → controller returns 400 at line 214
- Update never executes
Environment
- .NET SDK version: 10.0
- .NET Core version: net10.0 (FluentValidation 12.1.1, AutoMapper 16.1.1)
- OS: macOS 15.0 / Ubuntu 24.04 / Windows 11
Additional Context
Why the existing unit tests do not catch this bug
Two compounding factors hide the bug from the test suite:
1. Controller unit tests mock the validator
PlayerMocks.InitControllerMocks() returns a Mock<IValidator<PlayerRequestModel>>. Every PUT test manually stubs .ValidateAsync() to return an empty ValidationResult (no errors). The real BeUniqueSquadNumber logic never executes in any controller test.
2. Validator unit tests only cover POST (create) scenarios
PlayerValidatorTests.cs does exercise the real validator, including the uniqueness rule — but all fixtures use MakeRequestModelForCreate(). There is no test for the PUT scenario: "validating a player whose squad number already exists in the DB." That is precisely the case where the bug manifests.
There are also no integration tests that wire the real validator to a real (or in-memory) database and exercise the full PUT pipeline end-to-end, which would have caught this immediately.
The bug lives in the gap between the two isolated unit test layers. The fix should include a new validator test:
ValidateAsync_SquadNumber_BelongsToPlayerBeingUpdated_ReturnsNoErrors to prevent regression.
Possible Solutions
Two approaches were considered:
Option A — FluentValidation Rule Sets ✅ Chosen
Use FluentValidation's built-in rule sets to scope rules by operation. All rules are organized into explicit "Create" and "Update" rule sets; BeUniqueSquadNumber is included only in "Create".
- POST validates with
opts.IncludeRuleSets("Create")
- PUT validates with
opts.IncludeRuleSets("Update")
Why chosen: Minimal footprint (3 files), no model or service signature changes, explicit CRUD-named rule sets serve as documentation of intent — valuable for an educational codebase. The "Update" rule set also provides a ready home for any future update-specific rules (e.g. squad number immutability).
Files affected:
Validators/PlayerRequestModelValidator.cs — split all rules into RuleSet("Create") and RuleSet("Update"); BeUniqueSquadNumber only in "Create"
Controllers/PlayerController.cs — PostAsync and PutAsync pass explicit rule set options
Tests/Unit/PlayerValidatorTests.cs — update existing tests to specify rule sets; add ValidateAsync_SquadNumber_BelongsToPlayerBeingUpdated_ReturnsNoErrors
Option B — Split Models
Split PlayerRequestModel into two purpose-specific models with separate validators:
| Model |
Used by |
Validator |
PlayerCreateModel |
POST |
PlayerCreateModelValidator — includes BeUniqueSquadNumber |
PlayerUpdateModel |
PUT |
PlayerUpdateModelValidator — omits BeUniqueSquadNumber |
Why not chosen: Requires changes to 6+ files (Models, Validators, Controller, Service, Mappings, Tests). Introduces model and mapping duplication for no immediate benefit — the two models are structurally identical. Adds complexity that can be avoided with rule sets.
Files affected:
Models/PlayerRequestModel.cs → split into PlayerCreateModel.cs / PlayerUpdateModel.cs
Validators/PlayerRequestModelValidator.cs → split into PlayerCreateModelValidator.cs / PlayerUpdateModelValidator.cs
Controllers/PlayerController.cs — update POST and PUT action signatures
Services/PlayerService.cs — update method signatures
Mappings/PlayerMappingProfile.cs — add mappings for new models
Tests/ — add ValidateAsync_SquadNumber_BelongsToPlayerBeingUpdated_ReturnsNoErrors validator test
Description
The
BeUniqueSquadNumbervalidation rule inPlayerRequestModelValidatoris applied to both POST and PUT requests without discrimination. On PUT, the player being updated already exists in the database, so the uniqueness check always fails — causing every update to return400 Bad Requestwith"SquadNumber must be unique."before any business logic executes.Steps to Reproduce
PUT /players/squadNumber/10with a valid request body containing"squadNumber": 10400 Bad Request—"SquadNumber must be unique."Expected Behavior
PUT /players/squadNumber/10with a matching body should update the player and return204 No Content.Actual Behavior
400 Bad Requestis returned with FluentValidation error"SquadNumber must be unique."becauseBeUniqueSquadNumberfinds the existing player in the DB and returnsfalse.Execution order in
PlayerController.PutAsync(lines 197–257):validator.ValidateAsync(player)is called at line 202 — before the route/body mismatch checkBeUniqueSquadNumbercallsFindBySquadNumberAsync(squadNumber)→ finds the existing player → returnsfalsevalidation.IsValidisfalse→ controller returns400at line 214Environment
Additional Context
Why the existing unit tests do not catch this bug
Two compounding factors hide the bug from the test suite:
1. Controller unit tests mock the validator
PlayerMocks.InitControllerMocks()returns aMock<IValidator<PlayerRequestModel>>. Every PUT test manually stubs.ValidateAsync()to return an emptyValidationResult(no errors). The realBeUniqueSquadNumberlogic never executes in any controller test.2. Validator unit tests only cover POST (create) scenarios
PlayerValidatorTests.csdoes exercise the real validator, including the uniqueness rule — but all fixtures useMakeRequestModelForCreate(). There is no test for the PUT scenario: "validating a player whose squad number already exists in the DB." That is precisely the case where the bug manifests.There are also no integration tests that wire the real validator to a real (or in-memory) database and exercise the full PUT pipeline end-to-end, which would have caught this immediately.
The bug lives in the gap between the two isolated unit test layers. The fix should include a new validator test:
ValidateAsync_SquadNumber_BelongsToPlayerBeingUpdated_ReturnsNoErrorsto prevent regression.Possible Solutions
Two approaches were considered:
Option A — FluentValidation Rule Sets ✅ Chosen
Use FluentValidation's built-in rule sets to scope rules by operation. All rules are organized into explicit
"Create"and"Update"rule sets;BeUniqueSquadNumberis included only in"Create".opts.IncludeRuleSets("Create")opts.IncludeRuleSets("Update")Why chosen: Minimal footprint (3 files), no model or service signature changes, explicit CRUD-named rule sets serve as documentation of intent — valuable for an educational codebase. The
"Update"rule set also provides a ready home for any future update-specific rules (e.g. squad number immutability).Files affected:
Validators/PlayerRequestModelValidator.cs— split all rules intoRuleSet("Create")andRuleSet("Update");BeUniqueSquadNumberonly in"Create"Controllers/PlayerController.cs—PostAsyncandPutAsyncpass explicit rule set optionsTests/Unit/PlayerValidatorTests.cs— update existing tests to specify rule sets; addValidateAsync_SquadNumber_BelongsToPlayerBeingUpdated_ReturnsNoErrorsOption B — Split Models
Split
PlayerRequestModelinto two purpose-specific models with separate validators:PlayerCreateModelPlayerCreateModelValidator— includesBeUniqueSquadNumberPlayerUpdateModelPlayerUpdateModelValidator— omitsBeUniqueSquadNumberWhy not chosen: Requires changes to 6+ files (Models, Validators, Controller, Service, Mappings, Tests). Introduces model and mapping duplication for no immediate benefit — the two models are structurally identical. Adds complexity that can be avoided with rule sets.
Files affected:
Models/PlayerRequestModel.cs→ split intoPlayerCreateModel.cs/PlayerUpdateModel.csValidators/PlayerRequestModelValidator.cs→ split intoPlayerCreateModelValidator.cs/PlayerUpdateModelValidator.csControllers/PlayerController.cs— update POST and PUT action signaturesServices/PlayerService.cs— update method signaturesMappings/PlayerMappingProfile.cs— add mappings for new modelsTests/— addValidateAsync_SquadNumber_BelongsToPlayerBeingUpdated_ReturnsNoErrorsvalidator test