Skip to content

Instantly share code, notes, and snippets.

@emctague
Created January 14, 2020 17:18
Show Gist options
  • Save emctague/385cf9470cc2be9ff401d215cd5381ca to your computer and use it in GitHub Desktop.
Save emctague/385cf9470cc2be9ff401d215cd5381ca to your computer and use it in GitHub Desktop.
When - Conditional EntityFramework Validation using Attributes
//
// When
//
// Allows for vertain validation rules to only apply under certain conditions, using only an Attribute.
// e.g., if a certain string should match the regex `^.+@.+$` when another property, "WantsEmail", is equal to `true`:
//
// [When("WantsEmail", true, typeof(DoesRegexMatch), "^.+@.+$", Error = "Wants email, but none provided!"]
// public string AProperty { get; set; }
//
using System;
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
namespace When
{
/// <summary>
/// Provides conditional Entity Framework verification, in which a particular
/// condition on the current attribute is only necessary when a different attribute
/// matches a particular value.
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public sealed class When : ValidationAttribute
{
string PropertyName { get; set; }
object ExpectedValue { get; set; }
Type VerifierType { get; set; }
public string Error { get; set; }
bool WhenNotEqual { get; set; }
object[] Parameters;
/// <summary>
/// Create a conditional verification.
/// </summary>
/// <param name="property">The name of a property of the current model that should be compared to <c>value</c>. Prefix with an exclamation mark (<c>!</c>) to check for inequality instead of equality.</param>
/// <param name="value">The value that <c>property</c> should be equal to for verification to be necessary.</param>
/// <param name="verifierType">A type that provides verification, such as <c>IsNot</c> or <c>DoesRegexMatch</c>. This must inherit from <c>WhenConditional</c>.</param>
/// <param name="parameters">Any parameters the verifier requires on its <c>Check</c> method.</param>
/// <example>
/// [When("WantsPromotionalEmails", true, typeof(DoesRegexMatch), "(.+)@(.+)", Error = "Invalid email given, but needed to send promotional emails!")]
/// </example>
public When(string property, object value, Type verifierType, params object[] parameters)
{
if (!verifierType.IsAssignableFrom(typeof(WhenConditional)))
{
throw new ArgumentException("verifierType does not implement WhenConditional.");
}
PropertyName = property;
ExpectedValue = value;
VerifierType = verifierType;
Parameters = parameters ?? new object[] { };
WhenNotEqual = false;
if (PropertyName[0] == '!')
{
PropertyName = PropertyName.Substring(1);
WhenNotEqual = !WhenNotEqual;
}
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
bool equalResult = validationContext.ObjectInstance.GetType().GetProperty(PropertyName).GetValue(value) == this.ExpectedValue;
if (WhenNotEqual != equalResult)
{
// Add the attribute's value as the final parameter.
var method = VerifierType.GetMethod("Check", System.Reflection.BindingFlags.Public);
Array.Resize<object>(ref Parameters, Parameters.Length + 1);
Parameters[Parameters.Length - 1] = value;
if ((bool)method.Invoke(null, Parameters)) return ValidationResult.Success;
else return new ValidationResult(Error);
}
else return ValidationResult.Success;
}
}
/// <summary>
/// A generic interface that all valid conditionals for <c>When</c> inherit from.
/// Each <c>When</c> conditional has a method <c>Check</c>, which accepts some
/// specific number of objects as parameters and returns true or false.
/// </summary>
public interface WhenConditional
{
}
/// <summary>
/// Verifies the inverse of the given check type parameter <c>T</c>.
/// </summary>
/// <typeparam name="T">Another checker type (<c>WhenConditional</c>) to test for.</typeparam>
public sealed class IsNot<T> : WhenConditional where T: WhenConditional
{
public bool Check(params object[] list)
{
return !(bool) typeof(T).GetMethod("Check", System.Reflection.BindingFlags.Public).Invoke(null, list);
}
}
/// <summary>
/// Verifies that the property (which must be a string) matches the given
/// regular expression.
/// </summary>
public sealed class DoesRegexMatch : WhenConditional
{
public bool Check(string Pattern, object input)
{
if (input is string)
{
return new Regex(Pattern).IsMatch((string)input);
} else
{
return false;
}
}
}
/// <summary>
/// Verifies that the property is not null.
/// </summary>
public sealed class IsNull : WhenConditional
{
public bool Check(object input)
{
return input != null;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment