diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs b/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs index f301afa6b0b9..ec764efc685b 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Reflection; using System.Threading; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Formatters; @@ -134,6 +135,11 @@ internal static void ConfigureAdditionalModelMetadataDetailsProviders(IList + { + var excludeFilter = Assert.IsType(provider); + Assert.Equal(typeof(Delegate), excludeFilter.Type); + }, + provider => + { + var excludeFilter = Assert.IsType(provider); + Assert.Equal(typeof(MethodInfo), excludeFilter.Type); + }, + provider => + { + var excludeFilter = Assert.IsType(provider); + Assert.Equal(typeof(MemberInfo), excludeFilter.Type); + }, + provider => + { + var excludeFilter = Assert.IsType(provider); + Assert.Equal(typeof(ParameterInfo), excludeFilter.Type); + }, + provider => + { + var excludeFilter = Assert.IsType(provider); + Assert.Equal(typeof(Assembly), excludeFilter.Type); + }, + provider => { var excludeFilter = Assert.IsType(provider); Assert.Equal(typeof(Uri), excludeFilter.Type); diff --git a/src/Mvc/test/Mvc.IntegrationTests/ModelBindingTestHelper.cs b/src/Mvc/test/Mvc.IntegrationTests/ModelBindingTestHelper.cs index 1c9a120a1576..2d6a65a62fdf 100644 --- a/src/Mvc/test/Mvc.IntegrationTests/ModelBindingTestHelper.cs +++ b/src/Mvc/test/Mvc.IntegrationTests/ModelBindingTestHelper.cs @@ -87,7 +87,8 @@ public static ParameterBinder GetParameterBinder(IServiceProvider serviceProvide public static ParameterBinder GetParameterBinder( IModelMetadataProvider metadataProvider, IModelBinderProvider binderProvider = null, - MvcOptions mvcOptions = null) + MvcOptions mvcOptions = null, + ObjectModelValidator validator = null) { var services = GetServices(metadataProvider, mvcOptions: mvcOptions); var options = services.GetRequiredService>(); @@ -97,13 +98,15 @@ public static ParameterBinder GetParameterBinder( options.Value.ModelBinderProviders.Insert(0, binderProvider); } + validator ??= new DefaultObjectValidator( + metadataProvider, + new[] { new CompositeModelValidatorProvider(GetModelValidatorProviders(options)) }, + options.Value); + return new ParameterBinder( metadataProvider, new ModelBinderFactory(metadataProvider, options, services), - new DefaultObjectValidator( - metadataProvider, - new[] { new CompositeModelValidatorProvider(GetModelValidatorProviders(options)) }, - options.Value), + validator, options, NullLoggerFactory.Instance); } diff --git a/src/Mvc/test/Mvc.IntegrationTests/ValidationIntegrationTests.cs b/src/Mvc/test/Mvc.IntegrationTests/ValidationIntegrationTests.cs index c544a5f4a65f..6bc7a558da09 100644 --- a/src/Mvc/test/Mvc.IntegrationTests/ValidationIntegrationTests.cs +++ b/src/Mvc/test/Mvc.IntegrationTests/ValidationIntegrationTests.cs @@ -18,6 +18,8 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Xunit; @@ -2437,10 +2439,105 @@ public async Task Validation_InifnitelyRecursiveModel_ValidationOnTopLevelParame private static void Validation_InifnitelyRecursiveModel_ValidationOnTopLevelParameterMethod([Required] RecursiveModel model) { } + [Fact] + public async Task Validation_ModelWithNonNullableReferenceTypes_DoesNotValidateNonNullablePropertiesOnFrameworkTypes() + { + // Arrange + var parameterInfo = GetType().GetMethod(nameof(Validation_ModelWithNonNullableReferenceTypes_DoesNotValidateNonNullablePropertiesOnFrameworkTypesAction), BindingFlags.NonPublic | BindingFlags.Static) + .GetParameters() + .First(); + + var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); + var services = ModelBindingTestHelper.GetServices(modelMetadataProvider); + var modelMetadata = modelMetadataProvider.GetMetadataForParameter(parameterInfo); + var options = services.GetRequiredService>().Value; + var validator = new RecordingObjectValidator( + modelMetadataProvider, + TestModelValidatorProvider.CreateDefaultProvider().ValidatorProviders, + options); + var parameterBinder = ModelBindingTestHelper.GetParameterBinder(modelMetadataProvider, mvcOptions: options, validator: validator); + + var parameter = new ParameterDescriptor() + { + Name = parameterInfo.Name, + ParameterType = parameterInfo.ParameterType, + }; + + var testContext = ModelBindingTestHelper.GetTestContext(request => + { + request.QueryString = new QueryString("?Name=CoolName"); + }); + var modelState = testContext.ModelState; + + // Act + var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext, modelMetadataProvider, modelMetadata); + + // Assert + Assert.True(modelBindingResult.IsModelSet); + + var model = Assert.IsType(modelBindingResult.Model); + Assert.Equal("CoolName", model.Name); + + Assert.True(modelState.IsValid); + Assert.Equal(ModelValidationState.Valid, modelState.ValidationState); + + var visited = validator.ValidationVisitor.Visited; + Assert.Collection( + visited, + v => Assert.Equal(typeof(ModelWithNonNullableReferenceTypeProperties), v.ModelType), + v => Assert.Equal(typeof(string), v.ModelType), + v => Assert.Equal(typeof(Delegate), v.ModelType)); + } + +#nullable enable + private static void Validation_ModelWithNonNullableReferenceTypes_DoesNotValidateNonNullablePropertiesOnFrameworkTypesAction(ModelWithNonNullableReferenceTypeProperties model) { } + + public class ModelWithNonNullableReferenceTypeProperties + { + public string Name { get; set; } = default!; + + public Delegate Delegate { get; set; } = typeof(ModelWithNonNullableReferenceTypeProperties).GetMethod(nameof(SomeMethod))!.CreateDelegate(); + + public static void SomeMethod() { } + } +#nullable restore + private static void AssertRequiredError(string key, ModelError error) { Assert.Equal(ValidationAttributeUtil.GetRequiredErrorMessage(key), error.ErrorMessage); Assert.Null(error.Exception); } + + private class RecordingObjectValidator : DefaultObjectValidator + { + public RecordingObjectValidator(IModelMetadataProvider modelMetadataProvider, IList validatorProviders, MvcOptions mvcOptions) + : base(modelMetadataProvider, validatorProviders, mvcOptions) + { + } + + public RecordingValidationVisitor ValidationVisitor { get; private set; } + + public override ValidationVisitor GetValidationVisitor(ActionContext actionContext, IModelValidatorProvider validatorProvider, ValidatorCache validatorCache, IModelMetadataProvider metadataProvider, ValidationStateDictionary validationState) + { + ValidationVisitor = new RecordingValidationVisitor(actionContext, validatorProvider, validatorCache, metadataProvider, validationState); + return ValidationVisitor; + } + } + + private class RecordingValidationVisitor : ValidationVisitor + { + public RecordingValidationVisitor(ActionContext actionContext, IModelValidatorProvider validatorProvider, ValidatorCache validatorCache, IModelMetadataProvider metadataProvider, ValidationStateDictionary validationState) + : base(actionContext, validatorProvider, validatorCache, metadataProvider, validationState) + { + } + + public List Visited = new(); + + protected override bool Visit(ModelMetadata metadata, string key, object model) + { + Visited.Add(metadata); + return base.Visit(metadata, key, model); + } + } } }