Skip to content

Commit

Permalink
Add PropertyPath placeholder (#2134)
Browse files Browse the repository at this point in the history
  • Loading branch information
JeremySkinner committed Aug 8, 2023
1 parent 1f9102d commit 622de25
Show file tree
Hide file tree
Showing 11 changed files with 69 additions and 18 deletions.
4 changes: 4 additions & 0 deletions Changelog.txt
@@ -1,3 +1,7 @@
11.7.0 -
Add additional constructor for combining multiple ValidationResult instances (#2125)
Add PropertyPath placeholder (#2134)

11.6.0 - 4 Jul 2023
Add OnFailurecCreated callback in ValidatorOptions.Global (#2120)
Fix typo in Russian localization (#2102)
Expand Down
23 changes: 22 additions & 1 deletion docs/built-in-validators.md
Expand Up @@ -14,6 +14,7 @@ Example error: *'Surname' must not be empty.*
String format args:
* `{PropertyName}` – Name of the property being validated
* `{PropertyValue}` – Current value of the property
* `{PropertyPath}` - The full path of the property

## NotEmpty Validator
Ensures that the specified property is not null, an empty string or whitespace (or the default value for value types, e.g., 0 for `int`).
Expand All @@ -27,6 +28,7 @@ Example error: *'Surname' should not be empty.*
String format args:
* `{PropertyName}` – Name of the property being validated
* `{PropertyValue}` – Current value of the property
* `{PropertyPath}` - The full path of the property

## NotEqual Validator

Expand All @@ -47,6 +49,7 @@ String format args:
* `{ComparisonValue}` – Value that the property should not equal
* `{ComparisonProperty}` – Name of the property being compared against (if any)
* `{PropertyValue}` – Current value of the property
* `{PropertyPath}` - The full path of the property

Optionally, a comparer can be provided to ensure a specific type of comparison is performed:

Expand Down Expand Up @@ -83,6 +86,7 @@ String format args:
* `{ComparisonValue}` – Value that the property should equal
* `{ComparisonProperty}` – Name of the property being compared against (if any)
* `{PropertyValue}` – Current value of the property
* `{PropertyPath}` - The full path of the property

```csharp
RuleFor(customer => customer.Surname).Equal("Foo", StringComparer.OrdinalIgnoreCase);
Expand Down Expand Up @@ -118,6 +122,7 @@ String format args:
* `{MaxLength}` – Maximum length
* `{TotalLength}` – Number of characters entered
* `{PropertyValue}` – Current value of the property
* `{PropertyPath}` - The full path of the property

## MaxLength Validator
Ensures that the length of a particular string property is no longer than the specified value.
Expand All @@ -135,6 +140,7 @@ String format args:
* `{MaxLength}` – Maximum length
* `{TotalLength}` – Number of characters entered
* `{PropertyValue}` – Current value of the property
* `{PropertyPath}` - The full path of the property

## MinLength Validator
Ensures that the length of a particular string property is longer than the specified value.
Expand All @@ -152,7 +158,7 @@ String format args:
* `{MinLength}` – Minimum length
* `{TotalLength}` – Number of characters entered
* `{PropertyValue}` – Current value of the property

* `{PropertyPath}` - The full path of the property

## Less Than Validator
Ensures that the value of the specified property is less than a particular value (or less than the value of another property).
Expand All @@ -174,6 +180,7 @@ String format args:
* `{ComparisonValue}` – Value to which the property was compared
* `{ComparisonProperty}` – Name of the property being compared against (if any)
* `{PropertyValue}` – Current value of the property
* `{PropertyPath}` - The full path of the property

## Less Than Or Equal Validator
Ensures that the value of the specified property is less than or equal to a particular value (or less than or equal to the value of another property).
Expand All @@ -192,6 +199,7 @@ Notes: Only valid on types that implement `IComparable<T>`
* `{ComparisonValue}` – Value to which the property was compared
* `{ComparisonProperty}` – Name of the property being compared against (if any)
* `{PropertyValue}` – Current value of the property
* `{PropertyPath}` - The full path of the property

## Greater Than Validator
Ensures that the value of the specified property is greater than a particular value (or greater than the value of another property).
Expand All @@ -210,6 +218,7 @@ Notes: Only valid on types that implement `IComparable<T>`
* `{ComparisonValue}` – Value to which the property was compared
* `{ComparisonProperty}` – Name of the property being compared against (if any)
* `{PropertyValue}` – Current value of the property
* `{PropertyPath}` - The full path of the property

## Greater Than Or Equal Validator
Ensures that the value of the specified property is greater than or equal to a particular value (or greater than or equal to the value of another property).
Expand All @@ -228,6 +237,7 @@ Notes: Only valid on types that implement `IComparable<T>`
* `{ComparisonValue}` – Value to which the property was compared
* `{ComparisonProperty}` – Name of the property being compared against (if any)
* `{PropertyValue}` – Current value of the property
* `{PropertyPath}` - The full path of the property

## Predicate Validator
(Also known as `Must`)
Expand All @@ -244,6 +254,7 @@ Example error: *The specified condition was not met for 'Surname'*
String format args:
* `{PropertyName}` – Name of the property being validated
* `{PropertyValue}` – Current value of the property
* `{PropertyPath}` - The full path of the property

Note that there is an additional overload for `Must` that also accepts an instance of the parent object being validated. This can be useful if you want to compare the current property with another property from inside the predicate:

Expand All @@ -265,6 +276,7 @@ String format args:
* `{PropertyName}` – Name of the property being validated
* `{PropertyValue}` – Current value of the property
* `{RegularExpression}` – Regular expression that was not matched
* `{PropertyPath}` - The full path of the property

## Email Validator
Ensures that the value of the specified property is a valid email address format.
Expand All @@ -278,6 +290,7 @@ Example error: *'Email' is not a valid email address.*
String format args:
* `{PropertyName}` – Name of the property being validated
* `{PropertyValue}` – Current value of the property
* `{PropertyPath}` - The full path of the property

The email address validator can work in 2 modes. The default mode just performs a simple check that the string contains an "@" sign which is not at the beginning or the end of the string. This is an intentionally naive check to match the behaviour of ASP.NET Core's `EmailAddressAttribute`, which performs the same check. For the reasoning behind this, see [this post](https://github.com/dotnet/corefx/issues/32740):

Expand All @@ -304,6 +317,7 @@ Example error: *'Credit Card' is not a valid credit card number.*
String format args:
* `{PropertyName}` – Name of the property being validated
* `{PropertyValue}` – Current value of the property
* `{PropertyPath}` - The full path of the property

## Enum Validator
Checks whether a numeric value is valid to be in that enum. This is used to prevent numeric values from being cast to an enum type when the resulting value would be invalid. For example, the following is possible:
Expand Down Expand Up @@ -335,6 +349,7 @@ Example error: *'Error Level' has a range of values which does not include '4'.*
String format args:
* `{PropertyName}` – Name of the property being validated
* `{PropertyValue}` – Current value of the property
* `{PropertyPath}` - The full path of the property

## Enum Name Validator
Checks whether a string is a valid enum name.
Expand All @@ -352,6 +367,7 @@ Example error: *'Error Level' has a range of values which does not include 'Foo'
String format args:
* `{PropertyName}` – Name of the property being validated
* `{PropertyValue}` – Current value of the property
* `{PropertyPath}` - The full path of the property

## Empty Validator
Opposite of the `NotEmpty` validator. Checks if a property value is null, or is the default value for the type.
Expand All @@ -366,6 +382,7 @@ Example error: *'Surname' must be empty.*
String format args:
* `{PropertyName}` – Name of the property being validated
* `{PropertyValue}` – Current value of the property
* `{PropertyPath}` - The full path of the property

## Null Validator
Opposite of the `NotNull` validator. Checks if a property value is null.
Expand All @@ -379,6 +396,7 @@ Example error: *'Surname' must be empty.*
String format args:
* `{PropertyName}` – Name of the property being validated
* `{PropertyValue}` – Current value of the property
* `{PropertyPath}` - The full path of the property

## ExclusiveBetween Validator
Checks whether the property value is in a range between the two specified numbers (exclusive).
Expand All @@ -394,6 +412,7 @@ String format args:
* `{PropertyValue}` – Current value of the property
* `{From}` – Lower bound of the range
* `{To}` – Upper bound of the range
* `{PropertyPath}` - The full path of the property

## InclusiveBetween Validator
Checks whether the property value is in a range between the two specified numbers (inclusive).
Expand All @@ -409,6 +428,7 @@ String format args:
* `{PropertyValue}` – Current value of the property
* `{From}` – Lower bound of the range
* `{To}` – Upper bound of the range
* `{PropertyPath}` - The full path of the property

## PrecisionScale Validator
Checks whether a decimal value has the specified precision and scale.
Expand All @@ -426,6 +446,7 @@ String format args:
* `{ExpectedScale}` – Expected scale
* `{Digits}` – Total number of digits in the property value
* `{ActualScale}` – Actual scale of the property value
* `{PropertyPath}` - The full path of the property

Note that the 3rd parameter of this method is `ignoreTrailingZeros`. When set to `true`, trailing zeros after the decimal point will not count towards the expected number of decimal places.

Expand Down
14 changes: 14 additions & 0 deletions src/FluentValidation.Tests/CustomMessageFormatTester.cs
Expand Up @@ -18,6 +18,7 @@

namespace FluentValidation.Tests;

using System.Collections.Generic;
using System.Linq;
using Validators;
using Xunit;
Expand Down Expand Up @@ -85,4 +86,17 @@ public class CustomMessageFormatTester {
result.Errors.Single().ErrorMessage.ShouldEqual("Was ''");
}

[Fact]
public void Includes_property_path() {
validator.RuleFor(x => x.Surname).NotNull().WithMessage("{PropertyPath}");
validator.RuleForEach(x => x.Orders).NotNull().WithMessage("{PropertyPath}");

var result = validator.Validate(new Person {
Orders = new List<Order> {null}
});

result.Errors[0].ErrorMessage.ShouldEqual("Surname");
result.Errors[1].ErrorMessage.ShouldEqual("Orders[0]");
}

}
4 changes: 3 additions & 1 deletion src/FluentValidation.Tests/ExactLengthValidatorTester.cs
Expand Up @@ -74,17 +74,19 @@ public class ExactLengthValidatorTester {
error.PropertyName.ShouldEqual("Surname");
error.AttemptedValue.ShouldEqual("test");

error.FormattedMessagePlaceholderValues.Count.ShouldEqual(5);
error.FormattedMessagePlaceholderValues.Count.ShouldEqual(6);
error.FormattedMessagePlaceholderValues.ContainsKey("PropertyName").ShouldBeTrue();
error.FormattedMessagePlaceholderValues.ContainsKey("PropertyValue").ShouldBeTrue();
error.FormattedMessagePlaceholderValues.ContainsKey("MinLength").ShouldBeTrue();
error.FormattedMessagePlaceholderValues.ContainsKey("MaxLength").ShouldBeTrue();
error.FormattedMessagePlaceholderValues.ContainsKey("TotalLength").ShouldBeTrue();
error.FormattedMessagePlaceholderValues.ContainsKey("PropertyPath").ShouldBeTrue();

error.FormattedMessagePlaceholderValues["PropertyName"].ShouldEqual("Surname");
error.FormattedMessagePlaceholderValues["PropertyValue"].ShouldEqual("test");
error.FormattedMessagePlaceholderValues["MinLength"].ShouldEqual(2);
error.FormattedMessagePlaceholderValues["MaxLength"].ShouldEqual(2);
error.FormattedMessagePlaceholderValues["TotalLength"].ShouldEqual(4);
error.FormattedMessagePlaceholderValues["PropertyPath"].ShouldEqual("Surname");
}
}
4 changes: 3 additions & 1 deletion src/FluentValidation.Tests/PredicateValidatorTester.cs
Expand Up @@ -73,11 +73,13 @@ public class PredicateValidatorTester {
error.AttemptedValue.ShouldEqual("test");
error.ErrorCode.ShouldEqual("PredicateValidator");

error.FormattedMessagePlaceholderValues.Count.ShouldEqual(2);
error.FormattedMessagePlaceholderValues.Count.ShouldEqual(3);
error.FormattedMessagePlaceholderValues.ContainsKey("PropertyName").ShouldBeTrue();
error.FormattedMessagePlaceholderValues.ContainsKey("PropertyValue").ShouldBeTrue();
error.FormattedMessagePlaceholderValues.ContainsKey("PropertyPath").ShouldBeTrue();

error.FormattedMessagePlaceholderValues["PropertyName"].ShouldEqual("Forename");
error.FormattedMessagePlaceholderValues["PropertyValue"].ShouldEqual("test");
error.FormattedMessagePlaceholderValues["PropertyPath"].ShouldEqual("Forename");
}
}
15 changes: 9 additions & 6 deletions src/FluentValidation/IValidationContext.cs
Expand Up @@ -291,7 +291,7 @@ public ValidationContext(T instanceToValidate, PropertyChain propertyChain, IVal
public void AddFailure(string propertyName, string errorMessage) {
errorMessage.Guard("An error message must be specified when calling AddFailure.", nameof(errorMessage));
errorMessage = MessageFormatter.BuildMessage(errorMessage);
AddFailure(new ValidationFailure(PropertyChain.BuildPropertyName(propertyName ?? string.Empty), errorMessage));
AddFailure(new ValidationFailure(PropertyChain.BuildPropertyPath(propertyName ?? string.Empty), errorMessage));
}

/// <summary>
Expand All @@ -302,7 +302,7 @@ public ValidationContext(T instanceToValidate, PropertyChain propertyChain, IVal
public void AddFailure(string errorMessage) {
errorMessage.Guard("An error message must be specified when calling AddFailure.", nameof(errorMessage));
errorMessage = MessageFormatter.BuildMessage(errorMessage);
AddFailure(new ValidationFailure(PropertyName, errorMessage));
AddFailure(new ValidationFailure(PropertyPath, errorMessage));
}

private Func<ValidationContext<T>, string> _displayNameFunc;
Expand All @@ -313,15 +313,18 @@ public ValidationContext(T instanceToValidate, PropertyChain propertyChain, IVal
public string DisplayName => _displayNameFunc(this);

/// <summary>
/// The full name of the current property being validated.
/// The full path of the current property being validated.
/// If accessed inside a child validator, this will include the parent's path too.
/// </summary>
public string PropertyName { get; private set; }
public string PropertyPath { get; private set; }

[Obsolete("This property has been deprecated due to its misleading name. Use the PropertyPath property instead, which returns the same value.")]
public string PropertyName => PropertyPath;

internal string RawPropertyName { get; private set; }

internal void InitializeForPropertyValidator(string propertyName, Func<ValidationContext<T>, string> displayNameFunc, string rawPropertyName) {
PropertyName = propertyName;
internal void InitializeForPropertyValidator(string propertyPath, Func<ValidationContext<T>, string> displayNameFunc, string rawPropertyName) {
PropertyPath = propertyPath;
_displayNameFunc = displayNameFunc;
RawPropertyName = rawPropertyName;
}
Expand Down
6 changes: 3 additions & 3 deletions src/FluentValidation/Internal/CollectionPropertyRule.cs
Expand Up @@ -97,7 +97,7 @@ public CollectionPropertyRule(MemberInfo member, Func<T, IEnumerable<TElement>>
}

// Construct the full name of the property, taking into account overriden property names and the chain (if we're in a nested validator)
string propertyName = context.PropertyChain.BuildPropertyName(PropertyName ?? displayName);
string propertyName = context.PropertyChain.BuildPropertyPath(PropertyName ?? displayName);

if (string.IsNullOrEmpty(propertyName)) {
propertyName = InferPropertyName(Expression);
Expand Down Expand Up @@ -164,9 +164,9 @@ public CollectionPropertyRule(MemberInfo member, Func<T, IEnumerable<TElement>>
context.PropertyChain.AddIndexer(indexer, useDefaultIndexFormat);

var valueToValidate = element;
var propertyNameToValidate = context.PropertyChain.ToString();
var propertyPath = context.PropertyChain.ToString();
var totalFailuresInner = context.Failures.Count;
context.InitializeForPropertyValidator(propertyNameToValidate, GetDisplayName, PropertyName);
context.InitializeForPropertyValidator(propertyPath, GetDisplayName, PropertyName);

foreach (var component in filteredValidators) {
context.MessageFormatter.Reset();
Expand Down
2 changes: 1 addition & 1 deletion src/FluentValidation/Internal/MessageBuilderContext.cs
Expand Up @@ -37,7 +37,7 @@ public IPropertyValidator PropertyValidator

// public IValidationRule<T> Rule => _innerContext.Rule;

public string PropertyName => _innerContext.PropertyName;
public string PropertyName => _innerContext.PropertyPath;

public string DisplayName => _innerContext.DisplayName;

Expand Down
6 changes: 5 additions & 1 deletion src/FluentValidation/Internal/PropertyChain.cs
Expand Up @@ -142,10 +142,14 @@ public class PropertyChain {
return ToString().StartsWith(parentChain.ToString());
}

[Obsolete("BuildPropertyName is deprecated due to its misleading name. Use BuildPropertyPath instead which does the same thing.")]
public string BuildPropertyName(string propertyName)
=> BuildPropertyPath(propertyName);

/// <summary>
/// Builds a property path.
/// </summary>
public string BuildPropertyName(string propertyName) {
public string BuildPropertyPath(string propertyName) {
if (_memberNames.Count == 0) {
return propertyName;
}
Expand Down
6 changes: 3 additions & 3 deletions src/FluentValidation/Internal/PropertyRule.cs
Expand Up @@ -90,11 +90,11 @@ TProperty PropertyFunc(T instance)
}

// Construct the full name of the property, taking into account overriden property names and the chain (if we're in a nested validator)
string propertyName = context.PropertyChain.BuildPropertyName(PropertyName ?? displayName);
string propertyPath = context.PropertyChain.BuildPropertyPath(PropertyName ?? displayName);

// Ensure that this rule is allowed to run.
// The validatselector has the opportunity to veto this before any of the validators execute.
if (!context.Selector.CanExecute(this, propertyName, context)) {
if (!context.Selector.CanExecute(this, propertyPath, context)) {
return;
}

Expand All @@ -118,7 +118,7 @@ TProperty PropertyFunc(T instance)
var cascade = CascadeMode;
var accessor = new Lazy<TProperty>(() => PropertyFunc(context.InstanceToValidate), LazyThreadSafetyMode.None);
var totalFailures = context.Failures.Count;
context.InitializeForPropertyValidator(propertyName, GetDisplayName, PropertyName);
context.InitializeForPropertyValidator(propertyPath, GetDisplayName, PropertyName);

// Invoke each validator and collect its results.
foreach (var component in Components) {
Expand Down
3 changes: 2 additions & 1 deletion src/FluentValidation/Internal/RuleBase.cs
Expand Up @@ -286,6 +286,7 @@ public string GetDisplayName(ValidationContext<T> context)
protected void PrepareMessageFormatterForValidationError(ValidationContext<T> context, TValue value) {
context.MessageFormatter.AppendPropertyName(context.DisplayName);
context.MessageFormatter.AppendPropertyValue(value);
context.MessageFormatter.AppendArgument("PropertyPath", context.PropertyPath);

// If there's a collection index cached in the root context data then add it
// to the message formatter. This happens when a child validator is executed
Expand All @@ -312,7 +313,7 @@ public string GetDisplayName(ValidationContext<T> context)
? MessageBuilder(new MessageBuilderContext<T, TValue>(context, value, component))
: component.GetErrorMessage(context, value);

var failure = new ValidationFailure(context.PropertyName, error, value);
var failure = new ValidationFailure(context.PropertyPath, error, value);

failure.FormattedMessagePlaceholderValues = new Dictionary<string, object>(context.MessageFormatter.PlaceholderValues);
failure.ErrorCode = component.ErrorCode ?? ValidatorOptions.Global.ErrorCodeResolver(component.Validator);
Expand Down

0 comments on commit 622de25

Please sign in to comment.