Skip to content

Commit 4a7f943

Browse files
committed
Add OnValidationError event to ValidateContext, use it to get field container instances in Blazor validation
1 parent 7eea69d commit 4a7f943

File tree

6 files changed

+42
-21
lines changed

6 files changed

+42
-21
lines changed

src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,10 @@ private bool TryValidateTypeInfo(ValidationContext validationContext)
173173
ValidationContext = validationContext,
174174
};
175175

176+
var containerMapping = new Dictionary<string, object?>();
177+
178+
validateContext.OnValidationError += (key, _, container) => containerMapping[key] = container;
179+
176180
var validationTask = typeInfo.ValidateAsync(_editContext.Model, validateContext, CancellationToken.None);
177181

178182
if (!validationTask.IsCompleted)
@@ -187,11 +191,16 @@ private bool TryValidateTypeInfo(ValidationContext validationContext)
187191

188192
if (validationErrors is not null && validationErrors.Count > 0)
189193
{
190-
foreach (var (fieldPath, messages) in validationErrors)
194+
foreach (var (fieldKey, messages) in validationErrors)
191195
{
192-
var dotSegments = fieldPath.Split('.');
193-
var fieldName = dotSegments[^1];
194-
var fieldContainer = GetFieldContainer(_editContext.Model, dotSegments[..^1]);
196+
// Reverse mapping based on storing references during validation
197+
var fieldContainer = containerMapping[fieldKey] ?? _editContext.Model;
198+
199+
// Alternative: Reverse mapping based on object graph walk
200+
//var fieldContainer = GetFieldContainer(_editContext.Model, fieldKey);
201+
202+
var lastDotIndex = fieldKey.LastIndexOf('.');
203+
var fieldName = lastDotIndex >= 0 ? fieldKey[(lastDotIndex + 1)..] : fieldKey;
195204

196205
_messages.Add(new FieldIdentifier(fieldContainer, fieldName), messages);
197206
}
@@ -201,12 +210,13 @@ private bool TryValidateTypeInfo(ValidationContext validationContext)
201210
}
202211
#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.
203212

