Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# MiniValidation
A minimalistic validation library built atop the existing features in .NET's `System.ComponentModel.DataAnnotations` namespace. Adds support for single-line validation calls and recursion with cycle detection.
A minimalistic validation library built atop the existing features in .NET's `System.ComponentModel.DataAnnotations` namespace. Adds support for single-line validation calls for public properties and fields, plus recursion with cycle detection.

Supports .NET Standard 2.0 compliant runtimes.

Expand Down
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>

<PropertyGroup>
<VersionPrefix>0.10.0</VersionPrefix>
<VersionPrefix>0.11.0</VersionPrefix>
<!-- VersionSuffix used for local builds -->
<VersionSuffix>dev</VersionSuffix>
<!-- VersionSuffix to be used for CI builds -->
Expand Down
84 changes: 44 additions & 40 deletions src/MiniValidation/MiniValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
namespace MiniValidation;

/// <summary>
/// Contains methods and properties for performing validation operations with <see cref="Validator"/> on objects whos properties
/// Contains methods for performing validation operations with <see cref="Validator"/> on objects whose public properties or fields
/// are decorated with <see cref="ValidationAttribute"/>s.
/// </summary>
public static class MiniValidator
Expand Down Expand Up @@ -44,7 +44,7 @@ public static bool RequiresValidation(Type targetType, bool recurse = true)
return typeof(IValidatableObject).IsAssignableFrom(targetType)
|| typeof(IAsyncValidatableObject).IsAssignableFrom(targetType)
|| (recurse && typeof(IEnumerable).IsAssignableFrom(targetType))
|| _typeDetailsCache.Get(targetType).Properties.Any(p => p.HasValidationAttributes || recurse);
|| _typeDetailsCache.Get(targetType).Members.Any(m => m.HasValidationAttributes || recurse);
}

