Use default values when binding action arguments

Fixes #545
This commit is contained in:
Pranav K 2014-05-22 10:34:16 -07:00
parent 1b7d52c84c
commit 822d84a2b4
3 changed files with 212 additions and 26 deletions

View File

@ -261,7 +261,7 @@ namespace Microsoft.AspNet.Mvc
await InvokeActionMethodFilter(); await InvokeActionMethodFilter();
} }
private async Task<IDictionary<string, object>> GetActionArguments(ModelStateDictionary modelState) internal async Task<IDictionary<string, object>> GetActionArguments(ModelStateDictionary modelState)
{ {
var actionBindingContext = await _bindingProvider.GetActionBindingContextAsync(_actionContext); var actionBindingContext = await _bindingProvider.GetActionBindingContextAsync(_actionContext);
var parameters = _descriptor.Parameters; var parameters = _descriptor.Parameters;
@ -304,8 +304,10 @@ namespace Microsoft.AspNet.Mvc
HttpContext = actionBindingContext.ActionContext.HttpContext, HttpContext = actionBindingContext.ActionContext.HttpContext,
FallbackToEmptyPrefix = true FallbackToEmptyPrefix = true
}; };
await actionBindingContext.ModelBinder.BindModelAsync(modelBindingContext); if (await actionBindingContext.ModelBinder.BindModelAsync(modelBindingContext))
parameterValues[parameter.Name] = modelBindingContext.Model; {
parameterValues[parameter.Name] = modelBindingContext.Model;
}
} }
} }

View File

