Introducing Controller.TryUpdateModel

This changeset reintroduces some of the overloads for
Controller.TryUpdateModel.

Fixes #415
This commit is contained in:
Pranav K 2014-07-11 17:24:25 -07:00
parent da0bf6f7d8
commit 24f74222f5
6 changed files with 416 additions and 1 deletions

View File

@ -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());
}
}
/// <summary>
/// Updates the specified model instance using values from the controller's current value provider.
/// </summary>
/// <typeparam name="TModel">The type of the model object.</typeparam>
/// <param name="model">The model instance to update.</param>
/// <returns>true if the update is successful; otherwise, false.</returns>
[NonAction]
public virtual Task<bool> TryUpdateModelAsync<TModel>([NotNull] TModel model)
where TModel : class
{
return TryUpdateModelAsync(model, prefix: typeof(TModel).Name);
}
/// <summary>
/// Updates the specified model instance using values from the controller's current value provider
/// and a prefix.
/// </summary>
/// <typeparam name="TModel">The type of the model object.</typeparam>
/// <param name="model">The model instance to update.</param>
/// <param name="prefix">The prefix to use when looking up values in the value provider.</param>
/// <returns>true if the update is successful; otherwise, false.</returns>
[NonAction]
public virtual async Task<bool> TryUpdateModelAsync<TModel>([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);
}
/// <summary>
/// Updates the specified model instance using the value provider and a prefix.
/// </summary>
/// <typeparam name="TModel">The type of the model object.</typeparam>
/// <param name="model">The model instance to update.</param>
/// <param name="prefix">The prefix to use when looking up values in the value provider.</param>
/// <param name="valueProvider">The value provider used for looking up values.</param>
/// <returns>true if the update is successful; otherwise, false.</returns>
[NonAction]
public virtual async Task<bool> TryUpdateModelAsync<TModel>([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);
}
}
}

View File

@ -47,6 +47,7 @@
<Compile Include="Formatters\OutputFormatterContext.cs" />
<Compile Include="Formatters\JsonOutputFormatter.cs" />
<Compile Include="Formatters\OutputFormatter.cs" />
<Compile Include="ParameterBinding\ModelBindingHelper.cs" />
<Compile Include="ReflectedActionDescriptor.cs" />
<Compile Include="ReflectedActionDescriptorProvider.cs" />
<Compile Include="ReflectedModelBuilder\IReflectedApplicationModelConvention.cs" />

View File

@ -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
{
/// <summary>
/// Updates the specified model instance using the specified binder and value provider and
/// executes validation using the specified sequence of validator providers.
/// </summary>
/// <typeparam name="TModel">The type of the model object.</typeparam>
/// <param name="model">The model instance to update.</param>
/// <param name="prefix">The prefix to use when looking up values in the value provider.</param>
/// <param name="httpContext">The context for the current executing request.</param>
/// <param name="modelState">The ModelStateDictionary used for maintaining state and
/// results of model-binding validation.</param>
/// <param name="metadataProvider">The provider used for reading metadata for the model type.</param>
/// <param name="modelBinder">The model binder used for binding.</param>
/// <param name="valueProvider">The value provider used for looking up values.</param>
/// <param name="validatorProviders">The validator providers used for executing validation
/// on the model instance.</param>
/// <returns>A Task with a value representing if the the update is successful.</returns>
public static async Task<bool> TryUpdateModelAsync<TModel>(
[NotNull] TModel model,
[NotNull] string prefix,
[NotNull] HttpContext httpContext,
[NotNull] ModelStateDictionary modelState,
[NotNull] IModelMetadataProvider metadataProvider,
[NotNull] IModelBinder modelBinder,
[NotNull] IValueProvider valueProvider,
[NotNull] IEnumerable<IModelValidatorProvider> 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;
}
}
}

View File