/// <summary>
Expand Down Expand Up @@ -389,46 +389,46 @@ private static async Task<bool> TryValidateImpl(
// Add current target to tracking dictionary in null (validating) state
validatedObjects.Add(target, null);

var (typeProperties, _) = _typeDetailsCache.Get(targetType);
var (typeMembers, _) = _typeDetailsCache.Get(targetType);

var isValid = true;
var propertiesToRecurse = recurse ? new Dictionary<PropertyDetails, object>() : null;
var membersToRecurse = recurse ? new Dictionary<MemberDetails, object>() : null;
var validationContext = new ValidationContext(target, serviceProvider: serviceProvider, items: null);

foreach (var property in typeProperties)
foreach (var member in typeMembers)
{
// Skip properties that don't have validation attributes if we're not recursing
if (!(property.HasValidationAttributes || recurse))
// Skip members that don't have validation attributes if we're not recursing
if (!(member.HasValidationAttributes || recurse))
{
continue;
}

var propertyValue = property.GetValue(target);
var propertyValueType = propertyValue?.GetType();
var (properties, _) = _typeDetailsCache.Get(propertyValueType);
var memberValue = member.GetValue(target);
var memberValueType = memberValue?.GetType();
var (members, _) = _typeDetailsCache.Get(memberValueType);

if (property.HasValidationAttributes)
if (member.HasValidationAttributes)
{
validationContext.MemberName = property.Name;
validationContext.DisplayName = GetDisplayName(property);
validationContext.MemberName = member.Name;
validationContext.DisplayName = GetDisplayName(member);
validationResults ??= new();
var propertyIsValid = Validator.TryValidateValue(propertyValue!, validationContext, validationResults, property.ValidationAttributes);
var memberIsValid = Validator.TryValidateValue(memberValue!, validationContext, validationResults, member.ValidationAttributes);

if (!propertyIsValid)
if (!memberIsValid)
{
ProcessValidationResults(property.Name, validationResults, workingErrors, prefix);
ProcessValidationResults(member.Name, validationResults, workingErrors, prefix);
isValid = false;
}
}

if (recurse && propertyValue is not null &&
!TypeDetailsCache.IsNonValidatableType(propertyValueType!) &&
(property.Recurse
|| typeof(IValidatableObject).IsAssignableFrom(propertyValueType)
|| typeof(IAsyncValidatableObject).IsAssignableFrom(propertyValueType)
|| properties.Any(p => p.Recurse)))
if (recurse && memberValue is not null &&
!TypeDetailsCache.IsNonValidatableType(memberValueType!) &&
(member.Recurse
|| typeof(IValidatableObject).IsAssignableFrom(memberValueType)
|| typeof(IAsyncValidatableObject).IsAssignableFrom(memberValueType)
|| members.Any(p => p.Recurse)))
{
propertiesToRecurse!.Add(property, propertyValue);
membersToRecurse!.Add(member, memberValue);
}
}

Expand All @@ -455,23 +455,23 @@ private static async Task<bool> TryValidateImpl(
isValid = await validateTask.ConfigureAwait(false) && isValid;
}

// Validate complex properties
if (propertiesToRecurse!.Count > 0)
// Validate complex members
if (membersToRecurse!.Count > 0)
{
foreach (var property in propertiesToRecurse)
foreach (var member in membersToRecurse)
{
var propertyDetails = property.Key;
var propertyValue = property.Value;
var memberDetails = member.Key;
var memberValue = member.Value;

if (propertyValue != null)
if (memberValue != null)
{
RuntimeHelpers.EnsureSufficientExecutionStack();

if (propertyDetails.IsEnumerable && propertyValue is IEnumerable propertyValues)
if (memberDetails.IsEnumerable && memberValue is IEnumerable memberValues)
{
var thePrefix = $"{prefix}{propertyDetails.Name}";
var thePrefix = $"{prefix}{memberDetails.Name}";

var validateTask = TryValidateEnumerable(propertyValues, serviceProvider, recurse, allowAsync, workingErrors, validatedObjects, validationResults, thePrefix, currentDepth);
var validateTask = TryValidateEnumerable(memberValues, serviceProvider, recurse, allowAsync, workingErrors, validatedObjects, validationResults, thePrefix, currentDepth);
try
{
ThrowIfAsyncNotAllowed(validateTask.IsCompleted, allowAsync);
Expand All @@ -485,11 +485,11 @@ private static async Task<bool> TryValidateImpl(

isValid = await validateTask.ConfigureAwait(false) && isValid;
}
else if (!propertyDetails.IsEnumerable)
else if (!memberDetails.IsEnumerable)
{
var thePrefix = $"{prefix}{propertyDetails.Name}."; // <-- Note trailing '.' here
var thePrefix = $"{prefix}{memberDetails.Name}."; // <-- Note trailing '.' here

var validateTask = TryValidateImpl(propertyValue, serviceProvider, recurse, allowAsync, workingErrors, validatedObjects, validationResults, thePrefix, currentDepth + 1);
var validateTask = TryValidateImpl(memberValue, serviceProvider, recurse, allowAsync, workingErrors, validatedObjects, validationResults, thePrefix, currentDepth + 1);
try
{
ThrowIfAsyncNotAllowed(validateTask.IsCompleted, allowAsync);
Expand Down Expand Up @@ -542,9 +542,9 @@ private static async Task<bool> TryValidateImpl(

return isValid;

static string GetDisplayName(PropertyDetails property)
static string GetDisplayName(MemberDetails member)
{
return property.DisplayAttribute?.GetName() ?? property.Name;
return member.DisplayAttribute?.GetName() ?? member.Name;
}
}

Expand Down Expand Up @@ -670,21 +670,25 @@ static string GetClassLevelKey(string? prefix)
}
}

private static void ProcessValidationResults(string propertyName, ICollection<ValidationResult> validationResults, Dictionary<string, List<string>> errors, string? prefix)
private static void ProcessValidationResults(string memberName, ICollection<ValidationResult> validationResults, Dictionary<string, List<string>> errors, string? prefix)
{
if (validationResults.Count == 0)
{
return;
}

var errorsList = new List<string>(validationResults.Count);
var key = $"{prefix}{memberName}";
if (!errors.TryGetValue(key, out var errorsList))
{
errorsList = new List<string>(validationResults.Count);
errors.Add(key, errorsList);
}

foreach (var result in validationResults)
{
errorsList.Add(result.ErrorMessage ?? "");
}

errors.Add($"{prefix}{propertyName}", errorsList);
validationResults.Clear();
}
}
19 changes: 19 additions & 0 deletions src/MiniValidation/PropertyHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

using System;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Reflection;

namespace MiniValidation
Expand Down Expand Up @@ -37,6 +38,24 @@ internal static class PropertyHelper
CallNullSafePropertyGetterByReferenceOpenGenericMethod);
}

public static Func<object, object?> MakeNullSafeFastFieldGetter(FieldInfo fieldInfo)
{
Debug.Assert(fieldInfo != null);
Debug.Assert(!fieldInfo!.IsStatic);

var target = Expression.Parameter(typeof(object), "target");
var body = Expression.Condition(
Expression.Equal(target, Expression.Constant(null)),
Expression.Constant(null, typeof(object)),
Expression.Convert(
Expression.Field(
Expression.Convert(target, fieldInfo.DeclaringType!),
fieldInfo),
typeof(object)));

return Expression.Lambda<Func<object, object?>>(body, target).Compile();
}

private static Func<object, object?> MakeFastPropertyGetter(
PropertyInfo propertyInfo,
MethodInfo propertyGetterWrapperMethod,
Expand Down
4 changes: 2 additions & 2 deletions src/MiniValidation/SkipRecursionAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
namespace MiniValidation;

/// <summary>
/// Indicates that a property should be ignored during recursive validation when using
/// Indicates that a property or field should be ignored during recursive validation when using
/// <see cref="MiniValidator.TryValidate{TTarget}(TTarget, out System.Collections.Generic.IDictionary{string, string[]})"/>.
/// Note that any validation attributes on the property itself will still be validated.
/// Note that any validation attributes on the property or field itself will still be validated.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class SkipRecursionAttribute : Attribute
Expand Down
Loading