Refactor Argument Binder

This change just rearranges some code in the argument binder with a mind
towards performance and clarity. We're removing a few Task<T>'s here as
well in certain cases, but not yet all of them. We additionally save a
dictionary in the case where you have bound properties.

Hopefully these changes break the code into more discrete and sensible
units without multiple levels of indirection without abstraction.
- Main 'driver' code
- BindModel
- ActivateProperty
This commit is contained in:
Ryan Nowak 2016-04-26 11:17:57 -07:00
parent 4d63ffa879
commit 128d74e2a0
9 changed files with 178 additions and 174 deletions

View File

@ -9,15 +9,20 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
/// <summary>
/// Provides a dictionary of action arguments.
/// </summary>
public interface IControllerActionArgumentBinder
public interface IControllerArgumentBinder
{
/// <summary>
/// Returns a dictionary of the parameter-argument name-value pairs,
/// Asyncronously binds a dictionary of the parameter-argument name-value pairs,
/// which can be used to invoke the action. Also binds properties explicitly marked properties on the
/// <paramref name="controller"/>.
/// </summary>
/// <param name="context">The <see cref="ControllerContext"/> associated with the current action.</param>
/// <param name="controllerContext">The <see cref="ControllerContext"/> associated with the current action.</param>
/// <param name="controller">The controller object which contains the action.</param>
Task<IDictionary<string, object>> BindActionArgumentsAsync(ControllerContext context, object controller);
/// <param name="arguments">The arguments dictionary.</param>
/// <returns>A <see cref="Task"/> which, when completed signals the completion of argument binding.</returns>
Task BindArgumentsAsync(
ControllerContext controllerContext,
object controller,
IDictionary<string, object> arguments);
}
}

View File

@ -172,7 +172,7 @@ namespace Microsoft.Extensions.DependencyInjection
ServiceDescriptor.Transient<IActionInvokerProvider, ControllerActionInvokerProvider>());
// These are stateless
services.TryAddSingleton<IControllerActionArgumentBinder, ControllerArgumentBinder>();
services.TryAddSingleton<IControllerArgumentBinder, ControllerArgumentBinder>();
services.TryAddSingleton<ControllerActionInvokerCache>();
services.TryAddEnumerable(
ServiceDescriptor.Singleton<IFilterProvider, DefaultFilterProvider>());

View File

