Adding Support for TryUpdateModel using include expressions and predicate.

This commit is contained in:
Harsh Gupta 2014-11-10 13:24:51 -08:00
parent b54c326ee6
commit 2353bd911a
15 changed files with 1354 additions and 118 deletions

View File

@ -3,6 +3,7 @@
using System;
using System.IO;
using System.Linq.Expressions;
using System.Security.Principal;
using System.Text;
using System.Threading.Tasks;
@ -654,26 +655,28 @@ namespace Microsoft.AspNet.Mvc
}
/// <summary>
/// Updates the specified model instance using values from the controller's current value provider.
/// Updates the specified <paramref name="model"/> instance using values from the controller's current
/// <see cref="IValueProvider"/>.
/// </summary>
/// <typeparam name="TModel">The type of the model object.</typeparam>
/// <param name="model">The model instance to update.</param>
/// <returns>true if the update is successful; otherwise, false.</returns>
/// <returns>A <see cref="Task"/> that on completion returns <c>true</c> if the update is successful</returns>
[NonAction]
public virtual Task<bool> TryUpdateModelAsync<TModel>([NotNull] TModel model)
where TModel : class
{
return TryUpdateModelAsync(model, prefix: typeof(TModel).Name);
return TryUpdateModelAsync(model, prefix: null);
}
/// <summary>
/// Updates the specified model instance using values from the controller's current value provider
/// and a prefix.
/// Updates the specified <paramref name="model"/> instance using values from the controller's current
/// <see cref="IValueProvider"/> and a <paramref name="prefix"/>.
/// </summary>
/// <typeparam name="TModel">The type of the model object.</typeparam>
/// <param name="model">The model instance to update.</param>
/// <param name="prefix">The prefix to use when looking up values in the value provider.</param>
/// <returns>true if the update is successful; otherwise, false.</returns>
/// <param name="prefix">The prefix to use when looking up values in the current <see cref="IValueProvider"/>
/// </param>
/// <returns>A <see cref="Task"/> that on completion returns <c>true</c> if the update is successful</returns>
[NonAction]
public virtual async Task<bool> TryUpdateModelAsync<TModel>([NotNull] TModel model,
[NotNull] string prefix)
@ -681,7 +684,8 @@ namespace Microsoft.AspNet.Mvc
{
if (BindingContextProvider == null)
{
var message = Resources.FormatPropertyOfTypeCannotBeNull("BindingContextProvider", GetType().FullName);
var message = Resources.FormatPropertyOfTypeCannotBeNull(nameof(BindingContextProvider),
GetType().FullName);
throw new InvalidOperationException(message);
}
@ -690,13 +694,15 @@ namespace Microsoft.AspNet.Mvc
}
/// <summary>
/// Updates the specified model instance using the value provider and a prefix.
/// Updates the specified <paramref name="model"/> instance using the <paramref name="valueProvider"/> and a
/// <paramref name="prefix"/>.
/// </summary>
/// <typeparam name="TModel">The type of the model object.</typeparam>
/// <param name="model">The model instance to update.</param>
/// <param name="prefix">The prefix to use when looking up values in the value provider.</param>
/// <param name="valueProvider">The value provider used for looking up values.</param>
/// <returns>true if the update is successful; otherwise, false.</returns>
/// <param name="prefix">The prefix to use when looking up values in the <paramref name="valueProvider"/>.
/// </param>
/// <param name="valueProvider">The <see cref="IValueProvider"/> used for looking up values.</param>
/// <returns>A <see cref="Task"/> that on completion returns <c>true</c> if the update is successful</returns>
[NonAction]
public virtual async Task<bool> TryUpdateModelAsync<TModel>([NotNull] TModel model,
[NotNull] string prefix,
@ -705,7 +711,8 @@ namespace Microsoft.AspNet.Mvc
{
if (BindingContextProvider == null)
{
var message = Resources.FormatPropertyOfTypeCannotBeNull("BindingContextProvider", GetType().FullName);
var message = Resources.FormatPropertyOfTypeCannotBeNull(nameof(BindingContextProvider),
GetType().FullName);
throw new InvalidOperationException(message);
}
@ -720,6 +727,156 @@ namespace Microsoft.AspNet.Mvc
bindingContext.ValidatorProvider);
}
/// <summary>
/// Updates the specified <paramref name="model"/> instance using values from the controller's current
/// <see cref="IValueProvider"/> and a <paramref name="prefix"/>.
/// </summary>
/// <typeparam name="TModel">The type of the model object.</typeparam>
/// <param name="model">The model instance to update.</param>
/// <param name="prefix">The prefix to use when looking up values in the current <see cref="IValueProvider"/>.
/// </param>
/// <param name="includeExpressions"> <see cref="Expression"/>(s) which represent top-level properties
/// which need to be included for the current model.</param>
/// <returns>A <see cref="Task"/> that on completion returns <c>true</c> if the update is successful</returns>
[NonAction]
public async Task<bool> TryUpdateModelAsync<TModel>(
[NotNull] TModel model,
string prefix,
[NotNull] params Expression<Func<TModel, object>>[] includeExpressions)
where TModel : class
{
if (BindingContextProvider == null)
{
var message = Resources.FormatPropertyOfTypeCannotBeNull(nameof(BindingContextProvider),
GetType().FullName);
throw new InvalidOperationException(message);
}
var bindingContext = await BindingContextProvider.GetActionBindingContextAsync(ActionContext);
return await ModelBindingHelper.TryUpdateModelAsync(model,
prefix,
ActionContext.HttpContext,
ModelState,
bindingContext.MetadataProvider,
bindingContext.ModelBinder,
bindingContext.ValueProvider,
bindingContext.ValidatorProvider,
includeExpressions);
}
/// <summary>
/// Updates the specified <paramref name="model"/> instance using values from the controller's current
/// <see cref="IValueProvider"/> and a <paramref name="prefix"/>.
/// </summary>
/// <typeparam name="TModel">The type of the model object.</typeparam>
/// <param name="model">The model instance to update.</param>
/// <param name="prefix">The prefix to use when looking up values in the current <see cref="IValueProvider"/>.
/// </param>
/// <param name="predicate">A predicate which can be used to filter properties at runtime.</param>
/// <returns>A <see cref="Task"/> that on completion returns <c>true</c> if the update is successful</returns>
[NonAction]
public async Task<bool> TryUpdateModelAsync<TModel>(
[NotNull] TModel model,
string prefix,
[NotNull] Func<ModelBindingContext, string, bool> predicate)
where TModel : class
{
if (BindingContextProvider == null)
{
var message = Resources.FormatPropertyOfTypeCannotBeNull(nameof(BindingContextProvider),
GetType().FullName);
throw new InvalidOperationException(message);
}
var bindingContext = await BindingContextProvider.GetActionBindingContextAsync(ActionContext);
return await ModelBindingHelper.TryUpdateModelAsync(model,
prefix,
ActionContext.HttpContext,
ModelState,
bindingContext.MetadataProvider,
bindingContext.ModelBinder,
bindingContext.ValueProvider,
bindingContext.ValidatorProvider,
predicate);
}
/// <summary>
/// Updates the specified <paramref name="model"/> instance using the <paramref name="valueProvider"/> and a
/// <paramref name="prefix"/>.
/// </summary>
/// <typeparam name="TModel">The type of the model object.</typeparam>
/// <param name="model">The model instance to update.</param>
/// <param name="prefix">The prefix to use when looking up values in the <paramref name="valueProvider"/>
/// </param>
/// <param name="valueProvider">The <see cref="IValueProvider"/> used for looking up values.</param>
/// <param name="includeExpressions"> <see cref="Expression"/>(s) which represent top-level properties
/// which need to be included for the current model.</param>
/// <returns>A <see cref="Task"/> that on completion returns <c>true</c> if the update is successful</returns>
[NonAction]
public async Task<bool> TryUpdateModelAsync<TModel>(
[NotNull] TModel model,
string prefix,
[NotNull] IValueProvider valueProvider,
[NotNull] params Expression<Func<TModel, object>>[] includeExpressions)
where TModel : class
{
if (BindingContextProvider == null)
{
var message = Resources.FormatPropertyOfTypeCannotBeNull(nameof(BindingContextProvider),
GetType().FullName);
throw new InvalidOperationException(message);
}
var bindingContext = await BindingContextProvider.GetActionBindingContextAsync(ActionContext);
return await ModelBindingHelper.TryUpdateModelAsync(model,
prefix,
ActionContext.HttpContext,
ModelState,
bindingContext.MetadataProvider,
bindingContext.ModelBinder,
valueProvider,
bindingContext.ValidatorProvider,
includeExpressions);
}
/// <summary>
/// Updates the specified <paramref name="model"/> instance using the <paramref name="valueProvider"/> and a
/// <paramref name="prefix"/>.
/// </summary>
/// <typeparam name="TModel">The type of the model object.</typeparam>
/// <param name="model">The model instance to update.</param>
/// <param name="prefix">The prefix to use when looking up values in the <paramref name="valueProvider"/>
/// </param>
/// <param name="valueProvider">The <see cref="IValueProvider"/> used for looking up values.</param>
/// <param name="predicate">A predicate which can be used to filter properties at runtime.</param>
/// <returns>A <see cref="Task"/> that on completion returns <c>true</c> if the update is successful</returns>
[NonAction]
public async Task<bool> TryUpdateModelAsync<TModel>(
[NotNull] TModel model,
string prefix,
[NotNull] IValueProvider valueProvider,
[NotNull] Func<ModelBindingContext, string, bool> predicate)
where TModel : class
{
if (BindingContextProvider == null)
{
var message = Resources.FormatPropertyOfTypeCannotBeNull(nameof(BindingContextProvider),
GetType().FullName);
throw new InvalidOperationException(message);
}
var bindingContext = await BindingContextProvider.GetActionBindingContextAsync(ActionContext);
return await ModelBindingHelper.TryUpdateModelAsync(model,
prefix,
ActionContext.HttpContext,
ModelState,
bindingContext.MetadataProvider,
bindingContext.ModelBinder,
valueProvider,
bindingContext.ValidatorProvider,
predicate);
}
public void Dispose()
{
Dispose(disposing: true);
@ -729,6 +886,5 @@ namespace Microsoft.AspNet.Mvc
protected virtual void Dispose(bool disposing)
{
}
}
}

