Declarative conditional validation of a field based on other one from parent model

1.1k views Asked by At

I have two separate types:

public class Person
{
    public string Name { get; set; }
    public bool IsActive { get; set; }

    public Contact ContactDetails { get; set; }
}

public class Contact
{
    [RequiredIfActive]
    public string Email { get; set; }
}

What I need is to perform conditional declarative validation of internal model field, based on some state of the parent model field - in this particular example an Email has to be filled, if IsActive option is enabled.

I do not want to reorganize these models taxonomy, while in the same time I need to use attribute-based approach. It seems that from within an attribute there is no access to the validation context of the parent model. How to reach or inject it there?

public class RequiredIfActiveAttribute : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, 
                                                ValidationContext validationContext)
    {
        /* validationContext.ObjectInstance gives access to the current 
           Contact type, but is there any way of accessing Person type? */

Edit:

I know how conditional validation can be implemented using Fluent Validation, but I'm NOT asking about that (I don't need support regarding Fluent Validation). I'd like to know however, if exists any way to access parent model from inside System.ComponentModel.DataAnnotations.ValidationAttribute.

2

There are 2 answers

1
Chamaququm On

My Suggestion

Go to Tools => Library Package Manager => Package Manager Console and install Fluent Validation.

enter image description here

Action Methods

[HttpGet]
public ActionResult Index()
{
    var model = new Person
    {
        Name = "PKKG",
        IsActive = true,
        ContactDetails = new Contact { Email = "[email protected]" }
    };
    return View(model);
}
[HttpPost]
public ActionResult Index(Person p)
{
    return View(p);
}

Fluent Validation Rules

public class MyPersonModelValidator : AbstractValidator<Person>
{
    public MyPersonModelValidator()
    {
        RuleFor(x => x.ContactDetails.Email)
            .EmailAddress()
            .WithMessage("Please enter valid email address")
            .NotNull().When(i => i.IsActive)
            .WithMessage("Please enter email");
    }
}

View Models

[Validator(typeof(MyPersonModelValidator))]
public class Person
{
    [Display(Name = "Name")]
    public string Name { get; set; }

    [Display(Name = "IsActive")]
    public bool IsActive { get; set; }

    public Contact ContactDetails { get; set; }
}

public class Contact
{
    [Display(Name = "Email")]
    public string Email { get; set; }
}

View

@{
    var actionURL = Url.Action("Action", "Controller", new { area = "AreaName" },
                           Request.Url.Scheme);
}
@using (Html.BeginForm("Action", "Controller", FormMethod.Post, 
                                                    new { @action = actionURL }))
    @Html.EditorFor(i => i.Name);
    @Html.ValidationMessageFor(i => i.Name);

    @Html.EditorFor(i => i.IsActive);
    @Html.ValidationMessageFor(i => i.IsActive);

    @Html.EditorFor(i => i.ContactDetails.Email);
    @Html.ValidationMessageFor(i => i.ContactDetails.Email);
    <button type="submit">
        OK</button>
}
2
Nicole Calinoiu On

This cannot be done via an attribute on Contact.Email since, as you have already discovered, the parent Person is not available from the attribute context at runtime. To enable this scenario via a validation attribute, the attribute must decorate the Person class. You have two choices for this with System.ComponentModel.DataAnnotations attributes: CustomValidationAttribute or a custom ValidationAttribute subclass that targets Person.

Here's what the two classes might look like when using CustomValidationAttribute:

[CustomValidation(typeof(Person), "ValidateContactEmail")]
public class Person
{
    public string Name { get; set; }
    public bool IsActive { get; set; }
    public Contact ContactDetails { get; set; }

    public static ValidationResult ValidateContactEmail(Person person, ValidationContext context)
    {
        var result = ValidationResult.Success;
        if (person.IsActive)
        {
            if ((person.ContactDetails == null) || string.IsNullOrEmpty(person.ContactDetails.Email))
            {
                result = new ValidationResult("An e-mail address must be provided for an active person.", new string[] { "ContactDetails.Email" });
            }
        }

        return result;
    }
}

public class Contact
{
    public string Email { get; set; }
}