Skip to content

Commit

Permalink
Merge pull request #11 from luboshl/feature/skip-validation
Browse files Browse the repository at this point in the history
Skip validation
  • Loading branch information
luboshl authored Jul 17, 2024
2 parents ce18f14 + 5c64f81 commit d039656
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 13 deletions.
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,3 +217,48 @@ class WidgetWithCustomValidation : Widget, IValidatableObject
}
}
```

## Skip validation

You can set property to be skipped when validation is performed by setting `SkipValidationAttribute`
on it. If you want to perform validation on the property, but not to validate it recursively
(validate properties of the property), you can set `SkipRecursionAttribute` on it.

When you use `SkipValidationAttribute` on a property, recursion is also skipped for that property.

### Examples

```csharp
class Model
{
[SkipValidation]
public string Name => throw new InvalidOperationException();

public ValidChild ValidChild { get; set; }

public ValidChildWithInvalidSkippedProperty ValidChildWithInvalidSkippedProperty { get; set; }

[SkipRecursion]
public InvalidChild InvalidChild { get; set; }
}

class ValidChild
{
[Range(10, 100)]
public int TenOrMore { get; set; } = 10;
}

class ValidChildWithInvalidSkippedProperty
{
// Property is invalid but is skipped
[SkipValidation]
public string Name { get; set; } = null!;
}

