();
+builder.Services.AddValidation();
+
var app = builder.Build();
// Configure the HTTP request pipeline.
diff --git a/src/Components/Samples/BlazorUnitedApp/Shared/NavMenu.razor b/src/Components/Samples/BlazorUnitedApp/Shared/NavMenu.razor
index 3801dfc18932..b5e52b76837d 100644
--- a/src/Components/Samples/BlazorUnitedApp/Shared/NavMenu.razor
+++ b/src/Components/Samples/BlazorUnitedApp/Shared/NavMenu.razor
@@ -14,6 +14,11 @@
Home
+
+
+ Form validation
+
+
Counter
diff --git a/src/Components/Samples/BlazorUnitedApp/Validation/AddressModel.cs b/src/Components/Samples/BlazorUnitedApp/Validation/AddressModel.cs
new file mode 100644
index 000000000000..5c86af8d8cc9
--- /dev/null
+++ b/src/Components/Samples/BlazorUnitedApp/Validation/AddressModel.cs
@@ -0,0 +1,16 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.ComponentModel.DataAnnotations;
+
+namespace BlazorUnitedApp.Validation;
+
+public class AddressModel
+{
+ [Required(ErrorMessage = "Street is required.")]
+ public string? Street { get; set; }
+
+ [Required(ErrorMessage = "Zip Code is required.")]
+ [StringLength(10, MinimumLength = 5, ErrorMessage = "Zip Code must be between 5 and 10 characters.")]
+ public string? ZipCode { get; set; }
+}
diff --git a/src/Components/Samples/BlazorUnitedApp/Validation/CustomerModel.cs b/src/Components/Samples/BlazorUnitedApp/Validation/CustomerModel.cs
new file mode 100644
index 000000000000..6ba42f858a59
--- /dev/null
+++ b/src/Components/Samples/BlazorUnitedApp/Validation/CustomerModel.cs
@@ -0,0 +1,15 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace BlazorUnitedApp.Validation;
+
+public class CustomerModel
+{
+ [Required(ErrorMessage = "Full Name is required.")]
+ public string? FullName { get; set; }
+
+ [Required(ErrorMessage = "Email is required.")]
+ [EmailAddress(ErrorMessage = "Invalid Email Address.")]
+ public string? Email { get; set; }
+
+ public AddressModel ShippingAddress { get; set; } = new AddressModel();
+}
diff --git a/src/Components/Samples/BlazorUnitedApp/Validation/OrderItemModel.cs b/src/Components/Samples/BlazorUnitedApp/Validation/OrderItemModel.cs
new file mode 100644
index 000000000000..38f2c28d41f5
--- /dev/null
+++ b/src/Components/Samples/BlazorUnitedApp/Validation/OrderItemModel.cs
@@ -0,0 +1,15 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.ComponentModel.DataAnnotations;
+
+namespace BlazorUnitedApp.Validation;
+
+public class OrderItemModel
+{
+ [Required(ErrorMessage = "Product Name is required.")]
+ public string? ProductName { get; set; }
+
+ [Range(1, 100, ErrorMessage = "Quantity must be between 1 and 100.")]
+ public int Quantity { get; set; } = 1;
+}
diff --git a/src/Components/Samples/BlazorUnitedApp/Validation/OrderModel.cs b/src/Components/Samples/BlazorUnitedApp/Validation/OrderModel.cs
new file mode 100644
index 000000000000..342101fbe332
--- /dev/null
+++ b/src/Components/Samples/BlazorUnitedApp/Validation/OrderModel.cs
@@ -0,0 +1,23 @@
+using System.ComponentModel.DataAnnotations;
+using Microsoft.AspNetCore.Http.Validation;
+
+namespace BlazorUnitedApp.Validation;
+
+#pragma warning disable ASP0029 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
+[ValidatableType]
+#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.
+public class OrderModel
+{
+ [Required(ErrorMessage = "Order Name is required.")]
+ [StringLength(100, ErrorMessage = "Order Name cannot be longer than 100 characters.")]
+ public string? OrderName { get; set; }
+
+ public CustomerModel CustomerDetails { get; set; } = new CustomerModel();
+
+ public List OrderItems { get; set; } = new List();
+
+ public OrderModel()
+ {
+ OrderItems.Add(new OrderItemModel());
+ }
+}
diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
index 8cb332fa9f20..04c61f46b185 100644
--- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
+++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
@@ -23,6 +23,7 @@ Microsoft.AspNetCore.Http.Validation.ValidateContext.CurrentDepth.get -> int
Microsoft.AspNetCore.Http.Validation.ValidateContext.CurrentDepth.set -> void
Microsoft.AspNetCore.Http.Validation.ValidateContext.CurrentValidationPath.get -> string!
Microsoft.AspNetCore.Http.Validation.ValidateContext.CurrentValidationPath.set -> void
+Microsoft.AspNetCore.Http.Validation.ValidateContext.OnValidationError -> System.Action?
Microsoft.AspNetCore.Http.Validation.ValidateContext.ValidateContext() -> void
Microsoft.AspNetCore.Http.Validation.ValidateContext.ValidationContext.get -> System.ComponentModel.DataAnnotations.ValidationContext!
Microsoft.AspNetCore.Http.Validation.ValidateContext.ValidationContext.set -> void
diff --git a/src/Http/Http.Abstractions/src/Validation/ValidatableParameterInfo.cs b/src/Http/Http.Abstractions/src/Validation/ValidatableParameterInfo.cs
index 48de32c0daff..de9340e50c92 100644
--- a/src/Http/Http.Abstractions/src/Validation/ValidatableParameterInfo.cs
+++ b/src/Http/Http.Abstractions/src/Validation/ValidatableParameterInfo.cs
@@ -77,7 +77,7 @@ public virtual async Task ValidateAsync(object? value, ValidateContext context,
if (result is not null && result != ValidationResult.Success && result.ErrorMessage is not null)
{
var key = string.IsNullOrEmpty(context.CurrentValidationPath) ? Name : $"{context.CurrentValidationPath}.{Name}";
- context.AddValidationError(key, [result.ErrorMessage]);
+ context.AddValidationError(key, [result.ErrorMessage], null);
return;
}
}
@@ -92,13 +92,13 @@ public virtual async Task ValidateAsync(object? value, ValidateContext context,
if (result is not null && result != ValidationResult.Success && result.ErrorMessage is not null)
{
var key = string.IsNullOrEmpty(context.CurrentValidationPath) ? Name : $"{context.CurrentValidationPath}.{Name}";
- context.AddOrExtendValidationErrors(key, [result.ErrorMessage]);
+ context.AddOrExtendValidationErrors(key, [result.ErrorMessage], null);
}
}
catch (Exception ex)
{
var key = string.IsNullOrEmpty(context.CurrentValidationPath) ? Name : $"{context.CurrentValidationPath}.{Name}";
- context.AddValidationError(key, [ex.Message]);
+ context.AddValidationError(key, [ex.Message], null);
}
}
diff --git a/src/Http/Http.Abstractions/src/Validation/ValidatablePropertyInfo.cs b/src/Http/Http.Abstractions/src/Validation/ValidatablePropertyInfo.cs
index 0b16e34d1dc9..4093455d2179 100644
--- a/src/Http/Http.Abstractions/src/Validation/ValidatablePropertyInfo.cs
+++ b/src/Http/Http.Abstractions/src/Validation/ValidatablePropertyInfo.cs
@@ -85,14 +85,14 @@ public virtual async Task ValidateAsync(object? value, ValidateContext context,
if (result is not null && result != ValidationResult.Success && result.ErrorMessage is not null)
{
- context.AddValidationError(context.CurrentValidationPath, [result.ErrorMessage]);
+ context.AddValidationError(context.CurrentValidationPath, [result.ErrorMessage], value);
context.CurrentValidationPath = originalPrefix; // Restore prefix
return;
}
}
// Validate any other attributes
- ValidateValue(propertyValue, context.CurrentValidationPath, validationAttributes);
+ ValidateValue(propertyValue, context.CurrentValidationPath, validationAttributes, value);
// Check if we've reached the maximum depth before validating complex properties
if (context.CurrentDepth >= context.ValidationOptions.MaxDepth)
@@ -150,7 +150,7 @@ public virtual async Task ValidateAsync(object? value, ValidateContext context,
context.CurrentValidationPath = originalPrefix;
}
- void ValidateValue(object? val, string errorPrefix, ValidationAttribute[] validationAttributes)
+ void ValidateValue(object? val, string errorPrefix, ValidationAttribute[] validationAttributes, object? container)
{
for (var i = 0; i < validationAttributes.Length; i++)
{
@@ -160,12 +160,12 @@ void ValidateValue(object? val, string errorPrefix, ValidationAttribute[] valida
var result = attribute.GetValidationResult(val, context.ValidationContext);
if (result is not null && result != ValidationResult.Success && result.ErrorMessage is not null)
{
- context.AddOrExtendValidationErrors(errorPrefix.TrimStart('.'), [result.ErrorMessage]);
+ context.AddOrExtendValidationErrors(errorPrefix.TrimStart('.'), [result.ErrorMessage], container);
}
}
catch (Exception ex)
{
- context.AddOrExtendValidationErrors(errorPrefix.TrimStart('.'), [ex.Message]);
+ context.AddOrExtendValidationErrors(errorPrefix.TrimStart('.'), [ex.Message], container);
}
}
}
diff --git a/src/Http/Http.Abstractions/src/Validation/ValidatableTypeInfo.cs b/src/Http/Http.Abstractions/src/Validation/ValidatableTypeInfo.cs
index 6245c43c1b69..eebf279242e2 100644
--- a/src/Http/Http.Abstractions/src/Validation/ValidatableTypeInfo.cs
+++ b/src/Http/Http.Abstractions/src/Validation/ValidatableTypeInfo.cs
@@ -109,13 +109,13 @@ public virtual async Task ValidateAsync(object? value, ValidateContext context,
var key = string.IsNullOrEmpty(originalPrefix) ?
memberName :
$"{originalPrefix}.{memberName}";
- context.AddOrExtendValidationError(key, validationResult.ErrorMessage);
+ context.AddOrExtendValidationError(key, validationResult.ErrorMessage, value);
}
if (!validationResult.MemberNames.Any())
{
// If no member names are specified, then treat this as a top-level error
- context.AddOrExtendValidationError(string.Empty, validationResult.ErrorMessage);
+ context.AddOrExtendValidationError(string.Empty, validationResult.ErrorMessage, value);
}
}
}
diff --git a/src/Http/Http.Abstractions/src/Validation/ValidateContext.cs b/src/Http/Http.Abstractions/src/Validation/ValidateContext.cs
index d38ada2ddeb1..7db61b12ff5f 100644
--- a/src/Http/Http.Abstractions/src/Validation/ValidateContext.cs
+++ b/src/Http/Http.Abstractions/src/Validation/ValidateContext.cs
@@ -60,14 +60,20 @@ public sealed class ValidateContext
///
public int CurrentDepth { get; set; }
- internal void AddValidationError(string key, string[] error)
+ ///
+ /// Optional event raised when a validation error is reported.
+ ///
+ public event Action? OnValidationError;
+
+ internal void AddValidationError(string key, string[] error, object? container)
{
ValidationErrors ??= [];
ValidationErrors[key] = error;
+ OnValidationError?.Invoke(key, error, container);
}
- internal void AddOrExtendValidationErrors(string key, string[] errors)
+ internal void AddOrExtendValidationErrors(string key, string[] errors, object? container)
{
ValidationErrors ??= [];
@@ -82,9 +88,11 @@ internal void AddOrExtendValidationErrors(string key, string[] errors)
{
ValidationErrors[key] = errors;
}
+
+ OnValidationError?.Invoke(key, errors, container);
}
- internal void AddOrExtendValidationError(string key, string error)
+ internal void AddOrExtendValidationError(string key, string error, object? container)
{
ValidationErrors ??= [];
@@ -96,5 +104,7 @@ internal void AddOrExtendValidationError(string key, string error)
{
ValidationErrors[key] = [error];
}
+
+ OnValidationError?.Invoke(key, [error], container);
}
}