Skip to content

Commit

Permalink
Switch to the new Transform syntax. (#1613)
Browse files Browse the repository at this point in the history
  • Loading branch information
JeremySkinner committed Jan 31, 2021
1 parent 6042b4f commit 350c6c6
Show file tree
Hide file tree
Showing 10 changed files with 193 additions and 25 deletions.
3 changes: 3 additions & 0 deletions Changelog.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
9.5.0 - 31 January 2021
Introduce new syntax for applying transformations and deprecate the old syntax (#1613)

9.4.0 - 14 January 2021
ChildRules now work as expected when inside a ruleset (#1597)
Added ImplicitlyValidateRootCollectionElements option to MVC integration (#1585)
Expand Down
37 changes: 20 additions & 17 deletions docs/transform.md
Original file line number Diff line number Diff line change
@@ -1,36 +1,39 @@
# Transforming Values

As of FluentValidation 9.0, you can use the `Transform` method to transform a property value prior to validation being performed against it. For example, if you have property of type `string` that atually contains numeric input, you could use `Transform` to convert the string value to a number.
As of FluentValidation 9.5, you can apply a transformation to a property value prior to validation being performed against it. For example, if you have property of type `string` that actually contains numeric input, you could apply a transformation to convert the string value to a number.


```csharp
RuleFor(x => x.SomeStringProperty)
.Transform(value => int.TryParse(value, out int val) ? (int?) val : null)
Transform(from: x => x.SomeStringProperty, to: value => int.TryParse(value, out int val) ? (int?) val : null)
.GreaterThan(10);
```

This rule transforms the value from a `string` to an nullable `int` (returning `null` if the value couldn't be converted). A greater-than check is then performed on the resulting value.
This rule transforms the value from a `string` to an nullable `int` (returning `null` if the value couldn't be converted). A greater-than check is then performed on the resulting value.

Syntactically this is not particularly nice to read, so this can be cleaned up by using an extension method:
Syntactically this is not particularly nice to read, so the logic for the transformation can optionally be moved into a separate method:

```csharp
public static class ValidationExtensions {
public static IRuleBuilder<T, int?> TransformToInt<T>(this IRuleBuilderInitial<T, string> ruleBuilder) {
return ruleBuilder.Transform(value => int.TryParse(value, out int val) ? (int?) val : null);
}
}
Transform(x => x.SomeStringProperty, StringToNullableInt)
.GreaterThan(10);

int? StringToNullableInt(string value)
=> int.TryParse(value, out int val) ? (int?) val : null;

```

The rule can then be written as:
This syntax is available in FluentValidation 9.5 and newer.

There is also a `TransformForEach` method available, which performs the transformation against each item in a collection.


# Transforming Values (9.0 - 9.4)

Prior to FluentValidation 9.5, you can use the `Transform` method after a call to `RuleFor` to achieve the same result.

```csharp
RuleFor(x => x.SomeStringProperty)
.TransformToInt()
.Transform(value => int.TryParse(value, out int val) ? (int?) val : null)
.GreaterThan(10);
```


```eval_rst
.. note::
FluentValidation 8.x supported a limited version of the Transform method that could only be used to perform transformations on the same type (e.g., if the property is a string, the result of the transformation must also be a string). FluentValidation 9.0 allows transformations to be performed that change the type.
```
This `Transform` method is marked as obsolete as of FluentValidation 9.5 and is removed in FluentValidation 10.0. In newer versions of FluentValidation the transformation should be applied by calling `Transform` as the first method in the chain (see above).
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<VersionPrefix>9.4.0</VersionPrefix>
<VersionPrefix>9.5.0</VersionPrefix>
<VersionSuffix></VersionSuffix>
<!-- Use CI build number as version suffix (if defined) -->
<!--<VersionSuffix Condition="'$(GITHUB_RUN_NUMBER)'!=''">ci-$(GITHUB_RUN_NUMBER)</VersionSuffix>-->
Expand Down
70 changes: 66 additions & 4 deletions src/FluentValidation.Tests/TransformTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ namespace FluentValidation.Tests {
using Xunit;

public class TransformTests {
#pragma warning disable 618

[Fact]
public void Transforms_property_value() {
public void Transforms_property_value_old() {
var validator = new InlineValidator<Person>();
validator.RuleFor(x => x.Surname).Transform(name => "foo" + name).Equal("foobar");

Expand All @@ -32,7 +34,7 @@ public class TransformTests {
}

[Fact]
public void Transforms_property_value_to_another_type() {
public void Transforms_property_value_to_another_type_old() {
var validator = new InlineValidator<Person>();
validator.RuleFor(x => x.Surname).Transform(name => 1).GreaterThan(10);

Expand All @@ -42,7 +44,7 @@ public class TransformTests {
}

[Fact]
public void Transforms_collection_element() {
public void Transforms_collection_element_old() {
var validator = new InlineValidator<Person>();
validator.RuleForEach(x => x.Orders)
.Transform(order => order.Amount)
Expand All @@ -53,7 +55,7 @@ public class TransformTests {
}

[Fact]
public async Task Transforms_collection_element_async() {
public async Task Transforms_collection_element_async_old() {
var validator = new InlineValidator<Person>();
validator.RuleForEach(x => x.Orders)
.Transform(order => order.Amount)
Expand All @@ -62,6 +64,66 @@ public class TransformTests {
var result = await validator.ValidateAsync(new Person() {Orders = new List<Order> {new Order()}});
result.Errors.Count.ShouldEqual(1);
}
#pragma warning restore 618

[Fact]
public void Transforms_property_value() {
var validator = new InlineValidator<Person>();
validator.Transform(x => x.Surname, name => "foo" + name).Equal("foobar");

var result = validator.Validate(new Person {Surname = "bar"});
result.IsValid.ShouldBeTrue();
}

[Fact]
public void Transforms_property_value_to_another_type() {
var validator = new InlineValidator<Person>();
validator.Transform(x => x.Surname, name => 1).GreaterThan(10);

var result = validator.Validate(new Person {Surname = "bar"});
result.IsValid.ShouldBeFalse();
result.Errors[0].ErrorCode.ShouldEqual("GreaterThanValidator");
}

[Fact]
public void Transforms_collection_element() {
var validator = new InlineValidator<Person>();
validator.TransformForEach(x => x.Orders, to: order => order.Amount)
.GreaterThan(0);

var result = validator.Validate(new Person() {Orders = new List<Order> {new Order()}});
result.Errors.Count.ShouldEqual(1);
}

[Fact]
public async Task Transforms_collection_element_async() {
var validator = new InlineValidator<Person>();
validator.TransformForEach(x => x.Orders, to: order => order.Amount)
.MustAsync((amt, token) => Task.FromResult(amt > 0));

var result = await validator.ValidateAsync(new Person() {Orders = new List<Order> {new Order()}});
result.Errors.Count.ShouldEqual(1);
}

[Fact]
public void Transform_collection_index_builder_and_condition() {
var validator = new InlineValidator<Person>();
validator.TransformForEach(x => x.Orders, to: order => order.Amount)
.Where(amt => amt < 20)
.OverrideIndexer((person, collection, amt, numericIndex) => $"[{numericIndex}_{amt}]")
.LessThan(10);

var result = validator.Validate(new Person {
Orders = new List<Order> {
new Order {Amount = 21}, // Fails condition, skips validation
new Order {Amount = 12}, // Passes condition, fails validation
new Order {Amount = 9}, // Passes condition, passes validation
}
});

result.Errors.Count.ShouldEqual(1);
result.Errors[0].PropertyName.ShouldEqual("Orders[1_12]");
}

}
}
39 changes: 36 additions & 3 deletions src/FluentValidation/AbstractValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -196,16 +196,33 @@ public abstract class AbstractValidator<T> : IValidator<T>, IEnumerable<IValidat
/// <returns>an IRuleBuilder instance on which validators can be defined</returns>
public IRuleBuilderInitial<T, TProperty> RuleFor<TProperty>(Expression<Func<T, TProperty>> expression) {
expression.Guard("Cannot pass null to RuleFor", nameof(expression));
// If rule-level caching is enabled, then bypass the expression-level cache.
// Otherwise we essentially end up caching expressions twice unnecessarily.
var rule = PropertyRule.Create(expression, () => CascadeMode);
AddRule(rule);
var ruleBuilder = new RuleBuilder<T, TProperty>(rule, this);
return ruleBuilder;
}

/// <summary>
/// Invokes a rule for each item in the collection
/// Defines a validation rule for a specify property and transform it to a different type.
/// </summary>
/// <example>
/// Transform(x => x.OrderNumber, to: orderNumber => orderNumber.ToString())...
/// </example>
/// <typeparam name="TProperty">The type of property being validated</typeparam>
/// <typeparam name="TTransformed">The type after the transformer has been applied</typeparam>
/// <param name="from">The expression representing the property to transform</param>
/// <param name="to">Function to transform the property value into a different type</param>
/// <returns>an IRuleBuilder instance on which validators can be defined</returns>
public IRuleBuilderInitial<T, TTransformed> Transform<TProperty, TTransformed>(Expression<Func<T, TProperty>> from, Func<TProperty, TTransformed> to) {
from.Guard("Cannot pass null to Transform", nameof(from));
var rule = PropertyRule.Create(from, to, () => CascadeMode);
AddRule(rule);
var ruleBuilder = new RuleBuilder<T, TTransformed>(rule, this);
return ruleBuilder;
}

/// <summary>
/// Invokes a rule for each item in the collection.
/// </summary>
/// <typeparam name="TElement">Type of property</typeparam>
/// <param name="expression">Expression representing the collection to validate</param>
Expand All @@ -218,6 +235,22 @@ public abstract class AbstractValidator<T> : IValidator<T>, IEnumerable<IValidat
return ruleBuilder;
}

/// <summary>
/// Invokes a rule for each item in the collection, transforming the element from one type to another.
/// </summary>
/// <typeparam name="TElement">Type of property</typeparam>
/// <typeparam name="TTransformed">The type after the transformer has been applied</typeparam>
/// <param name="expression">Expression representing the collection to validate</param>
/// <param name="to">Function to transform the collection element into a different type</param>
/// <returns>An IRuleBuilder instance on which validators can be defined</returns>
public IRuleBuilderInitialCollection<T, TTransformed> TransformForEach<TElement, TTransformed>(Expression<Func<T, IEnumerable<TElement>>> expression, Func<TElement, TTransformed> to) {
expression.Guard("Cannot pass null to RuleForEach", nameof(expression));
var rule = CollectionPropertyRule<T, TTransformed>.CreateTransformed<TElement>(expression, to, () => CascadeMode);
AddRule(rule);
var ruleBuilder = new RuleBuilder<T, TTransformed>(rule, this);
return ruleBuilder;
}

/// <summary>
/// Defines a RuleSet that can be used to group together several validators.
/// </summary>
Expand Down
3 changes: 3 additions & 0 deletions src/FluentValidation/FluentValidation.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
<PackageReleaseNotes>
FluentValidation 9 is a major release. Please read the upgrade notes at https://docs.fluentvalidation.net/en/latest/upgrading-to-9.html

Changes in 9.5.0:
* Introduce new syntax for applying transformations and deprecate the old syntax

Changes in 9.4.0:
* ChildRules now work as expected when inside a ruleset
* Added ImplicitlyValidateRootCollectionElements option to MVC integration
Expand Down
32 changes: 32 additions & 0 deletions src/FluentValidation/Internal/CollectionPropertyRule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,34 @@ public class CollectionPropertyRule<T, TElement> : PropertyRule {
return new CollectionPropertyRule<T, TElement>(member, compiled.CoerceToNonGeneric(), expression, cascadeModeThunk, typeof(TElement), typeof(T));
}

/// <summary>
/// Creates a new property rule from a lambda expression.
/// </summary>
internal static CollectionPropertyRule<T, TElement> CreateTransformed<TOriginal>(Expression<Func<T, IEnumerable<TOriginal>>> expression, Func<TOriginal, TElement> transformer, Func<CascadeMode> cascadeModeThunk) {
var member = expression.GetMember();
var compiled = expression.Compile();

object PropertyFunc(object instance) =>
compiled((T)instance).Select(transformer);

return new CollectionPropertyRule<T, TElement>(member, PropertyFunc, expression, cascadeModeThunk, typeof(TElement), typeof(T));
}

/// <summary>
/// Creates a new property rule from a lambda expression.
/// </summary>
internal static CollectionPropertyRule<T, TElement> CreateTransformed<TOriginal>(Expression<Func<T, IEnumerable<TOriginal>>> expression, Func<T, TOriginal, TElement> transformer, Func<CascadeMode> cascadeModeThunk) {
var member = expression.GetMember();
var compiled = expression.Compile();

object PropertyFunc(object instance) {
var actualInstance = (T) instance;
return compiled((T) instance).Select(element => transformer(actualInstance, element));
}

return new CollectionPropertyRule<T, TElement>(member, PropertyFunc, expression, cascadeModeThunk, typeof(TOriginal), typeof(T));
}

/// <summary>
/// Invokes the validator asynchronously
/// </summary>
Expand Down Expand Up @@ -123,9 +151,11 @@ public class CollectionPropertyRule<T, TElement> : PropertyRule {
object valueToValidate = element;
#pragma warning disable 618
if (Transformer != null) {
valueToValidate = Transformer(element);
}
#pragma warning restore 618
var newPropertyContext = new PropertyValidatorContext(newContext, this, newContext.PropertyChain.ToString(), valueToValidate);
newPropertyContext.MessageFormatter.AppendArgument("CollectionIndex", index);
Expand Down Expand Up @@ -216,9 +246,11 @@ public class CollectionPropertyRule<T, TElement> : PropertyRule {

object valueToValidate = element;

#pragma warning disable 618
if (Transformer != null) {
valueToValidate = Transformer(element);
}
#pragma warning restore 618

var newPropertyContext = new PropertyValidatorContext(newContext, this, newContext.PropertyChain.ToString(), valueToValidate);
newPropertyContext.MessageFormatter.AppendArgument("CollectionIndex", index);
Expand Down
29 changes: 29 additions & 0 deletions src/FluentValidation/Internal/PropertyRule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,32 @@ public class PropertyRule : IValidationRule {
return new PropertyRule(member, compiled.CoerceToNonGeneric(), expression, cascadeModeThunk, typeof(TProperty), typeof(T));
}

/// <summary>
/// Creates a new property rule from a lambda expression.
/// </summary>
internal static PropertyRule Create<T, TProperty, TTransformed>(Expression<Func<T, TProperty>> expression, Func<TProperty, TTransformed> transformer, Func<CascadeMode> cascadeModeThunk, bool bypassCache = false) {
var member = expression.GetMember();
var compiled = AccessorCache<T>.GetCachedAccessor(member, expression, bypassCache);

object PropertyFunc(object instance)
=> transformer(compiled((T)instance));

return new PropertyRule(member, PropertyFunc, expression, cascadeModeThunk, typeof(TProperty), typeof(T));
}

/// <summary>
/// Creates a new property rule from a lambda expression.
/// </summary>
internal static PropertyRule Create<T, TProperty, TTransformed>(Expression<Func<T, TProperty>> expression, Func<T, TProperty, TTransformed> transformer, Func<CascadeMode> cascadeModeThunk, bool bypassCache = false) {
var member = expression.GetMember();
var compiled = AccessorCache<T>.GetCachedAccessor(member, expression, bypassCache);

object PropertyFunc(object instance)
=> transformer((T)instance, compiled((T)instance));

return new PropertyRule(member, PropertyFunc, expression, cascadeModeThunk, typeof(TProperty), typeof(T));
}

/// <summary>
/// Adds a validator to the rule.
/// </summary>
Expand Down Expand Up @@ -241,6 +267,7 @@ public class PropertyRule : IValidationRule {
/// </summary>
public List<IValidationRule> DependentRules { get; }

[Obsolete("This property will be removed in FluentValidation 10")]
public Func<object, object> Transformer { get; set; }

/// <summary>
Expand Down Expand Up @@ -499,7 +526,9 @@ public class PropertyRule : IValidationRule {
/// <returns>The value to be validated</returns>
internal virtual object GetPropertyValue(object instanceToValidate) {
var value = PropertyFunc(instanceToValidate);
#pragma warning disable 618
if (Transformer != null) value = Transformer(value);
#pragma warning restore 618
return value;
}

Expand Down
1 change: 1 addition & 0 deletions src/FluentValidation/Internal/RuleBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ public class RuleBuilder<T, TProperty> : IRuleBuilderOptions<T, TProperty>, IRul
return this;
}

[Obsolete("Use RuleFor(x => x.Property, transformer) instead. This method will be removed in FluentValidation 10.")]
public IRuleBuilderInitial<T, TNew> Transform<TNew>(Func<TProperty, TNew> transformationFunc) {
if (transformationFunc == null) throw new ArgumentNullException(nameof(transformationFunc));
Rule.Transformer = transformationFunc.CoerceToNonGeneric();
Expand Down
2 changes: 2 additions & 0 deletions src/FluentValidation/Syntax.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public interface IRuleBuilderInitial<T, out TProperty> : IRuleBuilder<T, TProper
/// <typeparam name="TNew"></typeparam>
/// <param name="transformationFunc"></param>
/// <returns></returns>
[Obsolete("Use RuleFor(x => x.Property, transformer) instead. This method will be removed in FluentValidation 10.")]
IRuleBuilderInitial<T, TNew> Transform<TNew>(Func<TProperty, TNew> transformationFunc);
}

Expand Down Expand Up @@ -96,6 +97,7 @@ public interface IRuleBuilderInitialCollection<T, TElement> : IRuleBuilder<T, TE
/// </summary>
/// <param name="transformationFunc"></param>
/// <returns></returns>
[Obsolete("Use RuleFor(x => x.Property, transformer) instead. This method will be removed in FluentValidation 10.")]
IRuleBuilderInitial<T, TNew> Transform<TNew>(Func<TElement, TNew> transformationFunc);
}

Expand Down

0 comments on commit 350c6c6

Please sign in to comment.