// Internal for testing. internal ModelValidationNode ProcessDto( ModelBindingContext bindingContext, ComplexModelDto dto, ModelValidationNode validationNode) { var metadataProvider = bindingContext.OperationBindingContext.MetadataProvider; var modelExplorer = metadataProvider.GetModelExplorerForType(bindingContext.ModelType, bindingContext.Model); var validationInfo = GetPropertyValidationInfo(bindingContext); // Eliminate provided properties from requiredProperties; leaving just *missing* required properties. var boundProperties = dto.Results.Where(p => p.Value.IsModelSet).Select(p => p.Key.PropertyName); validationInfo.RequiredProperties.ExceptWith(boundProperties); foreach (var missingRequiredProperty in validationInfo.RequiredProperties) { var addedError = false; // We want to provide the 'null' value, not the value of model.Property, // so avoiding modelExplorer.GetProperty here which would call the actual getter on the // model. This avoids issues with value types, or properties with pre-initialized values. var propertyExplorer = modelExplorer.GetExplorerForProperty(missingRequiredProperty, model: null); var propertyName = propertyExplorer.Metadata.BinderModelName ?? missingRequiredProperty; var modelStateKey = ModelNames.CreatePropertyModelName( bindingContext.ModelName, propertyName); // Get the first 'required' validator (if any) to get custom error message. var validatorProviderContext = new ModelValidatorProviderContext(propertyExplorer.Metadata); bindingContext.OperationBindingContext.ValidatorProvider.GetValidators(validatorProviderContext); var validator = validatorProviderContext.Validators.FirstOrDefault(v => v.IsRequired); if (validator != null) { addedError = RunValidator(validator, bindingContext, propertyExplorer, modelStateKey); } // Fall back to default message if BindingBehaviorAttribute required this property and we have no // actual validator for it. if (!addedError) { bindingContext.ModelState.TryAddModelError( modelStateKey, Resources.FormatModelBinding_MissingRequiredMember(propertyName)); } } // For each property that ComplexModelDtoModelBinder attempted to bind, call the setter, recording // exceptions as necessary. foreach (var entry in dto.Results) { var dtoResult = entry.Value; if (dtoResult != null) { var propertyMetadata = entry.Key; SetProperty(bindingContext, modelExplorer, propertyMetadata, dtoResult); var dtoValidationNode = dtoResult.ValidationNode; if (dtoValidationNode == null) { // Make sure that irrespective of if the properties of the model were bound with a value, // create a validation node so that these get validated. dtoValidationNode = new ModelValidationNode(dtoResult.Key, entry.Key, dtoResult.Model); } validationNode.ChildNodes.Add(dtoValidationNode); } } return(validationNode); }
private async Task <bool> CanValueBindAnyModelProperties(MutableObjectBinderContext context) { // We want to check to see if any of the properties of the model can be bound using the value providers, // because that's all that MutableObjectModelBinder can handle. // // However, because a property might specify a custom binding source ([FromForm]), it's not correct // for us to just try bindingContext.ValueProvider.ContainsPrefixAsync(bindingContext.ModelName), // because that may include ALL value providers - that would lead us to mistakenly create the model // when the data is coming from a source we should use (ex: value found in query string, but the // model has [FromForm]). // // To do this we need to enumerate the properties, and see which of them provide a binding source // through metadata, then we decide what to do. // // If a property has a binding source, and it's a greedy source, then it's not // allowed to come from a value provider, so we skip it. // // If a property has a binding source, and it's a non-greedy source, then we'll filter the // the value providers to just that source, and see if we can find a matching prefix // (see CanBindValue). // // If a property does not have a binding source, then it's fair game for any value provider. // // If any property meets the above conditions and has a value from valueproviders, then we'll // create the model and try to bind it. OR if ALL properties of the model have a greedy source, // then we go ahead and create it. // var isAnyPropertyEnabledForValueProviderBasedBinding = false; foreach (var propertyMetadata in context.PropertyMetadata) { // This check will skip properties which are marked explicitly using a non value binder. var bindingSource = propertyMetadata.BindingSource; if (bindingSource == null || !bindingSource.IsGreedy) { isAnyPropertyEnabledForValueProviderBasedBinding = true; var propertyModelName = ModelNames.CreatePropertyModelName( context.ModelBindingContext.ModelName, propertyMetadata.BinderModelName ?? propertyMetadata.PropertyName); var propertyModelBindingContext = ModelBindingContext.GetChildModelBindingContext( context.ModelBindingContext, propertyModelName, propertyMetadata); // If any property can return a true value. if (await CanBindValue(propertyModelBindingContext)) { return(true); } } } if (!isAnyPropertyEnabledForValueProviderBasedBinding) { // Either there are no properties or all the properties are marked as // a non value provider based marker. // This would be the case when the model has all its properties annotated with // a IBinderMetadata. We want to be able to create such a model. return(true); } return(false); }