204-
// TODO(OR): Replace this with a more robust implementation or a different approach. Ideally, collect references during the validation process itself.
213+
// TODO(OR): Replace this with a more robust implementation or a different approach. E.g. collect references during the validation process itself.
205214
[UnconditionalSuppressMessage("Trimming", "IL2075", Justification = "Model types are expected to be defined in assemblies that do not get trimmed.")]
206-
private static object GetFieldContainer(object obj, string[] dotSegments)
215+
private static object GetFieldContainer(object obj, string fieldKey)
207216
{
208-
// The method does not check all possiblle null access and index bound errors as the path is constructed internally and assumed to be correct.
209-
object currentObject = obj;
217+
// The method does not check all possible null access and index bound errors as the path is constructed internally and assumed to be correct.
218+
var dotSegments = fieldKey.Split('.')[..^1];
219+
var currentObject = obj;
210220

211221
for (int i = 0; i < dotSegments.Length; i++)
212222
{

src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Microsoft.AspNetCore.Http.Validation.ValidateContext.CurrentDepth.get -> int
2323
Microsoft.AspNetCore.Http.Validation.ValidateContext.CurrentDepth.set -> void
2424
Microsoft.AspNetCore.Http.Validation.ValidateContext.CurrentValidationPath.get -> string!
2525
Microsoft.AspNetCore.Http.Validation.ValidateContext.CurrentValidationPath.set -> void
26+
Microsoft.AspNetCore.Http.Validation.ValidateContext.OnValidationError -> System.Action<string!, string![]!, object?>?
2627
Microsoft.AspNetCore.Http.Validation.ValidateContext.ValidateContext() -> void
2728
Microsoft.AspNetCore.Http.Validation.ValidateContext.ValidationContext.get -> System.ComponentModel.DataAnnotations.ValidationContext!
2829
Microsoft.AspNetCore.Http.Validation.ValidateContext.ValidationContext.set -> void

src/Http/Http.Abstractions/src/Validation/ValidatableParameterInfo.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public virtual async Task ValidateAsync(object? value, ValidateContext context,
7777
if (result is not null && result != ValidationResult.Success && result.ErrorMessage is not null)
7878
{
7979
var key = string.IsNullOrEmpty(context.CurrentValidationPath) ? Name : $"{context.CurrentValidationPath}.{Name}";
80-
context.AddValidationError(key, [result.ErrorMessage]);
80+
context.AddValidationError(key, [result.ErrorMessage], null);
8181
return;
8282
}
8383
}
@@ -92,13 +92,13 @@ public virtual async Task ValidateAsync(object? value, ValidateContext context,
9292
if (result is not null && result != ValidationResult.Success && result.ErrorMessage is not null)
9393
{
9494
var key = string.IsNullOrEmpty(context.CurrentValidationPath) ? Name : $"{context.CurrentValidationPath}.{Name}";
95-
context.AddOrExtendValidationErrors(key, [result.ErrorMessage]);
95+
context.AddOrExtendValidationErrors(key, [result.ErrorMessage], null);
9696
}
9797
}
9898
catch (Exception ex)
9999
{
100100
var key = string.IsNullOrEmpty(context.CurrentValidationPath) ? Name : $"{context.CurrentValidationPath}.{Name}";
101-
context.AddValidationError(key, [ex.Message]);
101+
context.AddValidationError(key, [ex.Message], null);
102102
}
103103
}
104104

src/Http/Http.Abstractions/src/Validation/ValidatablePropertyInfo.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,14 +85,14 @@ public virtual async Task ValidateAsync(object? value, ValidateContext context,
8585

8686
if (result is not null && result != ValidationResult.Success && result.ErrorMessage is not null)
8787
{
88-
context.AddValidationError(context.CurrentValidationPath, [result.ErrorMessage]);
88+
context.AddValidationError(context.CurrentValidationPath, [result.ErrorMessage], value);
8989
context.CurrentValidationPath = originalPrefix; // Restore prefix
9090
return;
9191
}
9292
}
9393

9494
// Validate any other attributes
95-
ValidateValue(propertyValue, context.CurrentValidationPath, validationAttributes);
95+
ValidateValue(propertyValue, context.CurrentValidationPath, validationAttributes, value);
9696

9797
// Check if we've reached the maximum depth before validating complex properties
9898
if (context.CurrentDepth >= context.ValidationOptions.MaxDepth)
@@ -150,7 +150,7 @@ public virtual async Task ValidateAsync(object? value, ValidateContext context,
150150
context.CurrentValidationPath = originalPrefix;
151151
}
152152

153-
void ValidateValue(object? val, string errorPrefix, ValidationAttribute[] validationAttributes)
153+
void ValidateValue(object? val, string errorPrefix, ValidationAttribute[] validationAttributes, object? container)
154154
{
155155
for (var i = 0; i < validationAttributes.Length; i++)
156156
{
@@ -160,12 +160,12 @@ void ValidateValue(object? val, string errorPrefix, ValidationAttribute[] valida
160160
var result = attribute.GetValidationResult(val, context.ValidationContext);
161161
if (result is not null && result != ValidationResult.Success && result.ErrorMessage is not null)
162162
{
163-
context.AddOrExtendValidationErrors(errorPrefix.TrimStart('.'), [result.ErrorMessage]);
163+
context.AddOrExtendValidationErrors(errorPrefix.TrimStart('.'), [result.ErrorMessage], container);
164164
}
165165
}
166166
catch (Exception ex)
167167
{
168-
context.AddOrExtendValidationErrors(errorPrefix.TrimStart('.'), [ex.Message]);
168+
context.AddOrExtendValidationErrors(errorPrefix.TrimStart('.'), [ex.Message], container);
169169
}
170170
}
171171
}

src/Http/Http.Abstractions/src/Validation/ValidatableTypeInfo.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,13 +109,13 @@ public virtual async Task ValidateAsync(object? value, ValidateContext context,
109109
var key = string.IsNullOrEmpty(originalPrefix) ?
110110
memberName :
111111
$"{originalPrefix}.{memberName}";
112-
context.AddOrExtendValidationError(key, validationResult.ErrorMessage);
112+
context.AddOrExtendValidationError(key, validationResult.ErrorMessage, value);
113113
}
114114

115115
if (!validationResult.MemberNames.Any())
116116
{
117117
// If no member names are specified, then treat this as a top-level error
118-
context.AddOrExtendValidationError(string.Empty, validationResult.ErrorMessage);
118+
context.AddOrExtendValidationError(string.Empty, validationResult.ErrorMessage, value);
119119
}
120120
}
121121
}

src/Http/Http.Abstractions/src/Validation/ValidateContext.cs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,20 @@ public sealed class ValidateContext
6060
/// </summary>
6161
public int CurrentDepth { get; set; }
6262

63-
internal void AddValidationError(string key, string[] error)
63+
/// <summary>
64+
/// Optional event raised when a validation error is reported.
65+
/// </summary>
66+
public event Action<string, string[], object?>? OnValidationError;
67+
68+
internal void AddValidationError(string key, string[] error, object? container)
6469
{
6570
ValidationErrors ??= [];
6671

6772
ValidationErrors[key] = error;
73+
OnValidationError?.Invoke(key, error, container);
6874
}
6975

70-
internal void AddOrExtendValidationErrors(string key, string[] errors)
76+
internal void AddOrExtendValidationErrors(string key, string[] errors, object? container)
7177
{
7278
ValidationErrors ??= [];
7379

@@ -82,9 +88,11 @@ internal void AddOrExtendValidationErrors(string key, string[] errors)
8288
{
8389
ValidationErrors[key] = errors;
8490
}
91+
92+
OnValidationError?.Invoke(key, errors, container);
8593
}
8694

87-
internal void AddOrExtendValidationError(string key, string error)
95+
internal void AddOrExtendValidationError(string key, string error, object? container)
8896
{
8997
ValidationErrors ??= [];
9098

@@ -96,5 +104,7 @@ internal void AddOrExtendValidationError(string key, string error)
96104
{
97105
ValidationErrors[key] = [error];
98106
}
107+
108+
OnValidationError?.Invoke(key, [error], container);
99109
}
100110
}

0 commit comments

Comments
 (0)