Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
472 changes: 472 additions & 0 deletions JMayer.Data/DataAnnotations/CompareToOtherMemberAttribute.cs

Large diffs are not rendered by default.

43 changes: 43 additions & 0 deletions JMayer.Data/DataAnnotations/ComparisonOperation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
namespace JMayer.Data.DataAnnotations;

/// <summary>
/// The enumeration for the comparison operations.
/// </summary>
public enum ComparisonOperation
{
/// <summary>
/// The CompareToOtherMember attribute will do an equal check on the member its registered too against
/// another member.
/// </summary>
Equal = 0,

/// <summary>
/// The CompareToOtherMember attribute will do a not equal check on the member its registered too against
/// another member.
/// </summary>
NotEqual,

/// <summary>
/// The CompareToOtherMember attribute will do a greater than check on the member its registered too against
/// another member.
/// </summary>
GreaterThan,

/// <summary>
/// The CompareToOtherMember attribute will do a greater than or equal check on the member its registered too against
/// another member.
/// </summary>
GreaterThanOrEqual,

/// <summary>
/// The CompareToOtherMember attribute will do a less than check on the member its registered too against
/// another member.
/// </summary>
LessThan,

/// <summary>
/// The CompareToOtherMember attribute will do a less than or equal check on the member its registered too against
/// another member.
/// </summary>
LessThanOrEqual,
}
107 changes: 107 additions & 0 deletions JMayer.Data/DataAnnotations/DataAnnotationMemberHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
using System.Reflection;

namespace JMayer.Data.DataAnnotations;

/// <summary>
/// The static class contains common contants and methods for member access.
/// </summary>
internal static class DataAnnotationMemberHelper
{
/// <summary>
/// The constant for an invalid type error message.
/// </summary>
public const string InvalidTypeErrorMessage = "Failed to vaidate because the type is not supported.";

/// <summary>
/// The constant for the member not found error message.
/// </summary>
public const string MemberNotFoundErrorMessage = "Failed to validate because the member name was not found.";

/// <summary>
/// The constant for the same member error message.
/// </summary>
public const string SameMemberErrorMessage = "Failed to validate because the other member name is the same as the member name the attribute's registered too.";

/// <summary>
/// The constant for the type mismatch error message.
/// </summary>
public const string TypeMismatchErrorMessage = "Failed to validate because there is a type mismatch between the values being compared.";

/// <summary>
/// The method returns the type from the member info.
/// </summary>
/// <param name="memberInfo">The information on the member.</param>
/// <returns>The type or null if member is not a field or property.</returns>
public static Type? GetMemberType(MemberInfo memberInfo)
{
if (memberInfo is PropertyInfo propertyInfo)
{
return propertyInfo.PropertyType;
}

return null;
}

/// <summary>
/// The method returns the value stored a member.
/// </summary>
/// <param name="memberInfo">Meta data about the member.</param>
/// <param name="objectInstance">The instance of the object being validated.</param>
/// <returns>The value or null if not found.</returns>
public static object? GetMemberValue(MemberInfo memberInfo, object objectInstance)
{
if (memberInfo is PropertyInfo propertyInfo)
{
return propertyInfo.GetValue(objectInstance);
}

return null;
}

/// <summary>
/// The method returns if the members are the same type or not.
/// </summary>
/// <param name="registeredMemberInfo">The information on the member in the validation context.</param>
/// <param name="otherMemberInfo">The information on the other member.</param>
/// <returns>True means the types are the same.</returns>
public static bool IsSameType(MemberInfo registeredMemberInfo, MemberInfo otherMemberInfo)
{
Type? registeredMemberType = GetMemberType(registeredMemberInfo);

if (registeredMemberType is null)
{
return false;
}

//Get the underlying type if nullable.
if (registeredMemberType.IsGenericType && registeredMemberType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
registeredMemberType = Nullable.GetUnderlyingType(registeredMemberType);

if (registeredMemberType is null)
{
return false;
}
}

Type? otherMemberType = GetMemberType(otherMemberInfo);

if (otherMemberType is null)
{
return false;
}

//Get the underlying type if nullable.
if (otherMemberType.IsGenericType && otherMemberType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
otherMemberType = Nullable.GetUnderlyingType(otherMemberType);

if (otherMemberType is null)
{
return false;
}
}

return registeredMemberType == otherMemberType;
}
}
130 changes: 130 additions & 0 deletions JMayer.Data/DataAnnotations/RequiredDependsOnAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
using System.ComponentModel.DataAnnotations;
using System.Reflection;

namespace JMayer.Data.DataAnnotations;

/// <summary>
/// The class does the required data annotation check if it matches a specific condition on another property in the same class and instance being validated.
/// The condition can be a boolean or an enum value.
/// </summary>
/// <remarks>
/// To use, add the attribute to the public property which is required on a condition. Set the dependentMemberName to the name of the public property
/// which has the condition that needs to be evaluated. Set the conditionValue to true, false or an enum value based on what condition needs to occur for the required
/// data annotation to be evaluated.
/// <code>
/// public class ObjectRequiringValidation
/// {
/// //A required validation check will be done if DependentBoolMember is true.
/// [RequiredDependsOn(dependentMemberName: nameof(DependentBoolMember), conditionValue: true)]
/// public string ConditionalRequiredProperty { get; set; }
///
/// public bool DependentBoolMember { get; set; }
/// }
/// </code>
/// or
/// <code>
/// public class ObjectRequiringValidation
/// {
/// //A required validation check will be done if DependentEnumMember is YourEnum.RequiredValue.
/// [RequiredDependsOn(dependentMemberName: nameof(DependentEnumMember), conditionValue: YourEnum.RequiredValue)]
/// public string ConditionalRequiredProperty { get; set; }
///
/// public YourEnum DependentEnumMember { get; set; }
/// }
/// </code>
/// </remarks>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class RequiredDependsOnAttribute : RequiredAttribute
{
/// <summary>
/// The value to run the Required data annotation.
/// </summary>
private readonly object _conditionValue;

/// <summary>
/// The name of the member the Required data annotation is dependent on.
/// </summary>
private readonly string _dependentMemberName;

/// <summary>
/// The constructor which takes the dependency and condition.
/// </summary>
/// <param name="dependentMemberName">The name of the member the Required data annotation is dependent on.</param>
/// <param name="conditionValue">The value to run the Required data annotation.</param>
/// <exception cref="ArgumentException">Thrown when the dependentMemberName parameter is null or an empty string.</exception>
/// <exception cref="ArgumentNullException">Thrown when the conditionValue parameter is null.</exception>
public RequiredDependsOnAttribute(string dependentMemberName, object conditionValue)
{
ArgumentNullException.ThrowIfNull(conditionValue);
ArgumentException.ThrowIfNullOrEmpty(dependentMemberName);

_conditionValue = conditionValue;
_dependentMemberName = dependentMemberName;
}

/// <summary>
/// The method returns if the dependent member matches the required condition.
/// </summary>
/// <param name="dependentMemberValue">The bool value of the dependent member.</param>
/// <param name="conditionValue">The bool value we are expecting for the dependent member.</param>
/// <returns>True means the Required data annotation check occurs.</returns>
private static bool IsRequiredCondition(bool dependentMemberValue, bool conditionValue) => dependentMemberValue == conditionValue;

/// <summary>
/// The method returns if the dependent member matches the required condition.
/// </summary>
/// <param name="dependentMemberValue">The bool value of the dependent member.</param>
/// <param name="conditionValue">The bool value we are expecting for the dependent member.</param>
/// <returns>True means the Required data annotation check occurs.</returns>
private static bool IsRequiredCondition(Enum dependentMemberValue, Enum conditionValue) => dependentMemberValue.Equals(conditionValue);

/// <inheritdoc/>
/// <remarks>Overridden to run the required data annotation if it matches the dependent condition. Success will be return when it doesn't match the dependent condition.</remarks>
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
{
if (_dependentMemberName == validationContext.MemberName)
{
return new ValidationResult(DataAnnotationMemberHelper.SameMemberErrorMessage);
}

MemberInfo? dependentMemberInfo = validationContext.ObjectType.FindMembers(MemberTypes.Property, BindingFlags.Public | BindingFlags.Instance, Type.FilterName, _dependentMemberName).FirstOrDefault();

if (dependentMemberInfo is null)
{
return new ValidationResult(DataAnnotationMemberHelper.MemberNotFoundErrorMessage);
}

object? dependentMemberValue = DataAnnotationMemberHelper.GetMemberValue(dependentMemberInfo, validationContext.ObjectInstance);

if (dependentMemberValue is null)
{
return ValidationResult.Success;
}

if (dependentMemberValue.GetType() != _conditionValue.GetType())
{
return new ValidationResult(DataAnnotationMemberHelper.TypeMismatchErrorMessage);
}

if (dependentMemberValue is bool dependentMemberBoolValue && _conditionValue is bool conditionBoolValue)
{
if (IsRequiredCondition(dependentMemberBoolValue, conditionBoolValue))
{
return base.IsValid(value, validationContext);
}
}
else if (dependentMemberValue is Enum dependentMemberEnumValue && _conditionValue is Enum conditionEnumValue)
{
if (IsRequiredCondition(dependentMemberEnumValue, conditionEnumValue))
{
return base.IsValid(value, validationContext);
}
}
else
{
return new ValidationResult(DataAnnotationMemberHelper.InvalidTypeErrorMessage);
}

return ValidationResult.Success;
}
}
2 changes: 1 addition & 1 deletion JMayer.Data/JMayer.Data.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>9.0.0</Version>
<Version>9.1.0</Version>
<Authors>jmayer913</Authors>
<Company>jmayer913</Company>
<PackageDescription>Package contains generic data access via HTTP or a Database.</PackageDescription>
Expand Down
Loading
Loading