3
votes

I have 3 entities.

Let's say I have Event entity, and 2 "derived" entities: Accident, Repair.

They provide some additional fields over Event entity.

Event have StartDate and EndDate which are always required so I mark them with [Required] attribute. That's ok. But I have some additional validation logic that checks if the Event is Repair, then some other Event fields are also required. For this I provide custom validator.

The problem is that the properties marked with [Required] attribute are always checked before other validators.

What I want achieve: If Event is Accident I want to make Event.SomeField required. Validation summary should show now contain 3 validation errors at the very first validation attempt.

How it behaves now: If Event is Accident first validation attempt shows 2 errors of 2 properties marked as [Required]. Only after I fill those, on the next validation attempt fires my custom validator which also states that 3rd Event.SomeField is also required.

I want all the required fields to validate at the same time.

Is this possible? How to achieve this?

1

1 Answers

1
votes

For the case that a property is required based on a certain condition, i use a custom attribute, that was initially provided by Jeff Handley in his blog.

Here´s the code of the Attribute:

    [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = true)]
public class ConditionallyRequiredAttribute : RequiredAttribute {
    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;
    }

    /// <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) {
        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)) {
                ValidationResult 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;
        }

        throw new InvalidOperationException(
            "ConditionallyRequiredAttribute could not discover member: " + this.ConditionMember);
    }

    /// <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
}

Here´s an example:

public class Dummy{

  public bool IsCondition {get; set;}

  [ConditionallyRequired("IsCondition", true)]
  public string SometimesRequired {get; set;}
}