@@ -11,8 +11,24 @@ namespace Dotnet.Samples.AspNetCore.WebApi.Validators;
1111/// PlayerRequestModel.
1212/// </summary>
1313/// <remarks>
14- /// This class is part of the FluentValidation library, which provides a fluent
15- /// interface for building validation rules.
14+ /// Rules are organized into CRUD-named rule sets to make their intent explicit.
15+ /// This prevents <c>BeUniqueSquadNumber</c> from running on PUT requests, where
16+ /// the player's squad number already exists in the database by definition.
17+ ///
18+ /// <list type="bullet">
19+ /// <item><description>
20+ /// <c>"Create"</c> — used by <c>POST /players</c>; includes all rules plus
21+ /// the uniqueness check for <c>SquadNumber</c>.
22+ /// </description></item>
23+ /// <item><description>
24+ /// <c>"Update"</c> — used by <c>PUT /players/squadNumber/{n}</c>; same
25+ /// rules, but <c>BeUniqueSquadNumber</c> is intentionally omitted.
26+ /// </description></item>
27+ /// </list>
28+ ///
29+ /// Controllers pass <c>opts.IncludeRuleSets("Create")</c> or
30+ /// <c>opts.IncludeRuleSets("Update")</c> so that only the appropriate rule
31+ /// set runs for each operation.
1632/// </remarks>
1733public class PlayerRequestModelValidator : AbstractValidator < PlayerRequestModel >
1834{
@@ -26,35 +42,88 @@ public PlayerRequestModelValidator(
2642 _playerRepository = playerRepository ;
2743 var clock = timeProvider ?? TimeProvider . System ;
2844
29- RuleFor ( player => player . FirstName ) . NotEmpty ( ) . WithMessage ( "FirstName is required." ) ;
45+ // "Create" rule set — POST /players
46+ // Includes BeUniqueSquadNumber to prevent duplicate squad numbers on insert.
47+ RuleSet (
48+ "Create" ,
49+ ( ) =>
50+ {
51+ RuleFor ( player => player . FirstName )
52+ . NotEmpty ( )
53+ . WithMessage ( "FirstName is required." ) ;
54+
55+ RuleFor ( player => player . LastName ) . NotEmpty ( ) . WithMessage ( "LastName is required." ) ;
3056
31- RuleFor ( player => player . LastName ) . NotEmpty ( ) . WithMessage ( "LastName is required." ) ;
57+ RuleFor ( player => player . SquadNumber )
58+ . NotEmpty ( )
59+ . WithMessage ( "SquadNumber is required." )
60+ . GreaterThan ( 0 )
61+ . WithMessage ( "SquadNumber must be greater than 0." )
62+ . MustAsync ( BeUniqueSquadNumber )
63+ . WithMessage ( "SquadNumber must be unique." ) ;
3264
33- RuleFor ( player => player . SquadNumber )
34- . NotEmpty ( )
35- . WithMessage ( "SquadNumber is required." )
36- . GreaterThan ( 0 )
37- . WithMessage ( "SquadNumber must be greater than 0." )
38- . MustAsync ( BeUniqueSquadNumber )
39- . WithMessage ( "SquadNumber must be unique." ) ;
65+ RuleFor ( player => player . AbbrPosition )
66+ . NotEmpty ( )
67+ . WithMessage ( "AbbrPosition is required." )
68+ . Must ( Position . IsValidAbbr )
69+ . WithMessage ( "AbbrPosition is invalid." ) ;
4070
41- RuleFor ( player => player . AbbrPosition )
42- . NotEmpty ( )
43- . WithMessage ( "AbbrPosition is required." )
44- . Must ( Position . IsValidAbbr )
45- . WithMessage ( "AbbrPosition is invalid." ) ;
71+ When (
72+ player => player . DateOfBirth . HasValue ,
73+ ( ) =>
74+ {
75+ RuleFor ( player => player . DateOfBirth )
76+ . Must ( date => date ! . Value . Date < clock . GetUtcNow ( ) . Date )
77+ . WithMessage ( "DateOfBirth must be a date in the past." )
78+ . Must ( date =>
79+ date ! . Value . Date
80+ >= new DateTime ( 1900 , 1 , 1 , 0 , 0 , 0 , DateTimeKind . Utc )
81+ )
82+ . WithMessage ( "DateOfBirth must be on or after January 1, 1900." ) ;
83+ }
84+ ) ;
85+ }
86+ ) ;
4687
47- When (
48- player => player . DateOfBirth . HasValue ,
88+ // "Update" rule set — PUT /players/squadNumber/{n}
89+ // BeUniqueSquadNumber is intentionally omitted: on PUT the player being
90+ // updated already exists in the database, so the check would always fail.
91+ RuleSet (
92+ "Update" ,
4993 ( ) =>
5094 {
51- RuleFor ( player => player . DateOfBirth )
52- . Must ( date => date ! . Value . Date < clock . GetUtcNow ( ) . Date )
53- . WithMessage ( "DateOfBirth must be a date in the past." )
54- . Must ( date =>
55- date ! . Value . Date >= new DateTime ( 1900 , 1 , 1 , 0 , 0 , 0 , DateTimeKind . Utc )
56- )
57- . WithMessage ( "DateOfBirth must be on or after January 1, 1900." ) ;
95+ RuleFor ( player => player . FirstName )
96+ . NotEmpty ( )
97+ . WithMessage ( "FirstName is required." ) ;
98+
99+ RuleFor ( player => player . LastName ) . NotEmpty ( ) . WithMessage ( "LastName is required." ) ;
100+
101+ RuleFor ( player => player . SquadNumber )
102+ . NotEmpty ( )
103+ . WithMessage ( "SquadNumber is required." )
104+ . GreaterThan ( 0 )
105+ . WithMessage ( "SquadNumber must be greater than 0." ) ;
106+
107+ RuleFor ( player => player . AbbrPosition )
108+ . NotEmpty ( )
109+ . WithMessage ( "AbbrPosition is required." )
110+ . Must ( Position . IsValidAbbr )
111+ . WithMessage ( "AbbrPosition is invalid." ) ;
112+
113+ When (
114+ player => player . DateOfBirth . HasValue ,
115+ ( ) =>
116+ {
117+ RuleFor ( player => player . DateOfBirth )
118+ . Must ( date => date ! . Value . Date < clock . GetUtcNow ( ) . Date )
119+ . WithMessage ( "DateOfBirth must be a date in the past." )
120+ . Must ( date =>
121+ date ! . Value . Date
122+ >= new DateTime ( 1900 , 1 , 1 , 0 , 0 , 0 , DateTimeKind . Utc )
123+ )
124+ . WithMessage ( "DateOfBirth must be on or after January 1, 1900." ) ;
125+ }
126+ ) ;
58127 }
59128 ) ;
60129 }
0 commit comments