Fix #7503 change to model name for IValidableObject
This change undoes a breaking change introduced by the 2.1 model
validation changes. Now an implementation of IValidableObject on a
top-level model will be called correctly with the 'empty' prefix instead
of the parameter name.
When fixing this we undid a workaround for another issue.
When validating a parameter that didn't bind we didn't correctly compute
the model name for 'fallback to empty prefix' cases.
(cherry picked from commit 7a1096a72b)
This commit is contained in:
parent
264f9c871e
commit
f20bf9ea02
|
|
@ -249,6 +249,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||||
EnforceBindRequiredAndValidate(
|
EnforceBindRequiredAndValidate(
|
||||||
baseObjectValidator,
|
baseObjectValidator,
|
||||||
actionContext,
|
actionContext,
|
||||||
|
parameter,
|
||||||
metadata,
|
metadata,
|
||||||
modelBindingContext,
|
modelBindingContext,
|
||||||
modelBindingResult);
|
modelBindingResult);
|
||||||
|
|
@ -276,6 +277,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||||
private void EnforceBindRequiredAndValidate(
|
private void EnforceBindRequiredAndValidate(
|
||||||
ObjectModelValidator baseObjectValidator,
|
ObjectModelValidator baseObjectValidator,
|
||||||
ActionContext actionContext,
|
ActionContext actionContext,
|
||||||
|
ParameterDescriptor parameter,
|
||||||
ModelMetadata metadata,
|
ModelMetadata metadata,
|
||||||
ModelBindingContext modelBindingContext,
|
ModelBindingContext modelBindingContext,
|
||||||
ModelBindingResult modelBindingResult)
|
ModelBindingResult modelBindingResult)
|
||||||
|
|
@ -287,7 +289,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||||
var message = metadata.ModelBindingMessageProvider.MissingBindRequiredValueAccessor(modelName);
|
var message = metadata.ModelBindingMessageProvider.MissingBindRequiredValueAccessor(modelName);
|
||||||
actionContext.ModelState.TryAddModelError(modelName, message);
|
actionContext.ModelState.TryAddModelError(modelName, message);
|
||||||
}
|
}
|
||||||
else if (modelBindingResult.IsModelSet || metadata.IsRequired)
|
else if (modelBindingResult.IsModelSet)
|
||||||
{
|
{
|
||||||
// Enforce any other validation rules
|
// Enforce any other validation rules
|
||||||
baseObjectValidator.Validate(
|
baseObjectValidator.Validate(
|
||||||
|
|
@ -297,6 +299,35 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||||
modelBindingResult.Model,
|
modelBindingResult.Model,
|
||||||
metadata);
|
metadata);
|
||||||
}
|
}
|
||||||
|
else if (metadata.IsRequired)
|
||||||
|
{
|
||||||
|
// We need to special case the model name for cases where a 'fallback' to empty
|
||||||
|
// prefix occurred but binding wasn't successful. For these cases there will be no
|
||||||
|
// entry in validation state to match and determine the correct key.
|
||||||
|
//
|
||||||
|
// See https://github.com/aspnet/Mvc/issues/7503
|
||||||
|
//
|
||||||
|
// This is to avoid adding validation errors for an 'empty' prefix when a simple
|
||||||
|
// type fails to bind. The fix for #7503 uncovered this issue, and was likely the
|
||||||
|
// original problem being worked around that regressed #7503.
|
||||||
|
string modelName = modelBindingContext.ModelName;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(modelBindingContext.ModelName) &&
|
||||||
|
(parameter.BindingInfo?.BinderModelName ?? metadata.BinderModelName) == null)
|
||||||
|
{
|
||||||
|
// If we get here then this is a fallback case. The model name wasn't explicitly set
|
||||||
|
// and we ended up with an empty prefix.
|
||||||
|
modelName = modelBindingContext.FieldName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run validation, we expect this to validate [Required].
|
||||||
|
baseObjectValidator.Validate(
|
||||||
|
actionContext,
|
||||||
|
modelBindingContext.ValidationState,
|
||||||
|
modelName,
|
||||||
|
modelBindingResult.Model,
|
||||||
|
metadata);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -146,13 +146,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
|
||||||
var result = results[i];
|
var result = results[i];
|
||||||
var key = ModelNames.CreatePropertyModelName(Key, result.MemberName);
|
var key = ModelNames.CreatePropertyModelName(Key, result.MemberName);
|
||||||
|
|
||||||
// If this is a top-level parameter/property, the key would be empty.
|
// It's OK for key to be the empty string here. This can happen when a top
|
||||||
// So, use the name of the top-level property/property.
|
// level object implements IValidatableObject.
|
||||||
if (string.IsNullOrEmpty(key) && Metadata.Name != null)
|
|
||||||
{
|
|
||||||
key = Metadata.Name;
|
|
||||||
}
|
|
||||||
|
|
||||||
ModelState.TryAddModelError(key, result.Message);
|
ModelState.TryAddModelError(key, result.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -381,7 +381,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||||
ModelMetadata metadata)
|
ModelMetadata metadata)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var expectedKey = metadata.Name ?? string.Empty;
|
var expectedKey = string.Empty;
|
||||||
var expectedFieldName = metadata.Name ?? nameof(Person);
|
var expectedFieldName = metadata.Name ?? nameof(Person);
|
||||||
|
|
||||||
var actionContext = GetControllerContext();
|
var actionContext = GetControllerContext();
|
||||||
|
|
|
||||||
|
|
@ -578,7 +578,10 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||||
[InlineData("displayNameStringLengthParam", "abc", true)]
|
[InlineData("displayNameStringLengthParam", "abc", true)]
|
||||||
[InlineData("displayNameStringLengthParam", "abcTooLong", false, "My Display Name")]
|
[InlineData("displayNameStringLengthParam", "abcTooLong", false, "My Display Name")]
|
||||||
public async Task ActionParameter_EnforcesDataAnnotationsAttributes(
|
public async Task ActionParameter_EnforcesDataAnnotationsAttributes(
|
||||||
string paramName, string input, bool isValid, string displayName = null)
|
string paramName,
|
||||||
|
string input,
|
||||||
|
bool isValid,
|
||||||
|
string displayName = null)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
|
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
|
||||||
|
|
@ -614,11 +617,114 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||||
Assert.Equal(isValid, modelState.IsValid);
|
Assert.Equal(isValid, modelState.IsValid);
|
||||||
if (!isValid)
|
if (!isValid)
|
||||||
{
|
{
|
||||||
var message = modelState[paramName].Errors.Single().ErrorMessage;
|
var entry = modelState[paramName];
|
||||||
|
Assert.NotNull(entry);
|
||||||
|
|
||||||
|
var message = entry.Errors.Single().ErrorMessage;
|
||||||
Assert.Contains(displayName ?? parameter.Name, message);
|
Assert.Contains(displayName ?? parameter.Name, message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ActionParameter_CanRunIValidatableObject_EmptyPrefix()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
|
||||||
|
var parameterInfo = BindingAndValidationController.ValidatableObjectParameterInfo;
|
||||||
|
var parameter = new ParameterDescriptor()
|
||||||
|
{
|
||||||
|
Name = parameterInfo.Name,
|
||||||
|
ParameterType = parameterInfo.ParameterType,
|
||||||
|
};
|
||||||
|
|
||||||
|
var testContext = ModelBindingTestHelper.GetTestContext(request =>
|
||||||
|
{
|
||||||
|
request.QueryString = QueryString.Create(nameof(ModelWithIValidatableObject.FirstName), "Billy");
|
||||||
|
});
|
||||||
|
|
||||||
|
var modelState = testContext.ModelState;
|
||||||
|
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
|
||||||
|
var modelMetadata = modelMetadataProvider
|
||||||
|
.GetMetadataForParameter(parameterInfo);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var modelBindingResult = await parameterBinder.BindModelAsync(
|
||||||
|
parameter,
|
||||||
|
testContext,
|
||||||
|
modelMetadataProvider,
|
||||||
|
modelMetadata);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(modelBindingResult.IsModelSet, "model is set");
|
||||||
|
Assert.False(modelState.IsValid, "model is valid");
|
||||||
|
|
||||||
|
var entry = modelState[string.Empty];
|
||||||
|
Assert.NotNull(entry);
|
||||||
|
var message = entry.Errors.Single().ErrorMessage;
|
||||||
|
Assert.Equal("Not valid.", message);
|
||||||
|
|
||||||
|
entry = modelState[nameof(ModelWithIValidatableObject.FirstName)];
|
||||||
|
Assert.NotNull(entry);
|
||||||
|
message = entry.Errors.Single().ErrorMessage;
|
||||||
|
Assert.Equal("FirstName Not valid.", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ActionParameter_CanRunIValidatableObject_WithPrefix()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
|
||||||
|
var parameterInfo = BindingAndValidationController.ValidatableObjectParameterInfo;
|
||||||
|
var parameter = new ParameterDescriptor()
|
||||||
|
{
|
||||||
|
Name = parameterInfo.Name,
|
||||||
|
ParameterType = parameterInfo.ParameterType,
|
||||||
|
};
|
||||||
|
|
||||||
|
var testContext = ModelBindingTestHelper.GetTestContext(request =>
|
||||||
|
{
|
||||||
|
var key = ModelNames.CreatePropertyModelName(parameter.Name, nameof(ModelWithIValidatableObject.FirstName));
|
||||||
|
request.QueryString = QueryString.Create(key, "Billy");
|
||||||
|
});
|
||||||
|
|
||||||
|
var modelState = testContext.ModelState;
|
||||||
|
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
|
||||||
|
var modelMetadata = modelMetadataProvider
|
||||||
|
.GetMetadataForParameter(parameterInfo);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var modelBindingResult = await parameterBinder.BindModelAsync(
|
||||||
|
parameter,
|
||||||
|
testContext,
|
||||||
|
modelMetadataProvider,
|
||||||
|
modelMetadata);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(modelBindingResult.IsModelSet, "model is set");
|
||||||
|
Assert.False(modelState.IsValid, "model is valid");
|
||||||
|
|
||||||
|
var entry = modelState[parameter.Name];
|
||||||
|
Assert.NotNull(entry);
|
||||||
|
var message = entry.Errors.Single().ErrorMessage;
|
||||||
|
Assert.Equal("Not valid.", message);
|
||||||
|
|
||||||
|
entry = modelState[ModelNames.CreatePropertyModelName(parameter.Name, nameof(ModelWithIValidatableObject.FirstName))];
|
||||||
|
Assert.NotNull(entry);
|
||||||
|
message = entry.Errors.Single().ErrorMessage;
|
||||||
|
Assert.Equal("FirstName Not valid.", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ModelWithIValidatableObject : IValidatableObject
|
||||||
|
{
|
||||||
|
public string FirstName { get; set; }
|
||||||
|
|
||||||
|
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||||
|
{
|
||||||
|
yield return new ValidationResult("Not valid.");
|
||||||
|
yield return new ValidationResult("FirstName Not valid.", new string[] { nameof(FirstName) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private struct PointStruct
|
private struct PointStruct
|
||||||
{
|
{
|
||||||
public PointStruct(double x, double y)
|
public PointStruct(double x, double y)
|
||||||
|
|
@ -669,7 +775,8 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||||
[BindNever] int bindNeverParam,
|
[BindNever] int bindNeverParam,
|
||||||
[BindRequired] int bindRequiredParam,
|
[BindRequired] int bindRequiredParam,
|
||||||
[Required, StringLength(3)] string requiredAndStringLengthParam,
|
[Required, StringLength(3)] string requiredAndStringLengthParam,
|
||||||
[Display(Name = "My Display Name"), StringLength(3)] string displayNameStringLengthParam)
|
[Display(Name = "My Display Name"), StringLength(3)] string displayNameStringLengthParam,
|
||||||
|
ModelWithIValidatableObject validatableObject)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -682,6 +789,8 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||||
public static ParameterInfo BindRequiredParamInfo
|
public static ParameterInfo BindRequiredParamInfo
|
||||||
=> MyActionMethodInfo.GetParameters()[1];
|
=> MyActionMethodInfo.GetParameters()[1];
|
||||||
|
|
||||||
|
public static ParameterInfo ValidatableObjectParameterInfo => MyActionMethodInfo.GetParameters()[4];
|
||||||
|
|
||||||
public static ParameterInfo GetParameterInfo(string parameterName)
|
public static ParameterInfo GetParameterInfo(string parameterName)
|
||||||
{
|
{
|
||||||
return MyActionMethodInfo
|
return MyActionMethodInfo
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue