// 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.ComponentModel.DataAnnotations; using System.Linq; using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.JsonPatch; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.DataAnnotations; using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Options; using Moq; using Xunit; namespace Microsoft.AspNetCore.Mvc.ModelBinding { public class ParameterBinderTest { private static readonly IOptions _optionsAccessor = Options.Create(new MvcOptions { AllowValidatingTopLevelNodes = true, }); public static TheoryData BindModelAsyncData { get { var emptyBindingInfo = new BindingInfo(); var bindingInfoWithName = new BindingInfo { BinderModelName = "bindingInfoName", BinderType = typeof(Person), }; // parameterBindingInfo, metadataBinderModelName, parameterName, expectedBinderModelName return new TheoryData { // If the parameter name is not a prefix match, it is ignored. But name is required to create a // ModelBindingContext. { null, null, "parameterName", string.Empty }, { emptyBindingInfo, null, "parameterName", string.Empty }, { bindingInfoWithName, null, "parameterName", "bindingInfoName" }, { null, "modelBinderName", "parameterName", "modelBinderName" }, { null, null, "parameterName", string.Empty }, // Parameter's BindingInfo has highest precedence { bindingInfoWithName, "modelBinderName", "parameterName", "bindingInfoName" }, }; } } [Theory] [MemberData(nameof(BindModelAsyncData))] public async Task ObsoleteBindModelAsync_PassesExpectedBindingInfoAndMetadata_IfPrefixDoesNotMatch( BindingInfo parameterBindingInfo, string metadataBinderModelName, string parameterName, string expectedModelName) { // Arrange var binderExecuted = false; var metadataProvider = new TestModelMetadataProvider(); metadataProvider.ForType().BindingDetails(binding => { binding.BinderModelName = metadataBinderModelName; }); var metadata = metadataProvider.GetMetadataForType(typeof(Person)); var modelBinder = new Mock(); modelBinder .Setup(b => b.BindModelAsync(It.IsAny())) .Callback((ModelBindingContext context) => { Assert.Equal(expectedModelName, context.ModelName, StringComparer.Ordinal); }) .Returns(Task.CompletedTask); var parameterDescriptor = new ParameterDescriptor { BindingInfo = parameterBindingInfo, Name = parameterName, ParameterType = typeof(Person), }; var factory = new Mock(MockBehavior.Strict); factory .Setup(f => f.CreateBinder(It.IsAny())) .Callback((ModelBinderFactoryContext context) => { binderExecuted = true; // Confirm expected data is passed through to ModelBindingFactory. Assert.Same(parameterDescriptor.BindingInfo, context.BindingInfo); Assert.Same(parameterDescriptor, context.CacheToken); Assert.Equal(metadata, context.Metadata); }) .Returns(modelBinder.Object); var parameterBinder = new ParameterBinder( metadataProvider, factory.Object, Mock.Of(), _optionsAccessor, NullLoggerFactory.Instance); var controllerContext = GetControllerContext(); // Act & Assert #pragma warning disable CS0618 // Type or member is obsolete await parameterBinder.BindModelAsync(controllerContext, new SimpleValueProvider(), parameterDescriptor); #pragma warning restore CS0618 // Type or member is obsolete Assert.True(binderExecuted); } [Fact] public async Task ObsoleteBindModelAsync_PassesExpectedBindingInfoAndMetadata_IfPrefixMatches() { // Arrange var expectedModelName = "expectedName"; var binderExecuted = false; var metadataProvider = new TestModelMetadataProvider(); var metadata = metadataProvider.GetMetadataForType(typeof(Person)); var modelBinder = new Mock(); modelBinder .Setup(b => b.BindModelAsync(It.IsAny())) .Callback((ModelBindingContext context) => { Assert.Equal(expectedModelName, context.ModelName, StringComparer.Ordinal); }) .Returns(Task.CompletedTask); var parameterDescriptor = new ParameterDescriptor { Name = expectedModelName, ParameterType = typeof(Person), }; var factory = new Mock(MockBehavior.Strict); factory .Setup(f => f.CreateBinder(It.IsAny())) .Callback((ModelBinderFactoryContext context) => { binderExecuted = true; // Confirm expected data is passed through to ModelBindingFactory. Assert.Null(context.BindingInfo); Assert.Same(parameterDescriptor, context.CacheToken); Assert.Equal(metadata, context.Metadata); }) .Returns(modelBinder.Object); var argumentBinder = new ParameterBinder( metadataProvider, factory.Object, Mock.Of(), _optionsAccessor, NullLoggerFactory.Instance); var valueProvider = new SimpleValueProvider { { expectedModelName, new object() }, }; var valueProviderFactory = new SimpleValueProviderFactory(valueProvider); var controllerContext = GetControllerContext(); // Act & Assert #pragma warning disable CS0618 // Type or member is obsolete await argumentBinder.BindModelAsync(controllerContext, valueProvider, parameterDescriptor); #pragma warning restore CS0618 // Type or member is obsolete Assert.True(binderExecuted); } [Fact] public async Task BindModelAsync_EnforcesTopLevelBindRequired() { // Arrange var actionContext = GetControllerContext(); var mockModelMetadata = CreateMockModelMetadata(); mockModelMetadata.Setup(o => o.IsBindingRequired).Returns(true); mockModelMetadata.Setup(o => o.DisplayName).Returns("Ignored Display Name"); // Bind attribute errors are phrased in terms of the model name, not display name var parameterBinder = CreateParameterBinder(mockModelMetadata.Object); var modelBindingResult = ModelBindingResult.Failed(); // Act var result = await parameterBinder.BindModelAsync( actionContext, CreateMockModelBinder(modelBindingResult), CreateMockValueProvider(), new ParameterDescriptor { Name = "myParam", ParameterType = typeof(Person) }, mockModelMetadata.Object, "ignoredvalue"); // Assert Assert.False(actionContext.ModelState.IsValid); Assert.Equal("myParam", actionContext.ModelState.Single().Key); Assert.Equal( new DefaultModelBindingMessageProvider().MissingBindRequiredValueAccessor("myParam"), actionContext.ModelState.Single().Value.Errors.Single().ErrorMessage); } [Fact] public async Task BindModelAsync_DoesNotEnforceTopLevelBindRequired_IfNotValidatingTopLevelNodes() { // Arrange var actionContext = GetControllerContext(); var mockModelMetadata = CreateMockModelMetadata(); mockModelMetadata.Setup(o => o.IsBindingRequired).Returns(true); // Bind attribute errors are phrased in terms of the model name, not display name mockModelMetadata.Setup(o => o.DisplayName).Returns("Ignored Display Name"); // Do not set AllowValidatingTopLevelNodes. var optionsAccessor = Options.Create(new MvcOptions()); var parameterBinder = CreateParameterBinder(mockModelMetadata.Object, optionsAccessor: optionsAccessor); var modelBindingResult = ModelBindingResult.Failed(); // Act var result = await parameterBinder.BindModelAsync( actionContext, CreateMockModelBinder(modelBindingResult), CreateMockValueProvider(), new ParameterDescriptor { Name = "myParam", ParameterType = typeof(Person) }, mockModelMetadata.Object, "ignoredvalue"); // Assert Assert.True(actionContext.ModelState.IsValid); Assert.Empty(actionContext.ModelState); } [Fact] public async Task BindModelAsync_EnforcesTopLevelRequired() { // Arrange var actionContext = GetControllerContext(); var mockModelMetadata = CreateMockModelMetadata(); mockModelMetadata.Setup(o => o.IsRequired).Returns(true); mockModelMetadata.Setup(o => o.DisplayName).Returns("My Display Name"); mockModelMetadata.Setup(o => o.ValidatorMetadata).Returns(new[] { new RequiredAttribute() }); var validator = new DataAnnotationsModelValidator( new ValidationAttributeAdapterProvider(), new RequiredAttribute(), stringLocalizer: null); var parameterBinder = CreateParameterBinder(mockModelMetadata.Object, validator); var modelBindingResult = ModelBindingResult.Success(null); // Act var result = await parameterBinder.BindModelAsync( actionContext, CreateMockModelBinder(modelBindingResult), CreateMockValueProvider(), new ParameterDescriptor { Name = "myParam", ParameterType = typeof(Person) }, mockModelMetadata.Object, "ignoredvalue"); // Assert Assert.False(actionContext.ModelState.IsValid); Assert.Equal("myParam", actionContext.ModelState.Single().Key); Assert.Equal( new RequiredAttribute().FormatErrorMessage("My Display Name"), actionContext.ModelState.Single().Value.Errors.Single().ErrorMessage); } [Fact] public async Task BindModelAsync_DoesNotEnforceTopLevelRequired_IfNotValidatingTopLevelNodes() { // Arrange var actionContext = GetControllerContext(); var mockModelMetadata = CreateMockModelMetadata(); mockModelMetadata.Setup(o => o.IsRequired).Returns(true); mockModelMetadata.Setup(o => o.DisplayName).Returns("My Display Name"); mockModelMetadata.Setup(o => o.ValidatorMetadata).Returns(new[] { new RequiredAttribute() }); var validator = new DataAnnotationsModelValidator( new ValidationAttributeAdapterProvider(), new RequiredAttribute(), stringLocalizer: null); // Do not set AllowValidatingTopLevelNodes. var optionsAccessor = Options.Create(new MvcOptions()); var parameterBinder = CreateParameterBinder(mockModelMetadata.Object, validator, optionsAccessor); var modelBindingResult = ModelBindingResult.Success(null); // Act var result = await parameterBinder.BindModelAsync( actionContext, CreateMockModelBinder(modelBindingResult), CreateMockValueProvider(), new ParameterDescriptor { Name = "myParam", ParameterType = typeof(Person) }, mockModelMetadata.Object, "ignoredvalue"); // Assert Assert.True(actionContext.ModelState.IsValid); Assert.Empty(actionContext.ModelState); } public static TheoryData EnforcesTopLevelRequiredDataSet { get { var attribute = new RequiredAttribute(); var bindingInfo = new BindingInfo { BinderModelName = string.Empty, }; var parameterDescriptor = new ParameterDescriptor { Name = string.Empty, BindingInfo = bindingInfo, ParameterType = typeof(Person), }; var method = typeof(Person).GetMethod(nameof(Person.Equals), new[] { typeof(Person) }); var parameter = method.GetParameters()[0]; // Equals(Person other) var controllerParameterDescriptor = new ControllerParameterDescriptor { Name = string.Empty, BindingInfo = bindingInfo, ParameterInfo = parameter, ParameterType = typeof(Person), }; var provider1 = new TestModelMetadataProvider(); provider1 .ForParameter(parameter) .ValidationDetails(d => { d.IsRequired = true; d.ValidatorMetadata.Add(attribute); }); provider1 .ForProperty(typeof(Family), nameof(Family.Mom)) .ValidationDetails(d => { d.IsRequired = true; d.ValidatorMetadata.Add(attribute); }); var provider2 = new TestModelMetadataProvider(); provider2 .ForType(typeof(Person)) .ValidationDetails(d => { d.IsRequired = true; d.ValidatorMetadata.Add(attribute); }); return new TheoryData { { attribute, parameterDescriptor, provider1.GetMetadataForParameter(parameter) }, { attribute, parameterDescriptor, provider1.GetMetadataForProperty(typeof(Family), nameof(Family.Mom)) }, { attribute, parameterDescriptor, provider2.GetMetadataForType(typeof(Person)) }, { attribute, controllerParameterDescriptor, provider2.GetMetadataForType(typeof(Person)) }, }; } } [Theory] [MemberData(nameof(EnforcesTopLevelRequiredDataSet))] public async Task BindModelAsync_EnforcesTopLevelRequiredAndLogsSuccessfully_WithEmptyPrefix( RequiredAttribute attribute, ParameterDescriptor parameterDescriptor, ModelMetadata metadata) { // Arrange var expectedKey = string.Empty; var expectedFieldName = metadata.Name ?? nameof(Person); var actionContext = GetControllerContext(); var validator = new DataAnnotationsModelValidator( new ValidationAttributeAdapterProvider(), attribute, stringLocalizer: null); var sink = new TestSink(); var loggerFactory = new TestLoggerFactory(sink, enabled: true); var parameterBinder = CreateParameterBinder(metadata, validator, loggerFactory: loggerFactory); var modelBindingResult = ModelBindingResult.Success(null); // Act var result = await parameterBinder.BindModelAsync( actionContext, CreateMockModelBinder(modelBindingResult), CreateMockValueProvider(), parameterDescriptor, metadata, "ignoredvalue"); // Assert Assert.False(actionContext.ModelState.IsValid); var modelState = Assert.Single(actionContext.ModelState); Assert.Equal(expectedKey, modelState.Key); var error = Assert.Single(modelState.Value.Errors); Assert.Equal(attribute.FormatErrorMessage(expectedFieldName), error.ErrorMessage); Assert.Equal(4, sink.Writes.Count()); } [Fact] public async Task BindModelAsync_EnforcesTopLevelDataAnnotationsAttribute() { // Arrange var actionContext = GetControllerContext(); var mockModelMetadata = CreateMockModelMetadata(); var validationAttribute = new RangeAttribute(1, 100); mockModelMetadata.Setup(o => o.DisplayName).Returns("My Display Name"); mockModelMetadata.Setup(o => o.ValidatorMetadata).Returns(new[] { validationAttribute }); var validator = new DataAnnotationsModelValidator( new ValidationAttributeAdapterProvider(), validationAttribute, stringLocalizer: null); var parameterBinder = CreateParameterBinder(mockModelMetadata.Object, validator); var modelBindingResult = ModelBindingResult.Success(123); // Act var result = await parameterBinder.BindModelAsync( actionContext, CreateMockModelBinder(modelBindingResult), CreateMockValueProvider(), new ParameterDescriptor { Name = "myParam", ParameterType = typeof(Person) }, mockModelMetadata.Object, 50); // This value is ignored, because test explicitly set the ModelBindingResult // Assert Assert.False(actionContext.ModelState.IsValid); Assert.Equal("myParam", actionContext.ModelState.Single().Key); Assert.Equal( validationAttribute.FormatErrorMessage("My Display Name"), actionContext.ModelState.Single().Value.Errors.Single().ErrorMessage); } [Fact] public async Task BindModelAsync_SupportsIObjectModelValidatorForBackCompat() { // Arrange var actionContext = GetControllerContext(); var mockValidator = new Mock(MockBehavior.Strict); mockValidator .Setup(o => o.Validate( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Callback((ActionContext context, ValidationStateDictionary validationState, string prefix, object model) => { context.ModelState.AddModelError(prefix, "Test validation message"); }); var modelMetadata = CreateMockModelMetadata().Object; var parameterBinder = CreateBackCompatParameterBinder( modelMetadata, mockValidator.Object); var modelBindingResult = ModelBindingResult.Success(123); // Act var result = await parameterBinder.BindModelAsync( actionContext, CreateMockModelBinder(modelBindingResult), CreateMockValueProvider(), new ParameterDescriptor { Name = "myParam", ParameterType = typeof(Person) }, modelMetadata, "ignored"); // Assert Assert.False(actionContext.ModelState.IsValid); Assert.Equal("myParam", actionContext.ModelState.Single().Key); Assert.Equal( "Test validation message", actionContext.ModelState.Single().Value.Errors.Single().ErrorMessage); } [Fact] public async Task BindModelAsync_ForParameter_UsesValidationFromActualModel_WhenDerivedModelIsSet() { // Arrange var method = GetType().GetMethod(nameof(TestMethodWithoutAttributes), BindingFlags.NonPublic | BindingFlags.Instance); var parameter = method.GetParameters()[0]; var parameterDescriptor = new ControllerParameterDescriptor { ParameterInfo = parameter, Name = parameter.Name, }; var actionContext = GetControllerContext(); var modelMetadataProvider = new TestModelMetadataProvider(); var model = new DerivedPerson(); var modelBindingResult = ModelBindingResult.Success(model); var parameterBinder = new ParameterBinder( modelMetadataProvider, Mock.Of(), new DefaultObjectValidator( modelMetadataProvider, new[] { TestModelValidatorProvider.CreateDefaultProvider() }), _optionsAccessor, NullLoggerFactory.Instance); var modelMetadata = modelMetadataProvider.GetMetadataForParameter(parameter); var modelBinder = CreateMockModelBinder(modelBindingResult); // Act var result = await parameterBinder.BindModelAsync( actionContext, modelBinder, CreateMockValueProvider(), parameterDescriptor, modelMetadata, value: null); // Assert Assert.True(result.IsModelSet); Assert.Same(model, result.Model); Assert.False(actionContext.ModelState.IsValid); Assert.Collection( actionContext.ModelState, kvp => { Assert.Equal($"{parameter.Name}.{nameof(DerivedPerson.DerivedProperty)}", kvp.Key); var error = Assert.Single(kvp.Value.Errors); Assert.Equal("The DerivedProperty field is required.", error.ErrorMessage); }); } [Fact] public async Task BindModelAsync_ForParameter_UsesValidationFromParameter_WhenDerivedModelIsSet() { // Arrange var method = GetType().GetMethod(nameof(TestMethodWithAttributes), BindingFlags.NonPublic | BindingFlags.Instance); var parameter = method.GetParameters()[0]; var parameterDescriptor = new ControllerParameterDescriptor { ParameterInfo = parameter, Name = parameter.Name, }; var actionContext = GetControllerContext(); var modelMetadataProvider = new TestModelMetadataProvider(); var model = new DerivedPerson { DerivedProperty = "SomeValue" }; var modelBindingResult = ModelBindingResult.Success(model); var parameterBinder = new ParameterBinder( modelMetadataProvider, Mock.Of(), new DefaultObjectValidator( modelMetadataProvider, new[] { TestModelValidatorProvider.CreateDefaultProvider() }), _optionsAccessor, NullLoggerFactory.Instance); var modelMetadata = modelMetadataProvider.GetMetadataForParameter(parameter); var modelBinder = CreateMockModelBinder(modelBindingResult); // Act var result = await parameterBinder.BindModelAsync( actionContext, modelBinder, CreateMockValueProvider(), parameterDescriptor, modelMetadata, value: null); // Assert Assert.True(result.IsModelSet); Assert.Same(model, result.Model); Assert.False(actionContext.ModelState.IsValid); Assert.Collection( actionContext.ModelState, kvp => { Assert.Equal(parameter.Name, kvp.Key); var error = Assert.Single(kvp.Value.Errors); Assert.Equal("Always Invalid", error.ErrorMessage); }); } [Fact] public async Task BindModelAsync_ForProperty_UsesValidationFromActualModel_WhenDerivedModelIsSet() { // Arrange var property = typeof(TestController).GetProperty(nameof(TestController.Model)); var parameterDescriptor = new ControllerBoundPropertyDescriptor { PropertyInfo = property, Name = property.Name, }; var actionContext = GetControllerContext(); var modelMetadataProvider = new TestModelMetadataProvider(); var model = new DerivedModel(); var modelBindingResult = ModelBindingResult.Success(model); var parameterBinder = new ParameterBinder( modelMetadataProvider, Mock.Of(), new DefaultObjectValidator( modelMetadataProvider, new[] { TestModelValidatorProvider.CreateDefaultProvider() }), _optionsAccessor, NullLoggerFactory.Instance); var modelMetadata = modelMetadataProvider.GetMetadataForProperty(property.DeclaringType, property.Name); var modelBinder = CreateMockModelBinder(modelBindingResult); // Act var result = await parameterBinder.BindModelAsync( actionContext, modelBinder, CreateMockValueProvider(), parameterDescriptor, modelMetadata, value: null); // Assert Assert.True(result.IsModelSet); Assert.Same(model, result.Model); Assert.False(actionContext.ModelState.IsValid); Assert.Collection( actionContext.ModelState, kvp => { Assert.Equal($"{property.Name}.{nameof(DerivedPerson.DerivedProperty)}", kvp.Key); var error = Assert.Single(kvp.Value.Errors); Assert.Equal("The DerivedProperty field is required.", error.ErrorMessage); }); } [Fact] public async Task BindModelAsync_ForProperty_UsesValidationOnProperty_WhenDerivedModelIsSet() { // Arrange var property = typeof(TestControllerWithValidatedProperties).GetProperty(nameof(TestControllerWithValidatedProperties.Model)); var parameterDescriptor = new ControllerBoundPropertyDescriptor { PropertyInfo = property, Name = property.Name, }; var actionContext = GetControllerContext(); var modelMetadataProvider = new TestModelMetadataProvider(); var model = new DerivedModel { DerivedProperty = "some value" }; var modelBindingResult = ModelBindingResult.Success(model); var parameterBinder = new ParameterBinder( modelMetadataProvider, Mock.Of(), new DefaultObjectValidator( modelMetadataProvider, new[] { TestModelValidatorProvider.CreateDefaultProvider() }), _optionsAccessor, NullLoggerFactory.Instance); var modelMetadata = modelMetadataProvider.GetMetadataForProperty(property.DeclaringType, property.Name); var modelBinder = CreateMockModelBinder(modelBindingResult); // Act var result = await parameterBinder.BindModelAsync( actionContext, modelBinder, CreateMockValueProvider(), parameterDescriptor, modelMetadata, value: null); // Assert Assert.True(result.IsModelSet); Assert.Same(model, result.Model); Assert.False(actionContext.ModelState.IsValid); Assert.Collection( actionContext.ModelState, kvp => { Assert.Equal($"{property.Name}", kvp.Key); var error = Assert.Single(kvp.Value.Errors); Assert.Equal("Always Invalid", error.ErrorMessage); }); } // Regression test 1 for aspnet/Mvc#7963. ModelState should never be valid. [Fact] public async Task BindModelAsync_ForOverlappingParametersWithSuppressions_InValid_WithValidSecondParameter() { // Arrange var parameterDescriptor = new ParameterDescriptor { Name = "patchDocument", ParameterType = typeof(IJsonPatchDocument), }; var actionContext = GetControllerContext(); var modelState = actionContext.ModelState; // First ModelState key is not empty to match SimpleTypeModelBinder. modelState.SetModelValue("id", "notAGuid", "notAGuid"); modelState.AddModelError("id", "This is not valid."); var modelMetadataProvider = new TestModelMetadataProvider(); modelMetadataProvider.ForType().ValidationDetails(v => v.ValidateChildren = false); var modelMetadata = modelMetadataProvider.GetMetadataForType(typeof(IJsonPatchDocument)); var parameterBinder = new ParameterBinder( modelMetadataProvider, Mock.Of(), new DefaultObjectValidator( modelMetadataProvider, new[] { TestModelValidatorProvider.CreateDefaultProvider() }), _optionsAccessor, NullLoggerFactory.Instance); // BodyModelBinder does not update ModelState in success case. var modelBindingResult = ModelBindingResult.Success(new JsonPatchDocument()); var modelBinder = CreateMockModelBinder(modelBindingResult); // Act var result = await parameterBinder.BindModelAsync( actionContext, modelBinder, new SimpleValueProvider(), parameterDescriptor, modelMetadata, value: null); // Assert Assert.True(result.IsModelSet); Assert.False(modelState.IsValid); Assert.Collection( modelState, kvp => { Assert.Equal("id", kvp.Key); Assert.Equal(ModelValidationState.Invalid, kvp.Value.ValidationState); var error = Assert.Single(kvp.Value.Errors); Assert.Equal("This is not valid.", error.ErrorMessage); }); } // Regression test 2 for aspnet/Mvc#7963. ModelState should never be valid. [Fact] public async Task BindModelAsync_ForOverlappingParametersWithSuppressions_InValid_WithInValidSecondParameter() { // Arrange var parameterDescriptor = new ParameterDescriptor { Name = "patchDocument", ParameterType = typeof(IJsonPatchDocument), }; var actionContext = GetControllerContext(); var modelState = actionContext.ModelState; // First ModelState key is not empty to match SimpleTypeModelBinder. modelState.SetModelValue("id", "notAGuid", "notAGuid"); modelState.AddModelError("id", "This is not valid."); // Second ModelState key is empty to match BodyModelBinder. modelState.AddModelError(string.Empty, "This is also not valid."); var modelMetadataProvider = new TestModelMetadataProvider(); modelMetadataProvider.ForType().ValidationDetails(v => v.ValidateChildren = false); var modelMetadata = modelMetadataProvider.GetMetadataForType(typeof(IJsonPatchDocument)); var parameterBinder = new ParameterBinder( modelMetadataProvider, Mock.Of(), new DefaultObjectValidator( modelMetadataProvider, new[] { TestModelValidatorProvider.CreateDefaultProvider() }), _optionsAccessor, NullLoggerFactory.Instance); var modelBindingResult = ModelBindingResult.Failed(); var modelBinder = CreateMockModelBinder(modelBindingResult); // Act var result = await parameterBinder.BindModelAsync( actionContext, modelBinder, new SimpleValueProvider(), parameterDescriptor, modelMetadata, value: null); // Assert Assert.False(result.IsModelSet); Assert.False(modelState.IsValid); Assert.Collection( modelState, kvp => { Assert.Empty(kvp.Key); Assert.Equal(ModelValidationState.Invalid, kvp.Value.ValidationState); var error = Assert.Single(kvp.Value.Errors); Assert.Equal("This is also not valid.", error.ErrorMessage); }, kvp => { Assert.Equal("id", kvp.Key); Assert.Equal(ModelValidationState.Invalid, kvp.Value.ValidationState); var error = Assert.Single(kvp.Value.Errors); Assert.Equal("This is not valid.", error.ErrorMessage); }); } private static ControllerContext GetControllerContext() { var services = new ServiceCollection(); services.AddSingleton(NullLoggerFactory.Instance); return new ControllerContext() { HttpContext = new DefaultHttpContext() { RequestServices = services.BuildServiceProvider() } }; } private static Mock CreateMockModelMetadata() { var mockModelMetadata = new Mock(); mockModelMetadata .Setup(o => o.ModelBindingMessageProvider) .Returns(new DefaultModelBindingMessageProvider()); return mockModelMetadata; } private static IModelBinder CreateMockModelBinder(ModelBindingResult modelBinderResult) { var mockBinder = new Mock(MockBehavior.Strict); mockBinder .Setup(o => o.BindModelAsync(It.IsAny())) .Returns(context => { context.Result = modelBinderResult; return Task.CompletedTask; }); return mockBinder.Object; } private static ParameterBinder CreateParameterBinder( ModelMetadata modelMetadata, IModelValidator validator = null, IOptions optionsAccessor = null, ILoggerFactory loggerFactory = null) { var mockModelMetadataProvider = new Mock(MockBehavior.Strict); mockModelMetadataProvider .Setup(o => o.GetMetadataForType(typeof(Person))) .Returns(modelMetadata); var mockModelBinderFactory = new Mock(MockBehavior.Strict); optionsAccessor = optionsAccessor ?? _optionsAccessor; return new ParameterBinder( mockModelMetadataProvider.Object, mockModelBinderFactory.Object, new DefaultObjectValidator( mockModelMetadataProvider.Object, new[] { GetModelValidatorProvider(validator) }), optionsAccessor, loggerFactory ?? NullLoggerFactory.Instance); } private static IModelValidatorProvider GetModelValidatorProvider(IModelValidator validator = null) { if (validator == null) { validator = Mock.Of(); } var validatorProvider = new Mock(); validatorProvider .Setup(p => p.CreateValidators(It.IsAny())) .Callback(context => { foreach (var result in context.Results) { result.Validator = validator; result.IsReusable = true; } }); return validatorProvider.Object; } private static ParameterBinder CreateBackCompatParameterBinder( ModelMetadata modelMetadata, IObjectModelValidator validator) { var mockModelMetadataProvider = new Mock(MockBehavior.Strict); mockModelMetadataProvider .Setup(o => o.GetMetadataForType(typeof(Person))) .Returns(modelMetadata); var mockModelBinderFactory = new Mock(MockBehavior.Strict); #pragma warning disable CS0618 // Type or member is obsolete return new ParameterBinder( mockModelMetadataProvider.Object, mockModelBinderFactory.Object, validator); #pragma warning restore CS0618 // Type or member is obsolete } private static IValueProvider CreateMockValueProvider() { var mockValueProvider = new Mock(MockBehavior.Strict); mockValueProvider .Setup(o => o.ContainsPrefix(It.IsAny())) .Returns(true); return mockValueProvider.Object; } private class Person : IEquatable, IEquatable { public string Name { get; set; } public bool Equals(Person other) { return other != null && string.Equals(Name, other.Name, StringComparison.Ordinal); } bool IEquatable.Equals(object obj) { return Equals(obj as Person); } } private class Family { public Person Dad { get; set; } public Person Mom { get; set; } public IList Kids { get; } = new List(); } private class DerivedPerson : Person { [Required] public string DerivedProperty { get; set; } } public abstract class FakeModelMetadata : ModelMetadata { public FakeModelMetadata() : base(ModelMetadataIdentity.ForType(typeof(string))) { } } private void TestMethodWithoutAttributes(Person person) { } private void TestMethodWithAttributes([Required][AlwaysInvalid] Person person) { } private class TestController { public BaseModel Model { get; set; } } private class TestControllerWithValidatedProperties { [AlwaysInvalid] [Required] public BaseModel Model { get; set; } } private class BaseModel { } private class DerivedModel { [Required] public string DerivedProperty { get; set; } } private class AlwaysInvalidAttribute : ValidationAttribute { public AlwaysInvalidAttribute() { ErrorMessage = "Always Invalid"; } public override bool IsValid(object value) { return false; } } } }