diff --git a/src/Microsoft.AspNetCore.Mvc.Core/CompatibilityVersion.cs b/src/Microsoft.AspNetCore.Mvc.Core/CompatibilityVersion.cs index aa63713383..d35cdd6110 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/CompatibilityVersion.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/CompatibilityVersion.cs @@ -73,6 +73,7 @@ namespace Microsoft.AspNetCore.Mvc /// ApiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses /// MvcDataAnnotationsLocalizationOptions.AllowDataAnnotationsLocalizationForEnumDisplayAttributes /// + /// /// /// RazorPagesOptions.AllowDefaultHandlingForOptionsRequests /// RazorViewEngineOptions.AllowRecompilingViewsOnFileChange diff --git a/src/Microsoft.AspNetCore.Mvc.Core/DefaultObjectValidator.cs b/src/Microsoft.AspNetCore.Mvc.Core/DefaultObjectValidator.cs index 8fedb7702b..ff175dc5cf 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/DefaultObjectValidator.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/DefaultObjectValidator.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; -using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Mvc { @@ -46,6 +45,7 @@ namespace Microsoft.AspNetCore.Mvc validationState); visitor.MaxValidationDepth = _mvcOptions.MaxValidationDepth; + visitor.AllowShortCircuitingValidationWhenNoValidatorsArePresent = _mvcOptions.AllowShortCircuitingValidationWhenNoValidatorsArePresent; return visitor; } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/MvcOptionsConfigureCompatibilityOptions.cs b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/MvcOptionsConfigureCompatibilityOptions.cs index dcc5243fbb..850d24e385 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/MvcOptionsConfigureCompatibilityOptions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/MvcOptionsConfigureCompatibilityOptions.cs @@ -38,6 +38,9 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure // Matches JsonSerializerSettingsProvider.DefaultMaxDepth values[nameof(MvcOptions.MaxValidationDepth)] = 32; + + values[nameof(MvcOptions.AllowShortCircuitingValidationWhenNoValidatorsArePresent)] = true; + } return values; diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Validation/ValidationVisitor.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Validation/ValidationVisitor.cs index e62f825887..0b277c68c6 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Validation/ValidationVisitor.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Validation/ValidationVisitor.cs @@ -110,6 +110,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation /// public bool ValidateComplexTypesIfChildValidationFails { get; set; } + /// + /// Gets or sets a value that determines if can short circuit validation when a model + /// does not have any associated validators. + /// + public bool AllowShortCircuitingValidationWhenNoValidatorsArePresent { get; set; } + /// /// Validates a object. /// @@ -267,7 +273,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation } // If the metadata indicates that no validators exist AND the aggregate state for the key says that the model graph // is not invalid (i.e. is one of Unvalidated, Valid, or Skipped) we can safely mark the graph as valid. - else if (metadata.HasValidators == false && + else if ( + AllowShortCircuitingValidationWhenNoValidatorsArePresent && + metadata.HasValidators == false && ModelState.GetFieldValidationState(key) != ModelValidationState.Invalid) { // No validators will be created for this graph of objects. Mark it as valid if it wasn't previously validated. diff --git a/src/Microsoft.AspNetCore.Mvc.Core/MvcOptions.cs b/src/Microsoft.AspNetCore.Mvc.Core/MvcOptions.cs index dd8e716d18..a97c9a965e 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/MvcOptions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/MvcOptions.cs @@ -32,6 +32,7 @@ namespace Microsoft.AspNetCore.Mvc private readonly CompatibilitySwitch _suppressBindingUndefinedValueToEnumType; private readonly CompatibilitySwitch _enableEndpointRouting; private readonly NullableCompatibilitySwitch _maxValidationDepth; + private readonly CompatibilitySwitch _allowShortCircuitingValidationWhenNoValidatorsArePresent; private readonly ICompatibilitySwitch[] _switches; /// @@ -58,6 +59,7 @@ namespace Microsoft.AspNetCore.Mvc _suppressBindingUndefinedValueToEnumType = new CompatibilitySwitch(nameof(SuppressBindingUndefinedValueToEnumType)); _enableEndpointRouting = new CompatibilitySwitch(nameof(EnableEndpointRouting)); _maxValidationDepth = new NullableCompatibilitySwitch(nameof(MaxValidationDepth)); + _allowShortCircuitingValidationWhenNoValidatorsArePresent = new CompatibilitySwitch(nameof(AllowShortCircuitingValidationWhenNoValidatorsArePresent)); _switches = new ICompatibilitySwitch[] { @@ -68,6 +70,7 @@ namespace Microsoft.AspNetCore.Mvc _suppressBindingUndefinedValueToEnumType, _enableEndpointRouting, _maxValidationDepth, + _allowShortCircuitingValidationWhenNoValidatorsArePresent, }; } @@ -442,6 +445,44 @@ namespace Microsoft.AspNetCore.Mvc } } + /// + /// Gets or sets a value that determines if + /// can short-circuit validation when a model does not have any associated validators. + /// + /// + /// The default value is if the version is + /// or later; otherwise. + /// + /// + /// When is , that is, it is determined + /// that a model or any of it's properties or collection elements cannot have any validators, + /// can short-circuit validation for the model and mark the object + /// graph as valid. Setting this property to , allows to + /// perform this optimization. + /// + /// This property is associated with a compatibility switch and can provide a different behavior depending on + /// the configured compatibility version for the application. See for + /// guidance and examples of setting the application's compatibility version. + /// + /// + /// Configuring the desired value of the compatibility switch by calling this property's setter will take precedence + /// over the value implied by the application's . + /// + /// + /// If the application's compatibility version is set to then + /// this setting will have the value unless explicitly configured. + /// + /// + /// If the application's compatibility version is set to or + /// earlier then this setting will have the value unless explicitly configured. + /// + /// + public bool AllowShortCircuitingValidationWhenNoValidatorsArePresent + { + get => _allowShortCircuitingValidationWhenNoValidatorsArePresent.Value; + set => _allowShortCircuitingValidationWhenNoValidatorsArePresent.Value = value; + } + IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)_switches).GetEnumerator(); diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultObjectValidatorTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultObjectValidatorTests.cs index 8fad61aadd..83cf64890a 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultObjectValidatorTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultObjectValidatorTests.cs @@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal { public class DefaultObjectValidatorTests { - private readonly MvcOptions _options = new MvcOptions(); + private readonly MvcOptions _options = new MvcOptions { AllowShortCircuitingValidationWhenNoValidatorsArePresent = true }; private ModelMetadataProvider MetadataProvider { get; } = TestModelMetadataProvider.CreateDefaultProvider(); diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ModelBindingTestHelper.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ModelBindingTestHelper.cs index 91a290406a..3a882567d0 100644 --- a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ModelBindingTestHelper.cs +++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ModelBindingTestHelper.cs @@ -129,7 +129,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests return new DefaultObjectValidator( metadataProvider, GetModelValidatorProviders(options), - options?.Value ?? new MvcOptions()); + options?.Value ?? new MvcOptions { AllowShortCircuitingValidationWhenNoValidatorsArePresent = true }); } private static IList GetModelValidatorProviders(IOptions options) @@ -197,7 +197,8 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests serviceCollection .AddSingleton() .AddSingleton(NullLoggerFactory.Instance) - .AddTransient, Logger>(); + .AddTransient, Logger>() + .Configure(options => options.AllowShortCircuitingValidationWhenNoValidatorsArePresent = true); if (updateOptions != null) { diff --git a/test/Microsoft.AspNetCore.Mvc.Test/IntegrationTest/CompatibilitySwitchIntegrationTest.cs b/test/Microsoft.AspNetCore.Mvc.Test/IntegrationTest/CompatibilitySwitchIntegrationTest.cs index ea544c1d26..e4020042f3 100644 --- a/test/Microsoft.AspNetCore.Mvc.Test/IntegrationTest/CompatibilitySwitchIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Test/IntegrationTest/CompatibilitySwitchIntegrationTest.cs @@ -56,6 +56,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest Assert.True(razorViewEngineOptions.AllowRecompilingViewsOnFileChange); Assert.False(razorPagesOptions.AllowDefaultHandlingForOptionsRequests); Assert.False(xmlOptions.AllowRfc7807CompliantProblemDetailsFormat); + Assert.False(mvcOptions.AllowShortCircuitingValidationWhenNoValidatorsArePresent); } [Fact] @@ -93,6 +94,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest Assert.True(razorViewEngineOptions.AllowRecompilingViewsOnFileChange); Assert.False(razorPagesOptions.AllowDefaultHandlingForOptionsRequests); Assert.False(xmlOptions.AllowRfc7807CompliantProblemDetailsFormat); + Assert.False(mvcOptions.AllowShortCircuitingValidationWhenNoValidatorsArePresent); } [Fact] @@ -130,6 +132,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest Assert.False(razorViewEngineOptions.AllowRecompilingViewsOnFileChange); Assert.True(razorPagesOptions.AllowDefaultHandlingForOptionsRequests); Assert.True(xmlOptions.AllowRfc7807CompliantProblemDetailsFormat); + Assert.True(mvcOptions.AllowShortCircuitingValidationWhenNoValidatorsArePresent); } [Fact] @@ -167,6 +170,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest Assert.False(razorViewEngineOptions.AllowRecompilingViewsOnFileChange); Assert.True(razorPagesOptions.AllowDefaultHandlingForOptionsRequests); Assert.True(xmlOptions.AllowRfc7807CompliantProblemDetailsFormat); + Assert.True(mvcOptions.AllowShortCircuitingValidationWhenNoValidatorsArePresent); } // This just does the minimum needed to be able to resolve these options.