Skip to content
This repository has been archived by the owner on Dec 14, 2018. It is now read-only.

Commit

Permalink
Fix behavior of StartsWithPrefix
Browse files Browse the repository at this point in the history
This undoes a behavior change introduced in
7b18d1d.

The intent was to have ClearValidationState do the right thing for a case
where a collection was bound to the empty prefix, and then used again with
TryUpdateModel.

This change was implemented by saying that a key like "[0].Foo" is a match
for the prefix of "Foo". This isn't really right, and it's only
interesting for the ClearValidationState case.

The problem is that we don't know what the keys look like for a
collection. We can assume that they start with [0] but that's not really a
guarantee, it's a guess.

This change fixes the behavior of StartsWithModel, and move the
responsibility for this case back into ClearValidationState.

This change also removes the call to ClearValidationState from
TryUpdateModel. If you need this behavior, then call ClearValidationState
manually. Trying to bind and then re-bind a model object isn't really what
we intend.
  • Loading branch information
rynowak committed Feb 17, 2016
1 parent fd6d28d commit b557ca5
Show file tree
Hide file tree
Showing 8 changed files with 149 additions and 122 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -683,49 +683,30 @@ public static bool StartsWithPrefix(string prefix, string key)

if (prefix.Length == 0)
{
// Everything is prefixed by the empty string
// Everything is prefixed by the empty string.
return true;
}

if (key.Length < prefix.Length)
if (prefix.Length > key.Length)
{
return false;
return false; // Not long enough.
}

var subKeyIndex = 0;
if (!key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
if (key[0] == '[')
{
subKeyIndex = key.IndexOf('.') + 1;

if (string.Compare(key, subKeyIndex, prefix, 0, prefix.Length, StringComparison.OrdinalIgnoreCase) != 0)
{
return false;
}
else if (prefix.Length == (key.Length - subKeyIndex))
{
// prefix == subKey
return true;
}
}
else
{
return false;
}
return false;
}
else if (key.Length == prefix.Length)

if (key.Length == prefix.Length)
{
// key == prefix
// Exact match
return true;
}

var charAfterPrefix = key[subKeyIndex + prefix.Length];
switch (charAfterPrefix)
var charAfterPrefix = key[prefix.Length];
if (charAfterPrefix == '.' || charAfterPrefix == '[')
{
case '[':
case '.':
return true;
return true;
}

return false;
Expand Down
13 changes: 2 additions & 11 deletions src/Microsoft.AspNetCore.Mvc.Core/ControllerBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1429,21 +1429,12 @@ public virtual bool TryValidateModel(
{
throw new ArgumentNullException(nameof(model));
}

var modelName = prefix ?? string.Empty;

// Clear ModelStateDictionary entries for the model so that it will be re-validated.
ModelBindingHelper.ClearValidationStateForModel(
model.GetType(),
ModelState,
MetadataProvider,
modelName);


ObjectValidator.Validate(
ControllerContext,
new CompositeModelValidatorProvider(ControllerContext.ValidatorProviders),
validationState: null,
prefix: prefix,
prefix: prefix ?? string.Empty,
model: model);
return ModelState.IsValid;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public ElementalValueProvider(string key, string value, CultureInfo culture)

public bool ContainsPrefix(string prefix)
{
return PrefixContainer.IsPrefixMatch(prefix, Key);
return ModelStateDictionary.StartsWithPrefix(prefix, Key);
}

public ValueProviderResult GetValue(string key)
Expand Down
40 changes: 1 addition & 39 deletions src/Microsoft.AspNetCore.Mvc.Core/Internal/PrefixContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,45 +173,7 @@ private static void GetKeyFromNonEmptyPrefix(string prefix, string entry, IDicti
}
}

public static bool IsPrefixMatch(string prefix, string testString)
{
if (testString == null)
{
return false;
}

if (prefix.Length == 0)
{
return true; // shortcut - non-null testString matches empty prefix
}

if (prefix.Length > testString.Length)
{
return false; // not long enough
}

if (!testString.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
return false; // prefix doesn't match
}

if (testString.Length == prefix.Length)
{
return true; // exact match
}

// invariant: testString.Length > prefix.Length
switch (testString[prefix.Length])
{
case '.':
case '[':
return true; // known delimiters

default:
return false; // not known delimiter
}
}