@ -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<Controller>());
}
[Fact]
public async Task TryUpdateModel_UsesModelTypeNameIfNotSpecified()
{
var metadataProvider = new DataAnnotationsModelMetadataProvider();
var valueProvider = Mock.Of<IValueProvider>();
var binder = new Mock<IModelBinder>();
binder.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Callback((ModelBindingContext b) =>
{
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<HttpContext>(), new RouteData(), new ActionDescriptor());
var bindingContext = new ActionBindingContext(actionContext,
metadataProvider,
binder.Object,
valueProvider,
Mock.Of<IInputFormatterProvider>(),
Enumerable.Empty<IModelValidatorProvider>());
var bindingContextProvider = new Mock<IActionBindingContextProvider>();
bindingContextProvider.Setup(b => b.GetActionBindingContextAsync(actionContext))
.Returns(Task.FromResult(bindingContext));
var viewData = new ViewDataDictionary(metadataProvider, new ModelStateDictionary());
var controller = new Controller
{
ActionContext = actionContext,
BindingContextProvider = bindingContextProvider.Object,
ViewData = viewData
};
// Act
var result = await controller.TryUpdateModelAsync(model);
// Assert
binder.Verify();
}
[Fact]
public async Task TryUpdateModel_UsesModelTypeNameIfSpecified()
{
var metadataProvider = new DataAnnotationsModelMetadataProvider();
var valueProvider = Mock.Of<IValueProvider>();
var binder = new Mock<IModelBinder>();
var modelName = "mymodel";
binder.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Callback((ModelBindingContext b) =>
{
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<HttpContext>(), new RouteData(), new ActionDescriptor());
var bindingContext = new ActionBindingContext(actionContext,
metadataProvider,
binder.Object,
valueProvider,
Mock.Of<IInputFormatterProvider>(),
Enumerable.Empty<IModelValidatorProvider>());
var bindingContextProvider = new Mock<IActionBindingContextProvider>();
bindingContextProvider.Setup(b => b.GetActionBindingContextAsync(actionContext))
.Returns(Task.FromResult(bindingContext));
var viewData = new ViewDataDictionary(metadataProvider, new ModelStateDictionary());
var controller = new Controller
{
ActionContext = actionContext,
BindingContextProvider = bindingContextProvider.Object,
ViewData = viewData
};
// Act
var result = await controller.TryUpdateModelAsync(model, modelName);
// Assert
binder.Verify();
}
[Fact]
public async Task TryUpdateModel_UsesModelValueProviderIfSpecified()
{
var metadataProvider = new DataAnnotationsModelMetadataProvider();
var valueProvider = Mock.Of<IValueProvider>();
var binder = new Mock<IModelBinder>();
var modelName = "mymodel";
binder.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Callback((ModelBindingContext b) =>
{
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<HttpContext>(), new RouteData(), new ActionDescriptor());
var bindingContext = new ActionBindingContext(actionContext,
metadataProvider,
binder.Object,
Mock.Of<IValueProvider>(),
Mock.Of<IInputFormatterProvider>(),
Enumerable.Empty<IModelValidatorProvider>());
var bindingContextProvider = new Mock<IActionBindingContextProvider>();
bindingContextProvider.Setup(b => b.GetActionBindingContextAsync(actionContext))
.Returns(Task.FromResult(bindingContext));
var viewData = new ViewDataDictionary(metadataProvider, new ModelStateDictionary());
var controller = new Controller
{
ActionContext = actionContext,
BindingContextProvider = bindingContextProvider.Object,
ViewData = viewData
};
// Act
var result = await controller.TryUpdateModelAsync(model, modelName, valueProvider);
// Assert
binder.Verify();
}
#endif
private class MyModel
{
public string Foo { get; set; }
}
}
}

View File

@ -44,6 +44,7 @@
<Compile Include="ExpiringFileInfoCacheTest.cs" />
<Compile Include="DefaultActionDiscoveryConventionsTests.cs" />
<Compile Include="Formatters\OutputFormatterTests.cs" />
<Compile Include="ParameterBinding\ModelBindingHelperTest.cs" />
<Compile Include="ReflectedModelBuilder\ReflectedParameterModelTests.cs" />
<Compile Include="ReflectedModelBuilder\ReflectedActionModelTests.cs" />
<Compile Include="ReflectedModelBuilder\ReflectedControllerModelTests.cs" />

View File

@ -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<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>(),
Enumerable.Empty<IModelValidatorProvider>());
// 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<string, object>
{
{ "", null }
};
var valueProvider = new DictionaryBasedValueProvider(values);
// Act
var result = await ModelBindingHelper.TryUpdateModelAsync(
model,
"",
Mock.Of<HttpContext>(),
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<string, object>
{
{ "", null },
{ "MyProperty", "MyPropertyValue" }
};
var valueProvider = new DictionaryBasedValueProvider(values);
// Act
var result = await ModelBindingHelper.TryUpdateModelAsync(
model,
"",
Mock.Of<HttpContext>(),
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<IModelBinderProvider>();
binderProvider.SetupGet(p => p.ModelBinders)
.Returns(binders);
return new CompositeModelBinder(binderProvider.Object);
}
private class MyModel
{
[Required]
public string MyProperty { get; set; }
}
}
}
#endif