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.