Add `[ValidateNever]` and `IPropertyValidationFilter`
- #5642 - lazy-load `ValidationEntry.Model` - avoids `Exception`s when moving to a property that will not be validated nits: - remove duplicate code in `ValidationVisitor` - clarify "all properties of" doc comments - also add missing `<param>` doc in `ViewDataInfo`
This commit is contained in:
parent
07c22f2b29
commit
ce53675b87
|
|
@ -8,6 +8,7 @@ using System.ComponentModel;
|
|||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||
using Microsoft.Extensions.Internal;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||
|
|
@ -295,6 +296,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
/// </summary>
|
||||
public abstract string TemplateHint { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets an <see cref="IPropertyValidationFilter"/> implementation that indicates whether this model should be
|
||||
/// validated. If <c>null</c>, properties with this <see cref="ModelMetadata"/> are validated.
|
||||
/// </summary>
|
||||
/// <value>Defaults to <c>null</c>.</value>
|
||||
public virtual IPropertyValidationFilter PropertyValidationFilter => null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value that indicates whether properties or elements of the model should be validated.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
// 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 Microsoft.AspNetCore.Mvc.ModelBinding.Validation
|
||||
{
|
||||
/// <summary>
|
||||
/// Contract for attributes that determine whether associated properties should be validated. When the attribute is
|
||||
/// applied to a property, the validation system calls <see cref="ShouldValidateEntry"/> to determine whether to
|
||||
/// validate that property. When applied to a type, the validation system calls <see cref="ShouldValidateEntry"/>
|
||||
/// for each property that type defines to determine whether to validate it.
|
||||
/// </summary>
|
||||
public interface IPropertyValidationFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets an indication whether the <paramref name="entry"/> should be validated.
|
||||
/// </summary>
|
||||
/// <param name="entry"><see cref="ValidationEntry"/> to check.</param>
|
||||
/// <param name="parentEntry"><see cref="ValidationEntry"/> containing <paramref name="entry"/>.</param>
|
||||
/// <returns><c>true</c> if <paramref name="entry"/> should be validated; <c>false</c> otherwise.</returns>
|
||||
bool ShouldValidateEntry(ValidationEntry entry, ValidationEntry parentEntry);
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
|
|||
/// </summary>
|
||||
public struct ValidationEntry
|
||||
{
|
||||
private object _model;
|
||||
private Func<object> _modelAccessor;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="ValidationEntry"/>.
|
||||
/// </summary>
|
||||
|
|
@ -30,7 +33,37 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
|
|||
|
||||
Metadata = metadata;
|
||||
Key = key;
|
||||
Model = model;
|
||||
_model = model;
|
||||
_modelAccessor = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="ValidationEntry"/>.
|
||||
/// </summary>
|
||||
/// <param name="metadata">The <see cref="ModelMetadata"/> associated with the <see cref="Model"/>.</param>
|
||||
/// <param name="key">The model prefix associated with the <see cref="Model"/>.</param>
|
||||
/// <param name="modelAccessor">A delegate that will return the <see cref="Model"/>.</param>
|
||||
public ValidationEntry(ModelMetadata metadata, string key, Func<object> modelAccessor)
|
||||
{
|
||||
if (metadata == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(metadata));
|
||||
}
|
||||
|
||||
if (key == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
}
|
||||
|
||||
if (modelAccessor == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(modelAccessor));
|
||||
}
|
||||
|
||||
Metadata = metadata;
|
||||
Key = key;
|
||||
_model = null;
|
||||
_modelAccessor = modelAccessor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -46,6 +79,18 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
|
|||
/// <summary>
|
||||
/// The model object.
|
||||
/// </summary>
|
||||
public object Model { get; }
|
||||
public object Model
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_modelAccessor != null)
|
||||
{
|
||||
_model = _modelAccessor();
|
||||
_modelAccessor = null;
|
||||
}
|
||||
|
||||
return _model;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,36 +84,20 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
var propertyName = property.BinderModelName ?? property.PropertyName;
|
||||
var key = ModelNames.CreatePropertyModelName(_key, propertyName);
|
||||
|
||||
object model;
|
||||
|
||||
// Our property accessors don't work on Mono 4.0.4 - see https://github.com/aspnet/External/issues/44
|
||||
// This is a workaround for what the PropertyGetter does in the background.
|
||||
if (IsMono)
|
||||
if (_model == null)
|
||||
{
|
||||
if (_model == null)
|
||||
{
|
||||
model = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
var propertyInfo = _model.GetType().GetRuntimeProperty(property.PropertyName);
|
||||
try
|
||||
{
|
||||
model = propertyInfo.GetValue(_model);
|
||||
}
|
||||
catch (TargetInvocationException ex)
|
||||
{
|
||||
throw ex.InnerException;
|
||||
}
|
||||
}
|
||||
// Performance: Never create a delegate when container is null.
|
||||
_entry = new ValidationEntry(property, key, model: null);
|
||||
}
|
||||
else if (IsMono)
|
||||
{
|
||||
_entry = new ValidationEntry(property, key, () => GetModelOnMono(_model, property.PropertyName));
|
||||
}
|
||||
else
|
||||
{
|
||||
model = property.PropertyGetter(_model);
|
||||
_entry = new ValidationEntry(property, key, () => GetModel(_model, property));
|
||||
}
|
||||
|
||||
_entry = new ValidationEntry(property, key, model);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -125,6 +109,26 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private static object GetModel(object container, ModelMetadata property)
|
||||
{
|
||||
return property.PropertyGetter(container);
|
||||
}
|
||||
|
||||
// Our property accessors don't work on Mono 4.0.4 - see https://github.com/aspnet/External/issues/44
|
||||
// This is a workaround for what the PropertyGetter does in the background.
|
||||
private static object GetModelOnMono(object container, string propertyName)
|
||||
{
|
||||
var propertyInfo = container.GetType().GetRuntimeProperty(propertyName);
|
||||
try
|
||||
{
|
||||
return propertyInfo.GetValue(container);
|
||||
}
|
||||
catch (TargetInvocationException ex)
|
||||
{
|
||||
throw ex.InnerException;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||
|
||||
|
|
@ -34,6 +36,24 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IPropertyValidationFilter attributes on a type affect properties in that type, not properties that have
|
||||
// that type. Thus, we ignore context.TypeAttributes for properties and not check at all for types.
|
||||
if (context.Key.MetadataKind == ModelMetadataKind.Property)
|
||||
{
|
||||
var validationFilter = context.PropertyAttributes.OfType<IPropertyValidationFilter>().FirstOrDefault();
|
||||
if (validationFilter == null)
|
||||
{
|
||||
// No IPropertyValidationFilter attributes on the property.
|
||||
// Check if container has such an attribute.
|
||||
validationFilter = context.Key.ContainerType.GetTypeInfo()
|
||||
.GetCustomAttributes(inherit: true)
|
||||
.OfType<IPropertyValidationFilter>()
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
context.ValidationMetadata.PropertyValidationFilter = validationFilter;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,8 +7,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
{
|
||||
/// <summary>
|
||||
/// Indicates that a property should be excluded from model binding. When applied to a property, the model binding
|
||||
/// system excludes that property. When applied to a type, the model binding system excludes all properties of that
|
||||
/// type.
|
||||
/// system excludes that property. When applied to a type, the model binding system excludes all properties that
|
||||
/// type defines.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
|
||||
public sealed class BindNeverAttribute : BindingBehaviorAttribute
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
/// <summary>
|
||||
/// Indicates that a property is required for model binding. When applied to a property, the model binding system
|
||||
/// requires a value for that property. When applied to a type, the model binding system requires values for all
|
||||
/// properties of that type.
|
||||
/// properties that type defines.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
|
||||
public sealed class BindRequiredAttribute : BindingBehaviorAttribute
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
|
||||
{
|
||||
|
|
@ -531,6 +532,15 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
|
|||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IPropertyValidationFilter PropertyValidationFilter
|
||||
{
|
||||
get
|
||||
{
|
||||
return ValidationMetadata.PropertyValidationFilter;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool ValidateChildren
|
||||
{
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
|
|||
/// Creates a new <see cref="ExcludeBindingMetadataProvider"/> for the given <paramref name="type"/>.
|
||||
/// </summary>
|
||||
/// <param name="type">
|
||||
/// The <see cref="Type"/>. All properties of this <see cref="Type"/> will have
|
||||
/// The <see cref="Type"/>. All properties with this <see cref="Type"/> will have
|
||||
/// <see cref="ModelMetadata.IsBindingAllowed"/> set to <c>false</c>.
|
||||
/// </param>
|
||||
public ExcludeBindingMetadataProvider(Type type)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
|
||||
{
|
||||
|
|
@ -18,9 +19,15 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
|
|||
/// </summary>
|
||||
public bool? IsRequired { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an <see cref="IPropertyValidationFilter"/> implementation that indicates whether this model
|
||||
/// should be validated. See <see cref="ModelMetadata.PropertyValidationFilter"/>.
|
||||
/// </summary>
|
||||
public IPropertyValidationFilter PropertyValidationFilter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value that indicates whether children of the model should be validated. If <c>null</c>
|
||||
/// then <see cref="ModelMetadata.ValidateChildren"/> will be <c>true</c> if either of
|
||||
/// then <see cref="ModelMetadata.ValidateChildren"/> will be <c>true</c> if either of
|
||||
/// <see cref="ModelMetadata.IsComplexType"/> or <see cref="ModelMetadata.IsEnumerableType"/> is <c>true</c>;
|
||||
/// <c>false</c> otherwise.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IPropertyValidationFilter"/> implementation that unconditionally indicates a property should be
|
||||
/// excluded from validation. When applied to a property, the validation system excludes that property. When
|
||||
/// applied to a type, the validation system excludes all properties within that type.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
|
||||
public sealed class ValidateNeverAttribute : Attribute, IPropertyValidationFilter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public bool ShouldValidateEntry(ValidationEntry entry, ValidationEntry parentEntry)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -185,52 +185,26 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
|
|||
{
|
||||
if (_metadata.IsEnumerableType)
|
||||
{
|
||||
return VisitEnumerableType();
|
||||
return VisitComplexType(DefaultCollectionValidationStrategy.Instance);
|
||||
}
|
||||
else if (_metadata.IsComplexType)
|
||||
|
||||
if (_metadata.IsComplexType)
|
||||
{
|
||||
return VisitComplexType();
|
||||
}
|
||||
else
|
||||
{
|
||||
return VisitSimpleType();
|
||||
return VisitComplexType(DefaultComplexObjectValidationStrategy.Instance);
|
||||
}
|
||||
|
||||
return VisitSimpleType();
|
||||
}
|
||||
}
|
||||
|
||||
private bool VisitEnumerableType()
|
||||
// Covers everything VisitSimpleType does not i.e. both enumerations and complex types.
|
||||
private bool VisitComplexType(IValidationStrategy defaultStrategy)
|
||||
{
|
||||
var isValid = true;
|
||||
|
||||
if (_model != null && _metadata.ValidateChildren)
|
||||
{
|
||||
var strategy = _strategy ?? DefaultCollectionValidationStrategy.Instance;
|
||||
isValid = VisitChildren(strategy);
|
||||
}
|
||||
else if (_model != null)
|
||||
{
|
||||
// Suppress validation for the entries matching this prefix. This will temporarily set
|
||||
// the current node to 'skipped' but we're going to visit it right away, so subsequent
|
||||
// code will set it to 'valid' or 'invalid'
|
||||
SuppressValidation(_key);
|
||||
}
|
||||
|
||||
// Double-checking HasReachedMaxErrors just in case this model has no elements.
|
||||
if (isValid && !_modelState.HasReachedMaxErrors)
|
||||
{
|
||||
isValid &= ValidateNode();
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
private bool VisitComplexType()
|
||||
{
|
||||
var isValid = true;
|
||||
|
||||
if (_model != null && _metadata.ValidateChildren)
|
||||
{
|
||||
var strategy = _strategy ?? DefaultComplexObjectValidationStrategy.Instance;
|
||||
var strategy = _strategy ?? defaultStrategy;
|
||||
isValid = VisitChildren(strategy);
|
||||
}
|
||||
else if (_model != null)
|
||||
|
|
@ -265,14 +239,20 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
|
|||
{
|
||||
var isValid = true;
|
||||
var enumerator = strategy.GetChildren(_metadata, _key, _model);
|
||||
var parentEntry = new ValidationEntry(_metadata, _key, _model);
|
||||
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
var metadata = enumerator.Current.Metadata;
|
||||
var model = enumerator.Current.Model;
|
||||
var key = enumerator.Current.Key;
|
||||
var entry = enumerator.Current;
|
||||
var metadata = entry.Metadata;
|
||||
var key = entry.Key;
|
||||
if (metadata.PropertyValidationFilter?.ShouldValidateEntry(entry, parentEntry) == false)
|
||||
{
|
||||
SuppressValidation(key);
|
||||
continue;
|
||||
}
|
||||
|
||||
isValid &= Visit(metadata, key, model);
|
||||
isValid &= Visit(metadata, key, entry.Model);
|
||||
}
|
||||
|
||||
return isValid;
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ using System;
|
|||
namespace Microsoft.AspNetCore.Mvc
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates associated property or all properties of associated type should be edited using an <input>
|
||||
/// element of type "hidden".
|
||||
/// Indicates associated property or all properties with the associated type should be edited using an
|
||||
/// <input> element of type "hidden".
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When overriding a <see cref="HiddenInputAttribute"/> inherited from a base class, should apply both
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ViewDataInfo"/> class with info about a
|
||||
/// <see cref="ViewDataDictionary"/> lookup which is evaluated when <see cref="Value"/> is read.
|
||||
/// It uses <see cref="System.Reflection.PropertyInfo.GetValue(object)"/> on <paramref name="propertyInfo"/>
|
||||
/// <see cref="ViewDataDictionary"/> lookup which is evaluated when <see cref="Value"/> is read.
|
||||
/// It uses <see cref="System.Reflection.PropertyInfo.GetValue(object)"/> on <paramref name="propertyInfo"/>
|
||||
/// passing parameter <paramref name="container"/> to lazily evaluate the value.
|
||||
/// </summary>
|
||||
/// <param name="container">The <see cref="object"/> that <see cref="Value"/> will be evaluated from.</param>
|
||||
|
|
@ -45,7 +45,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
/// </summary>
|
||||
/// <param name="container">The <see cref="object"/> that has the <see cref="Value"/>.</param>
|
||||
/// <param name="propertyInfo">The <see cref="PropertyInfo"/> that represents <see cref="Value"/>'s property.</param>
|
||||
/// <param name="valueAccessor"></param>
|
||||
/// <param name="valueAccessor">A delegate that will return the <see cref="Value"/>.</param>
|
||||
public ViewDataInfo(object container, PropertyInfo propertyInfo, Func<object> valueAccessor)
|
||||
{
|
||||
Container = container;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using System.Collections;
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||
|
|
@ -576,6 +577,14 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
}
|
||||
}
|
||||
|
||||
public override IPropertyValidationFilter PropertyValidationFilter
|
||||
{
|
||||
get
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public override bool ValidateChildren
|
||||
{
|
||||
get
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
|
@ -12,7 +13,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
public class DefaultComplexObjectValidationStrategyTest
|
||||
{
|
||||
[Fact]
|
||||
public void EnumerateElements()
|
||||
public void GetChildren_ReturnsExpectedElements()
|
||||
{
|
||||
// Arrange
|
||||
var model = new Person()
|
||||
|
|
@ -31,23 +32,92 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
// Assert
|
||||
Assert.Collection(
|
||||
BufferEntries(enumerator).OrderBy(e => e.Key),
|
||||
e =>
|
||||
entry =>
|
||||
{
|
||||
Assert.Equal("prefix.Age", e.Key);
|
||||
Assert.Equal(23, e.Model);
|
||||
Assert.Same(metadata.Properties["Age"], e.Metadata);
|
||||
Assert.Equal("prefix.Age", entry.Key);
|
||||
Assert.Equal(23, entry.Model);
|
||||
Assert.Same(metadata.Properties["Age"], entry.Metadata);
|
||||
},
|
||||
e =>
|
||||
entry =>
|
||||
{
|
||||
Assert.Equal("prefix.Id", e.Key);
|
||||
Assert.Equal(1, e.Model);
|
||||
Assert.Same(metadata.Properties["Id"], e.Metadata);
|
||||
Assert.Equal("prefix.Id", entry.Key);
|
||||
Assert.Equal(1, entry.Model);
|
||||
Assert.Same(metadata.Properties["Id"], entry.Metadata);
|
||||
},
|
||||
e =>
|
||||
entry =>
|
||||
{
|
||||
Assert.Equal("prefix.Name", e.Key);
|
||||
Assert.Equal("Joey", e.Model);
|
||||
Assert.Same(metadata.Properties["Name"], e.Metadata);
|
||||
Assert.Equal("prefix.Name", entry.Key);
|
||||
Assert.Equal("Joey", entry.Model);
|
||||
Assert.Same(metadata.Properties["Name"], entry.Metadata);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetChildren_SetsModelNull_IfContainerNull()
|
||||
{
|
||||
// Arrange
|
||||
Person model = null;
|
||||
var metadata = TestModelMetadataProvider.CreateDefaultProvider().GetMetadataForType(typeof(Person));
|
||||
var strategy = DefaultComplexObjectValidationStrategy.Instance;
|
||||
|
||||
// Act
|
||||
var enumerator = strategy.GetChildren(metadata, "prefix", model);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
BufferEntries(enumerator).OrderBy(e => e.Key),
|
||||
entry =>
|
||||
{
|
||||
Assert.Equal("prefix.Age", entry.Key);
|
||||
Assert.Null(entry.Model);
|
||||
Assert.Same(metadata.Properties["Age"], entry.Metadata);
|
||||
},
|
||||
entry =>
|
||||
{
|
||||
Assert.Equal("prefix.Id", entry.Key);
|
||||
Assert.Null(entry.Model);
|
||||
Assert.Same(metadata.Properties["Id"], entry.Metadata);
|
||||
},
|
||||
entry =>
|
||||
{
|
||||
Assert.Equal("prefix.Name", entry.Key);
|
||||
Assert.Null(entry.Model);
|
||||
Assert.Same(metadata.Properties["Name"], entry.Metadata);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetChildren_LazyLoadsModel()
|
||||
{
|
||||
// Arrange
|
||||
var model = new LazyPerson(input: null);
|
||||
var metadata = TestModelMetadataProvider.CreateDefaultProvider().GetMetadataForType(typeof(LazyPerson));
|
||||
var strategy = DefaultComplexObjectValidationStrategy.Instance;
|
||||
|
||||
// Act
|
||||
var enumerator = strategy.GetChildren(metadata, "prefix", model);
|
||||
|
||||
// Assert
|
||||
// Note: NREs are not thrown until the Model property is accessed.
|
||||
Assert.Collection(
|
||||
BufferEntries(enumerator).OrderBy(e => e.Key),
|
||||
entry =>
|
||||
{
|
||||
Assert.Equal("prefix.Age", entry.Key);
|
||||
Assert.Equal(23, entry.Model);
|
||||
Assert.Same(metadata.Properties["Age"], entry.Metadata);
|
||||
},
|
||||
entry =>
|
||||
{
|
||||
Assert.Equal("prefix.Id", entry.Key);
|
||||
Assert.Throws<NullReferenceException>(() => entry.Model);
|
||||
Assert.Same(metadata.Properties["Id"], entry.Metadata);
|
||||
},
|
||||
entry =>
|
||||
{
|
||||
Assert.Equal("prefix.Name", entry.Key);
|
||||
Assert.Throws<NullReferenceException>(() => entry.Model);
|
||||
Assert.Same(metadata.Properties["Name"], entry.Metadata);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -70,5 +140,21 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
private class LazyPerson
|
||||
{
|
||||
private string _string;
|
||||
|
||||
public LazyPerson(string input)
|
||||
{
|
||||
_string = input;
|
||||
}
|
||||
|
||||
public int Id => _string.Length;
|
||||
|
||||
public int Age => 23;
|
||||
|
||||
public string Name => _string.Substring(3, 5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ using System.Collections.ObjectModel;
|
|||
using System.Linq;
|
||||
using System.Xml;
|
||||
using Microsoft.AspNetCore.Mvc.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -50,6 +51,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
|
|||
Assert.False(metadata.IsRequired); // Defaults to false for reference types
|
||||
Assert.True(metadata.ShowForDisplay);
|
||||
Assert.True(metadata.ShowForEdit);
|
||||
Assert.False(metadata.ValidateChildren); // Defaults to true for complex and enumerable types.
|
||||
|
||||
Assert.Null(metadata.DataTypeName);
|
||||
Assert.Null(metadata.Description);
|
||||
|
|
@ -60,9 +62,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
|
|||
Assert.Null(metadata.EnumGroupedDisplayNamesAndValues);
|
||||
Assert.Null(metadata.EnumNamesAndValues);
|
||||
Assert.Null(metadata.NullDisplayText);
|
||||
Assert.Null(metadata.TemplateHint);
|
||||
Assert.Null(metadata.PropertyValidationFilter);
|
||||
Assert.Null(metadata.SimpleDisplayProperty);
|
||||
Assert.Null(metadata.Placeholder);
|
||||
Assert.Null(metadata.TemplateHint);
|
||||
|
||||
Assert.Equal(10000, ModelMetadata.DefaultOrder);
|
||||
Assert.Equal(ModelMetadata.DefaultOrder, metadata.Order);
|
||||
|
|
@ -659,6 +662,42 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
|
|||
Assert.True(validateChildren);
|
||||
}
|
||||
|
||||
public static TheoryData<IPropertyValidationFilter> ValidationFilterData
|
||||
{
|
||||
get
|
||||
{
|
||||
return new TheoryData<IPropertyValidationFilter>
|
||||
{
|
||||
null,
|
||||
new ValidateNeverAttribute(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ValidationFilterData))]
|
||||
public void PropertyValidationFilter_ReflectsFilter_FromValidationMetadata(IPropertyValidationFilter value)
|
||||
{
|
||||
// Arrange
|
||||
var detailsProvider = new EmptyCompositeMetadataDetailsProvider();
|
||||
var provider = new DefaultModelMetadataProvider(detailsProvider);
|
||||
|
||||
var key = ModelMetadataIdentity.ForType(typeof(int));
|
||||
var cache = new DefaultMetadataDetails(key, new ModelAttributes(new object[0]));
|
||||
cache.ValidationMetadata = new ValidationMetadata
|
||||
{
|
||||
PropertyValidationFilter = value,
|
||||
};
|
||||
|
||||
var metadata = new DefaultModelMetadata(provider, detailsProvider, cache);
|
||||
|
||||
// Act
|
||||
var validationFilter = metadata.PropertyValidationFilter;
|
||||
|
||||
// Assert
|
||||
Assert.Same(value, validationFilter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateChildren_OverrideTrue()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -11,6 +11,104 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
|
|||
{
|
||||
public class DefaultValidationMetadataProviderTest
|
||||
{
|
||||
[Fact]
|
||||
public void PropertyValidationFilter_ShouldValidateEntry_False_IfPropertyHasValidateNever()
|
||||
{
|
||||
// Arrange
|
||||
var provider = new DefaultValidationMetadataProvider();
|
||||
|
||||
var attributes = new Attribute[] { new ValidateNeverAttribute() };
|
||||
var key = ModelMetadataIdentity.ForProperty(typeof(int), "Length", typeof(string));
|
||||
var context = new ValidationMetadataProviderContext(key, new ModelAttributes(attributes, new object[0]));
|
||||
|
||||
// Act
|
||||
provider.CreateValidationMetadata(context);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(context.ValidationMetadata.PropertyValidationFilter);
|
||||
Assert.False(context.ValidationMetadata.PropertyValidationFilter.ShouldValidateEntry(
|
||||
new ValidationEntry(),
|
||||
new ValidationEntry()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PropertyValidationFilter_Null_IfPropertyHasValidateNeverOnItsType()
|
||||
{
|
||||
// Arrange
|
||||
var provider = new DefaultValidationMetadataProvider();
|
||||
|
||||
var attributes = new Attribute[] { new ValidateNeverAttribute() };
|
||||
var key = ModelMetadataIdentity.ForProperty(typeof(int), "Length", typeof(string));
|
||||
var context = new ValidationMetadataProviderContext(key, new ModelAttributes(new object[0], attributes));
|
||||
|
||||
// Act
|
||||
provider.CreateValidationMetadata(context);
|
||||
|
||||
// Assert
|
||||
Assert.Null(context.ValidationMetadata.PropertyValidationFilter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PropertyValidationFilter_Null_ForType()
|
||||
{
|
||||
// Arrange
|
||||
var provider = new DefaultValidationMetadataProvider();
|
||||
|
||||
var attributes = new Attribute[] { new ValidateNeverAttribute() };
|
||||
var key = ModelMetadataIdentity.ForType(typeof(ValidateNeverClass));
|
||||
var context = new ValidationMetadataProviderContext(key, new ModelAttributes(attributes));
|
||||
|
||||
// Act
|
||||
provider.CreateValidationMetadata(context);
|
||||
|
||||
// Assert
|
||||
Assert.Null(context.ValidationMetadata.PropertyValidationFilter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PropertyValidationFilter_ShouldValidateEntry_False_IfContainingTypeHasValidateNever()
|
||||
{
|
||||
// Arrange
|
||||
var provider = new DefaultValidationMetadataProvider();
|
||||
|
||||
var key = ModelMetadataIdentity.ForProperty(
|
||||
typeof(string),
|
||||
nameof(ValidateNeverClass.ClassName),
|
||||
typeof(ValidateNeverClass));
|
||||
var context = new ValidationMetadataProviderContext(key, new ModelAttributes(new object[0], new object[0]));
|
||||
|
||||
// Act
|
||||
provider.CreateValidationMetadata(context);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(context.ValidationMetadata.PropertyValidationFilter);
|
||||
Assert.False(context.ValidationMetadata.PropertyValidationFilter.ShouldValidateEntry(
|
||||
new ValidationEntry(),
|
||||
new ValidationEntry()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PropertyValidationFilter_ShouldValidateEntry_False_IfContainingTypeInheritsValidateNever()
|
||||
{
|
||||
// Arrange
|
||||
var provider = new DefaultValidationMetadataProvider();
|
||||
|
||||
var key = ModelMetadataIdentity.ForProperty(
|
||||
typeof(string),
|
||||
nameof(ValidateNeverSubclass.SubclassName),
|
||||
typeof(ValidateNeverSubclass));
|
||||
var context = new ValidationMetadataProviderContext(key, new ModelAttributes(new object[0], new object[0]));
|
||||
|
||||
// Act
|
||||
provider.CreateValidationMetadata(context);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(context.ValidationMetadata.PropertyValidationFilter);
|
||||
Assert.False(context.ValidationMetadata.PropertyValidationFilter.ShouldValidateEntry(
|
||||
new ValidationEntry(),
|
||||
new ValidationEntry()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetValidationDetails_MarkedWithClientValidator_ReturnsValidator()
|
||||
{
|
||||
|
|
@ -69,6 +167,17 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
|
|||
Assert.Same(attribute, validatorMetadata);
|
||||
}
|
||||
|
||||
[ValidateNever]
|
||||
private class ValidateNeverClass
|
||||
{
|
||||
public string ClassName { get; set; }
|
||||
}
|
||||
|
||||
private class ValidateNeverSubclass : ValidateNeverClass
|
||||
{
|
||||
public string SubclassName { get; set; }
|
||||
}
|
||||
|
||||
private class TestModelValidationAttribute : Attribute, IModelValidator
|
||||
{
|
||||
public IEnumerable<ModelValidationResult> Validate(ModelValidationContext context)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ using Microsoft.AspNetCore.Http;
|
|||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -1222,6 +1224,347 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
Assert.Equal("The value '-123' is not valid for Zip.", error.ErrorMessage);
|
||||
}
|
||||
|
||||
private class NeverValid : IValidatableObject
|
||||
{
|
||||
public string NeverValidProperty { get; set; }
|
||||
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
return new[] { new ValidationResult("This is not valid.") };
|
||||
}
|
||||
}
|
||||
|
||||
private class NeverValidAttribute : ValidationAttribute
|
||||
{
|
||||
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
|
||||
{
|
||||
// By default, ValidationVisitor visits _all_ properties within a non-null complex object.
|
||||
// But, like most reasonable ValidationAttributes, NeverValidAttribute ignores null property values.
|
||||
if (value == null)
|
||||
{
|
||||
return ValidationResult.Success;
|
||||
}
|
||||
|
||||
return new ValidationResult("Properties with this are not valid.");
|
||||
}
|
||||
}
|
||||
|
||||
private class ValidateSomeProperties
|
||||
{
|
||||
public NeverValid NeverValid { get; set; }
|
||||
|
||||
[NeverValid]
|
||||
public string NeverValidBecauseAttribute { get; set; }
|
||||
|
||||
[ValidateNever]
|
||||
[NeverValid]
|
||||
public string ValidateNever { get; set; }
|
||||
|
||||
[ValidateNever]
|
||||
public int ValidateNeverLength => ValidateNever.Length;
|
||||
}
|
||||
|
||||
[ValidateNever]
|
||||
private class ValidateNoProperties : ValidateSomeProperties
|
||||
{
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IValidatableObject_IsValidated()
|
||||
{
|
||||
// Arrange
|
||||
var parameter = new ParameterDescriptor
|
||||
{
|
||||
Name = "parameter",
|
||||
ParameterType = typeof(ValidateSomeProperties),
|
||||
};
|
||||
|
||||
var testContext = ModelBindingTestHelper.GetTestContext(
|
||||
request => request.QueryString
|
||||
= new QueryString($"?{nameof(ValidateSomeProperties.NeverValid)}.{nameof(NeverValid.NeverValidProperty)}=1"));
|
||||
|
||||
var argumentBinder = ModelBindingTestHelper.GetArgumentBinder();
|
||||
var modelState = testContext.ModelState;
|
||||
|
||||
// Act
|
||||
var result = await argumentBinder.BindModelAsync(parameter, testContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsModelSet);
|
||||
var model = Assert.IsType<ValidateSomeProperties>(result.Model);
|
||||
Assert.Equal("1", model.NeverValid.NeverValidProperty);
|
||||
|
||||
Assert.False(modelState.IsValid);
|
||||
Assert.Equal(1, modelState.ErrorCount);
|
||||
Assert.Collection(
|
||||
modelState,
|
||||
state =>
|
||||
{
|
||||
Assert.Equal(nameof(ValidateSomeProperties.NeverValid), state.Key);
|
||||
Assert.Equal(ModelValidationState.Invalid, state.Value.ValidationState);
|
||||
|
||||
var error = Assert.Single(state.Value.Errors);
|
||||
Assert.Equal("This is not valid.", error.ErrorMessage);
|
||||
Assert.Null(error.Exception);
|
||||
},
|
||||
state =>
|
||||
{
|
||||
Assert.Equal(
|
||||
$"{nameof(ValidateSomeProperties.NeverValid)}.{nameof(NeverValid.NeverValidProperty)}",
|
||||
state.Key);
|
||||
Assert.Equal(ModelValidationState.Valid, state.Value.ValidationState);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CustomValidationAttribute_IsValidated()
|
||||
{
|
||||
// Arrange
|
||||
var parameter = new ParameterDescriptor
|
||||
{
|
||||
Name = "parameter",
|
||||
ParameterType = typeof(ValidateSomeProperties),
|
||||
};
|
||||
|
||||
var testContext = ModelBindingTestHelper.GetTestContext(
|
||||
request => request.QueryString
|
||||
= new QueryString($"?{nameof(ValidateSomeProperties.NeverValidBecauseAttribute)}=1"));
|
||||
|
||||
var argumentBinder = ModelBindingTestHelper.GetArgumentBinder();
|
||||
var modelState = testContext.ModelState;
|
||||
|
||||
// Act
|
||||
var result = await argumentBinder.BindModelAsync(parameter, testContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsModelSet);
|
||||
var model = Assert.IsType<ValidateSomeProperties>(result.Model);
|
||||
Assert.Equal("1", model.NeverValidBecauseAttribute);
|
||||
|
||||
Assert.False(modelState.IsValid);
|
||||
Assert.Equal(1, modelState.ErrorCount);
|
||||
var kvp = Assert.Single(modelState);
|
||||
Assert.Equal(nameof(ValidateSomeProperties.NeverValidBecauseAttribute), kvp.Key);
|
||||
var state = kvp.Value;
|
||||
Assert.NotNull(state);
|
||||
Assert.Equal(ModelValidationState.Invalid, state.ValidationState);
|
||||
var error = Assert.Single(state.Errors);
|
||||
Assert.Equal("Properties with this are not valid.", error.ErrorMessage);
|
||||
Assert.Null(error.Exception);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateNeverProperty_IsSkipped()
|
||||
{
|
||||
// Arrange
|
||||
var parameter = new ParameterDescriptor
|
||||
{
|
||||
Name = "parameter",
|
||||
ParameterType = typeof(ValidateSomeProperties),
|
||||
};
|
||||
|
||||
var testContext = ModelBindingTestHelper.GetTestContext(
|
||||
request => request.QueryString
|
||||
= new QueryString($"?{nameof(ValidateSomeProperties.ValidateNever)}=1"));
|
||||
|
||||
var argumentBinder = ModelBindingTestHelper.GetArgumentBinder();
|
||||
var modelState = testContext.ModelState;
|
||||
|
||||
// Act
|
||||
var result = await argumentBinder.BindModelAsync(parameter, testContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsModelSet);
|
||||
var model = Assert.IsType<ValidateSomeProperties>(result.Model);
|
||||
Assert.Equal("1", model.ValidateNever);
|
||||
|
||||
Assert.True(modelState.IsValid);
|
||||
var kvp = Assert.Single(modelState);
|
||||
Assert.Equal(nameof(ValidateSomeProperties.ValidateNever), kvp.Key);
|
||||
var state = kvp.Value;
|
||||
Assert.NotNull(state);
|
||||
Assert.Equal(ModelValidationState.Skipped, state.ValidationState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateNeverProperty_IsSkippedWithoutAccessingModel()
|
||||
{
|
||||
// Arrange
|
||||
var parameter = new ParameterDescriptor
|
||||
{
|
||||
Name = "parameter",
|
||||
ParameterType = typeof(ValidateSomeProperties),
|
||||
};
|
||||
|
||||
var testContext = ModelBindingTestHelper.GetTestContext();
|
||||
var argumentBinder = ModelBindingTestHelper.GetArgumentBinder();
|
||||
var modelState = testContext.ModelState;
|
||||
|
||||
// Act
|
||||
var result = await argumentBinder.BindModelAsync(parameter, testContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsModelSet);
|
||||
var model = Assert.IsType<ValidateSomeProperties>(result.Model);
|
||||
|
||||
// Note this Exception is not thrown earlier.
|
||||
Assert.Throws<NullReferenceException>(() => model.ValidateNeverLength);
|
||||
|
||||
Assert.True(modelState.IsValid);
|
||||
Assert.Empty(modelState);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(nameof(ValidateSomeProperties.NeverValid) + "." + nameof(NeverValid.NeverValidProperty))]
|
||||
[InlineData(nameof(ValidateSomeProperties.NeverValidBecauseAttribute))]
|
||||
[InlineData(nameof(ValidateSomeProperties.ValidateNever))]
|
||||
public async Task PropertyWithinValidateNeverType_IsSkipped(string propertyName)
|
||||
{
|
||||
// Arrange
|
||||
var parameter = new ParameterDescriptor
|
||||
{
|
||||
Name = "parameter",
|
||||
ParameterType = typeof(ValidateNoProperties),
|
||||
};
|
||||
|
||||
var testContext = ModelBindingTestHelper.GetTestContext(
|
||||
request => request.QueryString = new QueryString($"?{propertyName}=1"));
|
||||
|
||||
var argumentBinder = ModelBindingTestHelper.GetArgumentBinder();
|
||||
var modelState = testContext.ModelState;
|
||||
|
||||
// Act
|
||||
var result = await argumentBinder.BindModelAsync(parameter, testContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsModelSet);
|
||||
Assert.IsType<ValidateNoProperties>(result.Model);
|
||||
|
||||
Assert.True(modelState.IsValid);
|
||||
var kvp = Assert.Single(modelState);
|
||||
Assert.Equal(propertyName, kvp.Key);
|
||||
var state = kvp.Value;
|
||||
Assert.NotNull(state);
|
||||
Assert.Equal(ModelValidationState.Skipped, state.ValidationState);
|
||||
}
|
||||
|
||||
private class ValidateSometimesAttribute : Attribute, IPropertyValidationFilter
|
||||
{
|
||||
private readonly string _otherProperty;
|
||||
|
||||
public ValidateSometimesAttribute(string otherProperty)
|
||||
{
|
||||
// Would null-check otherProperty in real life.
|
||||
_otherProperty = otherProperty;
|
||||
}
|
||||
|
||||
public bool ShouldValidateEntry(ValidationEntry entry, ValidationEntry parentEntry)
|
||||
{
|
||||
if (entry.Metadata.MetadataKind == ModelMetadataKind.Property &&
|
||||
parentEntry.Metadata != null)
|
||||
{
|
||||
// In real life, would throw an InvalidOperationException if otherProperty were null i.e. the
|
||||
// property was not known. Could also assert container is non-null (see ValidationVisitor).
|
||||
var container = parentEntry.Model;
|
||||
var otherProperty = parentEntry.Metadata.Properties[_otherProperty];
|
||||
if (otherProperty.PropertyGetter(container) == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private class ValidateSomePropertiesSometimes
|
||||
{
|
||||
public string Control { get; set; }
|
||||
|
||||
[ValidateSometimes(nameof(Control))]
|
||||
public int ControlLength => Control.Length;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PropertyToSometimesSkip_IsSkipped_IfControlIsNull()
|
||||
{
|
||||
// Arrange
|
||||
var parameter = new ParameterDescriptor
|
||||
{
|
||||
Name = "parameter",
|
||||
ParameterType = typeof(ValidateSomePropertiesSometimes),
|
||||
};
|
||||
|
||||
var testContext = ModelBindingTestHelper.GetTestContext();
|
||||
var argumentBinder = ModelBindingTestHelper.GetArgumentBinder();
|
||||
var modelState = testContext.ModelState;
|
||||
|
||||
// Add an entry for the ControlLength property so that we can observe Skipped versus Valid states.
|
||||
modelState.SetModelValue(
|
||||
nameof(ValidateSomePropertiesSometimes.ControlLength),
|
||||
rawValue: null,
|
||||
attemptedValue: null);
|
||||
|
||||
// Act
|
||||
var result = await argumentBinder.BindModelAsync(parameter, testContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsModelSet);
|
||||
var model = Assert.IsType<ValidateSomePropertiesSometimes>(result.Model);
|
||||
Assert.Null(model.Control);
|
||||
|
||||
// Note this Exception is not thrown earlier.
|
||||
Assert.Throws<NullReferenceException>(() => model.ControlLength);
|
||||
|
||||
Assert.True(modelState.IsValid);
|
||||
var kvp = Assert.Single(modelState);
|
||||
Assert.Equal(nameof(ValidateSomePropertiesSometimes.ControlLength), kvp.Key);
|
||||
Assert.Equal(ModelValidationState.Skipped, kvp.Value.ValidationState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PropertyToSometimesSkip_IsValidated_IfControlIsNotNull()
|
||||
{
|
||||
// Arrange
|
||||
var parameter = new ParameterDescriptor
|
||||
{
|
||||
Name = "parameter",
|
||||
ParameterType = typeof(ValidateSomePropertiesSometimes),
|
||||
};
|
||||
|
||||
var testContext = ModelBindingTestHelper.GetTestContext(
|
||||
request => request.QueryString = new QueryString(
|
||||
$"?{nameof(ValidateSomePropertiesSometimes.Control)}=1"));
|
||||
|
||||
var argumentBinder = ModelBindingTestHelper.GetArgumentBinder();
|
||||
var modelState = testContext.ModelState;
|
||||
|
||||
// Add an entry for the ControlLength property so that we can observe Skipped versus Valid states.
|
||||
modelState.SetModelValue(
|
||||
nameof(ValidateSomePropertiesSometimes.ControlLength),
|
||||
rawValue: null,
|
||||
attemptedValue: null);
|
||||
|
||||
// Act
|
||||
var result = await argumentBinder.BindModelAsync(parameter, testContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsModelSet);
|
||||
var model = Assert.IsType<ValidateSomePropertiesSometimes>(result.Model);
|
||||
Assert.Equal("1", model.Control);
|
||||
Assert.Equal(1, model.ControlLength);
|
||||
|
||||
Assert.True(modelState.IsValid);
|
||||
Assert.Collection(
|
||||
modelState,
|
||||
state => Assert.Equal(nameof(ValidateSomePropertiesSometimes.Control), state.Key),
|
||||
state =>
|
||||
{
|
||||
Assert.Equal(nameof(ValidateSomePropertiesSometimes.ControlLength), state.Key);
|
||||
Assert.Equal(ModelValidationState.Valid, state.Value.ValidationState);
|
||||
});
|
||||
}
|
||||
|
||||
private class Order11
|
||||
{
|
||||
public IEnumerable<Address> ShippingAddresses { get; set; }
|
||||
|
|
|
|||
Loading…
Reference in New Issue