-
Notifications
You must be signed in to change notification settings - Fork 10.3k
Validation fixes for Blazor #14972
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Validation fixes for Blazor #14972
Changes from all commits
0c3f759
75a8633
75fde42
44de036
e3ea4cb
d780aac
648b559
58e5613
b89535c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
namespace System.ComponentModel.DataAnnotations | ||
{ | ||
/// <summary> | ||
/// A <see cref="ValidationAttribute"/> that compares two properties | ||
/// </summary> | ||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] | ||
public sealed class ComparePropertyAttribute : CompareAttribute | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This class should be in the System.ComponentModel.DataAnnotations project, not Blazor. To add this validation to a property in |
||
{ | ||
/// <summary> | ||
/// Initializes a new instance of <see cref="BlazorCompareAttribute"/>. | ||
/// </summary> | ||
/// <param name="otherProperty">The property to compare with the current property.</param> | ||
public ComparePropertyAttribute(string otherProperty) | ||
: base(otherProperty) | ||
{ | ||
} | ||
|
||
/// <inheritdoc /> | ||
protected override ValidationResult IsValid(object value, ValidationContext validationContext) | ||
{ | ||
var validationResult = base.IsValid(value, validationContext); | ||
if (validationResult == ValidationResult.Success) | ||
{ | ||
return validationResult; | ||
} | ||
|
||
return new ValidationResult(validationResult.ErrorMessage, new[] { validationContext.MemberName }); | ||
} | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>netstandard2.0</TargetFramework> | ||
<Description>Provides experimental support for validation using DataAnnotations.</Description> | ||
<IsShippingPackage>true</IsShippingPackage> | ||
<HasReferenceAssembly>false</HasReferenceAssembly> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<Reference Include="Microsoft.AspNetCore.Components.Forms" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<InternalsVisibleTo Include="Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.Tests" /> | ||
</ItemGroup> | ||
|
||
</Project> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System.Collections.Generic; | ||
using System.ComponentModel.DataAnnotations; | ||
using System.Linq; | ||
|
||
namespace Microsoft.AspNetCore.Components.Forms | ||
{ | ||
public class ObjectGraphDataAnnotationsValidator : ComponentBase | ||
{ | ||
private static readonly object ValidationContextValidatorKey = new object(); | ||
private static readonly object ValidatedObjectsKey = new object(); | ||
private ValidationMessageStore _validationMessageStore; | ||
|
||
[CascadingParameter] | ||
internal EditContext EditContext { get; set; } | ||
|
||
protected override void OnInitialized() | ||
{ | ||
_validationMessageStore = new ValidationMessageStore(EditContext); | ||
|
||
// Perform object-level validation (starting from the root model) on request | ||
EditContext.OnValidationRequested += (sender, eventArgs) => | ||
{ | ||
_validationMessageStore.Clear(); | ||
ValidateObject(EditContext.Model, new HashSet<object>()); | ||
EditContext.NotifyValidationStateChanged(); | ||
}; | ||
|
||
// Perform per-field validation on each field edit | ||
EditContext.OnFieldChanged += (sender, eventArgs) => | ||
ValidateField(EditContext, _validationMessageStore, eventArgs.FieldIdentifier); | ||
} | ||
|
||
internal void ValidateObject(object value, HashSet<object> visited) | ||
{ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we have a test for this that validates object graphs with cycles? (I haven't been able to find it). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
if (value is null) | ||
{ | ||
return; | ||
} | ||
|
||
if (!visited.Add(value)) | ||
{ | ||
// Already visited this object. | ||
return; | ||
} | ||
|
||
if (value is IEnumerable<object> enumerable) | ||
{ | ||
var index = 0; | ||
foreach (var item in enumerable) | ||
{ | ||
ValidateObject(item, visited); | ||
index++; | ||
} | ||
|
||
return; | ||
} | ||
|
||
var validationResults = new List<ValidationResult>(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You need to add the following code....
Otherwise you will validate every string in a |
||
ValidateObject(value, visited, validationResults); | ||
|
||
// Transfer results to the ValidationMessageStore | ||
foreach (var validationResult in validationResults) | ||
{ | ||
if (!validationResult.MemberNames.Any()) | ||
{ | ||
_validationMessageStore.Add(new FieldIdentifier(value, string.Empty), validationResult.ErrorMessage); | ||
continue; | ||
} | ||
|
||
foreach (var memberName in validationResult.MemberNames) | ||
{ | ||
var fieldIdentifier = new FieldIdentifier(value, memberName); | ||
_validationMessageStore.Add(fieldIdentifier, validationResult.ErrorMessage); | ||
} | ||
} | ||
} | ||
|
||
private void ValidateObject(object value, HashSet<object> visited, List<ValidationResult> validationResults) | ||
{ | ||
var validationContext = new ValidationContext(value); | ||
validationContext.Items.Add(ValidationContextValidatorKey, this); | ||
validationContext.Items.Add(ValidatedObjectsKey, visited); | ||
Validator.TryValidateObject(value, validationContext, validationResults, validateAllProperties: true); | ||
} | ||
|
||
internal static bool TryValidateRecursive(object value, ValidationContext validationContext) | ||
{ | ||
if (validationContext.Items.TryGetValue(ValidationContextValidatorKey, out var result) && result is ObjectGraphDataAnnotationsValidator validator) | ||
{ | ||
var visited = (HashSet<object>)validationContext.Items[ValidatedObjectsKey]; | ||
validator.ValidateObject(value, visited); | ||
|
||
return true; | ||
} | ||
|
||
return false; | ||
} | ||
|
||
private static void ValidateField(EditContext editContext, ValidationMessageStore messages, in FieldIdentifier fieldIdentifier) | ||
{ | ||
// DataAnnotations only validates public properties, so that's all we'll look for | ||
var propertyInfo = fieldIdentifier.Model.GetType().GetProperty(fieldIdentifier.FieldName); | ||
if (propertyInfo != null) | ||
{ | ||
var propertyValue = propertyInfo.GetValue(fieldIdentifier.Model); | ||
var validationContext = new ValidationContext(fieldIdentifier.Model) | ||
{ | ||
MemberName = propertyInfo.Name | ||
}; | ||
var results = new List<ValidationResult>(); | ||
|
||
Validator.TryValidateProperty(propertyValue, validationContext, results); | ||
messages.Clear(fieldIdentifier); | ||
messages.Add(fieldIdentifier, results.Select(result => result.ErrorMessage)); | ||
|
||
// We have to notify even if there were no messages before and are still no messages now, | ||
// because the "state" that changed might be the completion of some async validation task | ||
editContext.NotifyValidationStateChanged(); | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using Microsoft.AspNetCore.Components.Forms; | ||
|
||
namespace System.ComponentModel.DataAnnotations | ||
{ | ||
/// <summary> | ||
/// A <see cref="ValidationAttribute"/> that indicates that the property is a complex or collection type that further needs to be validated. | ||
/// <para> | ||
/// By default <see cref="Validator"/> does not recurse in to complex property types during validation. | ||
/// When used in conjunction with <see cref="ObjectGraphDataAnnotationsValidator"/>, this property allows the validation system to validate | ||
/// complex or collection type properties. | ||
/// </para> | ||
/// </summary> | ||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] | ||
public sealed class ValidateComplexTypeAttribute : ValidationAttribute | ||
{ | ||
/// <inheritdoc /> | ||
protected override ValidationResult IsValid(object value, ValidationContext validationContext) | ||
{ | ||
if (!ObjectGraphDataAnnotationsValidator.TryValidateRecursive(value, validationContext)) | ||
{ | ||
throw new InvalidOperationException($"{nameof(ValidateComplexTypeAttribute)} can only used with {nameof(ObjectGraphDataAnnotationsValidator)}."); | ||
} | ||
|
||
return ValidationResult.Success; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<Reference Include="Microsoft.AspNetCore.Blazor.DataAnnotations.Validation" /> | ||
</ItemGroup> | ||
|
||
</Project> |
Uh oh!
There was an error while loading. Please reload this page.