@ -21,14 +21,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{
private readonly ControllerActionDescriptor _descriptor;
private readonly IControllerFactory _controllerFactory;
private readonly IControllerActionArgumentBinder _argumentBinder;
private readonly IControllerArgumentBinder _argumentBinder;
public ControllerActionInvoker(
ActionContext actionContext,
ControllerActionInvokerCache controllerActionInvokerCache,
IControllerFactory controllerFactory,
ControllerActionDescriptor descriptor,
IControllerActionArgumentBinder argumentBinder,
IControllerArgumentBinder argumentBinder,
IReadOnlyList<IValueProviderFactory> valueProviderFactories,
ILogger logger,
DiagnosticSource diagnosticSource,
@ -111,9 +111,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal
return actionResult;
}
protected override Task<IDictionary<string, object>> BindActionArgumentsAsync()
protected override Task BindActionArgumentsAsync(IDictionary<string, object> arguments)
{
return _argumentBinder.BindActionArgumentsAsync(Context, Instance);
if (arguments == null)
{
throw new ArgumentNullException(nameof(arguments));
}
return _argumentBinder.BindArgumentsAsync(Context, Instance, arguments);
}
// Marking as internal for Unit Testing purposes.

View File

@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{
public class ControllerActionInvokerProvider : IActionInvokerProvider
{
private readonly IControllerActionArgumentBinder _argumentBinder;
private readonly IControllerArgumentBinder _argumentBinder;
private readonly IControllerFactory _controllerFactory;
private readonly ControllerActionInvokerCache _controllerActionInvokerCache;
private readonly IReadOnlyList<IValueProviderFactory> _valueProviderFactories;
@ -26,7 +26,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
public ControllerActionInvokerProvider(
IControllerFactory controllerFactory,
ControllerActionInvokerCache controllerActionInvokerCache,
IControllerActionArgumentBinder argumentBinder,
IControllerArgumentBinder argumentBinder,
IOptions<MvcOptions> optionsAccessor,
ILoggerFactory loggerFactory,
DiagnosticSource diagnosticSource)

View File

@ -15,10 +15,10 @@ using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Mvc.Internal
{
/// <summary>
/// Provides a default implementation of <see cref="IControllerActionArgumentBinder"/>.
/// Provides a default implementation of <see cref="IControllerArgumentBinder"/>.
/// Uses ModelBinding to populate action parameters.
/// </summary>
public class ControllerArgumentBinder : IControllerActionArgumentBinder
public class ControllerArgumentBinder : IControllerArgumentBinder
{
private static readonly MethodInfo CallPropertyAddRangeOpenGenericMethod =
typeof(ControllerArgumentBinder).GetTypeInfo().GetDeclaredMethod(
@ -38,9 +38,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
_validator = validator;
}
public Task<IDictionary<string, object>> BindActionArgumentsAsync(
public Task BindArgumentsAsync(
ControllerContext controllerContext,
object controller)
object controller,
IDictionary<string, object> arguments)
{
if (controllerContext == null)
{
@ -65,53 +66,54 @@ namespace Microsoft.AspNetCore.Mvc.Internal
if (actionDescriptor.BoundProperties.Count == 0 &&
actionDescriptor.Parameters.Count == 0)
{
return Task.FromResult<IDictionary<string, object>>(
new Dictionary<string, object>(StringComparer.Ordinal));
}
else if (actionDescriptor.BoundProperties.Count == 0)
{
return BindActionArgumentsCoreAsync(controllerContext, actionDescriptor);
}
else
{
return BindActionArgumentsAndPropertiesCoreAsync(
controllerContext,
controller,
actionDescriptor);
return TaskCache.CompletedTask;
}
return BindArgumentsCoreAsync(controllerContext, controller, arguments);
}
private async Task<IDictionary<string, object>> BindActionArgumentsCoreAsync(
ControllerContext controllerContext,
ControllerActionDescriptor actionDescriptor)
{
var valueProvider = await CompositeValueProvider.CreateAsync(controllerContext);
var actionArguments = await PopulateArgumentsAsync(
controllerContext,
actionDescriptor.Parameters,
valueProvider);
return actionArguments;
}
private async Task<IDictionary<string, object>> BindActionArgumentsAndPropertiesCoreAsync(
private async Task BindArgumentsCoreAsync(
ControllerContext controllerContext,
object controller,
ControllerActionDescriptor actionDescriptor)
IDictionary<string, object> arguments)
{
var valueProvider = await CompositeValueProvider.CreateAsync(controllerContext);
var controllerProperties = await PopulateArgumentsAsync(
controllerContext,
actionDescriptor.BoundProperties,
valueProvider);
ActivateProperties(actionDescriptor, controller, controllerProperties);
var parameters = controllerContext.ActionDescriptor.Parameters;
for (var i = 0; i < parameters.Count; i++)
{
var parameter = parameters[i];
var actionArguments = await PopulateArgumentsAsync(
controllerContext,
actionDescriptor.Parameters,
valueProvider);
return actionArguments;
var result = await BindModelAsync(parameter, controllerContext, valueProvider);
if (result != null && result.Value.IsModelSet)
{
arguments[parameter.Name] = result.Value.Model;
}
}
var properties = controllerContext.ActionDescriptor.BoundProperties;
if (properties.Count == 0)
{
// Perf: Early exit to avoid PropertyHelper lookup in the (common) case where we have no
// bound properties.
return;
}
var propertyHelpers = PropertyHelper.GetProperties(controller);
for (var i = 0; i < properties.Count; i++)
{
var property = properties[i];
var result = await BindModelAsync(property, controllerContext, valueProvider);
if (result != null && result.Value.IsModelSet)
{
var propertyHelper = FindPropertyHelper(propertyHelpers, property);
if (propertyHelper != null)
{
ActivateProperty(property, propertyHelper, controller, result.Value.Model);
}
}
}
}
public async Task<ModelBindingResult?> BindModelAsync(
@ -199,6 +201,52 @@ namespace Microsoft.AspNetCore.Mvc.Internal
return modelBindingResult;
}
private void ActivateProperty(
ParameterDescriptor property,
PropertyHelper propertyHelper,
object controller,
object value)
{
var propertyType = propertyHelper.Property.PropertyType;
var metadata = _modelMetadataProvider.GetMetadataForType(propertyType);
if (propertyHelper.Property.CanWrite && propertyHelper.Property.SetMethod?.IsPublic == true)
{
// Handle settable property. Do not set the property to null if the type is a non-nullable type.
if (value != null || metadata.IsReferenceOrNullableType)
{
propertyHelper.SetValue(controller, value);
}
return;
}
if (propertyType.IsArray)
{
// Do not attempt to copy values into an array because an array's length is immutable. This choice
// is also consistent with MutableObjectModelBinder's handling of a read-only array property.
return;
}
var target = propertyHelper.GetValue(controller);
if (value == null || target == null)
{
// Nothing to do when source or target is null.
return;
}
if (!metadata.IsCollectionType)
{
// Not a collection model.
return;
}
// Handle a read-only collection property.
var propertyAddRange = CallPropertyAddRangeOpenGenericMethod.MakeGenericMethod(
metadata.ElementMetadata.ModelType);
propertyAddRange.Invoke(obj: null, parameters: new[] { target, value });
}
// Called via reflection.
private static void CallPropertyAddRange<TElement>(object target, object source)
{
@ -214,92 +262,18 @@ namespace Microsoft.AspNetCore.Mvc.Internal
}
}
private void ActivateProperties(
ControllerActionDescriptor actionDescriptor,
object controller,
IDictionary<string, object> properties)
private static PropertyHelper FindPropertyHelper(PropertyHelper[] propertyHelpers, ParameterDescriptor property)
{
var propertyHelpers = PropertyHelper.GetProperties(controller);
for (var i = 0; i < actionDescriptor.BoundProperties.Count; i++)
for (var i = 0; i < propertyHelpers.Length; i++)
{
var property = actionDescriptor.BoundProperties[i];
PropertyHelper propertyHelper = null;
for (var j = 0; j < propertyHelpers.Length; j++)
var propertyHelper = propertyHelpers[i];
if (string.Equals(propertyHelper.Name, property.Name, StringComparison.Ordinal))
{
if (string.Equals(propertyHelpers[j].Name, property.Name, StringComparison.Ordinal))
{
propertyHelper = propertyHelpers[j];
break;
}
}
object value;
if (propertyHelper == null || !properties.TryGetValue(property.Name, out value))
{
continue;
}
var propertyType = propertyHelper.Property.PropertyType;
var metadata = _modelMetadataProvider.GetMetadataForType(propertyType);
if (propertyHelper.Property.CanWrite && propertyHelper.Property.SetMethod?.IsPublic == true)
{
// Handle settable property. Do not set the property to null if the type is a non-nullable type.
if (value != null || metadata.IsReferenceOrNullableType)
{
propertyHelper.SetValue(controller, value);
}
continue;
}
if (propertyType.IsArray)
{
// Do not attempt to copy values into an array because an array's length is immutable. This choice
// is also consistent with MutableObjectModelBinder's handling of a read-only array property.
continue;
}
var target = propertyHelper.GetValue(controller);
if (value == null || target == null)
{
// Nothing to do when source or target is null.
continue;
}
if (!metadata.IsCollectionType)
{
// Not a collection model.
continue;
}
// Handle a read-only collection property.
var propertyAddRange = CallPropertyAddRangeOpenGenericMethod.MakeGenericMethod(
metadata.ElementMetadata.ModelType);
propertyAddRange.Invoke(obj: null, parameters: new[] { target, value });
}
}
private async Task<IDictionary<string, object>> PopulateArgumentsAsync(
ControllerContext controllerContext,
IList<ParameterDescriptor> parameters,
IValueProvider valueProvider)
{
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
// Perf: Avoid allocations
for (var i = 0; i < parameters.Count; i++)
{
var parameter = parameters[i];
var modelBindingResult = await BindModelAsync(parameter, controllerContext, valueProvider);
if (modelBindingResult != null && modelBindingResult.Value.IsModelSet)
{
arguments[parameter.Name] = modelBindingResult.Value.Model;
return propertyHelper;
}
}
return arguments;
return null;
}
}
}

View File

@ -105,7 +105,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
protected abstract Task<IActionResult> InvokeActionAsync(ActionExecutingContext actionExecutingContext);
protected abstract Task<IDictionary<string, object>> BindActionArgumentsAsync();
protected abstract Task BindActionArgumentsAsync(IDictionary<string, object> arguments);
public virtual async Task InvokeAsync()
{
@ -456,13 +456,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
Instance = CreateInstance();
var arguments = await BindActionArgumentsAsync();
_actionExecutingContext = new ActionExecutingContext(
Context,
_filters,
arguments,
Instance);
var arguments = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
await BindActionArgumentsAsync(arguments);
_actionExecutingContext = new ActionExecutingContext(Context, _filters, arguments, Instance);
await InvokeActionFilterAsync();
}

View File

@ -2037,10 +2037,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal
.Setup(fp => fp.OnProvidersExecuted(It.IsAny<FilterProviderContext>()))
.Verifiable();
var actionArgumentsBinder = new Mock<IControllerActionArgumentBinder>();
actionArgumentsBinder.Setup(
b => b.BindActionArgumentsAsync(It.IsAny<ControllerContext>(), It.IsAny<object>()))
.Returns(Task.FromResult<IDictionary<string, object>>(new Dictionary<string, object>()));
var argumentBinder = new Mock<IControllerArgumentBinder>();
argumentBinder
.Setup(b => b.BindArgumentsAsync(
It.IsAny<ControllerContext>(),
It.IsAny<object>(),
It.IsAny<IDictionary<string, object>>()))
.Returns(TaskCache.CompletedTask);
filterProvider
.SetupGet(fp => fp.Order)
@ -2051,7 +2054,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
new[] { filterProvider.Object },
new MockControllerFactory(this),
actionDescriptor,
actionArgumentsBinder.Object,
argumentBinder.Object,
new IValueProviderFactory[0],
new NullLoggerFactory().CreateLogger<ControllerActionInvoker>(),
new DiagnosticListener("Microsoft.AspNetCore"),
@ -2227,7 +2230,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
IFilterProvider[] filterProviders,
MockControllerFactory controllerFactory,
ControllerActionDescriptor descriptor,
IControllerActionArgumentBinder controllerActionArgumentBinder,
IControllerArgumentBinder argumentBinder,
IReadOnlyList<IValueProviderFactory> valueProviderFactories,
ILogger logger,
DiagnosticSource diagnosticSource,
@ -2237,7 +2240,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
CreateFilterCache(filterProviders),
controllerFactory,
descriptor,
controllerActionArgumentBinder,
argumentBinder,
valueProviderFactories,
logger,
diagnosticSource,

View File

@ -39,12 +39,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var argumentBinder = GetArgumentBinder(factory);
var controllerContext = GetControllerContext(actionDescriptor);
var controller = new TestController();
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
// Act
var result = await argumentBinder.BindActionArgumentsAsync(controllerContext, new TestController());
await argumentBinder.BindArgumentsAsync(controllerContext, controller, arguments);
// Assert
Assert.Empty(result);
Assert.Empty(arguments);
}
[Fact]
@ -68,12 +70,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var argumentBinder = GetArgumentBinder(factory);
var controllerContext = GetControllerContext(actionDescriptor);
var controller = new TestController();
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
// Act
var result = await argumentBinder.BindActionArgumentsAsync(controllerContext, new TestController());
await argumentBinder.BindArgumentsAsync(controllerContext, controller, arguments);
// Assert
Assert.Empty(result);
Assert.Empty(arguments);
}
[Fact]
@ -106,13 +110,15 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var argumentBinder = GetArgumentBinder(factory);
var controllerContext = GetControllerContext(actionDescriptor);
var controller = new TestController();
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
// Act
var result = await argumentBinder.BindActionArgumentsAsync(controllerContext, new TestController());
await argumentBinder.BindArgumentsAsync(controllerContext, controller, arguments);
// Assert
Assert.Equal(1, result.Count);
Assert.Equal(value, result["foo"]);
Assert.Equal(1, arguments.Count);
Assert.Equal(value, arguments["foo"]);
}
[Fact]
@ -140,9 +146,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
It.IsAny<object>()));
var argumentBinder = GetArgumentBinder(factory, mockValidator.Object);
var controller = new TestController();
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
// Act
var result = await argumentBinder.BindActionArgumentsAsync(controllerContext, new TestController());
await argumentBinder.BindArgumentsAsync(controllerContext, controller, arguments);
// Assert
mockValidator
@ -169,6 +177,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
});
var controllerContext = GetControllerContext(actionDescriptor);
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
var binder = new Mock<IModelBinder>();
binder
@ -184,10 +193,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
It.IsAny<object>()));
var factory = GetModelBinderFactory(binder.Object);
var controller = new TestController();
var argumentBinder = GetArgumentBinder(factory, mockValidator.Object);
// Act
var result = await argumentBinder.BindActionArgumentsAsync(controllerContext, new TestController());
await argumentBinder.BindArgumentsAsync(controllerContext, controller, arguments);
// Assert
mockValidator
@ -212,6 +222,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
});
var controllerContext = GetControllerContext(actionDescriptor);
var controller = new TestController();
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
var mockValidator = new Mock<IObjectModelValidator>(MockBehavior.Strict);
mockValidator
@ -225,7 +237,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var argumentBinder = GetArgumentBinder(factory, mockValidator.Object);
// Act
var result = await argumentBinder.BindActionArgumentsAsync(controllerContext, new TestController());
await argumentBinder.BindArgumentsAsync(controllerContext, controller, arguments);
// Assert
mockValidator
@ -251,6 +263,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
});
var controllerContext = GetControllerContext(actionDescriptor);
var controller = new TestController();
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
var binder = new Mock<IModelBinder>();
binder
@ -269,7 +283,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var argumentBinder = GetArgumentBinder(factory, mockValidator.Object);
// Act
var result = await argumentBinder.BindActionArgumentsAsync(controllerContext, new TestController());
await argumentBinder.BindArgumentsAsync(controllerContext, controller, arguments);
// Assert
mockValidator
@ -295,14 +309,15 @@ namespace Microsoft.AspNetCore.Mvc.Internal
});
var controllerContext = GetControllerContext(actionDescriptor);
var controller = new TestController();
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
var factory = GetModelBinderFactory("Hello");
var argumentBinder = GetArgumentBinder(factory);
var controller = new TestController();
// Act
var result = await argumentBinder.BindActionArgumentsAsync(controllerContext, controller);
await argumentBinder.BindArgumentsAsync(controllerContext, controller, arguments);
// Assert
Assert.Equal("Hello", controller.StringProperty);
@ -324,15 +339,15 @@ namespace Microsoft.AspNetCore.Mvc.Internal
});
var controllerContext = GetControllerContext(actionDescriptor);
var controller = new TestController();
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
var expected = new List<string> { "Hello", "World", "!!" };
var factory = GetModelBinderFactory(expected);
var argumentBinder = GetArgumentBinder(factory);
var controller = new TestController();
// Act
var result = await argumentBinder.BindActionArgumentsAsync(controllerContext, controller);
await argumentBinder.BindArgumentsAsync(controllerContext, controller, arguments);
// Assert
Assert.Equal(expected, controller.CollectionProperty);
@ -356,18 +371,19 @@ namespace Microsoft.AspNetCore.Mvc.Internal
});
var controllerContext = GetControllerContext(actionDescriptor);
var controller = new TestController();
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
var binder = new StubModelBinder(ModelBindingResult.Success(string.Empty, model: null));
var factory = GetModelBinderFactory(binder);
var argumentBinder = GetArgumentBinder(factory);
var controller = new TestController();
// Some non default value.
controller.NonNullableProperty = -1;
// Act
var result = await argumentBinder.BindActionArgumentsAsync(controllerContext, controller);
await argumentBinder.BindArgumentsAsync(controllerContext, controller, arguments);
// Assert
Assert.Equal(-1, controller.NonNullableProperty);
@ -387,18 +403,19 @@ namespace Microsoft.AspNetCore.Mvc.Internal
});
var controllerContext = GetControllerContext(actionDescriptor);
var controller = new TestController();
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
var binder = new StubModelBinder(ModelBindingResult.Success(key: string.Empty, model: null));
var factory = GetModelBinderFactory(binder);
var argumentBinder = GetArgumentBinder(factory);
var controller = new TestController();
// Some non default value.
controller.NullableProperty = -1;
// Act
var result = await argumentBinder.BindActionArgumentsAsync(controllerContext, controller);
await argumentBinder.BindArgumentsAsync(controllerContext, controller, arguments);
// Assert
Assert.Null(controller.NullableProperty);
@ -463,14 +480,15 @@ namespace Microsoft.AspNetCore.Mvc.Internal
});
var controllerContext = GetControllerContext(actionDescriptor);
var controller = new TestController();
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
var factory = GetModelBinderFactory(inputValue);
var argumentBinder = GetArgumentBinder(factory);
var controller = new TestController();
// Act
var result = await argumentBinder.BindActionArgumentsAsync(controllerContext, controller);
await argumentBinder.BindArgumentsAsync(controllerContext, controller, arguments);
// Assert
Assert.Equal(expectedValue, propertyAccessor(controller));
@ -520,6 +538,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
}
var controllerContext = GetControllerContext(actionDescriptor);
var controller = new TestController();
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
var binder = new StubModelBinder(bindingContext =>
{
@ -540,10 +560,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
controllerContext.ValueProviderFactories.Add(new SimpleValueProviderFactory());
var argumentBinder = GetArgumentBinder(factory);
var controller = new TestController();
// Act
var result = await argumentBinder.BindActionArgumentsAsync(controllerContext, controller);
await argumentBinder.BindArgumentsAsync(controllerContext, controller, arguments);
// Assert
Assert.Equal(new string[] { "goodbye" }, controller.ArrayProperty); // Skipped

View File

@ -99,10 +99,11 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
},
actionDescriptor: actionDescriptor);
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
var modelState = testContext.ModelState;
// Act
var arguments = await argumentBinder.BindActionArgumentsAsync(testContext, new TestController());
await argumentBinder.BindArgumentsAsync(testContext, new TestController(), arguments);
// Assert
Assert.False(modelState.IsValid);
@ -138,10 +139,11 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
},
actionDescriptor: actionDescriptor);
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
var modelState = testContext.ModelState;
// Act
var arguments = await argumentBinder.BindActionArgumentsAsync(testContext, new TestController());
await argumentBinder.BindArgumentsAsync(testContext, new TestController(), arguments);
// Assert
Assert.True(modelState.IsValid);