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;
using System.IO; using System.IO;
using System.Linq.Expressions;
using System.Security.Principal; using System.Security.Principal;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -654,26 +655,28 @@ namespace Microsoft.AspNet.Mvc
} }
/// <summary> /// <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> /// </summary>
/// <typeparam name="TModel">The type of the model object.</typeparam> /// <typeparam name="TModel">The type of the model object.</typeparam>
/// <param name="model">The model instance to update.</param> /// <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] [NonAction]
public virtual Task<bool> TryUpdateModelAsync<TModel>([NotNull] TModel model) public virtual Task<bool> TryUpdateModelAsync<TModel>([NotNull] TModel model)
where TModel : class where TModel : class
{ {
return TryUpdateModelAsync(model, prefix: typeof(TModel).Name); return TryUpdateModelAsync(model, prefix: null);
} }
/// <summary> /// <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
/// and a prefix. /// <see cref="IValueProvider"/> and a <paramref name="prefix"/>.
/// </summary> /// </summary>
/// <typeparam name="TModel">The type of the model object.</typeparam> /// <typeparam name="TModel">The type of the model object.</typeparam>
/// <param name="model">The model instance to update.</param> /// <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="prefix">The prefix to use when looking up values in the current <see cref="IValueProvider"/>
/// <returns>true if the update is successful; otherwise, false.</returns> /// </param>
/// <returns>A <see cref="Task"/> that on completion returns <c>true</c> if the update is successful</returns>
[NonAction] [NonAction]
public virtual async Task<bool> TryUpdateModelAsync<TModel>([NotNull] TModel model, public virtual async Task<bool> TryUpdateModelAsync<TModel>([NotNull] TModel model,
[NotNull] string prefix) [NotNull] string prefix)
@ -681,7 +684,8 @@ namespace Microsoft.AspNet.Mvc
{ {
if (BindingContextProvider == null) if (BindingContextProvider == null)
{ {
var message = Resources.FormatPropertyOfTypeCannotBeNull("BindingContextProvider", GetType().FullName); var message = Resources.FormatPropertyOfTypeCannotBeNull(nameof(BindingContextProvider),
GetType().FullName);
throw new InvalidOperationException(message); throw new InvalidOperationException(message);
} }
@ -690,13 +694,15 @@ namespace Microsoft.AspNet.Mvc
} }
/// <summary> /// <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> /// </summary>
/// <typeparam name="TModel">The type of the model object.</typeparam> /// <typeparam name="TModel">The type of the model object.</typeparam>
/// <param name="model">The model instance to update.</param> /// <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="prefix">The prefix to use when looking up values in the <paramref name="valueProvider"/>.
/// <param name="valueProvider">The value provider used for looking up values.</param> /// </param>
/// <returns>true if the update is successful; otherwise, false.</returns> /// <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] [NonAction]
public virtual async Task<bool> TryUpdateModelAsync<TModel>([NotNull] TModel model, public virtual async Task<bool> TryUpdateModelAsync<TModel>([NotNull] TModel model,
[NotNull] string prefix, [NotNull] string prefix,
@ -705,7 +711,8 @@ namespace Microsoft.AspNet.Mvc
{ {
if (BindingContextProvider == null) if (BindingContextProvider == null)
{ {
var message = Resources.FormatPropertyOfTypeCannotBeNull("BindingContextProvider", GetType().FullName); var message = Resources.FormatPropertyOfTypeCannotBeNull(nameof(BindingContextProvider),
GetType().FullName);
throw new InvalidOperationException(message); throw new InvalidOperationException(message);
} }
@ -720,6 +727,156 @@ namespace Microsoft.AspNet.Mvc
bindingContext.ValidatorProvider); 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() public void Dispose()
{ {
Dispose(disposing: true); Dispose(disposing: true);
@ -729,6 +886,5 @@ namespace Microsoft.AspNet.Mvc
protected virtual void Dispose(bool disposing) protected virtual void Dispose(bool disposing)
{ {
} }
} }
} }

View File

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

View File

@ -1,8 +1,13 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // 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. // 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 System.Threading.Tasks;
using Microsoft.AspNet.Http; using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.ModelBinding;
namespace Microsoft.AspNet.Mvc namespace Microsoft.AspNet.Mvc
@ -10,22 +15,24 @@ namespace Microsoft.AspNet.Mvc
public static class ModelBindingHelper public static class ModelBindingHelper
{ {
/// <summary> /// <summary>
/// Updates the specified model instance using the specified binder and value provider and /// Updates the specified <paramref name="model"/> instance using the specified <paramref name="modelBinder"/>
/// executes validation using the specified sequence of validator providers. /// and the specified <paramref name="valueProvider"/> and executes validation using the specified
/// <paramref name="validatorProvider"/>.
/// </summary> /// </summary>
/// <typeparam name="TModel">The type of the model object.</typeparam> /// <typeparam name="TModel">The type of the model object.</typeparam>
/// <param name="model">The model instance to update.</param> /// <param name="model">The model instance to update and validate.</param>
/// <param name="prefix">The prefix to use when looking up values in the value provider.</param> /// <param name="prefix">The prefix to use when looking up values in the <paramref name="valueProvider"/>.
/// <param name="httpContext">The context for the current executing request.</param> /// </param>
/// <param name="modelState">The ModelStateDictionary used for maintaining state and /// <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> /// results of model-binding validation.</param>
/// <param name="metadataProvider">The provider used for reading metadata for the model type.</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="modelBinder">The <see cref="IModelBinder"/> used for binding.</param>
/// <param name="valueProvider">The value provider used for looking up values.</param> /// <param name="valueProvider">The <see cref="IValueProvider"/> used for looking up values.</param>
/// <param name="validatorProvider">The validator provider used for executing validation on the model /// <param name="validatorProvider">The <see cref="IModelValidatorProvider"/> used for executing validation
/// instance.</param> /// on the model instance.</param>
/// <returns>A Task with a value representing if the the update is successful.</returns> /// <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>( public static Task<bool> TryUpdateModelAsync<TModel>(
[NotNull] TModel model, [NotNull] TModel model,
[NotNull] string prefix, [NotNull] string prefix,
[NotNull] HttpContext httpContext, [NotNull] HttpContext httpContext,
@ -35,6 +42,99 @@ namespace Microsoft.AspNet.Mvc
[NotNull] IValueProvider valueProvider, [NotNull] IValueProvider valueProvider,
[NotNull] IModelValidatorProvider validatorProvider) [NotNull] IModelValidatorProvider validatorProvider)
where TModel : class 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( var modelMetadata = metadataProvider.GetMetadataForType(
modelAccessor: null, modelAccessor: null,
@ -56,6 +156,7 @@ namespace Microsoft.AspNet.Mvc
ValueProvider = valueProvider, ValueProvider = valueProvider,
FallbackToEmptyPrefix = true, FallbackToEmptyPrefix = true,
OperationBindingContext = operationBindingContext, OperationBindingContext = operationBindingContext,
PropertyFilter = predicate
}; };
if (await modelBinder.BindModelAsync(modelBindingContext)) if (await modelBinder.BindModelAsync(modelBindingContext))
@ -65,5 +166,91 @@ namespace Microsoft.AspNet.Mvc
return false; 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); 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> /// <summary>
/// The IModelMetadataProvider was unable to provide metadata for expression '{0}'. /// The IModelMetadataProvider was unable to provide metadata for expression '{0}'.
/// </summary> /// </summary>
@ -1403,7 +1419,7 @@ namespace Microsoft.AspNet.Mvc.Core
} }
/// <summary> /// <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> /// </summary>
internal static string AttributeRoute_MixedAttributeAndConventionallyRoutedActions_ForMethod internal static string AttributeRoute_MixedAttributeAndConventionallyRoutedActions_ForMethod
{ {
@ -1411,7 +1427,7 @@ namespace Microsoft.AspNet.Mvc.Core
} }
/// <summary> /// <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> /// </summary>
internal static string FormatAttributeRoute_MixedAttributeAndConventionallyRoutedActions_ForMethod(object p0, object p1, object p2) 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"> <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> <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>
<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"> <data name="HtmlHelper_NullModelMetadata" xml:space="preserve">
<value>The IModelMetadataProvider was unable to provide metadata for expression '{0}'.</value> <value>The IModelMetadataProvider was unable to provide metadata for expression '{0}'.</value>
</data> </data>

