SYSK 395: DataAnnotations: ConditionallyRequiredAttribute with lamda expressions
Back in 2010, Jeff Hanley published a sample of a ConditionallyRequiredAttribute that used a name of another property for runtime condition verification. The original source can be found athttp://jeffhandley.com/archive/2010/09/26/RiaServicesCustomReusableValidators.aspx.
I took the liberty to update it to support lambda expressions, allowing for more complex rules, e.g.
[ConditionallyRequired(typeof(YourClass), "(x) => { return x.Prop1 == 5 && x.Prop2; }")]
Here is the resulting code:
/// <summary>
/// Make a member required under a certain condition.
/// </summary>
/// <remarks>
/// Override the attribute usage to allow multiple attributes to be applied.
/// This requires that the TypeId property be overridden on the desktop framework.
/// </remarks>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = true)]
public class ConditionallyRequiredAttribute : RequiredAttribute
{
private Func<dynamic, bool> _predicate = null;
private MemberInfo _member;
/// <summary>
/// The name of the member that will return the state that indicates
/// whether or not the validated member is required.
/// </summary>
public string ConditionMember { get; private set; }
/// <summary>
/// The condition value under which this validator treats
/// the affected member as required.
/// </summary>
public object RequiredCondition { get; private set; }
/// <summary>
/// Comma-separated list of additional members to
/// add to validation errors. By default, the
/// <see cref="ConditionMember"/> is added.
/// </summary>
public string ErrorMembers { get; set; }
/// <summary>
/// Conditionally require a value, only when the specified
/// <paramref name="conditionMember"/> is <c>true</c>.
/// </summary>
/// <param name="conditionMember">
/// The member that must be <c>true</c> to require a value.
/// </param>
public ConditionallyRequiredAttribute(string conditionMember)
: this(conditionMember, true) { }
/// <summary>
/// Conditionally require a value, only when the specified
/// <paramref name="conditionMember"/> has a value that
/// exactly matches the <paramref name="requiredCondition"/>.
/// </summary>
/// <param name="conditionMember">
/// The member that will be evaluated to require a value.
/// </param>
/// <param name="requiredCondition">
/// The value the <paramref name="conditionMember"/> must
/// hold to require a value.
/// </param>
public ConditionallyRequiredAttribute(string conditionMember, object requiredCondition)
{
this.ConditionMember = conditionMember;
this.RequiredCondition = requiredCondition;
this.ErrorMembers = this.ConditionMember;
}
// NOTE: requires that the type being validated has a parameterized constructor!
public ConditionallyRequiredAttribute(Type type, string predicate)
{
_predicate = predicate.ToFunc(type);
}
/// <summary>
/// Override the base validation to only perform validation when the required
/// condition has been met. In the case of validation failure, augment the
/// validation result with the <see cref="ErrorMembers"/> as an additional
/// member names, as needed.
/// </summary>
/// <param name="value">The value being validated.</param>
/// <param name="validationContext">The validation context being used.</param>
/// <returns>
/// <see cref="ValidationResult.Success"/> if not currently required or if satisfied,
/// or a <see cref="ValidationResult"/> in the case of failure.
/// </returns>
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
ValidationResult result = ValidationResult.Success;
if (_predicate != null)
{
// Add SEH
bool condition = _predicate(validationContext.ObjectInstance);
if (condition)
result = base.IsValid(value, validationContext);
}
else if (this.DiscoverMember(validationContext.ObjectType))
{
object state = this.InvokeMember(validationContext.ObjectInstance);
// We are only required if the current state
// matches the specified condition.
if (Object.Equals(state, this.RequiredCondition))
{
result = base.IsValid(value, validationContext);
if (result != ValidationResult.Success && this.ErrorMembers != null && this.ErrorMembers.Any())
{
result = new ValidationResult(result.ErrorMessage,
result.MemberNames.Union(this.ErrorMembers.Split(',').Select(s
=> s.Trim())));
}
return result;
}
return ValidationResult.Success;
}
else
{
throw new InvalidOperationException(
"ConditionallyRequiredAttribute could not discover member: " + this.ConditionMember);
}
return result;
}
/// <summary>
/// Discover the member that we will evaluate for checking our condition.
/// </summary>
/// <param name="objectType"></param>
/// <returns></returns>
private bool DiscoverMember(Type objectType)
{
if (this._member == null)
{
this._member = (from member in objectType.GetMember(this.ConditionMember).Cast<MemberInfo>()
where IsSupportedProperty(member) || IsSupportedMethod(member)
select member).SingleOrDefault();
}
// If we didn't find 1 exact match, indicate that we could not discover the member
return this._member != null;
}
/// <summary>
/// Determine if a <paramref name="member"/> is a
/// method that accepts no parameters.
/// </summary>
/// <param name="member">The member to check.</param>
/// <returns>
/// <c>true</c> if the member is a parameterless method.
/// Otherwise, <c>false</c>.
/// </returns>
private bool IsSupportedMethod(MemberInfo member)
{
if (member.MemberType != MemberTypes.Method)
{
return false;
}
MethodInfo method = (MethodInfo)member;
return method.GetParameters().Length == 0
&& method.GetGenericArguments().Length == 0
&& method.ReturnType != typeof(void);
}
/// <summary>
/// Determine if a <paramref name="member"/> is a
/// property that has no indexer.
/// </summary>
/// <param name="member">The member to check.</param>
/// <returns>
/// <c>true</c> if the member is a non-indexed property.
/// Otherwise, <c>false</c>.
/// </returns>
private bool IsSupportedProperty(MemberInfo member)
{
if (member.MemberType != MemberTypes.Property)
{
return false;
}
PropertyInfo property = (PropertyInfo)member;
return property.GetIndexParameters().Length == 0;
}
/// <summary>
/// Invoke the member and return its value.
/// </summary>
/// <param name="objectInstance">The object to invoke against.</param>
/// <returns>The member's return value.</returns>
private object InvokeMember(object objectInstance)
{
if (this._member.MemberType == MemberTypes.Method)
{
MethodInfo method = (MethodInfo)this._member;
return method.Invoke(objectInstance, null);
}
PropertyInfo property = (PropertyInfo)this._member;
return property.GetValue(objectInstance, null);
}
#if
!SILVERLIGHT
/// <summary>
/// The desktop framework has this property and it must be
/// overridden when allowing multiple attributes, so that
/// attribute instances can be disambiguated based on
/// field values.
/// </summary>
public override object TypeId
{
get { return this; }
}
#endif
}
internal static class StringExtensions
{
public static Func<dynamic, bool> ToFunc(this string predicate, Type type)
{
Func<dynamic, bool> result = null;
// TODO: Add error checking/validation (e.g. check the the type being validated has a parameterized constructor, etc.)
Dictionary<string, string> providerOptions = new Dictionary<string, string>();
Version v = typeof(ConstantExpression).Assembly.GetName().Version;
providerOptions.Add("CompilerVersion", "v" + v.Major.ToString() + "." + v.Minor.ToString());
CSharpCodeProvider provider = new CSharpCodeProvider(providerOptions);
CompilerResults results = provider.CompileAssemblyFromSource(
new CompilerParameters(new[] { "System.Core.dll", "Microsoft.CSharp.dll" })
{
GenerateExecutable = false,
GenerateInMemory = true
},
@"using System;
using System.Linq.Expressions;
public class ExpressionExtensions
{
public static Func<dynamic, bool> expr { get { return " + predicate + @"; } }
}");
if (results.Errors.HasErrors)
{
foreach (CompilerError err in results.Errors)
{
// TODO: Log/throw as needed
System.Diagnostics.Debug.WriteLine(err.ErrorText);
}
}
else
{
var asm = results.CompiledAssembly;
var p = asm.GetType("ExpressionExtensions").GetProperty("expr", BindingFlags.Static | BindingFlags.Public);
result = (Func<dynamic, bool>)p.GetGetMethod().Invoke(Activator.CreateInstance(type), null);
}
return result;
}
}