Search code examples
c#data-annotationsasp.net-web-apirequiredfieldvalidator

Custom required attribute with C# & Web API and using private access modifier with validation context


I have the following custom required attribute:

public class RequiredIfAttribute : RequiredAttribute
{
    private string _DependentProperty;
    private object _TargetValue;

    public RequiredIfAttribute(string dependentProperty, object targetValue)
    {
        this._DependentProperty = dependentProperty;
        this._TargetValue = targetValue;
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var propertyTestedInfo = validationContext.ObjectType.GetProperty(this._DependentProperty);

        if (propertyTestedInfo == null)
        {
            return new ValidationResult(string.Format("{0} needs to be exist in this object.", this._DependentProperty));
        }

        var dependendValue = propertyTestedInfo.GetValue(validationContext.ObjectInstance, null);

        if (dependendValue == null)
        {
            return new ValidationResult(string.Format("{0} needs to be populated.", this._DependentProperty));
        }

        if (dependendValue.Equals(this._TargetValue))
        {
            var x = validationContext.ObjectType.GetProperty("_Mappings");

            var objectInstance = (Dictionary<object, string[]>)x.GetValue(validationContext.ObjectInstance, null);

            var isRequiredSatisfied = false;

            foreach (var kvp in objectInstance)
            {
                if (kvp.Key.Equals(this._TargetValue))
                {
                    foreach (var field in kvp.Value)
                    {
                        var fieldValue = validationContext.ObjectType.GetProperty(field).GetValue(validationContext.ObjectInstance, null);

                        if (fieldValue != null && field.Equals(validationContext.MemberName))
                        {
                            isRequiredSatisfied = true;
                            break;
                        }
                    }
                }
            }

            if (isRequiredSatisfied)
            {
                return ValidationResult.Success;
            }
            else
            {
                return new ValidationResult(string.Empty);
            }
        }
        else
        {
            // Must be ignored
            return ValidationResult.Success;
        }
    }
}

What I am trying to achieve with it is that I want to conditionally validate based on a property in a model. It also needs to be generic enough to re-use on more than one model. When a specified property has a specific value (which I specify in the attribute), the custom required validation needs to match on those values. For example in this model:

public class PaymentModel
{
    public Dictionary<object, string[]> _Mappings 
    {
        get
        {
            var mappings = new Dictionary<object, string[]>();

            mappings.Add(true, new string[] { "StockID" });
            mappings.Add(false, new string[] { "Amount" });

            return mappings;
        }
    }

    [Required]
    public bool IsBooking { get; set; }

    [RequiredIfAttribute("IsBooking", false)]
    public decimal? Amount { get; set; }

    [RequiredIf("IsBooking", true)]
    public int? StockID { get; set; }

    public PaymentModel()
    {

    }
}

If the IsBooking property is true, then I want StockId to be required, but if it is false, then Amount should be required.

Currently the solution I have works, but it has 2 problems:

  1. There is a dependency on the _Mappings property, which I would like to not have. Does anyone know how I will get around doing it the way I have?
  2. If I have to use the _Mappings property as is, is there any way to use it as a private access modifier? Currently I can only make my solution work if _Mappings is public, because validationContext.ObjectType.GetProperty("_Mappings") cannot find private modifiers. (If I want to serialize this model to JSON in a Web API response, then I would ideally not want to send along my validation mappings.)

Solution

  • You don't have to use the _Mappings Property, the code below checks if the related Attribute has a value that matches what you specified in the attribute, If there is a match then it checks to see if the Property you are validating has a value.

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var propertyTestedInfo = validationContext.ObjectType.GetProperty(this._DependentProperty);
    
        if (propertyTestedInfo == null)
        {
            return new ValidationResult(string.Format("{0} needs to be exist in this object.", this._DependentProperty));
        }
    
        var dependendValue = propertyTestedInfo.GetValue(validationContext.ObjectInstance, null);
    
        if (dependendValue == null)
        {
            return new ValidationResult(string.Format("{0} needs to be populated.", this._DependentProperty));
        }
    
        if (dependendValue.Equals(this._TargetValue))
        {
            var fieldValue = validationContext.ObjectType.GetProperty(validationContext.MemberName).GetValue(validationContext.ObjectInstance, null);
    
    
            if (fieldValue != null)
            {
                return ValidationResult.Success;
            }
            else
            {
                return new ValidationResult(string.Format("{0} cannot be null", validationContext.MemberName));
            }
        }
        else
        {
            // Must be ignored
            return ValidationResult.Success;
        }
    }