@ -5,8 +5,10 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
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.Routing; using Microsoft.AspNet.Routing;
using Microsoft.AspNet.Testing; using Microsoft.AspNet.Testing;
using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.DependencyInjection;
@ -367,11 +369,11 @@ namespace Microsoft.AspNet.Mvc
var actionFilter = new Mock<IActionFilter>(MockBehavior.Strict); var actionFilter = new Mock<IActionFilter>(MockBehavior.Strict);
var resultFilter = new Mock<IResultFilter>(MockBehavior.Strict); var resultFilter = new Mock<IResultFilter>(MockBehavior.Strict);
var invoker = CreateInvoker(new IFilter[] var invoker = CreateInvoker(new IFilter[]
{ {
exceptionFilter.Object, exceptionFilter.Object,
authorizationFilter1.Object, authorizationFilter1.Object,
authorizationFilter2.Object, authorizationFilter2.Object,
actionFilter.Object, actionFilter.Object,
resultFilter.Object, resultFilter.Object,
}); });
@ -653,7 +655,7 @@ namespace Microsoft.AspNet.Mvc
// Act & Assert // Act & Assert
await ExceptionAssert.ThrowsAsync<InvalidOperationException>( await ExceptionAssert.ThrowsAsync<InvalidOperationException>(
async () => await invoker.InvokeActionAsync(), async () => await invoker.InvokeActionAsync(),
message); message);
} }
@ -667,7 +669,7 @@ namespace Microsoft.AspNet.Mvc
filter.Setup(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>())).Verifiable(); filter.Setup(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>())).Verifiable();
filter filter
.Setup(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>())) .Setup(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()))
.Callback<ActionExecutedContext>(c => .Callback<ActionExecutedContext>(c =>
{ {
context = c; context = c;
@ -743,7 +745,7 @@ namespace Microsoft.AspNet.Mvc
var filter1 = new Mock<IAsyncActionFilter>(MockBehavior.Strict); var filter1 = new Mock<IAsyncActionFilter>(MockBehavior.Strict);
filter1 filter1
.Setup(f => f.OnActionExecutionAsync(It.IsAny<ActionExecutingContext>(), It.IsAny<ActionExecutionDelegate>())) .Setup(f => f.OnActionExecutionAsync(It.IsAny<ActionExecutingContext>(), It.IsAny<ActionExecutionDelegate>()))
.Returns<ActionExecutingContext, ActionExecutionDelegate>(async (c, next) => .Returns<ActionExecutingContext, ActionExecutionDelegate>(async (c, next) =>
{ {
context = await next(); context = await next();
@ -767,7 +769,7 @@ namespace Microsoft.AspNet.Mvc
// Assert // Assert
filter1.Verify( filter1.Verify(
f => f.OnActionExecutionAsync(It.IsAny<ActionExecutingContext>(), It.IsAny<ActionExecutionDelegate>()), f => f.OnActionExecutionAsync(It.IsAny<ActionExecutingContext>(), It.IsAny<ActionExecutionDelegate>()),
Times.Once()); Times.Once());
filter2.Verify(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>()), Times.Once()); filter2.Verify(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>()), Times.Once());
@ -857,7 +859,7 @@ namespace Microsoft.AspNet.Mvc
f => f.OnResultExecutionAsync(It.IsAny<ResultExecutingContext>(), It.IsAny<ResultExecutionDelegate>()), f => f.OnResultExecutionAsync(It.IsAny<ResultExecutingContext>(), It.IsAny<ResultExecutionDelegate>()),
Times.Once()); Times.Once());
} }
[Fact] [Fact]
public async Task InvokeAction_InvokesResultFilter_ShortCircuit() public async Task InvokeAction_InvokesResultFilter_ShortCircuit()
{ {
@ -870,7 +872,7 @@ namespace Microsoft.AspNet.Mvc
.Setup(f => f.OnResultExecuted(It.IsAny<ResultExecutedContext>())) .Setup(f => f.OnResultExecuted(It.IsAny<ResultExecutedContext>()))
.Callback<ResultExecutedContext>(c => context = c) .Callback<ResultExecutedContext>(c => context = c)
.Verifiable(); .Verifiable();
var filter2 = new Mock<IResultFilter>(MockBehavior.Strict); var filter2 = new Mock<IResultFilter>(MockBehavior.Strict);
filter2 filter2
.Setup(f => f.OnResultExecuting(It.IsAny<ResultExecutingContext>())) .Setup(f => f.OnResultExecuting(It.IsAny<ResultExecutingContext>()))
@ -909,11 +911,11 @@ namespace Microsoft.AspNet.Mvc
var filter2 = new Mock<IAsyncResultFilter>(MockBehavior.Strict); var filter2 = new Mock<IAsyncResultFilter>(MockBehavior.Strict);
filter2 filter2
.Setup(f => f.OnResultExecutionAsync(It.IsAny<ResultExecutingContext>(), It.IsAny<ResultExecutionDelegate>())) .Setup(f => f.OnResultExecutionAsync(It.IsAny<ResultExecutingContext>(), It.IsAny<ResultExecutionDelegate>()))
.Returns<ResultExecutingContext, ResultExecutionDelegate>((c, next) => .Returns<ResultExecutingContext, ResultExecutionDelegate>((c, next) =>
{ {
// Not calling next here // Not calling next here
c.Cancel = true; c.Cancel = true;
return Task.FromResult<object>(null); return Task.FromResult<object>(null);
}) })
.Verifiable(); .Verifiable();
@ -993,7 +995,7 @@ namespace Microsoft.AspNet.Mvc
}) })
.Verifiable(); .Verifiable();
var message = var message =
"If an IAsyncResultFilter cancels execution by setting the Cancel property of " + "If an IAsyncResultFilter cancels execution by setting the Cancel property of " +
"ResultExecutingContext to 'true', then it cannot call the next filter by invoking " + "ResultExecutingContext to 'true', then it cannot call the next filter by invoking " +
"ResultExecutionDelegate."; "ResultExecutionDelegate.";
@ -1033,7 +1035,7 @@ namespace Microsoft.AspNet.Mvc
// Assert // Assert
result.Verify(r => r.ExecuteResultAsync(It.IsAny<ActionContext>()), Times.Once()); result.Verify(r => r.ExecuteResultAsync(It.IsAny<ActionContext>()), Times.Once());
filter.Verify(f => f.OnResultExecuting(It.IsAny<ResultExecutingContext>()), Times.Once()); filter.Verify(f => f.OnResultExecuting(It.IsAny<ResultExecutingContext>()), Times.Once());
filter.Verify(f => f.OnResultExecuted(It.IsAny<ResultExecutedContext>()), Times.Once()); filter.Verify(f => f.OnResultExecuted(It.IsAny<ResultExecutedContext>()), Times.Once());
} }
@ -1059,8 +1061,8 @@ namespace Microsoft.AspNet.Mvc
filter filter
.Setup(f => f.OnResultExecuted(It.IsAny<ResultExecutedContext>())) .Setup(f => f.OnResultExecuted(It.IsAny<ResultExecutedContext>()))
.Callback<ResultExecutedContext>(c => .Callback<ResultExecutedContext>(c =>
{ {
context = c; context = c;
// Handle the exception // Handle the exception
@ -1078,7 +1080,7 @@ namespace Microsoft.AspNet.Mvc
Assert.Equal(exception, context.Exception); Assert.Equal(exception, context.Exception);
result.Verify(r => r.ExecuteResultAsync(It.IsAny<ActionContext>()), Times.Once()); result.Verify(r => r.ExecuteResultAsync(It.IsAny<ActionContext>()), Times.Once());
filter.Verify(f => f.OnResultExecuting(It.IsAny<ResultExecutingContext>()), Times.Once()); filter.Verify(f => f.OnResultExecuting(It.IsAny<ResultExecutingContext>()), Times.Once());
filter.Verify(f => f.OnResultExecuted(It.IsAny<ResultExecutedContext>()), Times.Once()); filter.Verify(f => f.OnResultExecuted(It.IsAny<ResultExecutedContext>()), Times.Once());
} }
@ -1099,7 +1101,7 @@ namespace Microsoft.AspNet.Mvc
var filter = new Mock<IAsyncResultFilter>(MockBehavior.Strict); var filter = new Mock<IAsyncResultFilter>(MockBehavior.Strict);
filter filter
.Setup(f => f.OnResultExecutionAsync(It.IsAny<ResultExecutingContext>(), It.IsAny<ResultExecutionDelegate>())) .Setup(f => f.OnResultExecutionAsync(It.IsAny<ResultExecutingContext>(), It.IsAny<ResultExecutionDelegate>()))
.Returns<ResultExecutingContext, ResultExecutionDelegate>(async (c, next) => .Returns<ResultExecutingContext, ResultExecutionDelegate>(async (c, next) =>
{ {
c.Result = result.Object; c.Result = result.Object;
@ -1122,7 +1124,7 @@ namespace Microsoft.AspNet.Mvc
result.Verify(r => r.ExecuteResultAsync(It.IsAny<ActionContext>()), Times.Once()); result.Verify(r => r.ExecuteResultAsync(It.IsAny<ActionContext>()), Times.Once());
filter.Verify( filter.Verify(
f => f.OnResultExecutionAsync(It.IsAny<ResultExecutingContext>(), It.IsAny<ResultExecutionDelegate>()), f => f.OnResultExecutionAsync(It.IsAny<ResultExecutingContext>(), It.IsAny<ResultExecutionDelegate>()),
Times.Once()); Times.Once());
} }
@ -1206,7 +1208,7 @@ namespace Microsoft.AspNet.Mvc
Assert.Equal(exception, context.Exception); Assert.Equal(exception, context.Exception);
resultFilter1.Verify( resultFilter1.Verify(
f => f.OnResultExecutionAsync(It.IsAny<ResultExecutingContext>(), It.IsAny<ResultExecutionDelegate>()), f => f.OnResultExecutionAsync(It.IsAny<ResultExecutingContext>(), It.IsAny<ResultExecutionDelegate>()),
Times.Once()); Times.Once());
resultFilter2.Verify(f => f.OnResultExecuting(It.IsAny<ResultExecutingContext>()), Times.Once()); resultFilter2.Verify(f => f.OnResultExecuting(It.IsAny<ResultExecutingContext>()), Times.Once());
@ -1319,6 +1321,168 @@ namespace Microsoft.AspNet.Mvc
return invoker; return invoker;
} }
[Fact]
public async Task GetActionArguments_DoesNotAddActionArgumentsToModelStateDictionary_IfBinderReturnsFalse()
{
// Arrange
Func<object, int> method = x => 1;
var actionDescriptor = new ReflectedActionDescriptor
{
MethodInfo = method.Method,
Parameters = new List<ParameterDescriptor>
{
new ParameterDescriptor
{
Name = "foo",
ParameterBindingInfo = new ParameterBindingInfo("foo", typeof(object))
}
}
};
var binder = new Mock<IModelBinder>();
binder.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Returns(Task.FromResult(result: false));
var actionContext = new ActionContext(new RouteContext(Mock.Of<HttpContext>()),
actionDescriptor);
var bindingContext = new ActionBindingContext(actionContext,
Mock.Of<IModelMetadataProvider>(),
binder.Object,
Mock.Of<IValueProvider>(),
Mock.Of<IInputFormatterProvider>(),
Enumerable.Empty<IModelValidatorProvider>());
var actionBindingContextProvider = new Mock<IActionBindingContextProvider>();
actionBindingContextProvider.Setup(p => p.GetActionBindingContextAsync(It.IsAny<ActionContext>()))
.Returns(Task.FromResult(bindingContext));
var invoker = new ReflectedActionInvoker(actionContext,
actionDescriptor,
Mock.Of<IControllerFactory>(),
actionBindingContextProvider.Object,
Mock.Of<INestedProviderManager<FilterProviderContext>>());
var modelStateDictionary = new ModelStateDictionary();
// Act
var result = await invoker.GetActionArguments(modelStateDictionary);
// Assert
Assert.Empty(result);
}
[Fact]
public async Task GetActionArguments_AddsActionArgumentsToModelStateDictionary_IfBinderReturnsTrue()
{
// Arrange
Func<object, int> method = x => 1;
var actionDescriptor = new ReflectedActionDescriptor
{
MethodInfo = method.Method,
Parameters = new List<ParameterDescriptor>
{
new ParameterDescriptor
{
Name = "foo",
ParameterBindingInfo = new ParameterBindingInfo("foo", typeof(object))
}
}
};
var value = "Hello world";
var binder = new Mock<IModelBinder>();
var metadataProvider = new EmptyModelMetadataProvider();
binder.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Callback((ModelBindingContext context) =>
{
context.ModelMetadata = metadataProvider.GetMetadataForType(modelAccessor: null,
modelType: typeof(string));
context.Model = value;
})
.Returns(Task.FromResult(result: true));
var actionContext = new ActionContext(new RouteContext(Mock.Of<HttpContext>()),
actionDescriptor);
var bindingContext = new ActionBindingContext(actionContext,
Mock.Of<IModelMetadataProvider>(),
binder.Object,
Mock.Of<IValueProvider>(),
Mock.Of<IInputFormatterProvider>(),
Enumerable.Empty<IModelValidatorProvider>());
var actionBindingContextProvider = new Mock<IActionBindingContextProvider>();
actionBindingContextProvider.Setup(p => p.GetActionBindingContextAsync(It.IsAny<ActionContext>()))
.Returns(Task.FromResult(bindingContext));
var invoker = new ReflectedActionInvoker(actionContext,
actionDescriptor,
Mock.Of<IControllerFactory>(),
actionBindingContextProvider.Object,
Mock.Of<INestedProviderManager<FilterProviderContext>>());
var modelStateDictionary = new ModelStateDictionary();
// Act
var result = await invoker.GetActionArguments(modelStateDictionary);
// Assert
Assert.Equal(1, result.Count);
Assert.Equal(value, result["foo"]);
}
[Fact]
public async Task Invoke_UsesDefaultValuesIfNotBound()
{
// Arrange
var actionDescriptor = new ReflectedActionDescriptor
{
MethodInfo = typeof(TestController).GetTypeInfo()
.DeclaredMethods
.First(m => m.Name.Equals("ActionMethodWithDefaultValues", StringComparison.Ordinal)),
Parameters = new List<ParameterDescriptor>
{
new ParameterDescriptor
{
Name = "value",
ParameterBindingInfo = new ParameterBindingInfo("value", typeof(int))
}
},
FilterDescriptors = new List<FilterDescriptor>()
};
var binder = new Mock<IModelBinder>();
var metadataProvider = new EmptyModelMetadataProvider();
binder.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Returns(Task.FromResult(result: false));
var context = new Mock<HttpContext>();
context.SetupGet(c => c.Items)
.Returns(new Dictionary<object, object>());
var routeContext = new RouteContext(context.Object);
var actionContext = new ActionContext(routeContext,
actionDescriptor);
var bindingContext = new ActionBindingContext(actionContext,
Mock.Of<IModelMetadataProvider>(),
binder.Object,
Mock.Of<IValueProvider>(),
Mock.Of<IInputFormatterProvider>(),
Enumerable.Empty<IModelValidatorProvider>());
var actionBindingContextProvider = new Mock<IActionBindingContextProvider>();
actionBindingContextProvider.Setup(p => p.GetActionBindingContextAsync(It.IsAny<ActionContext>()))
.Returns(Task.FromResult(bindingContext));
var controllerFactory = new Mock<IControllerFactory>();
controllerFactory.Setup(c => c.CreateController(It.IsAny<ActionContext>()))
.Returns(new TestController());
var invoker = new ReflectedActionInvoker(actionContext,
actionDescriptor,
controllerFactory.Object,
actionBindingContextProvider.Object,
Mock.Of<INestedProviderManager<FilterProviderContext>>());
// Act
await invoker.InvokeActionAsync();
// Assert
Assert.Equal(5, context.Object.Items["Result"]);
}
public JsonResult ActionMethod() public JsonResult ActionMethod()
{ {
return _result; return _result;
@ -1328,5 +1492,24 @@ namespace Microsoft.AspNet.Mvc
{ {
throw _actionException; throw _actionException;
} }
private sealed class TestController
{
public IActionResult ActionMethodWithDefaultValues(int value = 5)
{
return new TestActionResult { Value = value };
}
}
private sealed class TestActionResult : IActionResult
{
public int Value { get; set; }
public Task ExecuteResultAsync(ActionContext context)
{
context.HttpContext.Items["Result"] = Value;
return Task.FromResult(0);
}
}
} }
} }

View File

@ -30,6 +30,7 @@
<ItemGroup> <ItemGroup>
<Compile Include="ActionDescriptorCreationCounter.cs" /> <Compile Include="ActionDescriptorCreationCounter.cs" />
<Compile Include="Controllers\HomeController.cs" /> <Compile Include="Controllers\HomeController.cs" />
<Compile Include="Controllers\MonitorController.cs" />
<Compile Include="Startup.cs" /> <Compile Include="Startup.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>