Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
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;
}
}