Optimize allocations in argument binder

This change avoids a state machine allocation and a dictionary allocation
on the common case (no bound properties). Ugly? You bet. Worth it? Yeah,
seems worthwhile.

This is worth about 200 bytes/request - about 3% of allocated bytes in a
smallish API scenario.
This commit is contained in:
Ryan Nowak 2015-12-28 09:36:22 -08:00
parent 0a9804056e
commit d222900662
2 changed files with 48 additions and 25 deletions

View File

@ -56,7 +56,8 @@ namespace Microsoft.AspNet.Mvc.Controllers
nameof(ControllerContext)));
}
// Perf: Avoid allocating async state machines when we know there's nothing to bind.
// Perf: Avoid allocating async state machines where possible. We only need the state
// machine if you need to bind properties.
var actionDescriptor = context.ActionDescriptor;
if (actionDescriptor.BoundProperties.Count == 0 &&
actionDescriptor.Parameters.Count == 0)
@ -64,33 +65,34 @@ namespace Microsoft.AspNet.Mvc.Controllers
return Task.FromResult<IDictionary<string, object>>(
new Dictionary<string, object>(StringComparer.Ordinal));
}
else if (actionDescriptor.BoundProperties.Count == 0)
{
var operationBindingContext = GetOperationBindingContext(context);
return PopulateArgumentsAsync(operationBindingContext, actionDescriptor.Parameters);
}
else
{
return BindActionArgumentsCoreAsync(
return BindActionArgumentsAndPropertiesCoreAsync(
context,
controller,
actionDescriptor);
}
}
private async Task<IDictionary<string, object>> BindActionArgumentsCoreAsync(
private async Task<IDictionary<string, object>> BindActionArgumentsAndPropertiesCoreAsync(
ControllerContext context,
object controller,
ControllerActionDescriptor actionDescriptor)
{
var operationBindingContext = GetOperationBindingContext(context);
var controllerProperties = new Dictionary<string, object>(StringComparer.Ordinal);
await PopulateArgumentsAsync(
operationBindingContext,
controllerProperties,
actionDescriptor.BoundProperties);
var controllerType = actionDescriptor.ControllerTypeInfo.AsType();
ActivateProperties(controller, controllerType, controllerProperties);
var actionArguments = new Dictionary<string, object>(StringComparer.Ordinal);
await PopulateArgumentsAsync(
var controllerProperties = await PopulateArgumentsAsync(
operationBindingContext,
actionDescriptor.BoundProperties);
ActivateProperties(actionDescriptor, controller, controllerProperties);
var actionArguments = await PopulateArgumentsAsync(
operationBindingContext,
actionArguments,
actionDescriptor.Parameters);
return actionArguments;
}
@ -145,22 +147,41 @@ namespace Microsoft.AspNet.Mvc.Controllers
}
}
private void ActivateProperties(object controller, Type containerType, Dictionary<string, object> properties)
private void ActivateProperties(
ControllerActionDescriptor actionDescriptor,
object controller,
IDictionary<string, object> properties)
{
var propertyHelpers = PropertyHelper.GetProperties(controller);
foreach (var property in properties)
for (var i = 0; i < actionDescriptor.BoundProperties.Count; i++)
{
var propertyHelper = propertyHelpers.First(helper =>
string.Equals(helper.Name, property.Key, StringComparison.Ordinal));
var property = actionDescriptor.BoundProperties[i];
PropertyHelper propertyHelper = null;
for (var j = 0; j < propertyHelpers.Length; j++)
{
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);
var source = property.Value;
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 (source != null || metadata.IsReferenceOrNullableType)
if (value != null || metadata.IsReferenceOrNullableType)
{
propertyHelper.SetValue(controller, source);
propertyHelper.SetValue(controller, value);
}
continue;
@ -174,7 +195,7 @@ namespace Microsoft.AspNet.Mvc.Controllers
}
var target = propertyHelper.GetValue(controller);
if (source == null || target == null)
if (value == null || target == null)
{
// Nothing to do when source or target is null.
continue;
@ -189,15 +210,16 @@ namespace Microsoft.AspNet.Mvc.Controllers
// Handle a read-only collection property.
var propertyAddRange = CallPropertyAddRangeOpenGenericMethod.MakeGenericMethod(
metadata.ElementMetadata.ModelType);
propertyAddRange.Invoke(obj: null, parameters: new[] { target, source });
propertyAddRange.Invoke(obj: null, parameters: new[] { target, value });
}
}
private async Task PopulateArgumentsAsync(
private async Task<IDictionary<string, object>> PopulateArgumentsAsync(
OperationBindingContext operationContext,
IDictionary<string, object> arguments,
IList<ParameterDescriptor> parameterMetadata)
{
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
// Perf: Avoid allocations
for (var i = 0; i < parameterMetadata.Count; i++)
{
@ -208,6 +230,8 @@ namespace Microsoft.AspNet.Mvc.Controllers
arguments[parameter.Name] = modelBindingResult.Model;
}
}
return arguments;
}
private OperationBindingContext GetOperationBindingContext(ControllerContext context)

View File

@ -7,7 +7,6 @@ using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNet.Http.Internal;
using Microsoft.AspNet.Mvc.Abstractions;
using Microsoft.AspNet.Mvc.Formatters;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.ModelBinding.Test;
using Microsoft.AspNet.Mvc.ModelBinding.Validation;