diff --git a/src/Microsoft.AspNet.Mvc.Core/Controller.cs b/src/Microsoft.AspNet.Mvc.Core/Controller.cs index c617f4817f..2dc1125d70 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Controller.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Controller.cs @@ -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 } /// - /// Updates the specified model instance using values from the controller's current value provider. + /// Updates the specified instance using values from the controller's current + /// . /// /// The type of the model object. /// The model instance to update. - /// true if the update is successful; otherwise, false. + /// A that on completion returns true if the update is successful [NonAction] public virtual Task TryUpdateModelAsync([NotNull] TModel model) where TModel : class { - return TryUpdateModelAsync(model, prefix: typeof(TModel).Name); + return TryUpdateModelAsync(model, prefix: null); } /// - /// Updates the specified model instance using values from the controller's current value provider - /// and a prefix. + /// Updates the specified instance using values from the controller's current + /// and a . /// /// The type of the model object. /// The model instance to update. - /// The prefix to use when looking up values in the value provider. - /// true if the update is successful; otherwise, false. + /// The prefix to use when looking up values in the current + /// + /// A that on completion returns true if the update is successful [NonAction] public virtual async Task TryUpdateModelAsync([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 } /// - /// Updates the specified model instance using the value provider and a prefix. + /// Updates the specified instance using the and a + /// . /// /// The type of the model object. /// The model instance to update. - /// The prefix to use when looking up values in the value provider. - /// The value provider used for looking up values. - /// true if the update is successful; otherwise, false. + /// The prefix to use when looking up values in the . + /// + /// The used for looking up values. + /// A that on completion returns true if the update is successful [NonAction] public virtual async Task TryUpdateModelAsync([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); } + /// + /// Updates the specified instance using values from the controller's current + /// and a . + /// + /// The type of the model object. + /// The model instance to update. + /// The prefix to use when looking up values in the current . + /// + /// (s) which represent top-level properties + /// which need to be included for the current model. + /// A that on completion returns true if the update is successful + [NonAction] + public async Task TryUpdateModelAsync( + [NotNull] TModel model, + string prefix, + [NotNull] params Expression>[] 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); + } + + /// + /// Updates the specified instance using values from the controller's current + /// and a . + /// + /// The type of the model object. + /// The model instance to update. + /// The prefix to use when looking up values in the current . + /// + /// A predicate which can be used to filter properties at runtime. + /// A that on completion returns true if the update is successful + [NonAction] + public async Task TryUpdateModelAsync( + [NotNull] TModel model, + string prefix, + [NotNull] Func 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); + } + + /// + /// Updates the specified instance using the and a + /// . + /// + /// The type of the model object. + /// The model instance to update. + /// The prefix to use when looking up values in the + /// + /// The used for looking up values. + /// (s) which represent top-level properties + /// which need to be included for the current model. + /// A that on completion returns true if the update is successful + [NonAction] + public async Task TryUpdateModelAsync( + [NotNull] TModel model, + string prefix, + [NotNull] IValueProvider valueProvider, + [NotNull] params Expression>[] 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); + } + + /// + /// Updates the specified instance using the and a + /// . + /// + /// The type of the model object. + /// The model instance to update. + /// The prefix to use when looking up values in the + /// + /// The used for looking up values. + /// A predicate which can be used to filter properties at runtime. + /// A that on completion returns true if the update is successful + [NonAction] + public async Task TryUpdateModelAsync( + [NotNull] TModel model, + string prefix, + [NotNull] IValueProvider valueProvider, + [NotNull] Func 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) { } - } } diff --git a/src/Microsoft.AspNet.Mvc.Core/DefaultControllerActionArgumentBinder.cs b/src/Microsoft.AspNet.Mvc.Core/DefaultControllerActionArgumentBinder.cs index 111a13e380..9ba8c37656 100644 --- a/src/Microsoft.AspNet.Mvc.Core/DefaultControllerActionArgumentBinder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/DefaultControllerActionArgumentBinder.cs @@ -100,8 +100,8 @@ namespace Microsoft.AspNet.Mvc ActionBindingContext actionBindingContext, OperationBindingContext operationBindingContext) { - Predicate propertyFilter = - propertyName => BindAttribute.IsPropertyAllowed(propertyName, + Func propertyFilter = + (context, propertyName) => BindAttribute.IsPropertyAllowed(propertyName, modelMetadata.BinderIncludeProperties, modelMetadata.BinderExcludeProperties); diff --git a/src/Microsoft.AspNet.Mvc.Core/ParameterBinding/ModelBindingHelper.cs b/src/Microsoft.AspNet.Mvc.Core/ParameterBinding/ModelBindingHelper.cs index 1846f7dc20..cc26ad8d58 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ParameterBinding/ModelBindingHelper.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ParameterBinding/ModelBindingHelper.cs @@ -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 { /// - /// 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 instance using the specified + /// and the specified and executes validation using the specified + /// . /// /// The type of the model object. - /// The model instance to update. - /// The prefix to use when looking up values in the value provider. - /// The context for the current executing request. - /// The ModelStateDictionary used for maintaining state and + /// The model instance to update and validate. + /// The prefix to use when looking up values in the . + /// + /// The for the current executing request. + /// The used for maintaining state and /// results of model-binding validation. /// The provider used for reading metadata for the model type. - /// The model binder used for binding. - /// The value provider used for looking up values. - /// The validator provider used for executing validation on the model - /// instance. - /// A Task with a value representing if the the update is successful. - public static async Task TryUpdateModelAsync( + /// The used for binding. + /// The used for looking up values. + /// The used for executing validation + /// on the model instance. + /// A that on completion returns true if the update is successful + public static Task TryUpdateModelAsync( [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); + } + + /// + /// Updates the specified instance using the specified + /// and the specified and executes validation using the specified + /// . + /// + /// The type of the model object. + /// The model instance to update and validate. + /// The prefix to use when looking up values in the . + /// + /// The for the current executing request. + /// The used for maintaining state and + /// results of model-binding validation. + /// The provider used for reading metadata for the model type. + /// The used for binding. + /// The used for looking up values. + /// The used for executing validation + /// on the model + /// instance. + /// Expression(s) which represent top level properties + /// which need to be included for the current model. + /// A that on completion returns true if the update is successful + public static Task TryUpdateModelAsync( + [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>[] includeExpressions) + where TModel : class + { + var includeExpression = GetIncludePredicateExpression(prefix, includeExpressions); + Func predicate = includeExpression.Compile(); + + return TryUpdateModelAsync( + model, + prefix, + httpContext, + modelState, + metadataProvider, + modelBinder, + valueProvider, + validatorProvider, + predicate: predicate); + } + + /// + /// Updates the specified instance using the specified + /// and the specified and executes validation using the specified + /// . + /// + /// The type of the model object. + /// The model instance to update and validate. + /// The prefix to use when looking up values in the . + /// + /// The for the current executing request. + /// The used for maintaining state and + /// results of model-binding validation. + /// The provider used for reading metadata for the model type. + /// The used for binding. + /// The used for looking up values. + /// The used for executing validation + /// on the model instance. + /// A predicate which can be used to + /// filter properties(for inclusion/exclusion) at runtime. + /// A that on completion returns true if the update is successful + public static async Task TryUpdateModelAsync( + [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 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> GetIncludePredicateExpression + (string prefix, Expression>[] 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>( + orWrapperExpression, firstExpression.Parameters); + } + + private static Expression> GetPredicateExpression + (string prefix, Expression> 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; + } + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs index b7a013f594..714664f8df 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs @@ -618,6 +618,22 @@ namespace Microsoft.AspNet.Mvc.Core return string.Format(CultureInfo.CurrentCulture, GetString("ExpressionHelper_InvalidIndexerExpression"), p0, p1); } + /// + /// The passed expression of expression node type '{0}' is invalid. Only simple member access expressions for model properties are supported. + /// + internal static string Invalid_IncludePropertyExpression + { + get { return GetString("Invalid_IncludePropertyExpression"); } + } + + /// + /// The passed expression of expression node type '{0}' is invalid. Only simple member access expressions for model properties are supported. + /// + internal static string FormatInvalid_IncludePropertyExpression(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("Invalid_IncludePropertyExpression"), p0); + } + /// /// The IModelMetadataProvider was unable to provide metadata for expression '{0}'. /// @@ -1403,7 +1419,7 @@ namespace Microsoft.AspNet.Mvc.Core } /// - /// 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. /// internal static string AttributeRoute_MixedAttributeAndConventionallyRoutedActions_ForMethod { @@ -1411,7 +1427,7 @@ namespace Microsoft.AspNet.Mvc.Core } /// - /// 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. /// internal static string FormatAttributeRoute_MixedAttributeAndConventionallyRoutedActions_ForMethod(object p0, object p1, object p2) { diff --git a/src/Microsoft.AspNet.Mvc.Core/Resources.resx b/src/Microsoft.AspNet.Mvc.Core/Resources.resx index 6d2c64bbf7..87355a2b09 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Core/Resources.resx @@ -232,6 +232,9 @@ The expression compiler was unable to evaluate the indexer expression '{0}' because it references the model parameter '{1}' which is unavailable. + + The passed expression of expression node type '{0}' is invalid. Only simple member access expressions for model properties are supported. + The IModelMetadataProvider was unable to provide metadata for expression '{0}'. diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs index 204582e3f8..af58f66544 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs @@ -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 includeProperties, - IReadOnlyList 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() diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/ModelBindingContext.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/ModelBindingContext.cs index 14aaaef6ea..58042ebcac 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/ModelBindingContext.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/ModelBindingContext.cs @@ -13,12 +13,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding /// public class ModelBindingContext { - private static readonly Predicate _defaultPropertyFilter = _ => true; + private static readonly Func + _defaultPropertyFilter = (context, propertyName) => true; + private string _modelName; private ModelStateDictionary _modelState; private Dictionary _propertyMetadata; private ModelValidationNode _validationNode; - private Predicate _propertyFilter; + private Func _propertyFilter; /// /// Initializes a new instance of the class. @@ -160,7 +162,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding } } - public Predicate PropertyFilter + public Func PropertyFilter { get { diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ControllerTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ControllerTests.cs index 4f0b900fad..e097c02950 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ControllerTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ControllerTests.cs @@ -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()); } @@ -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()); } [Fact] - public async Task TryUpdateModel_UsesModelTypeNameIfNotSpecified() + public async Task TryUpdateModel_FallsBackOnEmptyPrefix_IfNotSpecified() { + // Arrange var metadataProvider = new DataAnnotationsModelMetadataProvider(); var valueProvider = Mock.Of(); var binder = new Mock(); binder.Setup(b => b.BindModelAsync(It.IsAny())) - .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(), new RouteData(), new ActionDescriptor()); - var bindingContext = new ActionBindingContext(actionContext, - metadataProvider, - binder.Object, - valueProvider, - Mock.Of(), - Mock.Of()); - var bindingContextProvider = new Mock(); - 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(); var binder = new Mock(); - var modelName = "mymodel"; binder.Setup(b => b.BindModelAsync(It.IsAny())) - .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(), new RouteData(), new ActionDescriptor()); - var bindingContext = new ActionBindingContext(actionContext, - metadataProvider, - binder.Object, - valueProvider, - Mock.Of(), - Mock.Of()); - var bindingContextProvider = new Mock(); - 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(); var binder = new Mock(); - var modelName = "mymodel"; binder.Setup(b => b.BindModelAsync(It.IsAny())) - .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(), new RouteData(), new ActionDescriptor()); - var bindingContext = new ActionBindingContext(actionContext, - metadataProvider, - binder.Object, - Mock.Of(), - Mock.Of(), - Mock.Of()); - var bindingContextProvider = new Mock(); - 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 includePredicate = + (context, propertyName) => + string.Equals(propertyName, "include1", StringComparison.OrdinalIgnoreCase) || + string.Equals(propertyName, "include2", StringComparison.OrdinalIgnoreCase); + + var binder = new Mock(); + var valueProvider = Mock.Of(); + binder.Setup(b => b.BindModelAsync(It.IsAny())) + .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 includePredicate = + (context, propertyName) => string.Equals(propertyName, "include1", StringComparison.OrdinalIgnoreCase) || + string.Equals(propertyName, "include2", StringComparison.OrdinalIgnoreCase); + + var binder = new Mock(); + var valueProvider = Mock.Of(); + binder.Setup(b => b.BindModelAsync(It.IsAny())) + .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(); + var valueProvider = Mock.Of(); + binder.Setup(b => b.BindModelAsync(It.IsAny())) + .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(); + var valueProvider = new Mock(); + binder.Setup(b => b.BindModelAsync(It.IsAny())) + .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(), new RouteData(), new ActionDescriptor()); + var bindingContext = new ActionBindingContext(actionContext, + metadataProvider, + binder, + provider ?? Mock.Of(), + Mock.Of(), + Mock.Of()); + var bindingContextProvider = new Mock(); + 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 diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ParameterBinding/ControllerActionArgumentBinderTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ParameterBinding/ControllerActionArgumentBinderTests.cs index a6bc2644d2..8c13fabdd0 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ParameterBinding/ControllerActionArgumentBinderTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ParameterBinding/ControllerActionArgumentBinderTests.cs @@ -70,8 +70,8 @@ namespace Microsoft.AspNet.Mvc.Core.Test modelMetadata, actionBindingContext, Mock.Of()); // 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()); // Assert - Assert.True(context.PropertyFilter("IncludedExplicitly1")); - Assert.True(context.PropertyFilter("IncludedExplicitly2")); + Assert.True(context.PropertyFilter(context, "IncludedExplicitly1")); + Assert.True(context.PropertyFilter(context, "IncludedExplicitly2")); } [Fact] diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ParameterBinding/ModelBindingHelperTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ParameterBinding/ModelBindingHelperTest.cs index 5bebb4b0ac..80b2e5c75c 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ParameterBinding/ModelBindingHelperTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ParameterBinding/ModelBindingHelperTest.cs @@ -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(); + metadataProvider.Setup(m => m.GetMetadataForType(null, It.IsAny())) + .Returns(new ModelMetadata(metadataProvider.Object, null, null, typeof(MyModel), null)) + .Verifiable(); + + var binder = new Mock(); + binder.Setup(b => b.BindModelAsync(It.IsAny())) + .Returns(Task.FromResult(false)); + var model = new MyModel(); + Func includePredicate = + (context, propertyName) => true; + // Act + var result = await ModelBindingHelper.TryUpdateModelAsync( + model, + null, + Mock.Of(), + new ModelStateDictionary(), + metadataProvider.Object, + GetCompositeBinder(binder.Object), + Mock.Of(), + Mock.Of(), + 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 + { + { "", null }, + { "MyProperty", "MyPropertyValue" }, + { "IncludedProperty", "IncludedPropertyValue" }, + { "ExcludedProperty", "ExcludedPropertyValue" } + }; + + Func includePredicate = + (context, propertyName) => + string.Equals(propertyName, "IncludedProperty", StringComparison.OrdinalIgnoreCase) || + string.Equals(propertyName, "MyProperty", StringComparison.OrdinalIgnoreCase); + + var valueProvider = new DictionaryBasedValueProvider(values); + + // Act + var result = await ModelBindingHelper.TryUpdateModelAsync( + model, + "", + Mock.Of(), + 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(); + metadataProvider.Setup(m => m.GetMetadataForType(null, It.IsAny())) + .Returns(new ModelMetadata(metadataProvider.Object, null, null, typeof(MyModel), null)) + .Verifiable(); + + var binder = new Mock(); + binder.Setup(b => b.BindModelAsync(It.IsAny())) + .Returns(Task.FromResult(false)); + var model = new MyModel(); + + // Act + var result = await ModelBindingHelper.TryUpdateModelAsync( + model, + null, + Mock.Of(), + new ModelStateDictionary(), + metadataProvider.Object, + GetCompositeBinder(binder.Object), + Mock.Of(), + Mock.Of(), + 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 + { + { "", null }, + { "MyProperty", "MyPropertyValue" }, + { "IncludedProperty", "IncludedPropertyValue" }, + { "ExcludedProperty", "ExcludedPropertyValue" } + }; + + var valueProvider = new DictionaryBasedValueProvider(values); + + // Act + var result = await ModelBindingHelper.TryUpdateModelAsync( + model, + "", + Mock.Of(), + 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 + { + { "", null }, + { "MyProperty", "MyPropertyValue" }, + { "IncludedProperty", "IncludedPropertyValue" }, + { "ExcludedProperty", "ExcludedPropertyValue" } + }; + + var valueProvider = new DictionaryBasedValueProvider(values); + + // Act + var result = await ModelBindingHelper.TryUpdateModelAsync( + model, + "", + Mock.Of(), + 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> 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> expression = m => m.Address.Street; + + // Act & Assert + var ex = Assert.Throws(() => + 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 InvalidExpressionDataSet + { + get + { + Expression> expression = m => new Func(() => 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> expression) + { + // Arrange Act & Assert + var ex = Assert.Throws(() => + 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> expression = m => someUser.Address; + + // Act & Assert + var ex = Assert.Throws(() => + 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, object>> expression = m => m[0]; + + // Act & Assert + var ex = Assert.Throws(() => + 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> expression = m => m._userId; + + // Act & Assert + var ex = Assert.Throws(() => + 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(); @@ -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 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 diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTests.cs index 422572019b..e1165a1842 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTests.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTests.cs @@ -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(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> expression = model => model.Address.Country; + + // Act + var ex = await Assert.ThrowsAsync(() => + 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(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(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(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(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(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(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); + } } } \ No newline at end of file diff --git a/test/WebSites/ModelBindingWebSite/Controllers/TryUpdateModelController.cs b/test/WebSites/ModelBindingWebSite/Controllers/TryUpdateModelController.cs new file mode 100644 index 0000000000..285bce79b4 --- /dev/null +++ b/test/WebSites/ModelBindingWebSite/Controllers/TryUpdateModelController.cs @@ -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 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 GetUserAsync_IncludeAllByDefault(int id) + { + var user = GetUser(id); + + await TryUpdateModelAsync(user, prefix: string.Empty); + return user; + } + + public async Task 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 CreateAndUpdateUser() + { + // don't update the id. + var user = new User(); + return await TryUpdateModelAsync(user, + prefix: string.Empty, + includeExpressions: model => model.RegisterationMonth); + } + + public async Task GetUserAsync_IncludeSpecificProperties(int id) + { + var user = GetUser(id); + await TryUpdateModelAsync(user, + prefix: string.Empty, + includeExpressions: model => model.RegisterationMonth); + + return user; + } + + public async Task TryUpdateModelFails(int id) + { + var user = GetUser(id); + return await TryUpdateModelAsync(user, + prefix: string.Empty, + valueProvider: new CustomValueProvider()); + } + + public async Task GetUserAsync_IncludeAndExcludeListNull(int id) + { + var user = GetUser(id); + await TryUpdateModelAsync(user); + + return user; + } + + public async Task GetUserAsync_IncludesAllSubProperties(int id) + { + var user = GetUser(id); + + await TryUpdateModelAsync(user, prefix: string.Empty, includeExpressions: model => model.Address); + + return user; + } + + public async Task 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 ContainsPrefixAsync(string prefix) + { + return Task.FromResult(false); + } + + public Task GetValueAsync(string key) + { + return Task.FromResult(null); + } + } + } +} \ No newline at end of file diff --git a/test/WebSites/ModelBindingWebSite/Models/Address.cs b/test/WebSites/ModelBindingWebSite/Models/Address.cs index f1917c5bfa..27e328c65d 100644 --- a/test/WebSites/ModelBindingWebSite/Models/Address.cs +++ b/test/WebSites/ModelBindingWebSite/Models/Address.cs @@ -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; } } } \ No newline at end of file diff --git a/test/WebSites/ModelBindingWebSite/Models/Country.cs b/test/WebSites/ModelBindingWebSite/Models/Country.cs new file mode 100644 index 0000000000..7463c54ed6 --- /dev/null +++ b/test/WebSites/ModelBindingWebSite/Models/Country.cs @@ -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; } + } +} \ No newline at end of file diff --git a/test/WebSites/ModelBindingWebSite/Models/User.cs b/test/WebSites/ModelBindingWebSite/Models/User.cs new file mode 100644 index 0000000000..e2e9280c64 --- /dev/null +++ b/test/WebSites/ModelBindingWebSite/Models/User.cs @@ -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; } + } +} \ No newline at end of file