From 24f74222f5b6d03d2896fdbe72741f7d8a3504d5 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Fri, 11 Jul 2014 17:24:25 -0700 Subject: [PATCH] Introducing Controller.TryUpdateModel This changeset reintroduces some of the overloads for Controller.TryUpdateModel. Fixes #415 --- src/Microsoft.AspNet.Mvc.Core/Controller.cs | 80 ++++++++++ .../Microsoft.AspNet.Mvc.Core.kproj | 1 + .../ParameterBinding/ModelBindingHelper.cs | 66 +++++++++ .../ControllerTests.cs | 130 +++++++++++++++- .../Microsoft.AspNet.Mvc.Core.Test.kproj | 1 + .../ModelBindingHelperTest.cs | 139 ++++++++++++++++++ 6 files changed, 416 insertions(+), 1 deletion(-) create mode 100644 src/Microsoft.AspNet.Mvc.Core/ParameterBinding/ModelBindingHelper.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/ParameterBinding/ModelBindingHelperTest.cs diff --git a/src/Microsoft.AspNet.Mvc.Core/Controller.cs b/src/Microsoft.AspNet.Mvc.Core/Controller.cs index 2df59735a7..1c80049c47 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Controller.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Controller.cs @@ -20,6 +20,11 @@ namespace Microsoft.AspNet.Mvc { get { + if (ActionContext == null) + { + return null; + } + return ActionContext.HttpContext; } } @@ -28,6 +33,11 @@ namespace Microsoft.AspNet.Mvc { get { + if (ViewData == null) + { + return null; + } + return ViewData.ModelState; } } @@ -38,6 +48,9 @@ namespace Microsoft.AspNet.Mvc [Activate] public IUrlHelper Url { get; set; } + [Activate] + public IActionBindingContextProvider BindingContextProvider { get; set; } + public IPrincipal User { get @@ -438,5 +451,72 @@ namespace Microsoft.AspNet.Mvc OnActionExecuted(await next()); } } + + /// + /// Updates the specified model instance using values from the controller's current value provider. + /// + /// The type of the model object. + /// The model instance to update. + /// true if the update is successful; otherwise, false. + [NonAction] + public virtual Task TryUpdateModelAsync([NotNull] TModel model) + where TModel : class + { + return TryUpdateModelAsync(model, prefix: typeof(TModel).Name); + } + + /// + /// Updates the specified model instance using values from the controller's current value provider + /// and a prefix. + /// + /// 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. + [NonAction] + public virtual async Task TryUpdateModelAsync([NotNull] TModel model, + [NotNull] string prefix) + where TModel : class + { + if (BindingContextProvider == null) + { + var message = Resources.FormatPropertyOfTypeCannotBeNull("BindingContextProvider", GetType().FullName); + throw new InvalidOperationException(message); + } + + var bindingContext = await BindingContextProvider.GetActionBindingContextAsync(ActionContext); + return await TryUpdateModelAsync(model, prefix, bindingContext.ValueProvider); + } + + /// + /// Updates the specified model instance using the value provider and a prefix. + /// + /// 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. + [NonAction] + public virtual async Task TryUpdateModelAsync([NotNull] TModel model, + [NotNull] string prefix, + [NotNull] IValueProvider valueProvider) + where TModel : class + { + if (BindingContextProvider == null) + { + var message = Resources.FormatPropertyOfTypeCannotBeNull("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.ValidatorProviders); + } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj b/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj index 5b4e537161..ee88bb41c8 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj +++ b/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj @@ -47,6 +47,7 @@ + diff --git a/src/Microsoft.AspNet.Mvc.Core/ParameterBinding/ModelBindingHelper.cs b/src/Microsoft.AspNet.Mvc.Core/ParameterBinding/ModelBindingHelper.cs new file mode 100644 index 0000000000..67846420d4 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ParameterBinding/ModelBindingHelper.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc.ModelBinding; + +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. + /// + /// 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 + /// 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 providers 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( + [NotNull] TModel model, + [NotNull] string prefix, + [NotNull] HttpContext httpContext, + [NotNull] ModelStateDictionary modelState, + [NotNull] IModelMetadataProvider metadataProvider, + [NotNull] IModelBinder modelBinder, + [NotNull] IValueProvider valueProvider, + [NotNull] IEnumerable validatorProviders) + where TModel : class + { + var modelMetadata = metadataProvider.GetMetadataForType( + modelAccessor: null, + modelType: typeof(TModel)); + + var modelBindingContext = new ModelBindingContext + { + ModelMetadata = modelMetadata, + ModelName = prefix, + Model = model, + ModelState = modelState, + ModelBinder = modelBinder, + ValueProvider = valueProvider, + ValidatorProviders = validatorProviders, + MetadataProvider = metadataProvider, + FallbackToEmptyPrefix = true, + HttpContext = httpContext + }; + + if (await modelBinder.BindModelAsync(modelBindingContext)) + { + return modelState.IsValid; + } + + return false; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ControllerTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ControllerTests.cs index c7c24f2a64..bdf3bbb3c5 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ControllerTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ControllerTests.cs @@ -13,6 +13,7 @@ using Microsoft.AspNet.Testing; using Moq; #endif using Xunit; +using Microsoft.AspNet.Http; namespace Microsoft.AspNet.Mvc.Test { @@ -25,7 +26,7 @@ namespace Microsoft.AspNet.Mvc.Test return typeof(Controller).GetTypeInfo() .DeclaredMethods .Where(method => method.IsPublic && !method.IsSpecialName) - .Select(method => new [] { method }); + .Select(method => new[] { method }); } } @@ -541,6 +542,133 @@ namespace Microsoft.AspNet.Mvc.Test await ActionFilterAttributeTests.ActionFilter_Calls_OnActionExecuted( new Mock()); } + + [Fact] + public async Task TryUpdateModel_UsesModelTypeNameIfNotSpecified() + { + var metadataProvider = new DataAnnotationsModelMetadataProvider(); + var valueProvider = Mock.Of(); + var binder = new Mock(); + binder.Setup(b => b.BindModelAsync(It.IsAny())) + .Callback((ModelBindingContext b) => + { + Assert.Equal(typeof(MyModel).Name, b.ModelName); + Assert.Same(valueProvider, b.ValueProvider); + }) + .Returns(Task.FromResult(false)) + .Verifiable(); + 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(), + Enumerable.Empty()); + 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); + + // Assert + binder.Verify(); + } + + [Fact] + public async Task TryUpdateModel_UsesModelTypeNameIfSpecified() + { + 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) => + { + Assert.Equal(modelName, b.ModelName); + Assert.Same(valueProvider, b.ValueProvider); + }) + .Returns(Task.FromResult(false)) + .Verifiable(); + 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(), + Enumerable.Empty()); + 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); + + // Assert + binder.Verify(); + } + + [Fact] + public async Task TryUpdateModel_UsesModelValueProviderIfSpecified() + { + 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) => + { + Assert.Equal(modelName, b.ModelName); + Assert.Same(valueProvider, b.ValueProvider); + }) + .Returns(Task.FromResult(false)) + .Verifiable(); + 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(), + Enumerable.Empty()); + 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); + + // Assert + binder.Verify(); + } #endif + + private class MyModel + { + public string Foo { get; set; } + } } } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj b/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj index 8e1dd889bb..557de45ae2 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj @@ -44,6 +44,7 @@ + diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ParameterBinding/ModelBindingHelperTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ParameterBinding/ModelBindingHelperTest.cs new file mode 100644 index 0000000000..b5c4815582 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ParameterBinding/ModelBindingHelperTest.cs @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#if NET45 +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc.ModelBinding; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Core.Test +{ + public class ModelBindingHelperTest + { + [Fact] + public async Task TryUpdateModel_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(), + Enumerable.Empty()); + + // Assert + Assert.False(result); + Assert.Null(model.MyProperty); + metadataProvider.Verify(); + } + + [Fact] + public async Task TryUpdateModel_ReturnsFalse_IfModelValidationFails() + { + // Arrange + var binders = new IModelBinder[] + { + new TypeConverterModelBinder(), + new ComplexModelDtoModelBinder(), + new MutableObjectModelBinder() + }; + + var validator = new DataAnnotationsModelValidatorProvider(); + var model = new MyModel(); + var modelStateDictionary = new ModelStateDictionary(); + var values = new Dictionary + { + { "", null } + }; + var valueProvider = new DictionaryBasedValueProvider(values); + + // Act + var result = await ModelBindingHelper.TryUpdateModelAsync( + model, + "", + Mock.Of(), + modelStateDictionary, + new DataAnnotationsModelMetadataProvider(), + GetCompositeBinder(binders), + valueProvider, + new[] { validator }); + + // Assert + Assert.False(result); + Assert.Equal("The MyProperty field is required.", + modelStateDictionary["MyProperty"].Errors[0].ErrorMessage); + } + + [Fact] + public async Task TryUpdateModel_ReturnsTrue_IfModelBindsAndValidatesSuccessfully() + { + // Arrange + var binders = new IModelBinder[] + { + new TypeConverterModelBinder(), + new ComplexModelDtoModelBinder(), + new MutableObjectModelBinder() + }; + + var validator = new DataAnnotationsModelValidatorProvider(); + var model = new MyModel { MyProperty = "Old-Value" }; + var modelStateDictionary = new ModelStateDictionary(); + var values = new Dictionary + { + { "", null }, + { "MyProperty", "MyPropertyValue" } + }; + var valueProvider = new DictionaryBasedValueProvider(values); + + // Act + var result = await ModelBindingHelper.TryUpdateModelAsync( + model, + "", + Mock.Of(), + modelStateDictionary, + new DataAnnotationsModelMetadataProvider(), + GetCompositeBinder(binders), + valueProvider, + new[] { validator }); + + // Assert + Assert.True(result); + Assert.Equal("MyPropertyValue", model.MyProperty); + } + + private static IModelBinder GetCompositeBinder(params IModelBinder[] binders) + { + var binderProvider = new Mock(); + binderProvider.SetupGet(p => p.ModelBinders) + .Returns(binders); + return new CompositeModelBinder(binderProvider.Object); + } + + private class MyModel + { + [Required] + public string MyProperty { get; set; } + } + } +} +#endif \ No newline at end of file