class InvalidChild
{
// Property is invalid
[Range(10, 100)]
public int TenOrMore { get; set; } = 3;
}
```
13 changes: 13 additions & 0 deletions src/MiniValidationPlus/SkipValidationAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;

namespace MiniValidationPlus;

/// <summary>
/// Indicates that a property should be ignored during validation when using
/// <see cref="MiniValidatorPlus.TryValidate{TTarget}(TTarget, out System.Collections.Generic.IDictionary{string, string[]})"/>.
/// Note that also recursive validation will be ignored on the property.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class SkipValidationAttribute : Attribute
{
}
19 changes: 14 additions & 5 deletions src/MiniValidationPlus/TypeDetailsCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ private void Visit(Type type, HashSet<Type> visited, ref bool requiresAsync)
continue;
}

var (validationAttributes, displayAttribute, skipRecursionAttribute) = TypeDetailsCache.GetPropertyAttributes(primaryCtorParams, property);
var (validationAttributes, displayAttribute, skipRecursionAttribute, skipValidationAttribute)
= TypeDetailsCache.GetPropertyAttributes(primaryCtorParams, property);
validationAttributes ??= Array.Empty<ValidationAttribute>();

#if NET6_0_OR_GREATER
Expand All @@ -102,6 +103,7 @@ private void Visit(Type type, HashSet<Type> visited, ref bool requiresAsync)

var hasValidationOnProperty = validationAttributes.Length > 0 || isNonNullableReferenceType;
var hasSkipRecursionOnProperty = skipRecursionAttribute is not null;
var hasSkipValidationAttribute = skipValidationAttribute is not null;
var enumerableType = GetEnumerableType(property.PropertyType);
if (enumerableType != null && property.PropertyType != typeof(string))
{
Expand All @@ -110,7 +112,7 @@ private void Visit(Type type, HashSet<Type> visited, ref bool requiresAsync)

// Defer fully checking properties that are of the same type we're currently building the cache for.
// We'll remove them at the end if any other validatable properties are present.
if (type == property.PropertyType && !hasSkipRecursionOnProperty)
if (type == property.PropertyType && !hasSkipRecursionOnProperty && !hasSkipValidationAttribute)
{
propertiesToValidate ??= new List<PropertyDetails>();
propertiesToValidate.Add(
Expand Down Expand Up @@ -140,7 +142,8 @@ private void Visit(Type type, HashSet<Type> visited, ref bool requiresAsync)
|| propertyTypeSupportsPolymorphism)
&& !hasSkipRecursionOnProperty;

if (recurse || hasValidationOnProperty || isNonNullableReferenceType)
if (!hasSkipValidationAttribute
&& (recurse || hasValidationOnProperty || isNonNullableReferenceType))
{
propertiesToValidate ??= new List<PropertyDetails>();
propertiesToValidate.Add(
Expand Down Expand Up @@ -203,11 +206,13 @@ private static bool DoNotRecurseIntoPropertiesOf(Type type) =>
// TODO: Add extension point to add other types to ignore
;

private static (ValidationAttribute[]?, DisplayAttribute?, SkipRecursionAttribute?) GetPropertyAttributes(ParameterInfo[]? primaryCtorParameters, PropertyInfo property)
private static (ValidationAttribute[]?, DisplayAttribute?, SkipRecursionAttribute?, SkipValidationAttribute?)
GetPropertyAttributes(ParameterInfo[]? primaryCtorParameters, PropertyInfo property)
{
List<ValidationAttribute>? validationAttributes = null;
DisplayAttribute? displayAttribute = null;
SkipRecursionAttribute? skipRecursionAttribute = null;
SkipValidationAttribute? skipValidationAttribute = null;

IEnumerable<Attribute>? paramAttributes = null;
if (primaryCtorParameters is { } ctorParams)
Expand Down Expand Up @@ -251,9 +256,13 @@ private static (ValidationAttribute[]?, DisplayAttribute?, SkipRecursionAttribut
{
skipRecursionAttribute = skipRecursionAttr;
}
else if (attr is SkipValidationAttribute skipValidationAttr)
{
skipValidationAttribute = skipValidationAttr;
}
}

return new(validationAttributes?.ToArray(), displayAttribute, skipRecursionAttribute);
return new(validationAttributes?.ToArray(), displayAttribute, skipRecursionAttribute, skipValidationAttribute);
}

private static bool TryGetAttributesViaTypeDescriptor(PropertyInfo property, [NotNullWhen(true)] out IEnumerable<Attribute>? typeDescriptorAttributes)
Expand Down
43 changes: 38 additions & 5 deletions tests/MiniValidationPlus.UnitTests/Recursion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public void Valid_When_Child_Invalid_And_Recurse_False()
[Fact]
public void Valid_When_Child_Invalid_And_Property_Decorated_With_SkipRecursion()
{
var thingToValidate = new TestType { SkippedChild = new TestChildType { RequiredCategory = null, MinLengthFive = "123" } };
var thingToValidate = new TestType { SkippedRecursionChild = new TestChildType { RequiredCategory = null, MinLengthFive = "123" } };

var result = MiniValidatorPlus.TryValidate(thingToValidate, recurse: false, out var errors);

Expand Down Expand Up @@ -90,7 +90,7 @@ public void Invalid_When_Enumerable_Item_Invalid_When_Recurse_True()
public void Valid_When_Enumerable_Item_Invalid_When_Recurse_False()
{
var thingToValidate = new List<TestType> { new() { Child = new TestChildType { RequiredCategory = null, MinLengthFive = "123" } } };

var result = MiniValidatorPlus.TryValidate(thingToValidate, recurse: false, out _);

Assert.True(result);
Expand All @@ -99,7 +99,7 @@ public void Valid_When_Enumerable_Item_Invalid_When_Recurse_False()
[Fact]
public void Valid_When_Enumerable_Item_Has_Invalid_Descendant_But_Property_Decorated_With_SkipRecursion()
{
var thingToValidate = new List<TestType> { new() { SkippedChild = new() { RequiredCategory = null } } };
var thingToValidate = new List<TestType> { new() { SkippedRecursionChild = new() { RequiredCategory = null } } };

var result = MiniValidatorPlus.TryValidate(thingToValidate, recurse: true, out _);

Expand Down Expand Up @@ -212,7 +212,7 @@ public void Valid_When_Descendant_Invalid_And_Property_Decorated_With_SkipRecurs
{
var thingToValidate = new TestType();
thingToValidate.Children.Add(new());
thingToValidate.Children.Add(new() { SkippedChild = new() { RequiredCategory = null } });
thingToValidate.Children.Add(new() { SkippedRecursionChild = new() { RequiredCategory = null } });

var result = MiniValidatorPlus.TryValidate(thingToValidate, recurse: false, out var errors);

Expand Down Expand Up @@ -467,4 +467,37 @@ public async Task Invalid_When_Polymorphic_AsyncValidatableOnlyChild_Is_Invalid(
Assert.Single(errors);
Assert.Equal($"{nameof(TestValidatableType.PocoChild)}.{nameof(TestAsyncValidatableChildType.TwentyOrMore)}", errors.Keys.First());
}
}

[Fact]
public void Valid_When_Child_Invalid_And_Decorated_With_SkipValidation()
{
var thingToValidate = new TestType { SkippedValidationChild = new TestChildType { RequiredCategory = null, MinLengthFive = "123" } };

var result = MiniValidatorPlus.TryValidate(thingToValidate, recurse: true, out var errors);

Assert.True(result);
Assert.Empty(errors);
}

[Fact]
public void Valid_When_Child_Has_Invalid_String_Property_Decorated_With_SkipValidation()
{
var thingToValidate = new TestType { Child = new TestChildType { SkippedValidationNonNullableString = null! } };

var result = MiniValidatorPlus.TryValidate(thingToValidate, recurse: true, out var errors);

Assert.True(result);
Assert.Empty(errors);
}

[Fact]
public void Valid_When_Child_Has_Invalid_Required_Property_Decorated_With_SkipValidation()
{
var thingToValidate = new TestType { Child = new TestChildType { SkippedValidationRequiredName = null! } };

var result = MiniValidatorPlus.TryValidate(thingToValidate, recurse: true, out var errors);

Assert.True(result);
Assert.Empty(errors);
}
}
26 changes: 23 additions & 3 deletions tests/MiniValidationPlus.UnitTests/TestTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,16 @@ class TestType
{
public string NonNullableString { get; set; } = "Default";

[SkipValidation]
public string SkippedValidationNonNullableString { get; set; } = "Default";

[Required]
public string? RequiredName { get; set; } = "Default";

[Required]
[SkipValidation]
public string? SkippedValidationRequiredName { get; set; } = "Default";

[Required, Display(Name = "Required name")]
public string? RequiredNameWithDisplay { get; set; } = "Default";

Expand All @@ -27,9 +34,15 @@ class TestType
public IAnInterface? InterfaceProperty { get; set; }

[SkipRecursion]
public TestChildType SkippedChild { get; set; } = new TestChildType();
public TestChildType SkippedRecursionChild { get; set; } = new TestChildType();

[SkipValidation]
public TestChildType SkippedValidationChild { get; set; } = new TestChildType();

public IList<TestChildType> Children { get; } = new List<TestChildType>();

[SkipValidation]
public string SkippedPropertyThrowingException => throw new InvalidOperationException();
}

class TestValidatableType : TestType, IValidatableObject
Expand Down Expand Up @@ -142,8 +155,15 @@ class TestChildType

public TestChildType? Child { get; set; }

[SkipValidation]
public string SkippedValidationNonNullableString { get; set; } = "Default";

[Required]
[SkipValidation]
public string? SkippedValidationRequiredName { get; set; } = "Default";

[SkipRecursion]
public virtual TestChildType? SkippedChild { get; set; }
public virtual TestChildType? SkippedRecursionChild { get; set; }

internal static void AddDescendents(TestChildType target, int maxDepth, int currentDepth = 1)
{
Expand Down Expand Up @@ -215,7 +235,7 @@ class TestSkippedChildType
{
[Required]
[SkipRecursion]
public TestChildType? RequiredSkippedChild { get; set; }
public TestChildType? RequiredSkippedRecursionChild { get; set; }
}

struct TestStruct
Expand Down
36 changes: 36 additions & 0 deletions tests/MiniValidationPlus.UnitTests/TryValidate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -531,4 +531,40 @@ public async Task TryValidateAsync_With_Attribute_Attached_Via_TypeDescriptor()
Assert.Single(errors["PropertyToBeRequired"]);
Assert.Single(errors["AnotherProperty"]);
}

[Fact]
public void Valid_When_String_Property_Invalid_And_Decorated_With_SkipValidation()
{
var thingToValidate = new TestType { SkippedValidationNonNullableString = null!};

var result = MiniValidatorPlus.TryValidate(thingToValidate, out var errors);

Assert.True(result);
Assert.Empty(errors);
}

[Fact]
public void Valid_When_Required_Property_Invalid_And_Decorated_With_SkipValidation()
{
var thingToValidate = new TestType { SkippedValidationRequiredName = null!};

var result = MiniValidatorPlus.TryValidate(thingToValidate, out var errors);

Assert.True(result);
Assert.Empty(errors);
}

[Fact]
public void Valid_When_Property_With_Setter_Throwing_Exception_And_Decorated_With_SkipValidation()
{
var thingToValidate = new TestType();
var setter = () => thingToValidate.SkippedPropertyThrowingException;

var exception = Record.Exception(setter);
var result = MiniValidatorPlus.TryValidate(thingToValidate, out var errors);

Assert.NotNull(exception);
Assert.True(result);
Assert.Empty(errors);
}
}

0 comments on commit d039656

Please sign in to comment.