View File

@ -100,8 +100,8 @@ namespace Microsoft.AspNet.Mvc
ActionBindingContext actionBindingContext,
OperationBindingContext operationBindingContext)
{
Predicate<string> propertyFilter =
propertyName => BindAttribute.IsPropertyAllowed(propertyName,
Func<ModelBindingContext, string, bool> propertyFilter =
(context, propertyName) => BindAttribute.IsPropertyAllowed(propertyName,
modelMetadata.BinderIncludeProperties,
modelMetadata.BinderExcludeProperties);

View File

@ -1,8 +1,13 @@
// 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.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.ModelBinding;
namespace Microsoft.AspNet.Mvc
@ -10,22 +15,24 @@ namespace Microsoft.AspNet.Mvc
public static class ModelBindingHelper
{
/// <summary>
/// Updates the specified model instance using the specified binder and value provider and
/// executes validation using the specified sequence of validator providers.
/// Updates the specified <paramref name="model"/> instance using the specified <paramref name="modelBinder"/>
/// and the specified <paramref name="valueProvider"/> and executes validation using the specified
/// <paramref name="validatorProvider"/>.
/// </summary>
/// <typeparam name="TModel">The type of the model object.</typeparam>
/// <param name="model">The model instance to update.</param>
/// <param name="prefix">The prefix to use when looking up values in the value provider.</param>
/// <param name="httpContext">The context for the current executing request.</param>
/// <param name="modelState">The ModelStateDictionary used for maintaining state and
/// <param name="model">The model instance to update and validate.</param>
/// <param name="prefix">The prefix to use when looking up values in the <paramref name="valueProvider"/>.
/// </param>
/// <param name="httpContext">The <see cref="HttpContext"/> for the current executing request.</param>
/// <param name="modelState">The <see cref="ModelStateDictionary"/> used for maintaining state and
/// results of model-binding validation.</param>
/// <param name="metadataProvider">The provider used for reading metadata for the model type.</param>
/// <param name="modelBinder">The model binder used for binding.</param>
/// <param name="valueProvider">The value provider used for looking up values.</param>
/// <param name="validatorProvider">The validator provider used for executing validation on the model
/// instance.</param>
/// <returns>A Task with a value representing if the the update is successful.</returns>
public static async Task<bool> TryUpdateModelAsync<TModel>(
/// <param name="modelBinder">The <see cref="IModelBinder"/> used for binding.</param>
/// <param name="valueProvider">The <see cref="IValueProvider"/> used for looking up values.</param>
/// <param name="validatorProvider">The <see cref="IModelValidatorProvider"/> used for executing validation
/// on the model instance.</param>
/// <returns>A <see cref="Task"/> that on completion returns <c>true</c> if the update is successful</returns>
public static Task<bool> TryUpdateModelAsync<TModel>(
[NotNull] TModel model,
[NotNull] string prefix,
[NotNull] HttpContext httpContext,
@ -35,6 +42,99 @@ namespace Microsoft.AspNet.Mvc
[NotNull] IValueProvider valueProvider,
[NotNull] IModelValidatorProvider validatorProvider)
where TModel : class
{
// Includes everything by default.
return TryUpdateModelAsync(
model,
prefix,
httpContext,
modelState,
metadataProvider,
modelBinder,
valueProvider,
validatorProvider,
predicate: (context, propertyName) => true);
}
/// <summary>
/// Updates the specified <paramref name="model"/> instance using the specified <paramref name="modelBinder"/>
/// and the specified <paramref name="valueProvider"/> and executes validation using the specified
/// <paramref name="validatorProvider"/>.
/// </summary>
/// <typeparam name="TModel">The type of the model object.</typeparam>
/// <param name="model">The model instance to update and validate.</param>
/// <param name="prefix">The prefix to use when looking up values in the <paramref name="valueProvider"/>.
/// </param>
/// <param name="httpContext">The <see cref="HttpContext"/> for the current executing request.</param>
/// <param name="modelState">The <see cref="ModelStateDictionary"/> used for maintaining state and
/// results of model-binding validation.</param>
/// <param name="metadataProvider">The provider used for reading metadata for the model type.</param>
/// <param name="modelBinder">The <see cref="IModelBinder"/> used for binding.</param>
/// <param name="valueProvider">The <see cref="IValueProvider"/> used for looking up values.</param>
/// <param name="validatorProvider">The <see cref="IModelValidatorProvider"/> used for executing validation
/// on the model
/// instance.</param>
/// <param name="includeExpressions">Expression(s) which represent top level properties
/// which need to be included for the current model.</param>
/// <returns>A <see cref="Task"/> that on completion returns <c>true</c> if the update is successful</returns>
public static Task<bool> TryUpdateModelAsync<TModel>(
[NotNull] TModel model,
[NotNull] string prefix,
[NotNull] HttpContext httpContext,
[NotNull] ModelStateDictionary modelState,
[NotNull] IModelMetadataProvider metadataProvider,
[NotNull] IModelBinder modelBinder,
[NotNull] IValueProvider valueProvider,
[NotNull] IModelValidatorProvider validatorProvider,
[NotNull] params Expression<Func<TModel, object>>[] includeExpressions)
where TModel : class
{
var includeExpression = GetIncludePredicateExpression(prefix, includeExpressions);
Func<ModelBindingContext, string, bool> predicate = includeExpression.Compile();
return TryUpdateModelAsync(
model,
prefix,
httpContext,
modelState,
metadataProvider,
modelBinder,
valueProvider,
validatorProvider,
predicate: predicate);
}
/// <summary>
/// Updates the specified <paramref name="model"/> instance using the specified <paramref name="modelBinder"/>
/// and the specified <paramref name="valueProvider"/> and executes validation using the specified
/// <paramref name="validatorProvider"/>.
/// </summary>
/// <typeparam name="TModel">The type of the model object.</typeparam>
/// <param name="model">The model instance to update and validate.</param>
/// <param name="prefix">The prefix to use when looking up values in the <paramref name="valueProvider"/>.
/// </param>
/// <param name="httpContext">The <see cref="HttpContext"/> for the current executing request.</param>
/// <param name="modelState">The <see cref="ModelStateDictionary"/> used for maintaining state and
/// results of model-binding validation.</param>
/// <param name="metadataProvider">The provider used for reading metadata for the model type.</param>
/// <param name="modelBinder">The <see cref="IModelBinder"/> used for binding.</param>
/// <param name="valueProvider">The <see cref="IValueProvider"/> used for looking up values.</param>
/// <param name="validatorProvider">The <see cref="IModelValidatorProvider"/> used for executing validation
/// on the model instance.</param>
/// <param name="predicate">A predicate which can be used to
/// filter properties(for inclusion/exclusion) at runtime.</param>
/// <returns>A <see cref="Task"/> that on completion returns <c>true</c> if the update is successful</returns>
public static async Task<bool> TryUpdateModelAsync<TModel>(
[NotNull] TModel model,
[NotNull] string prefix,
[NotNull] HttpContext httpContext,
[NotNull] ModelStateDictionary modelState,
[NotNull] IModelMetadataProvider metadataProvider,
[NotNull] IModelBinder modelBinder,
[NotNull] IValueProvider valueProvider,
[NotNull] IModelValidatorProvider validatorProvider,
[NotNull] Func<ModelBindingContext, string, bool> predicate)
where TModel : class
{
var modelMetadata = metadataProvider.GetMetadataForType(
modelAccessor: null,
@ -56,6 +156,7 @@ namespace Microsoft.AspNet.Mvc
ValueProvider = valueProvider,
FallbackToEmptyPrefix = true,
OperationBindingContext = operationBindingContext,
PropertyFilter = predicate
};
if (await modelBinder.BindModelAsync(modelBindingContext))
@ -65,5 +166,91 @@ namespace Microsoft.AspNet.Mvc
return false;
}
internal static string GetPropertyName(Expression expression)
{
if (expression.NodeType == ExpressionType.Convert ||
expression.NodeType == ExpressionType.ConvertChecked)
{
// For Boxed Value Types
expression = ((UnaryExpression)expression).Operand;
}
if (expression.NodeType != ExpressionType.MemberAccess)
{
throw new InvalidOperationException(Resources.FormatInvalid_IncludePropertyExpression(
expression.NodeType));
}
var memberExpression = (MemberExpression)expression;
var memberInfo = memberExpression.Member as PropertyInfo;
if (memberInfo != null)
{
if (memberExpression.Expression.NodeType != ExpressionType.Parameter)
{
// Chained expressions and non parameter based expressions are not supported.
throw new InvalidOperationException(
Resources.FormatInvalid_IncludePropertyExpression(expression.NodeType));
}
return memberInfo.Name;
}
else
{
// Fields are also not supported.
throw new InvalidOperationException(Resources.FormatInvalid_IncludePropertyExpression(
expression.NodeType));
}
}
private static Expression<Func<ModelBindingContext, string, bool>> GetIncludePredicateExpression<TModel>
(string prefix, Expression<Func<TModel, object>>[] expressions)
{
if (expressions.Length == 0)
{
// If nothing is included explcitly, treat everything as included.
return (context, propertyName) => true;
}
var firstExpression = GetPredicateExpression(prefix, expressions[0]);
var orWrapperExpression = firstExpression.Body;
foreach (var expression in expressions.Skip(1))
{
var predicate = GetPredicateExpression(prefix, expression);
orWrapperExpression = Expression.OrElse(orWrapperExpression,
Expression.Invoke(predicate, firstExpression.Parameters));
}
return Expression.Lambda<Func<ModelBindingContext, string, bool>>(
orWrapperExpression, firstExpression.Parameters);
}
private static Expression<Func<ModelBindingContext, string, bool>> GetPredicateExpression<TModel>
(string prefix, Expression<Func<TModel, object>> expression)
{
var propertyName = GetPropertyName(expression.Body);
var property = CreatePropertyModelName(prefix, propertyName);
return
(context, modelPropertyName) =>
property.Equals(CreatePropertyModelName(context.ModelName, modelPropertyName),
StringComparison.OrdinalIgnoreCase);
}
private static string CreatePropertyModelName(string prefix, string propertyName)
{
if (string.IsNullOrEmpty(prefix))
{
return propertyName ?? string.Empty;
}
else if (string.IsNullOrEmpty(propertyName))
{
return prefix ?? string.Empty;
}
else
{
return prefix + "." + propertyName;
}
}
}
}

View File

@ -618,6 +618,22 @@ namespace Microsoft.AspNet.Mvc.Core
return string.Format(CultureInfo.CurrentCulture, GetString("ExpressionHelper_InvalidIndexerExpression"), p0, p1);
}
/// <summary>
/// The passed expression of expression node type '{0}' is invalid. Only simple member access expressions for model properties are supported.
/// </summary>
internal static string Invalid_IncludePropertyExpression
{
get { return GetString("Invalid_IncludePropertyExpression"); }
}
/// <summary>
/// The passed expression of expression node type '{0}' is invalid. Only simple member access expressions for model properties are supported.
/// </summary>
internal static string FormatInvalid_IncludePropertyExpression(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("Invalid_IncludePropertyExpression"), p0);
}
/// <summary>
/// The IModelMetadataProvider was unable to provide metadata for expression '{0}'.
/// </summary>
@ -1403,7 +1419,7 @@ namespace Microsoft.AspNet.Mvc.Core
}
/// <summary>
/// A method '{0}' must not define attribute routed actions and non attribute routed actions at the same time:{1}{2}{1}Use 'AcceptVerbsAttribute' to create a single route that allows multiple HTTP verbs and defines a route, or set a route template in all attributes that constrain HTTP verbs.
/// A method '{0}' must not define attribute routed actions and non attribute routed actions at the same time:{1}{2}{1}{1}Use 'AcceptVerbsAttribute' to create a single route that allows multiple HTTP verbs and defines a route, or set a route template in all attributes that constrain HTTP verbs.
/// </summary>
internal static string AttributeRoute_MixedAttributeAndConventionallyRoutedActions_ForMethod
{
@ -1411,7 +1427,7 @@ namespace Microsoft.AspNet.Mvc.Core
}
/// <summary>
/// A method '{0}' must not define attribute routed actions and non attribute routed actions at the same time:{1}{2}{1}Use 'AcceptVerbsAttribute' to create a single route that allows multiple HTTP verbs and defines a route, or set a route template in all attributes that constrain HTTP verbs.
/// A method '{0}' must not define attribute routed actions and non attribute routed actions at the same time:{1}{2}{1}{1}Use 'AcceptVerbsAttribute' to create a single route that allows multiple HTTP verbs and defines a route, or set a route template in all attributes that constrain HTTP verbs.
/// </summary>
internal static string FormatAttributeRoute_MixedAttributeAndConventionallyRoutedActions_ForMethod(object p0, object p1, object p2)
{

View File

@ -232,6 +232,9 @@
<data name="ExpressionHelper_InvalidIndexerExpression" xml:space="preserve">
<value>The expression compiler was unable to evaluate the indexer expression '{0}' because it references the model parameter '{1}' which is unavailable.</value>
</data>
<data name="Invalid_IncludePropertyExpression" xml:space="preserve">
<value>The passed expression of expression node type '{0}' is invalid. Only simple member access expressions for model properties are supported.</value>
</data>
<data name="HtmlHelper_NullModelMetadata" xml:space="preserve">
<value>The IModelMetadataProvider was unable to provide metadata for expression '{0}'.</value>
</data>

View File

@ -271,7 +271,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var validationInfo = GetPropertyValidationInfo(bindingContext);
return bindingContext.ModelMetadata.Properties
.Where(propertyMetadata =>
bindingContext.PropertyFilter(propertyMetadata.PropertyName) &&
bindingContext.PropertyFilter(bindingContext, propertyMetadata.PropertyName) &&
(validationInfo.RequiredProperties.Contains(propertyMetadata.PropertyName) ||
!validationInfo.SkipProperties.Contains(propertyMetadata.PropertyName)) &&
CanUpdateProperty(propertyMetadata));
@ -473,21 +473,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return addedError;
}
private static bool IsPropertyAllowed(string propertyName,
IReadOnlyList<string> includeProperties,
IReadOnlyList<string> excludeProperties)
{
// We allow a property to be bound if its both in the include list AND not in the exclude list.
// An empty exclude list implies no properties are disallowed.
var includeProperty = (includeProperties != null) &&
includeProperties.Contains(propertyName, StringComparer.OrdinalIgnoreCase);
var excludeProperty = (excludeProperties != null) &&
excludeProperties.Contains(propertyName, StringComparer.OrdinalIgnoreCase);
return includeProperty && !excludeProperty;
}
internal sealed class PropertyValidationInfo
{
public PropertyValidationInfo()

View File

@ -13,12 +13,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// </summary>
public class ModelBindingContext
{
private static readonly Predicate<string> _defaultPropertyFilter = _ => true;
private static readonly Func<ModelBindingContext, string, bool>
_defaultPropertyFilter = (context, propertyName) => true;
private string _modelName;
private ModelStateDictionary _modelState;
private Dictionary<string, ModelMetadata> _propertyMetadata;
private ModelValidationNode _validationNode;
private Predicate<string> _propertyFilter;
private Func<ModelBindingContext, string, bool> _propertyFilter;
/// <summary>
/// Initializes a new instance of the <see cref="ModelBindingContext"/> class.
@ -160,7 +162,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
}
public Predicate<string> PropertyFilter
public Func<ModelBindingContext, string, bool> PropertyFilter
{
get
{

View File

@ -675,6 +675,7 @@ namespace Microsoft.AspNet.Mvc.Test
[Fact]
public async Task Controller_ActionFilter_SettingResult_ShortCircuits()
{
// Arrange, Act & Assert
await ActionFilterAttributeTests.ActionFilter_SettingResult_ShortCircuits(
new Mock<Controller>());
}
@ -682,42 +683,34 @@ namespace Microsoft.AspNet.Mvc.Test
[Fact]
public async Task Controller_ActionFilter_Calls_OnActionExecuted()
{
// Arrange, Act & Assert
await ActionFilterAttributeTests.ActionFilter_Calls_OnActionExecuted(
new Mock<Controller>());
}
[Fact]
public async Task TryUpdateModel_UsesModelTypeNameIfNotSpecified()
public async Task TryUpdateModel_FallsBackOnEmptyPrefix_IfNotSpecified()
{
// Arrange
var metadataProvider = new DataAnnotationsModelMetadataProvider();
var valueProvider = Mock.Of<IValueProvider>();
var binder = new Mock<IModelBinder>();
binder.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Callback((ModelBindingContext b) =>
.Callback((ModelBindingContext context) =>
{
Assert.Equal(typeof(MyModel).Name, b.ModelName);
Assert.Same(valueProvider, b.ValueProvider);
Assert.Empty(context.ModelName);
Assert.Same(valueProvider, context.ValueProvider);
// Include and exclude should be null, resulting in property
// being included.
Assert.True(context.PropertyFilter(context, "Property1"));
Assert.True(context.PropertyFilter(context, "Property2"));
})
.Returns(Task.FromResult(false))
.Verifiable();
var controller = GetController(binder.Object, valueProvider);
var model = new MyModel();
var actionContext = new ActionContext(Mock.Of<HttpContext>(), new RouteData(), new ActionDescriptor());
var bindingContext = new ActionBindingContext(actionContext,
metadataProvider,
binder.Object,
valueProvider,
Mock.Of<IInputFormatterSelector>(),
Mock.Of<IModelValidatorProvider>());
var bindingContextProvider = new Mock<IActionBindingContextProvider>();
bindingContextProvider.Setup(b => b.GetActionBindingContextAsync(actionContext))
.Returns(Task.FromResult(bindingContext));
var viewData = new ViewDataDictionary(metadataProvider, new ModelStateDictionary());
var controller = new Controller
{
ActionContext = actionContext,
BindingContextProvider = bindingContextProvider.Object,
ViewData = viewData
};
// Act
var result = await controller.TryUpdateModelAsync(model);
@ -729,36 +722,28 @@ namespace Microsoft.AspNet.Mvc.Test
[Fact]
public async Task TryUpdateModel_UsesModelTypeNameIfSpecified()
{
// Arrange
var modelName = "mymodel";
var metadataProvider = new DataAnnotationsModelMetadataProvider();
var valueProvider = Mock.Of<IValueProvider>();
var binder = new Mock<IModelBinder>();
var modelName = "mymodel";
binder.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Callback((ModelBindingContext b) =>
.Callback((ModelBindingContext context) =>
{
Assert.Equal(modelName, b.ModelName);
Assert.Same(valueProvider, b.ValueProvider);
Assert.Equal(modelName, context.ModelName);
Assert.Same(valueProvider, context.ValueProvider);
// Include and exclude should be null, resulting in property
// being included.
Assert.True(context.PropertyFilter(context, "Property1"));
Assert.True(context.PropertyFilter(context, "Property2"));
})
.Returns(Task.FromResult(false))
.Verifiable();
var controller = GetController(binder.Object, valueProvider);
var model = new MyModel();
var actionContext = new ActionContext(Mock.Of<HttpContext>(), new RouteData(), new ActionDescriptor());
var bindingContext = new ActionBindingContext(actionContext,
metadataProvider,
binder.Object,
valueProvider,
Mock.Of<IInputFormatterSelector>(),
Mock.Of<IModelValidatorProvider>());
var bindingContextProvider = new Mock<IActionBindingContextProvider>();
bindingContextProvider.Setup(b => b.GetActionBindingContextAsync(actionContext))
.Returns(Task.FromResult(bindingContext));
var viewData = new ViewDataDictionary(metadataProvider, new ModelStateDictionary());
var controller = new Controller
{
ActionContext = actionContext,
BindingContextProvider = bindingContextProvider.Object,
ViewData = viewData
};
// Act
var result = await controller.TryUpdateModelAsync(model, modelName);
@ -770,36 +755,27 @@ namespace Microsoft.AspNet.Mvc.Test
[Fact]
public async Task TryUpdateModel_UsesModelValueProviderIfSpecified()
{
var metadataProvider = new DataAnnotationsModelMetadataProvider();
// Arrange
var modelName = "mymodel";
var valueProvider = Mock.Of<IValueProvider>();
var binder = new Mock<IModelBinder>();
var modelName = "mymodel";
binder.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Callback((ModelBindingContext b) =>
.Callback((ModelBindingContext context) =>
{
Assert.Equal(modelName, b.ModelName);
Assert.Same(valueProvider, b.ValueProvider);
Assert.Equal(modelName, context.ModelName);
Assert.Same(valueProvider, context.ValueProvider);
// Include and exclude should be null, resulting in property
// being included.
Assert.True(context.PropertyFilter(context, "Property1"));
Assert.True(context.PropertyFilter(context, "Property2"));
})
.Returns(Task.FromResult(false))
.Verifiable();
var controller = GetController(binder.Object, provider: null);
var model = new MyModel();
var actionContext = new ActionContext(Mock.Of<HttpContext>(), new RouteData(), new ActionDescriptor());
var bindingContext = new ActionBindingContext(actionContext,
metadataProvider,
binder.Object,
Mock.Of<IValueProvider>(),
Mock.Of<IInputFormatterSelector>(),
Mock.Of<IModelValidatorProvider>());
var bindingContextProvider = new Mock<IActionBindingContextProvider>();
bindingContextProvider.Setup(b => b.GetActionBindingContextAsync(actionContext))
.Returns(Task.FromResult(bindingContext));
var viewData = new ViewDataDictionary(metadataProvider, new ModelStateDictionary());
var controller = new Controller
{
ActionContext = actionContext,
BindingContextProvider = bindingContextProvider.Object,
ViewData = viewData
};
// Act
var result = await controller.TryUpdateModelAsync(model, modelName, valueProvider);
@ -807,6 +783,151 @@ namespace Microsoft.AspNet.Mvc.Test
// Assert
binder.Verify();
}
[Fact]
public async Task TryUpdateModel_PredicateOverload_UsesPassedArguments()
{
// Arrange
var modelName = "mymodel";
Func<ModelBindingContext, string, bool> includePredicate =
(context, propertyName) =>
string.Equals(propertyName, "include1", StringComparison.OrdinalIgnoreCase) ||
string.Equals(propertyName, "include2", StringComparison.OrdinalIgnoreCase);
var binder = new Mock<IModelBinder>();
var valueProvider = Mock.Of<IValueProvider>();
binder.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Callback((ModelBindingContext context) =>
{
Assert.Equal(modelName, context.ModelName);
Assert.Same(valueProvider, context.ValueProvider);
Assert.True(context.PropertyFilter(context, "include1"));
Assert.True(context.PropertyFilter(context, "include2"));
Assert.False(context.PropertyFilter(context, "exclude1"));
Assert.False(context.PropertyFilter(context, "exclude2"));
})
.Returns(Task.FromResult(true))
.Verifiable();
var controller = GetController(binder.Object, valueProvider);
var model = new MyModel();
// Act
await controller.TryUpdateModelAsync(model, modelName, includePredicate);
// Assert
binder.Verify();
}
[Fact]
public async Task TryUpdateModel_PredicateWithValueProviderOverload_UsesPassedArguments()
{
// Arrange
var modelName = "mymodel";
Func<ModelBindingContext, string, bool> includePredicate =
(context, propertyName) => string.Equals(propertyName, "include1", StringComparison.OrdinalIgnoreCase) ||
string.Equals(propertyName, "include2", StringComparison.OrdinalIgnoreCase);
var binder = new Mock<IModelBinder>();
var valueProvider = Mock.Of<IValueProvider>();
binder.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Callback((ModelBindingContext context) =>
{
Assert.Equal(modelName, context.ModelName);
Assert.Same(valueProvider, context.ValueProvider);
Assert.True(context.PropertyFilter(context, "include1"));
Assert.True(context.PropertyFilter(context, "include2"));
Assert.False(context.PropertyFilter(context, "exclude1"));
Assert.False(context.PropertyFilter(context, "exclude2"));
})
.Returns(Task.FromResult(true))
.Verifiable();
var controller = GetController(binder.Object, provider: null);
var model = new MyModel();
// Act
await controller.TryUpdateModelAsync(model, modelName, valueProvider, includePredicate);
// Assert
binder.Verify();
}
[Theory]
[InlineData("")]
[InlineData("prefix")]
public async Task TryUpdateModel_IncludeExpressionOverload_UsesPassedArguments(string prefix)
{
// Arrange
var binder = new Mock<IModelBinder>();
var valueProvider = Mock.Of<IValueProvider>();
binder.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Callback((ModelBindingContext context) =>
{
Assert.Equal(prefix, context.ModelName);
Assert.Same(valueProvider, context.ValueProvider);
Assert.True(context.PropertyFilter(context, "Property1"));
Assert.True(context.PropertyFilter(context, "Property2"));
Assert.False(context.PropertyFilter(context, "exclude1"));
Assert.False(context.PropertyFilter(context, "exclude2"));
})
.Returns(Task.FromResult(true))
.Verifiable();
var controller = GetController(binder.Object, valueProvider);
var model = new MyModel();
// Act
await controller.TryUpdateModelAsync(model, prefix, m => m.Property1, m => m.Property2);
// Assert
binder.Verify();
}
[Theory]
[InlineData("")]
[InlineData("prefix")]
public async Task
TryUpdateModel_IncludeExpressionWithValueProviderOverload_UsesPassedArguments(string prefix)
{
// Arrange
var binder = new Mock<IModelBinder>();
var valueProvider = new Mock<IValueProvider>();
binder.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Callback((ModelBindingContext context) =>
{
Assert.Equal(prefix, context.ModelName);
Assert.Same(valueProvider.Object, context.ValueProvider);
Assert.True(context.PropertyFilter(context, "Property1"));
Assert.True(context.PropertyFilter(context, "Property2"));
Assert.False(context.PropertyFilter(context, "exclude1"));
Assert.False(context.PropertyFilter(context, "exclude2"));
})
.Returns(Task.FromResult(true))
.Verifiable();
var controller = GetController(binder.Object, provider: null);
var model = new MyModel();
// Act
await controller.TryUpdateModelAsync(model, prefix, valueProvider.Object, m => m.Property1, m => m.Property2);
// Assert
binder.Verify();
}
#endif
[Fact]
@ -966,9 +1087,55 @@ namespace Microsoft.AspNet.Mvc.Test
Assert.Same(viewEngine, result.ViewEngine);
}
private static Controller GetController(IModelBinder binder, IValueProvider provider)
{
var metadataProvider = new DataAnnotationsModelMetadataProvider();
var actionContext = new ActionContext(Mock.Of<HttpContext>(), new RouteData(), new ActionDescriptor());
var bindingContext = new ActionBindingContext(actionContext,
metadataProvider,
binder,
provider ?? Mock.Of<IValueProvider>(),
Mock.Of<IInputFormatterSelector>(),
Mock.Of<IModelValidatorProvider>());
var bindingContextProvider = new Mock<IActionBindingContextProvider>();
bindingContextProvider.Setup(b => b.GetActionBindingContextAsync(actionContext))
.Returns(Task.FromResult(bindingContext));
var viewData = new ViewDataDictionary(metadataProvider, new ModelStateDictionary());
return new Controller
{
ActionContext = actionContext,
BindingContextProvider = bindingContextProvider.Object,
ViewData = viewData
};
}
private class MyModel
{
public string Foo { get; set; }
public string Property1 { get; set; }
public string Property2 { get; set; }
}
private class User
{
public User(int id)
{
Id = id;
}
public int Id { get; set; }
public string Name { get; set; }
public Address Address { get; set; }
}
private class Address
{
public string Street { get; set; }
public string City { get; set; }
public int Zip { get; set; }
}
private class DisposableController : Controller

View File

@ -70,8 +70,8 @@ namespace Microsoft.AspNet.Mvc.Core.Test
modelMetadata, actionBindingContext, Mock.Of<OperationBindingContext>());
// Assert
Assert.False(context.PropertyFilter("Excluded1"));
Assert.False(context.PropertyFilter("Excluded2"));
Assert.False(context.PropertyFilter(context, "Excluded1"));
Assert.False(context.PropertyFilter(context, "Excluded2"));
}
[Fact]
@ -96,8 +96,8 @@ namespace Microsoft.AspNet.Mvc.Core.Test
modelMetadata, actionBindingContext, Mock.Of<OperationBindingContext>());
// Assert
Assert.True(context.PropertyFilter("IncludedExplicitly1"));
Assert.True(context.PropertyFilter("IncludedExplicitly2"));
Assert.True(context.PropertyFilter(context, "IncludedExplicitly1"));
Assert.True(context.PropertyFilter(context, "IncludedExplicitly2"));
}
[Fact]

View File

@ -3,8 +3,11 @@
#if ASPNET50
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc.ModelBinding;
@ -123,6 +126,346 @@ namespace Microsoft.AspNet.Mvc.Core.Test
Assert.Equal("MyPropertyValue", model.MyProperty);
}
[Fact]
public async Task TryUpdateModel_UsingIncludePredicateOverload_ReturnsFalse_IfBinderReturnsFalse()
{
// Arrange
var metadataProvider = new Mock<IModelMetadataProvider>();
metadataProvider.Setup(m => m.GetMetadataForType(null, It.IsAny<Type>()))
.Returns(new ModelMetadata(metadataProvider.Object, null, null, typeof(MyModel), null))
.Verifiable();
var binder = new Mock<IModelBinder>();
binder.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Returns(Task.FromResult(false));
var model = new MyModel();
Func<ModelBindingContext, string, bool> includePredicate =
(context, propertyName) => true;
// Act
var result = await ModelBindingHelper.TryUpdateModelAsync(
model,
null,
Mock.Of<HttpContext>(),
new ModelStateDictionary(),
metadataProvider.Object,
GetCompositeBinder(binder.Object),
Mock.Of<IValueProvider>(),
Mock.Of<IModelValidatorProvider>(),
includePredicate);
// Assert
Assert.False(result);
Assert.Null(model.MyProperty);
Assert.Null(model.IncludedProperty);
Assert.Null(model.ExcludedProperty);
metadataProvider.Verify();
}
[Fact]
public async Task TryUpdateModel_UsingIncludePredicateOverload_ReturnsTrue_ModelBindsAndValidatesSuccessfully()
{
// Arrange
var binders = new IModelBinder[]
{
new TypeConverterModelBinder(),
new ComplexModelDtoModelBinder(),
new MutableObjectModelBinder()
};
var validator = new DataAnnotationsModelValidatorProvider();
var model = new MyModel {
MyProperty = "Old-Value",
IncludedProperty = "Old-IncludedPropertyValue",
ExcludedProperty = "Old-ExcludedPropertyValue"
};
var modelStateDictionary = new ModelStateDictionary();
var values = new Dictionary<string, object>
{
{ "", null },
{ "MyProperty", "MyPropertyValue" },
{ "IncludedProperty", "IncludedPropertyValue" },
{ "ExcludedProperty", "ExcludedPropertyValue" }
};
Func<ModelBindingContext, string, bool> includePredicate =
(context, propertyName) =>
string.Equals(propertyName, "IncludedProperty", StringComparison.OrdinalIgnoreCase) ||
string.Equals(propertyName, "MyProperty", StringComparison.OrdinalIgnoreCase);
var valueProvider = new DictionaryBasedValueProvider<TestValueBinderMetadata>(values);
// Act
var result = await ModelBindingHelper.TryUpdateModelAsync(
model,
"",
Mock.Of<HttpContext>(),
modelStateDictionary,
new DataAnnotationsModelMetadataProvider(),
GetCompositeBinder(binders),
valueProvider,
validator,
includePredicate);
// Assert
Assert.True(result);
Assert.Equal("MyPropertyValue", model.MyProperty);
Assert.Equal("IncludedPropertyValue", model.IncludedProperty);
Assert.Equal("Old-ExcludedPropertyValue", model.ExcludedProperty);
}
[Fact]
public async Task TryUpdateModel_UsingIncludeExpressionOverload_ReturnsFalse_IfBinderReturnsFalse()
{
// Arrange
var metadataProvider = new Mock<IModelMetadataProvider>();
metadataProvider.Setup(m => m.GetMetadataForType(null, It.IsAny<Type>()))
.Returns(new ModelMetadata(metadataProvider.Object, null, null, typeof(MyModel), null))
.Verifiable();
var binder = new Mock<IModelBinder>();
binder.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Returns(Task.FromResult(false));
var model = new MyModel();
// Act
var result = await ModelBindingHelper.TryUpdateModelAsync(
model,
null,
Mock.Of<HttpContext>(),
new ModelStateDictionary(),
metadataProvider.Object,
GetCompositeBinder(binder.Object),
Mock.Of<IValueProvider>(),
Mock.Of<IModelValidatorProvider>(),
m => m.IncludedProperty );
// Assert
Assert.False(result);
Assert.Null(model.MyProperty);
Assert.Null(model.IncludedProperty);
Assert.Null(model.ExcludedProperty);
metadataProvider.Verify();
}
[Fact]
public async Task TryUpdateModel_UsingIncludeExpressionOverload_ReturnsTrue_ModelBindsAndValidatesSuccessfully()
{
// Arrange
var binders = new IModelBinder[]
{
new TypeConverterModelBinder(),
new ComplexModelDtoModelBinder(),
new MutableObjectModelBinder()
};
var validator = new DataAnnotationsModelValidatorProvider();
var model = new MyModel
{
MyProperty = "Old-Value",
IncludedProperty = "Old-IncludedPropertyValue",
ExcludedProperty = "Old-ExcludedPropertyValue"
};
var modelStateDictionary = new ModelStateDictionary();
var values = new Dictionary<string, object>
{
{ "", null },
{ "MyProperty", "MyPropertyValue" },
{ "IncludedProperty", "IncludedPropertyValue" },
{ "ExcludedProperty", "ExcludedPropertyValue" }
};
var valueProvider = new DictionaryBasedValueProvider<TestValueBinderMetadata>(values);
// Act
var result = await ModelBindingHelper.TryUpdateModelAsync(
model,
"",
Mock.Of<HttpContext>(),
modelStateDictionary,
new DataAnnotationsModelMetadataProvider(),
GetCompositeBinder(binders),
valueProvider,
validator,
m => m.IncludedProperty,
m => m.MyProperty);
// Assert
Assert.True(result);
Assert.Equal("MyPropertyValue", model.MyProperty);
Assert.Equal("IncludedPropertyValue", model.IncludedProperty);
Assert.Equal("Old-ExcludedPropertyValue", model.ExcludedProperty);
}
[Fact]
public async Task TryUpdateModel_UsingDefaultIncludeOverload_IncludesAllProperties()
{
// Arrange
var binders = new IModelBinder[]
{
new TypeConverterModelBinder(),
new ComplexModelDtoModelBinder(),
new MutableObjectModelBinder()
};
var validator = new DataAnnotationsModelValidatorProvider();
var model = new MyModel
{
MyProperty = "Old-Value",
IncludedProperty = "Old-IncludedPropertyValue",
ExcludedProperty = "Old-ExcludedPropertyValue"
};
var modelStateDictionary = new ModelStateDictionary();
var values = new Dictionary<string, object>
{
{ "", null },
{ "MyProperty", "MyPropertyValue" },
{ "IncludedProperty", "IncludedPropertyValue" },
{ "ExcludedProperty", "ExcludedPropertyValue" }
};
var valueProvider = new DictionaryBasedValueProvider<TestValueBinderMetadata>(values);
// Act
var result = await ModelBindingHelper.TryUpdateModelAsync(
model,
"",
Mock.Of<HttpContext>(),
modelStateDictionary,
new DataAnnotationsModelMetadataProvider(),
GetCompositeBinder(binders),
valueProvider,
validator);
// Assert
// Includes everything.
Assert.True(result);
Assert.Equal("MyPropertyValue", model.MyProperty);
Assert.Equal("IncludedPropertyValue", model.IncludedProperty);
Assert.Equal("ExcludedPropertyValue", model.ExcludedProperty);
}
[Fact]
public void GetPropertyName_PropertyMemberAccessReturnsPropertyName()
{
// Arrange
Expression<Func<User, object>> expression = m => m.Address;
// Act
var propertyName = ModelBindingHelper.GetPropertyName(expression.Body);
// Assert
Assert.Equal(nameof(User.Address), propertyName);
}
[Fact]
public void GetPropertyName_ChainedExpression_Throws()
{
// Arrange
Expression<Func<User, object>> expression = m => m.Address.Street;
// Act & Assert
var ex = Assert.Throws<InvalidOperationException>(() =>
ModelBindingHelper.GetPropertyName(expression.Body));
Assert.Equal(string.Format("The passed expression of expression node type '{0}' is invalid." +
" Only simple member access expressions for model properties are supported.",
expression.Body.NodeType),
ex.Message);
}
public static IEnumerable<object[]> InvalidExpressionDataSet
{
get
{
Expression<Func<User, object>> expression = m => new Func<User>(() => m);
yield return new object[] { expression }; // lambda expression.
expression = m => m.Save();
yield return new object[] { expression }; // method call expression.
expression = m => m.Friends[0]; // ArrayIndex expression.
yield return new object[] { expression };
expression = m => m.Colleagues[0]; // Indexer expression.
yield return new object[] { expression };
expression = m => m; // Parameter expression.
yield return new object[] { expression };
object someVariable = "something";
expression = m => someVariable; // Variable accessor.
yield return new object[] { expression };
}
}
[Theory]
[MemberData(nameof(InvalidExpressionDataSet))]
public void GetPropertyName_ExpressionsOtherThanMemberAccess_Throws(Expression<Func<User, object>> expression)
{
// Arrange Act & Assert
var ex = Assert.Throws<InvalidOperationException>(() =>
ModelBindingHelper.GetPropertyName(expression.Body));
Assert.Equal(string.Format("The passed expression of expression node type '{0}' is invalid."+
" Only simple member access expressions for model properties are supported.",
expression.Body.NodeType),
ex.Message);
}
[Fact]
public void GetPropertyName_NonParameterBasedExpression_Throws()
{
// Arrange
var someUser = new User();
// PropertyAccessor with a property name invalid as it originates from a variable accessor.
Expression<Func<User, object>> expression = m => someUser.Address;
// Act & Assert
var ex = Assert.Throws<InvalidOperationException>(() =>
ModelBindingHelper.GetPropertyName(expression.Body));
Assert.Equal(string.Format("The passed expression of expression node type '{0}' is invalid." +
" Only simple member access expressions for model properties are supported.",
expression.Body.NodeType),
ex.Message);
}
[Fact]
public void GetPropertyName_TopLevelCollectionIndexer_Throws()
{
// Arrange
Expression<Func<List<User>, object>> expression = m => m[0];
// Act & Assert
var ex = Assert.Throws<InvalidOperationException>(() =>
ModelBindingHelper.GetPropertyName(expression.Body));
Assert.Equal(string.Format("The passed expression of expression node type '{0}' is invalid." +
" Only simple member access expressions for model properties are supported.",
expression.Body.NodeType),
ex.Message);
}
[Fact]
public void GetPropertyName_FieldExpression_Throws()
{
// Arrange
Expression<Func<User, object>> expression = m => m._userId;
// Act & Assert
var ex = Assert.Throws<InvalidOperationException>(() =>
ModelBindingHelper.GetPropertyName(expression.Body));
Assert.Equal(string.Format("The passed expression of expression node type '{0}' is invalid." +
" Only simple member access expressions for model properties are supported.",
expression.Body.NodeType),
ex.Message);
}
private static IModelBinder GetCompositeBinder(params IModelBinder[] binders)
{
var binderProvider = new Mock<IModelBinderProvider>();
@ -131,10 +474,43 @@ namespace Microsoft.AspNet.Mvc.Core.Test
return new CompositeModelBinder(binderProvider.Object);
}
public class User
{
public string _userId;
public Address Address { get; set; }
public User[] Friends { get; set; }
public List<User> Colleagues { get; set; }
public bool IsReadOnly
{
get
{
throw new NotImplementedException();
}
}
public User Save()
{
return this;
}
}
public class Address
{
public string Street { get; set; }
}
private class MyModel
{
[Required]
public string MyProperty { get; set; }
public string IncludedProperty { get; set; }
public string ExcludedProperty { get; set; }
}
private class TestValueBinderMetadata : IValueProviderMetadata

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Net;
using System.Net.Http;
using System.Text;
@ -826,5 +827,179 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
Assert.Equal(string.Empty, await response.Content.ReadAsStringAsync());
}
[Fact]
public async Task TryUpdateModel_IncludeTopLevelProperty_IncludesAllSubProperties()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act
var response = await client.GetStringAsync("http://localhost/TryUpdateModel/" +
"GetUserAsync_IncludesAllSubProperties" +
"?id=123&Key=34&RegistrationMonth=March&Address.Street=123&Address.Country.Name=USA&" +
"Address.State=WA&Address.Country.Cities[0].CityName=Seattle&Address.Country.Cities[0].CityCode=SEA");
// Assert
var user = JsonConvert.DeserializeObject<User>(response);
// Should update everything under Address.
Assert.Equal(123, user.Address.Street); // Included by default as sub properties are included.
Assert.Equal("WA", user.Address.State); // Included by default as sub properties of address are included.
Assert.Equal("USA", user.Address.Country.Name); // Included by default.
Assert.Equal("Seattle", user.Address.Country.Cities[0].CityName); // Included by default.
Assert.Equal("SEA", user.Address.Country.Cities[0].CityCode); // Included by default.
// Should not update Any property at the same level as address.
// Key is id + 20.
Assert.Equal(143, user.Key);
Assert.Null(user.RegisterationMonth);
}
[Fact]
public async Task TryUpdateModel_ChainedPropertyExpression_Throws()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
Expression<Func<User, object>> expression = model => model.Address.Country;
// Act
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
client.GetAsync("http://localhost/TryUpdateModel/GetUserAsync_WithChainedProperties?id=123"));
Assert.Equal(string.Format("The passed expression of expression node type '{0}' is invalid." +
" Only simple member access expressions for model properties are supported.",
expression.Body.NodeType),
ex.Message);
}
[Fact]
public async Task TryUpdateModel_FailsToUpdateProperties()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act
var response = await client.GetStringAsync("http://localhost/TryUpdateModel/" +
"TryUpdateModelFails" +
"?id=123&RegisterationMonth=March&Key=123&UserName=SomeName");
// Assert
var result = JsonConvert.DeserializeObject<bool>(response);
// Act
Assert.False(result);
}
[Fact]
public async Task TryUpdateModel_IncludeExpression_WorksOnlyAtTopLevel()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act
var response = await client.GetStringAsync("http://localhost/TryUpdateModel/" +
"GetPerson" +
"?Parent.Name=fatherName&Parent.Parent.Name=grandFatherName");
// Assert
var person = JsonConvert.DeserializeObject<Person>(response);
// Act
Assert.Equal("fatherName", person.Parent.Name);
// Includes this as there is data from value providers, the include filter
// only works for top level objects.
Assert.Equal("grandFatherName", person.Parent.Parent.Name);
}
[Fact]
public async Task TryUpdateModel_Validates_ForTopLevelNotIncludedProperties()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act
var response = await client.GetStringAsync("http://localhost/TryUpdateModel/" +
"CreateAndUpdateUser" +
"?RegisterationMonth=March");
// Assert
var result = JsonConvert.DeserializeObject<bool>(response);
Assert.False(result);
}
[Fact]
public async Task TryUpdateModelExcludeSpecific_Properties()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act
var response = await client.GetStringAsync("http://localhost/TryUpdateModel/" +
"GetUserAsync_ExcludeSpecificProperties" +
"?id=123&RegisterationMonth=March&Key=123&UserName=SomeName");
// Assert
var user = JsonConvert.DeserializeObject<User>(response);
// Should not update excluded properties.
Assert.NotEqual(123, user.Key);
// Should Update all explicitly included properties.
Assert.Equal("March", user.RegisterationMonth);
Assert.Equal("SomeName", user.UserName);
}
[Fact]
public async Task TryUpdateModelIncludeSpecific_Properties()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act
var response = await client.GetStringAsync("http://localhost/TryUpdateModel/" +
"GetUserAsync_IncludeSpecificProperties" +
"?id=123&RegisterationMonth=March&Key=123&UserName=SomeName");
// Assert
var user = JsonConvert.DeserializeObject<User>(response);
// Should not update any not explicitly mentioned properties.
Assert.NotEqual("SomeName", user.UserName);
Assert.NotEqual(123, user.Key);
// Should Update all included properties.
Assert.Equal("March", user.RegisterationMonth);
}
[Fact]
public async Task TryUpdateModelIncludesAllProperties_ByDefault()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act
var response = await client.GetStringAsync("http://localhost/TryUpdateModel/" +
"GetUserAsync_IncludeAllByDefault" +
"?id=123&RegisterationMonth=March&Key=123&UserName=SomeName");
// Assert
var user = JsonConvert.DeserializeObject<User>(response);
// Should not update any not explicitly mentioned properties.
Assert.Equal("SomeName", user.UserName);
Assert.Equal(123, user.Key);
// Should Update all included properties.
Assert.Equal("March", user.RegisterationMonth);
}
}
}

