33
44using System . ComponentModel . DataAnnotations ;
55using System . Diagnostics . CodeAnalysis ;
6+ using System . Linq ;
67using System . Text . Json ;
78
89namespace Microsoft . AspNetCore . Http . Validation ;
@@ -68,29 +69,32 @@ public sealed class ValidateContext
6869 /// </summary>
6970 public JsonSerializerOptions ? SerializerOptions { get ; set ; }
7071
71- internal void AddValidationError ( string key , string [ ] error )
72+ internal void AddValidationError ( string key , string [ ] errors )
7273 {
7374 ValidationErrors ??= [ ] ;
7475
7576 var formattedKey = FormatKey ( key ) ;
76- ValidationErrors [ formattedKey ] = error ;
77+ var formattedErrors = errors . Select ( FormatErrorMessage ) . ToArray ( ) ;
78+ ValidationErrors [ formattedKey ] = formattedErrors ;
7779 }
7880
7981 internal void AddOrExtendValidationErrors ( string key , string [ ] errors )
8082 {
8183 ValidationErrors ??= [ ] ;
8284
8385 var formattedKey = FormatKey ( key ) ;
86+ var formattedErrors = errors . Select ( FormatErrorMessage ) . ToArray ( ) ;
87+
8488 if ( ValidationErrors . TryGetValue ( formattedKey , out var existingErrors ) )
8589 {
86- var newErrors = new string [ existingErrors . Length + errors . Length ] ;
90+ var newErrors = new string [ existingErrors . Length + formattedErrors . Length ] ;
8791 existingErrors . CopyTo ( newErrors , 0 ) ;
88- errors . CopyTo ( newErrors , existingErrors . Length ) ;
92+ formattedErrors . CopyTo ( newErrors , existingErrors . Length ) ;
8993 ValidationErrors [ formattedKey ] = newErrors ;
9094 }
9195 else
9296 {
93- ValidationErrors [ formattedKey ] = errors ;
97+ ValidationErrors [ formattedKey ] = formattedErrors ;
9498 }
9599 }
96100
@@ -99,13 +103,15 @@ internal void AddOrExtendValidationError(string key, string error)
99103 ValidationErrors ??= [ ] ;
100104
101105 var formattedKey = FormatKey ( key ) ;
102- if ( ValidationErrors . TryGetValue ( formattedKey , out var existingErrors ) && ! existingErrors . Contains ( error ) )
106+ var formattedError = FormatErrorMessage ( error ) ;
107+
108+ if ( ValidationErrors . TryGetValue ( formattedKey , out var existingErrors ) && ! existingErrors . Contains ( formattedError ) )
103109 {
104- ValidationErrors [ formattedKey ] = [ .. existingErrors , error ] ;
110+ ValidationErrors [ formattedKey ] = [ .. existingErrors , formattedError ] ;
105111 }
106112 else
107113 {
108- ValidationErrors [ formattedKey ] = [ error ] ;
114+ ValidationErrors [ formattedKey ] = [ formattedError ] ;
109115 }
110116 }
111117
@@ -146,7 +152,7 @@ private string FormatComplexKey(string key)
146152 if ( i > lastIndex )
147153 {
148154 string segment = key . Substring ( lastIndex , i - lastIndex ) ;
149- string formattedSegment = propertyNamingPolicy != null
155+ string formattedSegment = propertyNamingPolicy is not null
150156 ? propertyNamingPolicy . ConvertName ( segment )
151157 : segment ;
152158 result . Append ( formattedSegment ) ;
@@ -175,7 +181,7 @@ private string FormatComplexKey(string key)
175181 if ( i > lastIndex )
176182 {
177183 string segment = key . Substring ( lastIndex , i - lastIndex ) ;
178- string formattedSegment = propertyNamingPolicy != null
184+ string formattedSegment = propertyNamingPolicy is not null
179185 ? propertyNamingPolicy . ConvertName ( segment )
180186 : segment ;
181187 result . Append ( formattedSegment ) ;
@@ -191,7 +197,7 @@ private string FormatComplexKey(string key)
191197 if ( lastIndex < key . Length )
192198 {
193199 string segment = key . Substring ( lastIndex ) ;
194- if ( ! inBracket && propertyNamingPolicy != null )
200+ if ( ! inBracket && propertyNamingPolicy is not null )
195201 {
196202 segment = propertyNamingPolicy . ConvertName ( segment ) ;
197203 }
@@ -200,4 +206,50 @@ private string FormatComplexKey(string key)
200206
201207 return result . ToString ( ) ;
202208 }
209+
210+ // Format validation error messages to use the same property naming policy as the keys
211+ private string FormatErrorMessage ( string errorMessage )
212+ {
213+ if ( SerializerOptions ? . PropertyNamingPolicy is null )
214+ {
215+ return errorMessage ;
216+ }
217+
218+ // Common pattern: "The {PropertyName} field is required."
219+ const string pattern = "The " ;
220+ const string fieldPattern = " field " ;
221+
222+ int startIndex = errorMessage . IndexOf ( pattern , StringComparison . Ordinal ) ;
223+ if ( startIndex != 0 )
224+ {
225+ return errorMessage ; // Does not start with "The "
226+ }
227+
228+ int endIndex = errorMessage . IndexOf ( fieldPattern , pattern . Length , StringComparison . Ordinal ) ;
229+ if ( endIndex <= pattern . Length )
230+ {
231+ return errorMessage ; // Does not contain " field " or it's too early
232+ }
233+
234+ // Extract the property name between "The " and " field "
235+ // Use ReadOnlySpan<char> for better performance
236+ ReadOnlySpan < char > messageSpan = errorMessage . AsSpan ( ) ;
237+ ReadOnlySpan < char > propertyNameSpan = messageSpan . Slice ( pattern . Length , endIndex - pattern . Length ) ;
238+ string propertyName = propertyNameSpan . ToString ( ) ;
239+
240+ if ( string . IsNullOrWhiteSpace ( propertyName ) )
241+ {
242+ return errorMessage ;
243+ }
244+
245+ // Format the property name with the naming policy
246+ string formattedPropertyName = SerializerOptions . PropertyNamingPolicy . ConvertName ( propertyName ) ;
247+
248+ // Construct the new error message by combining parts
249+ return string . Concat (
250+ pattern ,
251+ formattedPropertyName ,
252+ messageSpan . Slice ( endIndex ) . ToString ( )
253+ ) ;
254+ }
203255}
0 commit comments