334 lines
14 KiB
C#
334 lines
14 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.Threading.Tasks;
|
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
|
using Microsoft.AspNetCore.Mvc.Internal;
|
|
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|
{
|
|
/// <summary>
|
|
/// Binds and validates models specified by a <see cref="ParameterDescriptor"/>.
|
|
/// </summary>
|
|
public class ParameterBinder
|
|
{
|
|
private readonly IModelMetadataProvider _modelMetadataProvider;
|
|
private readonly IModelBinderFactory _modelBinderFactory;
|
|
private readonly MvcOptions _mvcOptions;
|
|
private readonly IObjectModelValidator _objectModelValidator;
|
|
|
|
/// <summary>
|
|
/// <para>This constructor is obsolete and will be removed in a future version. The recommended alternative
|
|
/// is the overload that also takes a <see cref="MvcOptions"/> accessor and an <see cref="ILoggerFactory"/>.
|
|
/// </para>
|
|
/// <para>Initializes a new instance of <see cref="ParameterBinder"/>.</para>
|
|
/// </summary>
|
|
/// <param name="modelMetadataProvider">The <see cref="IModelMetadataProvider"/>.</param>
|
|
/// <param name="modelBinderFactory">The <see cref="IModelBinderFactory"/>.</param>
|
|
/// <param name="validator">The <see cref="IObjectModelValidator"/>.</param>
|
|
[Obsolete("This constructor is obsolete and will be removed in a future version. The recommended alternative"
|
|
+ " is the overload that also takes a " + nameof(MvcOptions) + " accessor and an "
|
|
+ nameof(ILoggerFactory) + " .")]
|
|
public ParameterBinder(
|
|
IModelMetadataProvider modelMetadataProvider,
|
|
IModelBinderFactory modelBinderFactory,
|
|
IObjectModelValidator validator)
|
|
: this(
|
|
modelMetadataProvider,
|
|
modelBinderFactory,
|
|
validator,
|
|
Options.Create(new MvcOptions()),
|
|
NullLoggerFactory.Instance)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of <see cref="ParameterBinder"/>.
|
|
/// </summary>
|
|
/// <param name="modelMetadataProvider">The <see cref="IModelMetadataProvider"/>.</param>
|
|
/// <param name="modelBinderFactory">The <see cref="IModelBinderFactory"/>.</param>
|
|
/// <param name="validator">The <see cref="IObjectModelValidator"/>.</param>
|
|
/// <param name="mvcOptions">The <see cref="MvcOptions"/> accessor.</param>
|
|
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
|
|
public ParameterBinder(
|
|
IModelMetadataProvider modelMetadataProvider,
|
|
IModelBinderFactory modelBinderFactory,
|
|
IObjectModelValidator validator,
|
|
IOptions<MvcOptions> mvcOptions,
|
|
ILoggerFactory loggerFactory)
|
|
{
|
|
if (modelMetadataProvider == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(modelMetadataProvider));
|
|
}
|
|
|
|
if (modelBinderFactory == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(modelBinderFactory));
|
|
}
|
|
|
|
if (validator == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(validator));
|
|
}
|
|
|
|
if (mvcOptions == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(mvcOptions));
|
|
}
|
|
|
|
if (loggerFactory == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(loggerFactory));
|
|
}
|
|
|
|
_modelMetadataProvider = modelMetadataProvider;
|
|
_modelBinderFactory = modelBinderFactory;
|
|
_objectModelValidator = validator;
|
|
_mvcOptions = mvcOptions.Value;
|
|
Logger = loggerFactory.CreateLogger(GetType());
|
|
}
|
|
|
|
/// <summary>
|
|
/// The <see cref="ILogger"/> used for logging in this binder.
|
|
/// </summary>
|
|
protected ILogger Logger { get; }
|
|
|
|
/// <summary>
|
|
/// Initializes and binds a model specified by <paramref name="parameter"/>.
|
|
/// </summary>
|
|
/// <param name="actionContext">The <see cref="ActionContext"/>.</param>
|
|
/// <param name="valueProvider">The <see cref="IValueProvider"/>.</param>
|
|
/// <param name="parameter">The <see cref="ParameterDescriptor"/></param>
|
|
/// <returns>The result of model binding.</returns>
|
|
public Task<ModelBindingResult> BindModelAsync(
|
|
ActionContext actionContext,
|
|
IValueProvider valueProvider,
|
|
ParameterDescriptor parameter)
|
|
{
|
|
return BindModelAsync(actionContext, valueProvider, parameter, value: null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Binds a model specified by <paramref name="parameter"/> using <paramref name="value"/> as the initial value.
|
|
/// </summary>
|
|
/// <param name="actionContext">The <see cref="ActionContext"/>.</param>
|
|
/// <param name="valueProvider">The <see cref="IValueProvider"/>.</param>
|
|
/// <param name="parameter">The <see cref="ParameterDescriptor"/></param>
|
|
/// <param name="value">The initial model value.</param>
|
|
/// <returns>The result of model binding.</returns>
|
|
public virtual Task<ModelBindingResult> BindModelAsync(
|
|
ActionContext actionContext,
|
|
IValueProvider valueProvider,
|
|
ParameterDescriptor parameter,
|
|
object value)
|
|
{
|
|
if (actionContext == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(actionContext));
|
|
}
|
|
|
|
if (valueProvider == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(valueProvider));
|
|
}
|
|
|
|
if (parameter == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(parameter));
|
|
}
|
|
|
|
var metadata = _modelMetadataProvider.GetMetadataForType(parameter.ParameterType);
|
|
var binder = _modelBinderFactory.CreateBinder(new ModelBinderFactoryContext
|
|
{
|
|
BindingInfo = parameter.BindingInfo,
|
|
Metadata = metadata,
|
|
CacheToken = parameter,
|
|
});
|
|
|
|
return BindModelAsync(
|
|
actionContext,
|
|
binder,
|
|
valueProvider,
|
|
parameter,
|
|
metadata,
|
|
value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Binds a model specified by <paramref name="parameter"/> using <paramref name="value"/> as the initial value.
|
|
/// </summary>
|
|
/// <param name="actionContext">The <see cref="ActionContext"/>.</param>
|
|
/// <param name="modelBinder">The <see cref="IModelBinder"/>.</param>
|
|
/// <param name="valueProvider">The <see cref="IValueProvider"/>.</param>
|
|
/// <param name="parameter">The <see cref="ParameterDescriptor"/></param>
|
|
/// <param name="metadata">The <see cref="ModelMetadata"/>.</param>
|
|
/// <param name="value">The initial model value.</param>
|
|
/// <returns>The result of model binding.</returns>
|
|
public virtual async Task<ModelBindingResult> BindModelAsync(
|
|
ActionContext actionContext,
|
|
IModelBinder modelBinder,
|
|
IValueProvider valueProvider,
|
|
ParameterDescriptor parameter,
|
|
ModelMetadata metadata,
|
|
object value)
|
|
{
|
|
if (actionContext == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(actionContext));
|
|
}
|
|
|
|
if (modelBinder == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(modelBinder));
|
|
}
|
|
|
|
if (valueProvider == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(valueProvider));
|
|
}
|
|
|
|
if (parameter == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(parameter));
|
|
}
|
|
|
|
if (metadata == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(metadata));
|
|
}
|
|
|
|
if (parameter.BindingInfo?.RequestPredicate?.Invoke(actionContext) == false)
|
|
{
|
|
return ModelBindingResult.Failed();
|
|
}
|
|
|
|
var modelBindingContext = DefaultModelBindingContext.CreateBindingContext(
|
|
actionContext,
|
|
valueProvider,
|
|
metadata,
|
|
parameter.BindingInfo,
|
|
parameter.Name);
|
|
modelBindingContext.Model = value;
|
|
|
|
Logger.AttemptingToBindParameterOrProperty(parameter, modelBindingContext);
|
|
|
|
var parameterModelName = parameter.BindingInfo?.BinderModelName ?? metadata.BinderModelName;
|
|
if (parameterModelName != null)
|
|
{
|
|
// The name was set explicitly, always use that as the prefix.
|
|
modelBindingContext.ModelName = parameterModelName;
|
|
}
|
|
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;
|
|
}
|
|
|
|
await modelBinder.BindModelAsync(modelBindingContext);
|
|
|
|
Logger.DoneAttemptingToBindParameterOrProperty(parameter, modelBindingContext);
|
|
|
|
var modelBindingResult = modelBindingContext.Result;
|
|
|
|
if (_mvcOptions.AllowValidatingTopLevelNodes &&
|
|
_objectModelValidator is ObjectModelValidator baseObjectValidator)
|
|
{
|
|
Logger.AttemptingToValidateParameterOrProperty(parameter, modelBindingContext);
|
|
|
|
EnforceBindRequiredAndValidate(
|
|
baseObjectValidator,
|
|
actionContext,
|
|
parameter,
|
|
metadata,
|
|
modelBindingContext,
|
|
modelBindingResult);
|
|
|
|
Logger.DoneAttemptingToValidateParameterOrProperty(parameter, modelBindingContext);
|
|
}
|
|
else
|
|
{
|
|
// For legacy implementations (which directly implemented IObjectModelValidator), fall back to the
|
|
// back-compatibility logic. In this scenario, top-level validation attributes will be ignored like
|
|
// they were historically.
|
|
if (modelBindingResult.IsModelSet)
|
|
{
|
|
_objectModelValidator.Validate(
|
|
actionContext,
|
|
modelBindingContext.ValidationState,
|
|
modelBindingContext.ModelName,
|
|
modelBindingResult.Model);
|
|
}
|
|
}
|
|
|
|
return modelBindingResult;
|
|
}
|
|
|
|
private void EnforceBindRequiredAndValidate(
|
|
ObjectModelValidator baseObjectValidator,
|
|
ActionContext actionContext,
|
|
ParameterDescriptor parameter,
|
|
ModelMetadata metadata,
|
|
ModelBindingContext modelBindingContext,
|
|
ModelBindingResult modelBindingResult)
|
|
{
|
|
if (!modelBindingResult.IsModelSet && metadata.IsBindingRequired)
|
|
{
|
|
// Enforce BindingBehavior.Required (e.g., [BindRequired])
|
|
var modelName = modelBindingContext.FieldName;
|
|
var message = metadata.ModelBindingMessageProvider.MissingBindRequiredValueAccessor(modelName);
|
|
actionContext.ModelState.TryAddModelError(modelName, message);
|
|
}
|
|
else if (modelBindingResult.IsModelSet)
|
|
{
|
|
// Enforce any other validation rules
|
|
baseObjectValidator.Validate(
|
|
actionContext,
|
|
modelBindingContext.ValidationState,
|
|
modelBindingContext.ModelName,
|
|
modelBindingResult.Model,
|
|
metadata);
|
|
}
|
|
else if (metadata.IsRequired)
|
|
{
|
|
// We need to special case the model name for cases where a 'fallback' to empty
|
|
// prefix occurred but binding wasn't successful. For these cases there will be no
|
|
// entry in validation state to match and determine the correct key.
|
|
//
|
|
// See https://github.com/aspnet/Mvc/issues/7503
|
|
//
|
|
// This is to avoid adding validation errors for an 'empty' prefix when a simple
|
|
// type fails to bind. The fix for #7503 uncovered this issue, and was likely the
|
|
// original problem being worked around that regressed #7503.
|
|
var modelName = modelBindingContext.ModelName;
|
|
|
|
if (string.IsNullOrEmpty(modelBindingContext.ModelName) &&
|
|
parameter.BindingInfo?.BinderModelName == null)
|
|
{
|
|
// If we get here then this is a fallback case. The model name wasn't explicitly set
|
|
// and we ended up with an empty prefix.
|
|
modelName = modelBindingContext.FieldName;
|
|
}
|
|
|
|
// Run validation, we expect this to validate [Required].
|
|
baseObjectValidator.Validate(
|
|
actionContext,
|
|
modelBindingContext.ValidationState,
|
|
modelName,
|
|
modelBindingResult.Model,
|
|
metadata);
|
|
}
|
|
}
|
|
}
|
|
}
|