зеркало из https://github.com/aspnet/Mvc.git
Issue #452: Changes to enable validation for objects generated by InputFormatters. UnitTests + FunctionalTests for the same.
This commit is contained in:
Родитель
2670b19f5c
Коммит
f1c1549267
|
@ -17,6 +17,7 @@ namespace Microsoft.AspNet.Mvc
|
||||||
{
|
{
|
||||||
private readonly IActionBindingContextProvider _bindingProvider;
|
private readonly IActionBindingContextProvider _bindingProvider;
|
||||||
private readonly INestedProviderManager<FilterProviderContext> _filterProvider;
|
private readonly INestedProviderManager<FilterProviderContext> _filterProvider;
|
||||||
|
private readonly IBodyModelValidator _modelValidator;
|
||||||
|
|
||||||
private IFilter[] _filters;
|
private IFilter[] _filters;
|
||||||
private FilterCursor _cursor;
|
private FilterCursor _cursor;
|
||||||
|
@ -34,11 +35,13 @@ namespace Microsoft.AspNet.Mvc
|
||||||
public FilterActionInvoker(
|
public FilterActionInvoker(
|
||||||
[NotNull] ActionContext actionContext,
|
[NotNull] ActionContext actionContext,
|
||||||
[NotNull] IActionBindingContextProvider bindingContextProvider,
|
[NotNull] IActionBindingContextProvider bindingContextProvider,
|
||||||
[NotNull] INestedProviderManager<FilterProviderContext> filterProvider)
|
[NotNull] INestedProviderManager<FilterProviderContext> filterProvider,
|
||||||
|
[NotNull] IBodyModelValidator modelValidator)
|
||||||
{
|
{
|
||||||
ActionContext = actionContext;
|
ActionContext = actionContext;
|
||||||
_bindingProvider = bindingContextProvider;
|
_bindingProvider = bindingContextProvider;
|
||||||
_filterProvider = filterProvider;
|
_filterProvider = filterProvider;
|
||||||
|
_modelValidator = modelValidator;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected ActionContext ActionContext { get; private set; }
|
protected ActionContext ActionContext { get; private set; }
|
||||||
|
@ -234,9 +237,11 @@ namespace Microsoft.AspNet.Mvc
|
||||||
for (var i = 0; i < parameters.Count; i++)
|
for (var i = 0; i < parameters.Count; i++)
|
||||||
{
|
{
|
||||||
var parameter = parameters[i];
|
var parameter = parameters[i];
|
||||||
|
var parameterType = parameter.BodyParameterInfo != null ?
|
||||||
|
parameter.BodyParameterInfo.ParameterType : parameter.ParameterBindingInfo.ParameterType;
|
||||||
|
|
||||||
if (parameter.BodyParameterInfo != null)
|
if (parameter.BodyParameterInfo != null)
|
||||||
{
|
{
|
||||||
var parameterType = parameter.BodyParameterInfo.ParameterType;
|
|
||||||
var formatterContext = new InputFormatterContext(actionBindingContext.ActionContext,
|
var formatterContext = new InputFormatterContext(actionBindingContext.ActionContext,
|
||||||
parameterType);
|
parameterType);
|
||||||
var inputFormatter = actionBindingContext.InputFormatterSelector.SelectFormatter(
|
var inputFormatter = actionBindingContext.InputFormatterSelector.SelectFormatter(
|
||||||
|
@ -250,15 +255,23 @@ namespace Microsoft.AspNet.Mvc
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
parameterValues[parameter.Name] = await inputFormatter.ReadAsync(formatterContext);
|
parameterValues[parameter.Name] = await inputFormatter.ReadAsync(formatterContext);
|
||||||
|
var modelMetadata =
|
||||||
|
metadataProvider.GetMetadataForType(modelAccessor: null, modelType: parameterType);
|
||||||
|
modelMetadata.Model = parameterValues[parameter.Name];
|
||||||
|
|
||||||
|
// Validate the generated object
|
||||||
|
var validationContext = new ModelValidationContext(metadataProvider,
|
||||||
|
actionBindingContext.ValidatorProvider,
|
||||||
|
modelState,
|
||||||
|
modelMetadata,
|
||||||
|
containerMetadata: null);
|
||||||
|
_modelValidator.Validate(validationContext, parameter.Name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var parameterType = parameter.ParameterBindingInfo.ParameterType;
|
var modelMetadata =
|
||||||
var modelMetadata = metadataProvider.GetMetadataForType(
|
metadataProvider.GetMetadataForType(modelAccessor: null, modelType: parameterType);
|
||||||
modelAccessor: null,
|
|
||||||
modelType: parameterType);
|
|
||||||
|
|
||||||
var modelBindingContext = new ModelBindingContext
|
var modelBindingContext = new ModelBindingContext
|
||||||
{
|
{
|
||||||
ModelName = parameter.Name,
|
ModelName = parameter.Name,
|
||||||
|
|
|
@ -2,11 +2,10 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNet.Mvc.Core;
|
using Microsoft.AspNet.Mvc.Core;
|
||||||
|
using Microsoft.AspNet.Mvc.ModelBinding;
|
||||||
using Microsoft.Framework.DependencyInjection;
|
using Microsoft.Framework.DependencyInjection;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc
|
namespace Microsoft.AspNet.Mvc
|
||||||
|
@ -22,8 +21,9 @@ namespace Microsoft.AspNet.Mvc
|
||||||
[NotNull] INestedProviderManager<FilterProviderContext> filterProvider,
|
[NotNull] INestedProviderManager<FilterProviderContext> filterProvider,
|
||||||
[NotNull] IControllerFactory controllerFactory,
|
[NotNull] IControllerFactory controllerFactory,
|
||||||
[NotNull] ReflectedActionDescriptor descriptor,
|
[NotNull] ReflectedActionDescriptor descriptor,
|
||||||
[NotNull] IInputFormattersProvider inputFormattersProvider)
|
[NotNull] IInputFormattersProvider inputFormattersProvider,
|
||||||
: base(actionContext, bindingContextProvider, filterProvider)
|
[NotNull] IBodyModelValidator modelValidator)
|
||||||
|
: base(actionContext, bindingContextProvider, filterProvider, modelValidator)
|
||||||
{
|
{
|
||||||
_descriptor = descriptor;
|
_descriptor = descriptor;
|
||||||
_controllerFactory = controllerFactory;
|
_controllerFactory = controllerFactory;
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using Microsoft.AspNet.Mvc.ModelBinding;
|
||||||
using Microsoft.Framework.DependencyInjection;
|
using Microsoft.Framework.DependencyInjection;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc
|
namespace Microsoft.AspNet.Mvc
|
||||||
|
@ -12,16 +13,19 @@ namespace Microsoft.AspNet.Mvc
|
||||||
private readonly IActionBindingContextProvider _bindingProvider;
|
private readonly IActionBindingContextProvider _bindingProvider;
|
||||||
private readonly IInputFormattersProvider _inputFormattersProvider;
|
private readonly IInputFormattersProvider _inputFormattersProvider;
|
||||||
private readonly INestedProviderManager<FilterProviderContext> _filterProvider;
|
private readonly INestedProviderManager<FilterProviderContext> _filterProvider;
|
||||||
|
private readonly IBodyModelValidator _modelValidator;
|
||||||
|
|
||||||
public ReflectedActionInvokerProvider(IControllerFactory controllerFactory,
|
public ReflectedActionInvokerProvider(IControllerFactory controllerFactory,
|
||||||
IActionBindingContextProvider bindingProvider,
|
IActionBindingContextProvider bindingProvider,
|
||||||
IInputFormattersProvider inputFormattersProvider,
|
IInputFormattersProvider inputFormattersProvider,
|
||||||
INestedProviderManager<FilterProviderContext> filterProvider)
|
INestedProviderManager<FilterProviderContext> filterProvider,
|
||||||
|
IBodyModelValidator modelValidator)
|
||||||
{
|
{
|
||||||
_controllerFactory = controllerFactory;
|
_controllerFactory = controllerFactory;
|
||||||
_bindingProvider = bindingProvider;
|
_bindingProvider = bindingProvider;
|
||||||
_inputFormattersProvider = inputFormattersProvider;
|
_inputFormattersProvider = inputFormattersProvider;
|
||||||
_filterProvider = filterProvider;
|
_filterProvider = filterProvider;
|
||||||
|
_modelValidator = modelValidator;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int Order
|
public int Order
|
||||||
|
@ -41,7 +45,8 @@ namespace Microsoft.AspNet.Mvc
|
||||||
_filterProvider,
|
_filterProvider,
|
||||||
_controllerFactory,
|
_controllerFactory,
|
||||||
actionDescriptor,
|
actionDescriptor,
|
||||||
_inputFormattersProvider);
|
_inputFormattersProvider,
|
||||||
|
_modelValidator);
|
||||||
}
|
}
|
||||||
|
|
||||||
callNext();
|
callNext();
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
// Copyright (c) Microsoft Open Technologies, Inc. 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.Reflection;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
internal class TypeHelper
|
||||||
|
{
|
||||||
|
internal static bool IsSimpleType(Type type)
|
||||||
|
{
|
||||||
|
return type.GetTypeInfo().IsPrimitive ||
|
||||||
|
type.Equals(typeof(decimal)) ||
|
||||||
|
type.Equals(typeof(string)) ||
|
||||||
|
type.Equals(typeof(DateTime)) ||
|
||||||
|
type.Equals(typeof(Guid)) ||
|
||||||
|
type.Equals(typeof(DateTimeOffset)) ||
|
||||||
|
type.Equals(typeof(TimeSpan));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,248 @@
|
||||||
|
// Copyright (c) Microsoft Open Technologies, Inc. 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;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.Contracts;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using Microsoft.AspNet.Mvc.ModelBinding.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Recursively validate an object.
|
||||||
|
/// </summary>
|
||||||
|
public class DefaultBodyModelValidator : IBodyModelValidator
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool Validate([NotNull] ModelValidationContext modelValidationContext, string keyPrefix)
|
||||||
|
{
|
||||||
|
var metadata = modelValidationContext.ModelMetadata;
|
||||||
|
var validationContext = new ValidationContext()
|
||||||
|
{
|
||||||
|
ModelValidationContext = modelValidationContext,
|
||||||
|
Visited = new HashSet<object>(ReferenceEqualityComparer.Instance),
|
||||||
|
KeyBuilders = new Stack<IKeyBuilder>(),
|
||||||
|
RootPrefix = keyPrefix
|
||||||
|
};
|
||||||
|
|
||||||
|
return ValidateNonVisitedNodeAndChildren(metadata, validationContext, validators: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ValidateNonVisitedNodeAndChildren(
|
||||||
|
ModelMetadata metadata, ValidationContext validationContext, IEnumerable<IModelValidator> validators)
|
||||||
|
{
|
||||||
|
// Recursion guard to avoid stack overflows
|
||||||
|
RuntimeHelpers.EnsureSufficientExecutionStack();
|
||||||
|
|
||||||
|
var isValid = true;
|
||||||
|
if (validators == null)
|
||||||
|
{
|
||||||
|
// The validators are not null in the case of validating an array. Since the validators are
|
||||||
|
// the same for all the elements of the array, we do not do GetValidators for each element,
|
||||||
|
// instead we just pass them over. See ValidateElements function.
|
||||||
|
validators = validationContext.ModelValidationContext.ValidatorProvider.GetValidators(metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't need to recursively traverse the graph for null values
|
||||||
|
if (metadata.Model == null)
|
||||||
|
{
|
||||||
|
return ShallowValidate(metadata, validationContext, validators);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't need to recursively traverse the graph for types that shouldn't be validated
|
||||||
|
var modelType = metadata.Model.GetType();
|
||||||
|
if (TypeHelper.IsSimpleType(modelType))
|
||||||
|
{
|
||||||
|
return ShallowValidate(metadata, validationContext, validators);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check to avoid infinite recursion. This can happen with cycles in an object graph.
|
||||||
|
if (validationContext.Visited.Contains(metadata.Model))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
validationContext.Visited.Add(metadata.Model);
|
||||||
|
|
||||||
|
// Validate the children first - depth-first traversal
|
||||||
|
var enumerableModel = metadata.Model as IEnumerable;
|
||||||
|
if (enumerableModel == null)
|
||||||
|
{
|
||||||
|
isValid = ValidateProperties(metadata, validationContext);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
isValid = ValidateElements(enumerableModel, validationContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isValid)
|
||||||
|
{
|
||||||
|
// Don't bother to validate this node if children failed.
|
||||||
|
isValid = ShallowValidate(metadata, validationContext, validators);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pop the object so that it can be validated again in a different path
|
||||||
|
validationContext.Visited.Remove(metadata.Model);
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ValidateProperties(ModelMetadata metadata, ValidationContext validationContext)
|
||||||
|
{
|
||||||
|
var isValid = true;
|
||||||
|
var propertyScope = new PropertyScope();
|
||||||
|
validationContext.KeyBuilders.Push(propertyScope);
|
||||||
|
foreach (var childMetadata in
|
||||||
|
validationContext.ModelValidationContext.MetadataProvider.GetMetadataForProperties(
|
||||||
|
metadata.Model, metadata.RealModelType))
|
||||||
|
{
|
||||||
|
propertyScope.PropertyName = childMetadata.PropertyName;
|
||||||
|
if (!ValidateNonVisitedNodeAndChildren(childMetadata, validationContext, validators: null))
|
||||||
|
{
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validationContext.KeyBuilders.Pop();
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ValidateElements(IEnumerable model, ValidationContext validationContext)
|
||||||
|
{
|
||||||
|
var isValid = true;
|
||||||
|
var elementType = GetElementType(model.GetType());
|
||||||
|
var elementMetadata =
|
||||||
|
validationContext.ModelValidationContext.MetadataProvider.GetMetadataForType(
|
||||||
|
modelAccessor: null, modelType: elementType);
|
||||||
|
|
||||||
|
var elementScope = new ElementScope() { Index = 0 };
|
||||||
|
validationContext.KeyBuilders.Push(elementScope);
|
||||||
|
var validators = validationContext.ModelValidationContext.ValidatorProvider.GetValidators(elementMetadata);
|
||||||
|
|
||||||
|
// If there are no validators or the object is null we bail out quickly
|
||||||
|
// when there are large arrays of null, this will save a significant amount of processing
|
||||||
|
// with minimal impact to other scenarios.
|
||||||
|
var anyValidatorsDefined = validators.Any();
|
||||||
|
|
||||||
|
foreach (var element in model)
|
||||||
|
{
|
||||||
|
// If the element is non null, the recursive calls might find more validators.
|
||||||
|
// If it's null, then a shallow validation will be performed.
|
||||||
|
if (element != null || anyValidatorsDefined)
|
||||||
|
{
|
||||||
|
elementMetadata.Model = element;
|
||||||
|
|
||||||
|
if (!ValidateNonVisitedNodeAndChildren(elementMetadata, validationContext, validators))
|
||||||
|
{
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
elementScope.Index++;
|
||||||
|
}
|
||||||
|
|
||||||
|
validationContext.KeyBuilders.Pop();
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validates a single node (not including children)
|
||||||
|
// Returns true if validation passes successfully
|
||||||
|
private static bool ShallowValidate(
|
||||||
|
ModelMetadata metadata,
|
||||||
|
ValidationContext validationContext,
|
||||||
|
[NotNull] IEnumerable<IModelValidator> validators)
|
||||||
|
{
|
||||||
|
var isValid = true;
|
||||||
|
string modelKey = null;
|
||||||
|
|
||||||
|
// When the are no validators we bail quickly. This saves a GetEnumerator allocation.
|
||||||
|
// In a large array (tens of thousands or more) scenario it's very significant.
|
||||||
|
var validatorsAsCollection = validators as ICollection;
|
||||||
|
if (validatorsAsCollection != null && validatorsAsCollection.Count == 0)
|
||||||
|
{
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
var modelValidationContext =
|
||||||
|
new ModelValidationContext(validationContext.ModelValidationContext, metadata);
|
||||||
|
foreach (var validator in validators)
|
||||||
|
{
|
||||||
|
foreach (var error in validator.Validate(modelValidationContext))
|
||||||
|
{
|
||||||
|
if (modelKey == null)
|
||||||
|
{
|
||||||
|
modelKey = validationContext.RootPrefix;
|
||||||
|
// This constructs the object heirarchy
|
||||||
|
// Example: prefix.Parent.Child
|
||||||
|
foreach (var keyBuilder in validationContext.KeyBuilders.Reverse())
|
||||||
|
{
|
||||||
|
modelKey = keyBuilder.AppendTo(modelKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var errorKey = ModelBindingHelper.CreatePropertyModelName(modelKey, error.MemberName);
|
||||||
|
validationContext.ModelValidationContext.ModelState.AddModelError(errorKey, error.Message);
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Type GetElementType(Type type)
|
||||||
|
{
|
||||||
|
Contract.Assert(typeof(IEnumerable).IsAssignableFrom(type));
|
||||||
|
if (type.IsArray)
|
||||||
|
{
|
||||||
|
return type.GetElementType();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var implementedInterface in type.GetInterfaces())
|
||||||
|
{
|
||||||
|
if (implementedInterface.IsGenericType() &&
|
||||||
|
implementedInterface.GetGenericTypeDefinition() == typeof(IEnumerable<>))
|
||||||
|
{
|
||||||
|
return implementedInterface.GetGenericArguments()[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface IKeyBuilder
|
||||||
|
{
|
||||||
|
string AppendTo(string prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class PropertyScope : IKeyBuilder
|
||||||
|
{
|
||||||
|
public string PropertyName { get; set; }
|
||||||
|
|
||||||
|
public string AppendTo(string prefix)
|
||||||
|
{
|
||||||
|
return ModelBindingHelper.CreatePropertyModelName(prefix, PropertyName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ElementScope : IKeyBuilder
|
||||||
|
{
|
||||||
|
public int Index { get; set; }
|
||||||
|
|
||||||
|
public string AppendTo(string prefix)
|
||||||
|
{
|
||||||
|
return ModelBindingHelper.CreateIndexModelName(prefix, Index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ValidationContext
|
||||||
|
{
|
||||||
|
public ModelValidationContext ModelValidationContext { get; set; }
|
||||||
|
public HashSet<object> Visited { get; set; }
|
||||||
|
public Stack<IKeyBuilder> KeyBuilders { get; set; }
|
||||||
|
public string RootPrefix { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Validates the body parameter of an action after the parameter
|
||||||
|
/// has been read by the Input Formatters.
|
||||||
|
/// </summary>
|
||||||
|
public interface IBodyModelValidator
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether the Model is valid
|
||||||
|
/// and adds any validation errors to the <see cref="ModelStateDictionary"/>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="modelValidaitonContext">The validation context which contains the model, metadata
|
||||||
|
/// and the validator providers.</param>
|
||||||
|
/// <param name="keyPrefix">The <see cref="string"/> to append to the key for any validation errors.</param>
|
||||||
|
/// <returns><c>true</c>if the model is valid, <c>false</c> otherwise.</returns>
|
||||||
|
bool Validate(ModelValidationContext modelValidationContext, string keyPrefix);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
// Copyright (c) Microsoft Open Technologies, Inc. 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.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
internal class ReferenceEqualityComparer : IEqualityComparer<object>
|
||||||
|
{
|
||||||
|
private static readonly ReferenceEqualityComparer _instance = new ReferenceEqualityComparer();
|
||||||
|
|
||||||
|
public static ReferenceEqualityComparer Instance
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public new bool Equals(object x, object y)
|
||||||
|
{
|
||||||
|
return ReferenceEquals(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetHashCode(object obj)
|
||||||
|
{
|
||||||
|
return RuntimeHelpers.GetHashCode(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -111,6 +111,8 @@ namespace Microsoft.AspNet.Mvc
|
||||||
DefaultViewComponentInvokerProvider>();
|
DefaultViewComponentInvokerProvider>();
|
||||||
yield return describe.Transient<IViewComponentHelper, DefaultViewComponentHelper>();
|
yield return describe.Transient<IViewComponentHelper, DefaultViewComponentHelper>();
|
||||||
|
|
||||||
|
yield return describe.Transient<IBodyModelValidator, DefaultBodyModelValidator>();
|
||||||
|
|
||||||
yield return describe.Transient<IAuthorizationService, DefaultAuthorizationService>();
|
yield return describe.Transient<IAuthorizationService, DefaultAuthorizationService>();
|
||||||
yield return describe.Singleton<IClaimUidExtractor, DefaultClaimUidExtractor>();
|
yield return describe.Singleton<IClaimUidExtractor, DefaultClaimUidExtractor>();
|
||||||
yield return describe.Singleton<AntiForgery, AntiForgery>();
|
yield return describe.Singleton<AntiForgery, AntiForgery>();
|
||||||
|
|
|
@ -0,0 +1,225 @@
|
||||||
|
// Copyright (c) Microsoft Open Technologies, Inc. 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.ComponentModel.DataAnnotations;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNet.Http;
|
||||||
|
using Microsoft.AspNet.Mvc.ModelBinding;
|
||||||
|
using Microsoft.AspNet.Mvc.OptionDescriptors;
|
||||||
|
using Microsoft.AspNet.Routing;
|
||||||
|
using Microsoft.Framework.DependencyInjection;
|
||||||
|
using Microsoft.Framework.OptionsModel;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.Core.Test
|
||||||
|
{
|
||||||
|
public class InputObjectBindingTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task GetArguments_UsingInputFormatter_DeserializesWithoutErrors_WhenValidationAttributesAreAbsent()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var sampleName = "SampleName";
|
||||||
|
var input = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
|
||||||
|
"<Person><Name>" + sampleName + "</Name></Person>";
|
||||||
|
var modelStateDictionary = new ModelStateDictionary();
|
||||||
|
var invoker = GetReflectedActionInvoker(
|
||||||
|
input, typeof(Person), new XmlSerializerInputFormatter(), "application/xml");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await invoker.GetActionArguments(modelStateDictionary);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(modelStateDictionary.IsValid);
|
||||||
|
Assert.Equal(0, modelStateDictionary.ErrorCount);
|
||||||
|
var model = result["foo"] as Person;
|
||||||
|
Assert.Equal(sampleName, model.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetArguments_UsingInputFormatter_DeserializesWithValidationError()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var sampleName = "SampleName";
|
||||||
|
var sampleUserName = "No5";
|
||||||
|
var input = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
|
||||||
|
"<User><Name>" + sampleName + "</Name><UserName>" + sampleUserName + "</UserName></User>";
|
||||||
|
var modelStateDictionary = new ModelStateDictionary();
|
||||||
|
var invoker = GetReflectedActionInvoker(input, typeof(User), new XmlSerializerInputFormatter(), "application/xml");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await invoker.GetActionArguments(modelStateDictionary);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(modelStateDictionary.IsValid);
|
||||||
|
Assert.Equal(1, modelStateDictionary.ErrorCount);
|
||||||
|
Assert.Equal(
|
||||||
|
"The field UserName must be a string or array type with a minimum length of '5'.",
|
||||||
|
Assert.Single(Assert.Single(modelStateDictionary.Values).Errors).ErrorMessage);
|
||||||
|
var model = result["foo"] as User;
|
||||||
|
Assert.Equal(sampleName, model.Name);
|
||||||
|
Assert.Equal(sampleUserName, model.UserName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetArguments_UsingInputFormatter_DeserializesArrays()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var sampleFirstUser = "FirstUser";
|
||||||
|
var sampleFirstUserName = "fuser";
|
||||||
|
var sampleSecondUser = "SecondUser";
|
||||||
|
var sampleSecondUserName = "suser";
|
||||||
|
var input = "{'Users': [{Name : '" + sampleFirstUser + "', UserName: '" + sampleFirstUserName +
|
||||||
|
"'}, {Name: '" + sampleSecondUser + "', UserName: '" + sampleSecondUserName + "'}]}";
|
||||||
|
var modelStateDictionary = new ModelStateDictionary();
|
||||||
|
var invoker = GetReflectedActionInvoker(input, typeof(Customers), new JsonInputFormatter(), "application/xml");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await invoker.GetActionArguments(modelStateDictionary);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(modelStateDictionary.IsValid);
|
||||||
|
Assert.Equal(0, modelStateDictionary.ErrorCount);
|
||||||
|
var model = result["foo"] as Customers;
|
||||||
|
Assert.Equal(2, model.Users.Count);
|
||||||
|
Assert.Equal(sampleFirstUser, model.Users[0].Name);
|
||||||
|
Assert.Equal(sampleFirstUserName, model.Users[0].UserName);
|
||||||
|
Assert.Equal(sampleSecondUser, model.Users[1].Name);
|
||||||
|
Assert.Equal(sampleSecondUserName, model.Users[1].UserName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetArguments_UsingInputFormatter_DeserializesArrays_WithErrors()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var sampleFirstUser = "FirstUser";
|
||||||
|
var sampleFirstUserName = "fusr";
|
||||||
|
var sampleSecondUser = "SecondUser";
|
||||||
|
var sampleSecondUserName = "susr";
|
||||||
|
var input = "{'Users': [{Name : '" + sampleFirstUser + "', UserName: '" + sampleFirstUserName +
|
||||||
|
"'}, {Name: '" + sampleSecondUser + "', UserName: '" + sampleSecondUserName + "'}]}";
|
||||||
|
var modelStateDictionary = new ModelStateDictionary();
|
||||||
|
var invoker = GetReflectedActionInvoker(input, typeof(Customers), new JsonInputFormatter(), "application/xml");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await invoker.GetActionArguments(modelStateDictionary);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(modelStateDictionary.IsValid);
|
||||||
|
Assert.Equal(2, modelStateDictionary.ErrorCount);
|
||||||
|
var model = result["foo"] as Customers;
|
||||||
|
Assert.Equal(
|
||||||
|
"The field UserName must be a string or array type with a minimum length of '5'.",
|
||||||
|
modelStateDictionary["foo.Users[0].UserName"].Errors[0].ErrorMessage);
|
||||||
|
Assert.Equal(
|
||||||
|
"The field UserName must be a string or array type with a minimum length of '5'.",
|
||||||
|
modelStateDictionary["foo.Users[1].UserName"].Errors[0].ErrorMessage);
|
||||||
|
Assert.Equal(2, model.Users.Count);
|
||||||
|
Assert.Equal(sampleFirstUser, model.Users[0].Name);
|
||||||
|
Assert.Equal(sampleFirstUserName, model.Users[0].UserName);
|
||||||
|
Assert.Equal(sampleSecondUser, model.Users[1].Name);
|
||||||
|
Assert.Equal(sampleSecondUserName, model.Users[1].UserName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ReflectedActionInvoker GetReflectedActionInvoker(
|
||||||
|
string input, Type parameterType, IInputFormatter selectedFormatter, string contentType)
|
||||||
|
{
|
||||||
|
var mvcOptions = new MvcOptions();
|
||||||
|
var setup = new MvcOptionsSetup();
|
||||||
|
|
||||||
|
setup.Setup(mvcOptions);
|
||||||
|
var accessor = new Mock<IOptionsAccessor<MvcOptions>>();
|
||||||
|
accessor.SetupGet(a => a.Options)
|
||||||
|
.Returns(mvcOptions);
|
||||||
|
var validatorProvider = new DefaultModelValidatorProviderProvider(
|
||||||
|
accessor.Object, Mock.Of<ITypeActivator>(), Mock.Of<IServiceProvider>());
|
||||||
|
|
||||||
|
Func<object, int> method = x => 1;
|
||||||
|
var actionDescriptor = new ReflectedActionDescriptor
|
||||||
|
{
|
||||||
|
MethodInfo = method.Method,
|
||||||
|
Parameters = new List<ParameterDescriptor>
|
||||||
|
{
|
||||||
|
new ParameterDescriptor
|
||||||
|
{
|
||||||
|
Name = "foo",
|
||||||
|
BodyParameterInfo = new BodyParameterInfo(parameterType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var metadataProvider = new EmptyModelMetadataProvider();
|
||||||
|
var actionContext = GetActionContext(
|
||||||
|
Encodings.UTF8EncodingWithoutBOM.GetBytes(input), actionDescriptor, contentType);
|
||||||
|
|
||||||
|
var inputFormatterSelector = new Mock<IInputFormatterSelector>();
|
||||||
|
inputFormatterSelector.Setup(a => a.SelectFormatter(It.IsAny<InputFormatterContext>()))
|
||||||
|
.Returns(selectedFormatter);
|
||||||
|
var bindingContext = new ActionBindingContext(actionContext,
|
||||||
|
metadataProvider,
|
||||||
|
Mock.Of<IModelBinder>(),
|
||||||
|
Mock.Of<IValueProvider>(),
|
||||||
|
inputFormatterSelector.Object,
|
||||||
|
new CompositeModelValidatorProvider(validatorProvider));
|
||||||
|
|
||||||
|
var actionBindingContextProvider = new Mock<IActionBindingContextProvider>();
|
||||||
|
actionBindingContextProvider.Setup(p => p.GetActionBindingContextAsync(It.IsAny<ActionContext>()))
|
||||||
|
.Returns(Task.FromResult(bindingContext));
|
||||||
|
|
||||||
|
var inputFormattersProvider = new Mock<IInputFormattersProvider>();
|
||||||
|
inputFormattersProvider.SetupGet(o => o.InputFormatters)
|
||||||
|
.Returns(new List<IInputFormatter>());
|
||||||
|
return new ReflectedActionInvoker(actionContext,
|
||||||
|
actionBindingContextProvider.Object,
|
||||||
|
Mock.Of<INestedProviderManager<FilterProviderContext>>(),
|
||||||
|
Mock.Of<IControllerFactory>(),
|
||||||
|
actionDescriptor,
|
||||||
|
inputFormattersProvider.Object,
|
||||||
|
new DefaultBodyModelValidator());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ActionContext GetActionContext(byte[] contentBytes,
|
||||||
|
ActionDescriptor actionDescriptor,
|
||||||
|
string contentType)
|
||||||
|
{
|
||||||
|
return new ActionContext(GetHttpContext(contentBytes, contentType),
|
||||||
|
new RouteData(),
|
||||||
|
actionDescriptor);
|
||||||
|
}
|
||||||
|
private static HttpContext GetHttpContext(byte[] contentBytes,
|
||||||
|
string contentType)
|
||||||
|
{
|
||||||
|
var request = new Mock<HttpRequest>();
|
||||||
|
var headers = new Mock<IHeaderDictionary>();
|
||||||
|
request.SetupGet(r => r.Headers).Returns(headers.Object);
|
||||||
|
request.SetupGet(f => f.Body).Returns(new MemoryStream(contentBytes));
|
||||||
|
request.SetupGet(f => f.ContentType).Returns(contentType);
|
||||||
|
|
||||||
|
var httpContext = new Mock<HttpContext>();
|
||||||
|
httpContext.SetupGet(c => c.Request).Returns(request.Object);
|
||||||
|
httpContext.SetupGet(c => c.Request).Returns(request.Object);
|
||||||
|
return httpContext.Object;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Person
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class User : Person
|
||||||
|
{
|
||||||
|
[MinLength(5)]
|
||||||
|
public string UserName { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Customers
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public List<User> Users { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -1345,7 +1345,8 @@ namespace Microsoft.AspNet.Mvc
|
||||||
filterProvider.Object,
|
filterProvider.Object,
|
||||||
controllerFactory,
|
controllerFactory,
|
||||||
actionDescriptor,
|
actionDescriptor,
|
||||||
inputFormattersProvider.Object);
|
inputFormattersProvider.Object,
|
||||||
|
new DefaultBodyModelValidator());
|
||||||
|
|
||||||
return invoker;
|
return invoker;
|
||||||
}
|
}
|
||||||
|
@ -1390,7 +1391,8 @@ namespace Microsoft.AspNet.Mvc
|
||||||
Mock.Of<INestedProviderManager<FilterProviderContext>>(),
|
Mock.Of<INestedProviderManager<FilterProviderContext>>(),
|
||||||
Mock.Of<IControllerFactory>(),
|
Mock.Of<IControllerFactory>(),
|
||||||
actionDescriptor,
|
actionDescriptor,
|
||||||
inputFormattersProvider.Object);
|
inputFormattersProvider.Object,
|
||||||
|
new DefaultBodyModelValidator());
|
||||||
|
|
||||||
var modelStateDictionary = new ModelStateDictionary();
|
var modelStateDictionary = new ModelStateDictionary();
|
||||||
|
|
||||||
|
@ -1449,7 +1451,8 @@ namespace Microsoft.AspNet.Mvc
|
||||||
Mock.Of<INestedProviderManager<FilterProviderContext>>(),
|
Mock.Of<INestedProviderManager<FilterProviderContext>>(),
|
||||||
Mock.Of<IControllerFactory>(),
|
Mock.Of<IControllerFactory>(),
|
||||||
actionDescriptor,
|
actionDescriptor,
|
||||||
inputFormattersProvider.Object);
|
inputFormattersProvider.Object,
|
||||||
|
new DefaultBodyModelValidator());
|
||||||
|
|
||||||
var modelStateDictionary = new ModelStateDictionary();
|
var modelStateDictionary = new ModelStateDictionary();
|
||||||
|
|
||||||
|
@ -1503,7 +1506,8 @@ namespace Microsoft.AspNet.Mvc
|
||||||
Mock.Of<INestedProviderManager<FilterProviderContext>>(),
|
Mock.Of<INestedProviderManager<FilterProviderContext>>(),
|
||||||
controllerFactory.Object,
|
controllerFactory.Object,
|
||||||
actionDescriptor,
|
actionDescriptor,
|
||||||
inputFormattersProvider.Object);
|
inputFormattersProvider.Object,
|
||||||
|
new DefaultBodyModelValidator());
|
||||||
|
|
||||||
|
|
||||||
var modelStateDictionary = new ModelStateDictionary();
|
var modelStateDictionary = new ModelStateDictionary();
|
||||||
|
@ -1570,7 +1574,8 @@ namespace Microsoft.AspNet.Mvc
|
||||||
Mock.Of<INestedProviderManager<FilterProviderContext>>(),
|
Mock.Of<INestedProviderManager<FilterProviderContext>>(),
|
||||||
controllerFactory.Object,
|
controllerFactory.Object,
|
||||||
actionDescriptor,
|
actionDescriptor,
|
||||||
inputFormattersProvider.Object);
|
inputFormattersProvider.Object,
|
||||||
|
new DefaultBodyModelValidator());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await invoker.InvokeAsync();
|
await invoker.InvokeAsync();
|
||||||
|
@ -1630,13 +1635,15 @@ namespace Microsoft.AspNet.Mvc
|
||||||
INestedProviderManager<FilterProviderContext> filterProvider,
|
INestedProviderManager<FilterProviderContext> filterProvider,
|
||||||
Mock<IControllerFactory> controllerFactoryMock,
|
Mock<IControllerFactory> controllerFactoryMock,
|
||||||
ReflectedActionDescriptor descriptor,
|
ReflectedActionDescriptor descriptor,
|
||||||
IInputFormattersProvider inputFormattersProvider) :
|
IInputFormattersProvider inputFormattersProvider,
|
||||||
|
IBodyModelValidator bodyModelValidator) :
|
||||||
base(actionContext,
|
base(actionContext,
|
||||||
bindingContextProvider,
|
bindingContextProvider,
|
||||||
filterProvider,
|
filterProvider,
|
||||||
controllerFactoryMock.Object,
|
controllerFactoryMock.Object,
|
||||||
descriptor,
|
descriptor,
|
||||||
inputFormattersProvider)
|
inputFormattersProvider,
|
||||||
|
bodyModelValidator)
|
||||||
{
|
{
|
||||||
_factoryMock = controllerFactoryMock;
|
_factoryMock = controllerFactoryMock;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
// Copyright (c) Microsoft Open Technologies, Inc. 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.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNet.Builder;
|
||||||
|
using Microsoft.AspNet.TestHost;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.FunctionalTests
|
||||||
|
{
|
||||||
|
public class InputObjectValidationTests
|
||||||
|
{
|
||||||
|
private readonly IServiceProvider _services = TestHelper.CreateServices("FormatterWebSite");
|
||||||
|
private readonly Action<IApplicationBuilder> _app = new FormatterWebSite.Startup().Configure;
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CheckIfObjectIsDeserializedWithoutErrors()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var server = TestServer.Create(_services, _app);
|
||||||
|
var client = server.CreateClient();
|
||||||
|
var sampleId = 2;
|
||||||
|
var sampleName = "SampleUser";
|
||||||
|
var sampleAlias = "SampleAlias";
|
||||||
|
var sampleDesignation = "HelloWorld";
|
||||||
|
var sampleDescription = "sample user";
|
||||||
|
var input = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
|
||||||
|
"<User xmlns=\"http://schemas.datacontract.org/2004/07/FormatterWebSite\"><Id>" + sampleId +
|
||||||
|
"</Id><Name>" + sampleName + "</Name><Alias>" + sampleAlias + "</Alias>" +
|
||||||
|
"<Designation>" + sampleDesignation + "</Designation><description>" +
|
||||||
|
sampleDescription + "</description></User>";
|
||||||
|
var content = new StringContent(input, Encoding.UTF8, "application/xml");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await client.PostAsync("http://localhost/Validation/Index", content);
|
||||||
|
|
||||||
|
//Assert
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.Equal("User has been registerd : " + sampleName,
|
||||||
|
await response.Content.ReadAsStringAsync());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CheckIfObjectIsDeserialized_WithErrors()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var server = TestServer.Create(_services, _app);
|
||||||
|
var client = server.CreateClient();
|
||||||
|
var sampleId = 0;
|
||||||
|
var sampleName = "user";
|
||||||
|
var sampleAlias = "a";
|
||||||
|
var sampleDesignation = "HelloWorld!";
|
||||||
|
var sampleDescription = "sample user";
|
||||||
|
var input = "{ Id:" + sampleId + ", Name:'" + sampleName + "', Alias:'" + sampleAlias +
|
||||||
|
"' ,Designation:'" + sampleDesignation + "', description:'" + sampleDescription + "'}";
|
||||||
|
var content = new StringContent(input, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await client.PostAsync("http://localhost/Validation/Index", content);
|
||||||
|
|
||||||
|
//Assert
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.Equal("The field Id must be between 1 and 2000.," +
|
||||||
|
"The field Name must be a string or array type with a minimum length of '5'.," +
|
||||||
|
"The field Alias must be a string with a minimum length of 3 and a maximum length of 15.," +
|
||||||
|
"The field Designation must match the regular expression '[0-9a-zA-Z]*'.",
|
||||||
|
await response.Content.ReadAsStringAsync());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,363 @@
|
||||||
|
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
#if ASPNET50
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Microsoft.AspNet.Mvc.OptionDescriptors;
|
||||||
|
using Microsoft.AspNet.Testing;
|
||||||
|
using Microsoft.Framework.DependencyInjection;
|
||||||
|
using Microsoft.Framework.OptionsModel;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
public class DefaultBodyModelValidatorTests
|
||||||
|
{
|
||||||
|
private static Person LonelyPerson;
|
||||||
|
|
||||||
|
static DefaultBodyModelValidatorTests()
|
||||||
|
{
|
||||||
|
LonelyPerson = new Person() { Name = "Reallllllllly Long Name" };
|
||||||
|
LonelyPerson.Friend = LonelyPerson;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<object[]> ValidationErrors
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
// returns an array of model, type of model and expected errors.
|
||||||
|
// Primitives
|
||||||
|
yield return new object[] { null, typeof(Person), new Dictionary<string, string>() };
|
||||||
|
yield return new object[] { 14, typeof(int), new Dictionary<string, string>() };
|
||||||
|
yield return new object[] { "foo", typeof(string), new Dictionary<string, string>() };
|
||||||
|
|
||||||
|
// Object Traversal : make sure we can traverse the object graph without throwing
|
||||||
|
yield return new object[] { new ValueType() { Reference = "ref", Value = 256 }, typeof(ValueType), new Dictionary<string, string>() };
|
||||||
|
yield return new object[] { new ReferenceType() { Reference = "ref", Value = 256 }, typeof(ReferenceType), new Dictionary<string, string>() };
|
||||||
|
|
||||||
|
// Classes
|
||||||
|
yield return new object[] { new Person() { Name = "Rick", Profession = "Astronaut" }, typeof(Person), new Dictionary<string, string>() };
|
||||||
|
yield return new object[] { new Person(), typeof(Person), new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{ "Name", "The Name field is required." },
|
||||||
|
{ "Profession", "The Profession field is required." }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
yield return new object[] { new Person() { Name = "Rick", Friend = new Person() }, typeof(Person), new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{ "Profession", "The Profession field is required." },
|
||||||
|
{ "Friend.Name", "The Name field is required." },
|
||||||
|
{ "Friend.Profession", "The Profession field is required." }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Collections
|
||||||
|
yield return new object[] { new Person[] { new Person(), new Person() }, typeof(Person[]), new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{ "[0].Name", "The Name field is required." },
|
||||||
|
{ "[0].Profession", "The Profession field is required." },
|
||||||
|
{ "[1].Name", "The Name field is required." },
|
||||||
|
{ "[1].Profession", "The Profession field is required." }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
yield return new object[] { new List<Person> { new Person(), new Person() }, typeof(Person[]), new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{ "[0].Name", "The Name field is required." },
|
||||||
|
{ "[0].Profession", "The Profession field is required." },
|
||||||
|
{ "[1].Name", "The Name field is required." },
|
||||||
|
{ "[1].Profession", "The Profession field is required." }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
yield return new object[] { new Dictionary<string, Person> { { "Joe", new Person() } , { "Mark", new Person() } }, typeof(Dictionary<string, Person>), new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{ "[0].Value.Name", "The Name field is required." },
|
||||||
|
{ "[0].Value.Profession", "The Profession field is required." },
|
||||||
|
{ "[1].Value.Name", "The Name field is required." },
|
||||||
|
{ "[1].Value.Profession", "The Profession field is required." }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// IValidatableObject's
|
||||||
|
yield return new object[] { new ValidatableModel(), typeof(ValidatableModel), new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{ "", "Error1" },
|
||||||
|
{ "Property1", "Error2" },
|
||||||
|
{ "Property2", "Error3" },
|
||||||
|
{ "Property3", "Error3" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
yield return new object[]{ new[] { new ValidatableModel() }, typeof(ValidatableModel[]), new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{ "[0]", "Error1" },
|
||||||
|
{ "[0].Property1", "Error2" },
|
||||||
|
{ "[0].Property2", "Error3" },
|
||||||
|
{ "[0].Property3", "Error3" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Nested Objects
|
||||||
|
yield return new object[] { new Org() {
|
||||||
|
Id = 1,
|
||||||
|
OrgName = "Org",
|
||||||
|
Dev = new Team
|
||||||
|
{
|
||||||
|
Id = 10,
|
||||||
|
TeamName = "HelloWorldTeam",
|
||||||
|
Lead = "SampleLeadDev",
|
||||||
|
TeamSize = 2
|
||||||
|
},
|
||||||
|
Test = new Team
|
||||||
|
{
|
||||||
|
Id = 11,
|
||||||
|
TeamName = "HWT",
|
||||||
|
Lead = "SampleTestLead",
|
||||||
|
TeamSize = 12
|
||||||
|
}
|
||||||
|
}, typeof(Org), new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{ "OrgName", "The field OrgName must be a string with a minimum length of 4 and a maximum length of 20." },
|
||||||
|
{ "Dev.Lead", "The field Lead must be a string or array type with a maximum length of '10'." },
|
||||||
|
{ "Dev.TeamSize", "The field TeamSize must be between 3 and 100." },
|
||||||
|
{ "Test.TeamName", "The field TeamName must be a string with a minimum length of 4 and a maximum length of 20." },
|
||||||
|
{ "Test.Lead", "The field Lead must be a string or array type with a maximum length of '10'." }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Testing we don't validate fields
|
||||||
|
yield return new object[] { new VariableTest() { test = 5 }, typeof(VariableTest), new Dictionary<string, string>() };
|
||||||
|
|
||||||
|
// Testing we don't blow up on cycles
|
||||||
|
yield return new object[] { LonelyPerson, typeof(Person), new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{ "Name", "The field Name must be a string with a maximum length of 10." },
|
||||||
|
{ "Profession", "The Profession field is required." }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[ReplaceCulture]
|
||||||
|
[MemberData(nameof(ValidationErrors))]
|
||||||
|
public void ExpectedValidationErrorsRaised(object model, Type type, Dictionary<string, string> expectedErrors)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var validationContext = GetModelValidationContext(model, type);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
Assert.DoesNotThrow(() =>
|
||||||
|
new DefaultBodyModelValidator().Validate(validationContext, keyPrefix: string.Empty)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var actualErrors = new Dictionary<string, string>();
|
||||||
|
foreach (var keyStatePair in validationContext.ModelState)
|
||||||
|
{
|
||||||
|
foreach (var error in keyStatePair.Value.Errors)
|
||||||
|
{
|
||||||
|
actualErrors.Add(keyStatePair.Key, error.ErrorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Equal(expectedErrors.Count, actualErrors.Count);
|
||||||
|
foreach (var keyErrorPair in expectedErrors)
|
||||||
|
{
|
||||||
|
Assert.Contains(keyErrorPair.Key, actualErrors.Keys);
|
||||||
|
Assert.Equal(keyErrorPair.Value, actualErrors[keyErrorPair.Key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This case should be handled in a better way.
|
||||||
|
// Issue - https://github.com/aspnet/Mvc/issues/1206 tracks this.
|
||||||
|
[Fact]
|
||||||
|
[ReplaceCulture]
|
||||||
|
public void BodyValidator_Throws_IfPropertyAccessorThrows()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var validationContext = GetModelValidationContext(new Uri("/api/values", UriKind.Relative), typeof(Uri));
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.Throws(
|
||||||
|
typeof(InvalidOperationException),
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
new DefaultBodyModelValidator().Validate(validationContext, keyPrefix: string.Empty);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[ReplaceCulture]
|
||||||
|
public void MultipleValidationErrorsOnSameMemberReported()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var model = new Address() { Street = "Microsoft Way" };
|
||||||
|
var validationContext = GetModelValidationContext(model, model.GetType());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
Assert.DoesNotThrow(() =>
|
||||||
|
new DefaultBodyModelValidator().Validate(validationContext, keyPrefix: string.Empty)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Contains("Street", validationContext.ModelState.Keys);
|
||||||
|
var streetState = validationContext.ModelState["Street"];
|
||||||
|
Assert.Equal(2, streetState.Errors.Count);
|
||||||
|
Assert.Equal(
|
||||||
|
"The field Street must be a string with a maximum length of 5.",
|
||||||
|
streetState.Errors[0].ErrorMessage);
|
||||||
|
Assert.Equal(
|
||||||
|
"The field Street must match the regular expression 'hehehe'.",
|
||||||
|
streetState.Errors[1].ErrorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Validate_DoesNotUseOverridden_GetHashCodeOrEquals()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var instance = new[] {
|
||||||
|
new TypeThatOverridesEquals { Funny = "hehe" },
|
||||||
|
new TypeThatOverridesEquals { Funny = "hehe" }
|
||||||
|
};
|
||||||
|
var validationContext = GetModelValidationContext(instance, typeof(TypeThatOverridesEquals[]));
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.DoesNotThrow(
|
||||||
|
() => new DefaultBodyModelValidator().Validate(validationContext, keyPrefix: string.Empty));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ModelValidationContext GetModelValidationContext(object model, Type type)
|
||||||
|
{
|
||||||
|
var modelStateDictionary = new ModelStateDictionary();
|
||||||
|
var mvcOptions = new MvcOptions();
|
||||||
|
var setup = new MvcOptionsSetup();
|
||||||
|
setup.Setup(mvcOptions);
|
||||||
|
var accessor = new Mock<IOptionsAccessor<MvcOptions>>();
|
||||||
|
accessor.SetupGet(a => a.Options)
|
||||||
|
.Returns(mvcOptions);
|
||||||
|
var modelMetadataProvider = new EmptyModelMetadataProvider();
|
||||||
|
return new ModelValidationContext(
|
||||||
|
modelMetadataProvider,
|
||||||
|
new CompositeModelValidatorProvider(
|
||||||
|
new DefaultModelValidatorProviderProvider(
|
||||||
|
accessor.Object, Mock.Of<ITypeActivator>(),
|
||||||
|
Mock.Of<IServiceProvider>())),
|
||||||
|
modelStateDictionary,
|
||||||
|
new ModelMetadata(
|
||||||
|
provider: modelMetadataProvider,
|
||||||
|
containerType: typeof(object),
|
||||||
|
modelAccessor: () => model,
|
||||||
|
modelType: type,
|
||||||
|
propertyName: null),
|
||||||
|
containerMetadata: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Person
|
||||||
|
{
|
||||||
|
[Required, StringLength(10)]
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string Profession { get; set; }
|
||||||
|
|
||||||
|
public Person Friend { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Address
|
||||||
|
{
|
||||||
|
[StringLength(5)]
|
||||||
|
[RegularExpression("hehehe")]
|
||||||
|
public string Street { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ValueType
|
||||||
|
{
|
||||||
|
public int Value;
|
||||||
|
public string Reference;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ReferenceType
|
||||||
|
{
|
||||||
|
public static string StaticProperty { get { return "static"; } }
|
||||||
|
public int Value;
|
||||||
|
public string Reference;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Pet
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public Person Owner { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ValidatableModel : IValidatableObject
|
||||||
|
{
|
||||||
|
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||||
|
{
|
||||||
|
yield return new ValidationResult("Error1", new string[] { });
|
||||||
|
yield return new ValidationResult("Error2", new[] { "Property1" });
|
||||||
|
yield return new ValidationResult("Error3", new[] { "Property2", "Property3" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TypeThatOverridesEquals
|
||||||
|
{
|
||||||
|
[StringLength(2)]
|
||||||
|
public string Funny { get; set; }
|
||||||
|
|
||||||
|
public override bool Equals(object obj)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class VariableTest
|
||||||
|
{
|
||||||
|
[Range(15, 25)]
|
||||||
|
public int test;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Team
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[StringLength(20, MinimumLength = 4)]
|
||||||
|
public string TeamName { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(10)]
|
||||||
|
public string Lead { get; set; }
|
||||||
|
|
||||||
|
[Range(3, 100)]
|
||||||
|
public int TeamSize { get; set; }
|
||||||
|
|
||||||
|
public string TeamDescription { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Org
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
[StringLength(20, MinimumLength = 4)]
|
||||||
|
public string OrgName { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public Team Dev { get; set; }
|
||||||
|
|
||||||
|
public Team Test { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
|
@ -0,0 +1,77 @@
|
||||||
|
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
public class ReferenceEqualityComparerTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Equals_ReturnsTrue_ForSameObject()
|
||||||
|
{
|
||||||
|
var o = new object();
|
||||||
|
Assert.True(ReferenceEqualityComparer.Instance.Equals(o, o));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Equals_ReturnsFalse_ForDifferentObject()
|
||||||
|
{
|
||||||
|
var o1 = new object();
|
||||||
|
var o2 = new object();
|
||||||
|
|
||||||
|
Assert.False(ReferenceEqualityComparer.Instance.Equals(o1, o2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Equals_DoesntCall_OverriddenEqualsOnTheType()
|
||||||
|
{
|
||||||
|
var t1 = new TypeThatOverridesEquals();
|
||||||
|
var t2 = new TypeThatOverridesEquals();
|
||||||
|
|
||||||
|
Assert.DoesNotThrow(() => ReferenceEqualityComparer.Instance.Equals(t1, t2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Equals_ReturnsFalse_ValueType()
|
||||||
|
{
|
||||||
|
Assert.False(ReferenceEqualityComparer.Instance.Equals(42, 42));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Equals_NullEqualsNull()
|
||||||
|
{
|
||||||
|
var comparer = ReferenceEqualityComparer.Instance;
|
||||||
|
Assert.True(comparer.Equals(null, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetHashCode_ReturnsSameValueForSameObject()
|
||||||
|
{
|
||||||
|
var o = new object();
|
||||||
|
var comparer = ReferenceEqualityComparer.Instance;
|
||||||
|
Assert.Equal(comparer.GetHashCode(o), comparer.GetHashCode(o));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetHashCode_DoesNotThrowForNull()
|
||||||
|
{
|
||||||
|
var comparer = ReferenceEqualityComparer.Instance;
|
||||||
|
Assert.DoesNotThrow(() => comparer.GetHashCode(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TypeThatOverridesEquals
|
||||||
|
{
|
||||||
|
public override bool Equals(object obj)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,7 +4,8 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.AspNet.Http": "1.0.0-*",
|
"Microsoft.AspNet.Http": "1.0.0-*",
|
||||||
"Microsoft.AspNet.Mvc.ModelBinding": "6.0.0-*",
|
"Microsoft.AspNet.Mvc": "",
|
||||||
|
"Microsoft.AspNet.Mvc.ModelBinding": "",
|
||||||
"Microsoft.AspNet.PipelineCore": "1.0.0-*",
|
"Microsoft.AspNet.PipelineCore": "1.0.0-*",
|
||||||
"Microsoft.AspNet.Routing": "1.0.0-*",
|
"Microsoft.AspNet.Routing": "1.0.0-*",
|
||||||
"Microsoft.AspNet.Testing": "1.0.0-*",
|
"Microsoft.AspNet.Testing": "1.0.0-*",
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using Microsoft.AspNet.Mvc;
|
||||||
|
|
||||||
|
namespace FormatterWebSite
|
||||||
|
{
|
||||||
|
public class ValidationController : Controller
|
||||||
|
{
|
||||||
|
[HttpPost]
|
||||||
|
public IActionResult Index([FromBody]User user)
|
||||||
|
{
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
return Content(ModelState["user.Id"].Errors[0].ErrorMessage + "," +
|
||||||
|
ModelState["user.Name"].Errors[0].ErrorMessage + "," +
|
||||||
|
ModelState["user.Alias"].Errors[0].ErrorMessage + "," +
|
||||||
|
ModelState["user.Designation"].Errors[0].ErrorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Content("User has been registerd : " + user.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace FormatterWebSite
|
||||||
|
{
|
||||||
|
public class User
|
||||||
|
{
|
||||||
|
[Required, Range(1, 2000)]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
[Required, MinLength(5)]
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
[StringLength(15, MinimumLength = 3)]
|
||||||
|
public string Alias { get; set; }
|
||||||
|
|
||||||
|
[RegularExpression("[0-9a-zA-Z]*")]
|
||||||
|
public string Designation { get; set; }
|
||||||
|
|
||||||
|
public string description { get; set; }
|
||||||
|
}
|
||||||
|
}
|
Загрузка…
Ссылка в новой задаче