aspnetcore/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerArgumentBinder.cs

263 lines
10 KiB
C#

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Mvc.Internal
{
/// <summary>
/// Provides a default implementation of <see cref="IControllerActionArgumentBinder"/>.
/// Uses ModelBinding to populate action parameters.
/// </summary>
public class ControllerArgumentBinder : IControllerActionArgumentBinder
{
private static readonly MethodInfo CallPropertyAddRangeOpenGenericMethod =
typeof(ControllerArgumentBinder).GetTypeInfo().GetDeclaredMethod(
nameof(CallPropertyAddRange));
private readonly IModelBinderFactory _modelBinderFactory;
private readonly IModelMetadataProvider _modelMetadataProvider;
private readonly IObjectModelValidator _validator;
public ControllerArgumentBinder(
IModelMetadataProvider modelMetadataProvider,
IModelBinderFactory modelBinderFactory,
IObjectModelValidator validator)
{
_modelMetadataProvider = modelMetadataProvider;
_modelBinderFactory = modelBinderFactory;
_validator = validator;
}
public Task<IDictionary<string, object>> BindActionArgumentsAsync(
ControllerContext controllerContext,
object controller)
{
if (controllerContext == null)
{
throw new ArgumentNullException(nameof(controllerContext));
}
if (controller == null)
{
throw new ArgumentNullException(nameof(controller));
}
if (controllerContext.ActionDescriptor == null)
{
throw new ArgumentException(Resources.FormatPropertyOfTypeCannotBeNull(
nameof(ControllerContext.ActionDescriptor),
nameof(ControllerContext)));
}
// Perf: Avoid allocating async state machines where possible. We only need the state
// machine if you need to bind properties.
var actionDescriptor = controllerContext.ActionDescriptor;
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 PopulateArgumentsAsync(controllerContext, actionDescriptor.Parameters);
}
else
{
return BindActionArgumentsAndPropertiesCoreAsync(
controllerContext,
controller,
actionDescriptor);
}
}
private async Task<IDictionary<string, object>> BindActionArgumentsAndPropertiesCoreAsync(
ControllerContext controllerContext,
object controller,
ControllerActionDescriptor actionDescriptor)
{
var controllerProperties = await PopulateArgumentsAsync(
controllerContext,
actionDescriptor.BoundProperties);
ActivateProperties(actionDescriptor, controller, controllerProperties);
var actionArguments = await PopulateArgumentsAsync(
controllerContext,
actionDescriptor.Parameters);
return actionArguments;
}
public async Task<ModelBindingResult?> BindModelAsync(
ParameterDescriptor parameter,
ControllerContext controllerContext)
{
if (parameter == null)
{
throw new ArgumentNullException(nameof(parameter));
}
if (controllerContext == null)
{
throw new ArgumentNullException(nameof(controllerContext));
}
var metadata = _modelMetadataProvider.GetMetadataForType(parameter.ParameterType);
var modelBindingContext = DefaultModelBindingContext.CreateBindingContext(
controllerContext,
new CompositeValueProvider(controllerContext.ValueProviders),
metadata,
parameter.BindingInfo,
parameter.Name);
if (parameter.BindingInfo?.BinderModelName != null)
{
// The name was set explicitly, always use that as the prefix.
modelBindingContext.ModelName = parameter.BindingInfo.BinderModelName;
}
else if (modelBindingContext.ValueProvider.ContainsPrefix(parameter.Name))
{
// We have a match for the parameter name, use that as that prefix.
modelBindingContext.ModelName = parameter.Name;
}
else
{
// No match, fallback to empty string as the prefix.
modelBindingContext.ModelName = string.Empty;
}
var binder = _modelBinderFactory.CreateBinder(new ModelBinderFactoryContext()
{
BindingInfo = parameter.BindingInfo,
Metadata = metadata,
CacheToken = parameter,
});
await binder.BindModelAsync(modelBindingContext);
var modelBindingResult = modelBindingContext.Result;
if (modelBindingResult != null && modelBindingResult.Value.IsModelSet)
{
_validator.Validate(
controllerContext,
modelBindingContext.ValidationState,
modelBindingResult.Value.Key,
modelBindingResult.Value.Model);
}
return modelBindingResult;
}
// Called via reflection.
private static void CallPropertyAddRange<TElement>(object target, object source)
{
var targetCollection = (ICollection<TElement>)target;
var sourceCollection = source as IEnumerable<TElement>;
if (sourceCollection != null && !targetCollection.IsReadOnly)
{
targetCollection.Clear();
foreach (var item in sourceCollection)
{
targetCollection.Add(item);
}
}
}
private void ActivateProperties(
ControllerActionDescriptor actionDescriptor,
object controller,
IDictionary<string, object> properties)
{
var propertyHelpers = PropertyHelper.GetProperties(controller);
for (var i = 0; i < actionDescriptor.BoundProperties.Count; i++)
{
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);
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)
{
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);
if (modelBindingResult != null && modelBindingResult.Value.IsModelSet)
{
arguments[parameter.Name] = modelBindingResult.Value.Model;
}
}
return arguments;
}
}
}