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;

    }

}