View File

@ -0,0 +1,124 @@
// 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.Threading.Tasks;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.ModelBinding;
namespace ModelBindingWebSite.Controllers
{
public class TryUpdateModelController : Controller
{
public async Task<Person> GetPerson()
{
// Person has a property of type Person. Only Top level should be updated.
var person = new Person();
await TryUpdateModelAsync(
person,
prefix: string.Empty,
includeExpressions: m => m.Parent);
return person;
}
public async Task<User> GetUserAsync_IncludeAllByDefault(int id)
{
var user = GetUser(id);
await TryUpdateModelAsync<User>(user, prefix: string.Empty);
return user;
}
public async Task<User> GetUserAsync_ExcludeSpecificProperties(int id)
{
var user = GetUser(id);
await TryUpdateModelAsync(
user,
prefix: string.Empty,
predicate:
(context, modelName) =>
!string.Equals(modelName, nameof(ModelBindingWebSite.User.Id), StringComparison.Ordinal) &&
!string.Equals(modelName, nameof(ModelBindingWebSite.User.Key), StringComparison.Ordinal));
return user;
}
public async Task<bool> CreateAndUpdateUser()
{
// don't update the id.
var user = new User();
return await TryUpdateModelAsync(user,
prefix: string.Empty,
includeExpressions: model => model.RegisterationMonth);
}
public async Task<User> GetUserAsync_IncludeSpecificProperties(int id)
{
var user = GetUser(id);
await TryUpdateModelAsync(user,
prefix: string.Empty,
includeExpressions: model => model.RegisterationMonth);
return user;
}
public async Task<bool> TryUpdateModelFails(int id)
{
var user = GetUser(id);
return await TryUpdateModelAsync(user,
prefix: string.Empty,
valueProvider: new CustomValueProvider());
}
public async Task<User> GetUserAsync_IncludeAndExcludeListNull(int id)
{
var user = GetUser(id);
await TryUpdateModelAsync(user);
return user;
}
public async Task<User> GetUserAsync_IncludesAllSubProperties(int id)
{
var user = GetUser(id);
await TryUpdateModelAsync(user, prefix: string.Empty, includeExpressions: model => model.Address);
return user;
}
public async Task<User> GetUserAsync_WithChainedProperties(int id)
{
var user = GetUser(id);
// Since this is a chained expression this would throw
await TryUpdateModelAsync(user, prefix: string.Empty, includeExpressions: model => model.Address.Country);
return user;
}
private User GetUser(int id)
{
return new User
{
UserName = "User_" + id,
Id = id,
Key = id + 20,
};
}
public class CustomValueProvider : IValueProvider
{
public Task<bool> ContainsPrefixAsync(string prefix)
{
return Task.FromResult(false);
}
public Task<ValueProviderResult> GetValueAsync(string key)
{
return Task.FromResult<ValueProviderResult>(null);
}
}
}
}

View File

@ -8,5 +8,7 @@ namespace ModelBindingWebSite
public int Street { get; set; }
public string State { get; set; }
public int Zip { get; set; }
public Country Country { get; set; }
}
}

View File

@ -0,0 +1,23 @@
// 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;
namespace ModelBindingWebSite
{
public class Country
{
public string Name { get; set; }
public City[] Cities { get; set; }
public int[] StateCodes { get; set; }
}
public class City
{
public string CityName { get; set; }
public string CityCode { get; set; }
}
}

View File

@ -0,0 +1,20 @@
// 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 ModelBindingWebSite
{
public class User
{
[Required]
public int Id { get; set; }
public int Key { get; set; }
[Required]
public string RegisterationMonth { get; set; }
public string UserName { get; set; }
public Address Address { get; set; }
}
}