11// Licensed to the .NET Foundation under one or more agreements.
22// The .NET Foundation licenses this file to you under the MIT license.
33
4+ using System . Collections ;
45using System . Collections . Concurrent ;
56using System . ComponentModel . DataAnnotations ;
67using System . Diagnostics . CodeAnalysis ;
8+ using System . Linq ;
79using System . Reflection ;
810using System . Reflection . Metadata ;
911using System . Runtime . InteropServices ;
12+ using System . Text . RegularExpressions ;
1013using Microsoft . AspNetCore . Http . Validation ;
1114using Microsoft . Extensions . DependencyInjection ;
1215using Microsoft . Extensions . Options ;
@@ -18,7 +21,7 @@ namespace Microsoft.AspNetCore.Components.Forms;
1821/// <summary>
1922/// Extension methods to add DataAnnotations validation to an <see cref="EditContext"/>.
2023/// </summary>
21- public static class EditContextDataAnnotationsExtensions
24+ public static partial class EditContextDataAnnotationsExtensions
2225{
2326 /// <summary>
2427 /// Adds DataAnnotations validation support to the <see cref="EditContext"/>.
@@ -62,7 +65,7 @@ private static void ClearCache(Type[]? _)
6265 }
6366#pragma warning restore IDE0051 // Remove unused private members
6467
65- private sealed class DataAnnotationsEventSubscriptions : IDisposable
68+ private sealed partial class DataAnnotationsEventSubscriptions : IDisposable
6669 {
6770 private static readonly ConcurrentDictionary < ( Type ModelType , string FieldName ) , PropertyInfo ? > _propertyInfoCache = new ( ) ;
6871
@@ -85,6 +88,7 @@ public DataAnnotationsEventSubscriptions(EditContext editContext, IServiceProvid
8588 }
8689 }
8790
91+ // TODO(OR): Should this also use ValidatablePropertyInfo.ValidateAsync?
8892 [ UnconditionalSuppressMessage ( "Trimming" , "IL2026" , Justification = "Model types are expected to be defined in assemblies that do not get trimmed." ) ]
8993 private void OnFieldChanged ( object ? sender , FieldChangedEventArgs eventArgs )
9094 {
@@ -180,22 +184,81 @@ private bool TryValidateTypeInfo(ValidationContext validationContext)
180184
181185 if ( validationErrors is not null && validationErrors . Count > 0 )
182186 {
183- foreach ( var ( key , value ) in validationErrors )
187+ foreach ( var ( fieldPath , messages ) in validationErrors )
184188 {
185- var keySegments = key . Split ( '.' ) ;
186- var container = keySegments . Length > 1
187- ? GetPropertyByPath ( _editContext . Model , keySegments [ ..^ 1 ] )
188- : _editContext . Model ;
189- var fieldName = keySegments [ ^ 1 ] ;
189+ var dotSegments = fieldPath . Split ( '.' ) ;
190+ var fieldName = dotSegments [ ^ 1 ] ;
191+ var fieldContainer = GetFieldContainer ( _editContext . Model , dotSegments [ ..^ 1 ] ) ;
190192
191- _messages . Add ( new FieldIdentifier ( container , fieldName ) , value ) ;
193+ _messages . Add ( new FieldIdentifier ( fieldContainer , fieldName ) , messages ) ;
192194 }
193195 }
194196
195197 return true ;
196198 }
197199#pragma warning restore ASP0029 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
198200
201+ // TODO(OR): Replace this with a more robust implementation or a different approach. Ideally, collect references during the validation process itself.
202+ private static object GetFieldContainer ( object obj , string [ ] dotSegments )
203+ {
204+ // The method does not check nullity and index bounds everywhere as the path is constructed internally and assumed to be correct.
205+ object currentObject = obj ;
206+
207+ for ( int i = 0 ; i < dotSegments . Length ; i ++ )
208+ {
209+ string segment = dotSegments [ i ] ;
210+
211+ if ( currentObject == null )
212+ {
213+ string traversedPath = string . Join ( "." , dotSegments . Take ( i ) ) ;
214+ throw new ArgumentException ( $ "Cannot access segment '{ segment } ' because the path '{ traversedPath } ' resolved to null.") ;
215+ }
216+
217+ Match match = _pathSegmentRegex . Match ( segment ) ;
218+ if ( ! match . Success )
219+ {
220+ throw new ArgumentException ( $ "Invalid path segment: '{ segment } '.") ;
221+ }
222+
223+ string propertyName = match . Groups [ 1 ] . Value ;
224+ string ? indexStr = match . Groups [ 2 ] . Success ? match . Groups [ 2 ] . Value : null ;
225+
226+ Type currentType = currentObject . GetType ( ) ;
227+ PropertyInfo propertyInfo = currentType . GetProperty ( propertyName , BindingFlags . Public | BindingFlags . Instance | BindingFlags . IgnoreCase ) ;
228+ object propertyValue = propertyInfo ! . GetValue ( currentObject ) ! ;
229+
230+ if ( indexStr != null ) // Indexed access
231+ {
232+ if ( ! int . TryParse ( indexStr , out int index ) )
233+ {
234+ throw new ArgumentException ( $ "Invalid index '{ indexStr } ' in segment '{ segment } '.") ;
235+ }
236+
237+ if ( propertyValue is Array array )
238+ {
239+ currentObject = array . GetValue ( index ) ! ;
240+ }
241+ else if ( propertyValue is IList list )
242+ {
243+ currentObject = list [ index ] ! ;
244+ }
245+ else if ( propertyValue is IEnumerable enumerable )
246+ {
247+ currentObject = enumerable . Cast < object > ( ) . ElementAt ( index ) ;
248+ }
249+ else
250+ {
251+ throw new ArgumentException ( $ "Property '{ propertyName } ' is not an array, list, or enumerable. Cannot access by index in segment '{ segment } '.") ;
252+ }
253+ }
254+ else // Simple property access
255+ {
256+ currentObject = propertyValue ;
257+ }
258+ }
259+ return currentObject ! ;
260+ }
261+
199262 public void Dispose ( )
200263 {
201264 _messages . Clear ( ) ;
@@ -231,19 +294,10 @@ internal void ClearCache()
231294 _propertyInfoCache . Clear ( ) ;
232295 }
233296
234- // TODO(OR): Replace this with more robust implementation.
235- private static object GetPropertyByPath ( object obj , string [ ] path )
236- {
237- var currentObject = obj ;
238-
239- foreach ( string propertyName in path )
240- {
241- Type currentType = currentObject ! . GetType ( ) ;
242- PropertyInfo propertyInfo = currentType . GetProperty ( propertyName ) ! ;
243- currentObject = propertyInfo . GetValue ( currentObject ) ;
244- }
297+ private static readonly Regex _pathSegmentRegex = PathSegmentRegexGen ( ) ;
245298
246- return currentObject ! ;
247- }
299+ // Regex to parse "PropertyName" or "PropertyName[index]"
300+ [ GeneratedRegex ( @"^([a-zA-Z_]\w*)(?:\[(\d+)\])?$" , RegexOptions . Compiled ) ]
301+ private static partial Regex PathSegmentRegexGen ( ) ;
248302 }
249303}
0 commit comments