// 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 { /// /// Binds and validates models specified by a . /// public class ParameterBinder { private readonly IModelMetadataProvider _modelMetadataProvider; private readonly IModelBinderFactory _modelBinderFactory; private readonly MvcOptions _mvcOptions; private readonly IObjectModelValidator _objectModelValidator; /// /// This constructor is obsolete and will be removed in a future version. The recommended alternative /// is the overload that also takes a accessor and an . /// /// Initializes a new instance of . /// /// The . /// The . /// The . [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) { } /// /// Initializes a new instance of . /// /// The . /// The . /// The . /// The accessor. /// The . public ParameterBinder( IModelMetadataProvider modelMetadataProvider, IModelBinderFactory modelBinderFactory, IObjectModelValidator validator, IOptions 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()); } /// /// The used for logging in this binder. /// protected ILogger Logger { get; } /// /// Initializes and binds a model specified by . /// /// The . /// The . /// The /// The result of model binding. public Task BindModelAsync( ActionContext actionContext, IValueProvider valueProvider, ParameterDescriptor parameter) { return BindModelAsync(actionContext, valueProvider, parameter, value: null); } /// /// Binds a model specified by using as the initial value. /// /// The . /// The . /// The /// The initial model value. /// The result of model binding. public virtual Task 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); } /// /// Binds a model specified by using as the initial value. /// /// The . /// The . /// The . /// The /// The . /// The initial model value. /// The result of model binding. public virtual async Task 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); } } } }