View File

@ -271,7 +271,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var validationInfo = GetPropertyValidationInfo(bindingContext); var validationInfo = GetPropertyValidationInfo(bindingContext);
return bindingContext.ModelMetadata.Properties return bindingContext.ModelMetadata.Properties
.Where(propertyMetadata => .Where(propertyMetadata =>
bindingContext.PropertyFilter(propertyMetadata.PropertyName) && bindingContext.PropertyFilter(bindingContext, propertyMetadata.PropertyName) &&
(validationInfo.RequiredProperties.Contains(propertyMetadata.PropertyName) || (validationInfo.RequiredProperties.Contains(propertyMetadata.PropertyName) ||
!validationInfo.SkipProperties.Contains(propertyMetadata.PropertyName)) && !validationInfo.SkipProperties.Contains(propertyMetadata.PropertyName)) &&
CanUpdateProperty(propertyMetadata)); CanUpdateProperty(propertyMetadata));
@ -473,21 +473,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
return addedError; 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 internal sealed class PropertyValidationInfo
{ {
public PropertyValidationInfo() public PropertyValidationInfo()

View File

@ -13,12 +13,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// </summary> /// </summary>
public class ModelBindingContext 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 string _modelName;
private ModelStateDictionary _modelState; private ModelStateDictionary _modelState;
private Dictionary<string, ModelMetadata> _propertyMetadata; private Dictionary<string, ModelMetadata> _propertyMetadata;
private ModelValidationNode _validationNode; private ModelValidationNode _validationNode;
private Predicate<string> _propertyFilter; private Func<ModelBindingContext, string, bool> _propertyFilter;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ModelBindingContext"/> class. /// 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 get
{ {

View File

@ -675,6 +675,7 @@ namespace Microsoft.AspNet.Mvc.Test
[Fact] [Fact]
public async Task Controller_ActionFilter_SettingResult_ShortCircuits() public async Task Controller_ActionFilter_SettingResult_ShortCircuits()
{ {
// Arrange, Act & Assert
await ActionFilterAttributeTests.ActionFilter_SettingResult_ShortCircuits( await ActionFilterAttributeTests.ActionFilter_SettingResult_ShortCircuits(
new Mock<Controller>()); new Mock<Controller>());
} }
@ -682,42 +683,34 @@ namespace Microsoft.AspNet.Mvc.Test
[Fact] [Fact]
public async Task Controller_ActionFilter_Calls_OnActionExecuted() public async Task Controller_ActionFilter_Calls_OnActionExecuted()
{ {
// Arrange, Act & Assert
await ActionFilterAttributeTests.ActionFilter_Calls_OnActionExecuted( await ActionFilterAttributeTests.ActionFilter_Calls_OnActionExecuted(
new Mock<Controller>()); new Mock<Controller>());
} }
[Fact] [Fact]
public async Task TryUpdateModel_UsesModelTypeNameIfNotSpecified() public async Task TryUpdateModel_FallsBackOnEmptyPrefix_IfNotSpecified()
{ {
// Arrange
var metadataProvider = new DataAnnotationsModelMetadataProvider(); var metadataProvider = new DataAnnotationsModelMetadataProvider();
var valueProvider = Mock.Of<IValueProvider>(); var valueProvider = Mock.Of<IValueProvider>();
var binder = new Mock<IModelBinder>(); var binder = new Mock<IModelBinder>();
binder.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>())) binder.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Callback((ModelBindingContext b) => .Callback((ModelBindingContext context) =>
{ {
Assert.Equal(typeof(MyModel).Name, b.ModelName); Assert.Empty(context.ModelName);
Assert.Same(valueProvider, b.ValueProvider); 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)) .Returns(Task.FromResult(false))
.Verifiable(); .Verifiable();
var controller = GetController(binder.Object, valueProvider);
var model = new MyModel(); 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 // Act
var result = await controller.TryUpdateModelAsync(model); var result = await controller.TryUpdateModelAsync(model);
@ -729,36 +722,28 @@ namespace Microsoft.AspNet.Mvc.Test
[Fact] [Fact]
public async Task TryUpdateModel_UsesModelTypeNameIfSpecified() public async Task TryUpdateModel_UsesModelTypeNameIfSpecified()
{ {
// Arrange
var modelName = "mymodel";
var metadataProvider = new DataAnnotationsModelMetadataProvider(); var metadataProvider = new DataAnnotationsModelMetadataProvider();
var valueProvider = Mock.Of<IValueProvider>(); var valueProvider = Mock.Of<IValueProvider>();
var binder = new Mock<IModelBinder>(); var binder = new Mock<IModelBinder>();
var modelName = "mymodel";
binder.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>())) binder.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Callback((ModelBindingContext b) => .Callback((ModelBindingContext context) =>
{ {
Assert.Equal(modelName, b.ModelName); Assert.Equal(modelName, context.ModelName);
Assert.Same(valueProvider, b.ValueProvider); 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)) .Returns(Task.FromResult(false))
.Verifiable(); .Verifiable();
var controller = GetController(binder.Object, valueProvider);
var model = new MyModel(); 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 // Act
var result = await controller.TryUpdateModelAsync(model, modelName); var result = await controller.TryUpdateModelAsync(model, modelName);
@ -770,36 +755,27 @@ namespace Microsoft.AspNet.Mvc.Test
[Fact] [Fact]
public async Task TryUpdateModel_UsesModelValueProviderIfSpecified() public async Task TryUpdateModel_UsesModelValueProviderIfSpecified()
{ {
var metadataProvider = new DataAnnotationsModelMetadataProvider(); // Arrange
var modelName = "mymodel";
var valueProvider = Mock.Of<IValueProvider>(); var valueProvider = Mock.Of<IValueProvider>();
var binder = new Mock<IModelBinder>(); var binder = new Mock<IModelBinder>();
var modelName = "mymodel";
binder.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>())) binder.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Callback((ModelBindingContext b) => .Callback((ModelBindingContext context) =>
{ {
Assert.Equal(modelName, b.ModelName); Assert.Equal(modelName, context.ModelName);
Assert.Same(valueProvider, b.ValueProvider); 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)) .Returns(Task.FromResult(false))
.Verifiable(); .Verifiable();
var controller = GetController(binder.Object, provider: null);
var model = new MyModel(); 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 // Act
var result = await controller.TryUpdateModelAsync(model, modelName, valueProvider); var result = await controller.TryUpdateModelAsync(model, modelName, valueProvider);
@ -807,6 +783,151 @@ namespace Microsoft.AspNet.Mvc.Test
// Assert // Assert
binder.Verify(); 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 #endif
[Fact] [Fact]
@ -966,9 +1087,55 @@ namespace Microsoft.AspNet.Mvc.Test
Assert.Same(viewEngine, result.ViewEngine); 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 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 private class DisposableController : Controller

View File

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

View File

@ -3,8 +3,11 @@
#if ASPNET50 #if ASPNET50
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Http; using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.ModelBinding;
@ -123,6 +126,346 @@ namespace Microsoft.AspNet.Mvc.Core.Test
Assert.Equal("MyPropertyValue", model.MyProperty); 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) private static IModelBinder GetCompositeBinder(params IModelBinder[] binders)
{ {
var binderProvider = new Mock<IModelBinderProvider>(); var binderProvider = new Mock<IModelBinderProvider>();
@ -131,10 +474,43 @@ namespace Microsoft.AspNet.Mvc.Core.Test
return new CompositeModelBinder(binderProvider.Object); 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 private class MyModel
{ {
[Required] [Required]
public string MyProperty { get; set; } public string MyProperty { get; set; }
public string IncludedProperty { get; set; }
public string ExcludedProperty { get; set; }
} }
private class TestValueBinderMetadata : IValueProviderMetadata private class TestValueBinderMetadata : IValueProviderMetadata

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Linq.Expressions;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Text; using System.Text;
@ -826,5 +827,179 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
Assert.Equal(string.Empty, await response.Content.ReadAsStringAsync()); 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 int Street { get; set; }
public string State { get; set; } public string State { get; set; }
public int Zip { 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; }
}
}