// This is tightly coupled to the definition at ModelStateDictionary.StartsWithPrefix
private int BinarySearch(string prefix)
{
var start = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -513,10 +513,7 @@ public static async Task<bool> TryUpdateModelAsync(
}

var modelMetadata = metadataProvider.GetMetadataForType(modelType);

// Clear ModelStateDictionary entries for the model so that it will be re-validated.
var modelState = actionContext.ModelState;
ClearValidationStateForModel(modelType, modelState, metadataProvider, prefix);

var operationBindingContext = new OperationBindingContext
{
Expand Down Expand Up @@ -659,27 +656,76 @@ public static void ClearValidationStateForModel(
throw new ArgumentNullException(nameof(metadataProvider));
}

// If modelkey is empty, we need to iterate through properties (obtained from ModelMetadata) and
// clear validation state for all entries in ModelStateDictionary that start with each property name.
// If modelkey is non-empty, clear validation state for all entries in ModelStateDictionary
// that start with modelKey
ClearValidationStateForModel(metadataProvider.GetMetadataForType(modelType), modelstate, modelKey);
}

/// <summary>
/// Clears <see cref="ModelStateDictionary"/> entries for <see cref="ModelMetadata"/>.
/// </summary>
/// <param name="modelMetadata">The <see cref="ModelMetadata"/>.</param>
/// <param name="modelKey">The entry to clear. </param>
/// <param name="modelMetadataProvider">The <see cref="IModelMetadataProvider"/>.</param>
public static void ClearValidationStateForModel(
ModelMetadata modelMetadata,
ModelStateDictionary modelstate,
string modelKey)
{
if (modelMetadata == null)
{
throw new ArgumentNullException(nameof(modelMetadata));
}

if (modelstate == null)
{
throw new ArgumentNullException(nameof(modelstate));
}

if (string.IsNullOrEmpty(modelKey))
{
var modelMetadata = metadataProvider.GetMetadataForType(modelType);
var elementMetadata = modelMetadata.ElementMetadata;
if (elementMetadata != null)
// If model key is empty, we have to do a best guess to try and clear the appropriate
// keys. Clearing the empty prefix would clear the state of ALL entries, which might wipe out
// data from other models.
if (modelMetadata.IsEnumerableType)
{
modelMetadata = elementMetadata;
// We expect that any key beginning with '[' is an index. We can't just infer the indexes
// used, so we clear all keys that look like <empty prefix -> index>.
//
// In the unlikely case that multiple top-level collections where bound to the empty prefix,
// you're just out of luck.
foreach (var kvp in modelstate)
{
if (kvp.Key.Length > 0 && kvp.Key[0] == '[')
{
// Starts with an indexer
kvp.Value.Errors.Clear();
kvp.Value.ValidationState = ModelValidationState.Unvalidated;
}
}
}

foreach (var property in modelMetadata.Properties)
else if (modelMetadata.IsComplexType)
{
var childKey = property.BinderModelName ?? property.PropertyName;
modelstate.ClearValidationState(childKey);
foreach (var property in modelMetadata.Properties)
{
modelstate.ClearValidationState(property.BinderModelName ?? property.PropertyName);
}
}
else
{
// Simple types bind to a single entry. So clear the entry with the empty-key, in the
// unlikely event that it has errors.
var entry = modelstate[string.Empty];
if (entry != null)
{
entry.Errors.Clear();
entry.ValidationState = ModelValidationState.Unvalidated;
}
}
}
else
{
// If model key is non-empty, we just want to clear all keys with that prefix. We expect
// model binding to have only used this key (and suffixes) for all entries related to
// this model.
modelstate.ClearValidationState(modelKey);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,8 +245,7 @@ public void GetValidationState_ReturnsValidationStateForKey_IgnoresChildren()
[Theory]
[InlineData("foo")]
[InlineData("foo.bar")]
[InlineData("[0].foo.bar")]
[InlineData("[0].foo.bar[0]")]
[InlineData("foo[bar]")]
public void GetFieldValidationState_ReturnsInvalidIfKeyChildContainsErrors(string key)
{
// Arrange
Expand All @@ -263,8 +262,7 @@ public void GetFieldValidationState_ReturnsInvalidIfKeyChildContainsErrors(strin
[Theory]
[InlineData("foo")]
[InlineData("foo.bar")]
[InlineData("[0].foo.bar")]
[InlineData("[0].foo.bar[0]")]
[InlineData("foo[bar]")]
public void GetFieldValidationState_ReturnsValidIfModelStateDoesNotContainErrors(string key)
{
// Arrange
Expand Down Expand Up @@ -505,19 +503,36 @@ public void GetFieldValidity_ReturnsUnvalidated_IfAnyItemInSubtreeIsInvalid()
}

[Theory]
[InlineData("")]
[InlineData("user")]
[InlineData("user.Age")]
[InlineData("product")]
public void GetFieldValidity_ReturnsInvalid_IfAllKeysAreValidatedAndAnyEntryIsInvalid(string key)
{
// Arrange
var dictionary = new ModelStateDictionary();
dictionary["user.Address"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid };
dictionary["user.Name"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid };
dictionary.AddModelError("user.Age", "Age is not a valid int");

// Act
var validationState = dictionary.GetFieldValidationState(key);

// Assert
Assert.Equal(ModelValidationState.Invalid, validationState);
}

[Theory]
[InlineData("")]
[InlineData("[0]")]
[InlineData("[0].product")]
public void GetFieldValidity_ReturnsInvalid_IfAllKeysAreValidatedAndAnyEntryIsInvalid_Collection(string key)
{
// Arrange
var dictionary = new ModelStateDictionary();

dictionary["[0].product.Name"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid };
dictionary["[0].product.Age[0]"] = new ModelStateEntry { ValidationState = ModelValidationState.Valid };
dictionary.AddModelError("[1].product.Name", "Name is invalid");
dictionary.AddModelError("[0].product.Name", "Name is invalid");

// Act
var validationState = dictionary.GetFieldValidationState(key);
Expand Down
Loading

0 comments on commit b557ca5

Please sign in to comment.