Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce new syntax for Transform, deprecate the old version #1613

Merged
merged 1 commit into from
Jan 31, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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