Sunday, December 18, 2016

RequiredIf Dependant Field IsNull - Conditional Validation Attribute using MVC / Web API

This topic illustrates how to extend ValidationAttribute to enforce customized validation by validating a field depending on dependents field value.

Scenario: Address.Country is required when Address.City value is not provided OR NULL.

We can achieve this by following below steps, and the illustration is developed using MVC 5, Web API 2, EF 6 and Mock

As a first step, Create a new class with name "RequiredIfDepFieldIsNullValidator" (preferably in a common location of the solution ie. either in Model or a Common project, as applied) and copy below code:
public class RequiredIfDepFieldIsNullValidator : ValidationAttribute
{
private readonly string _dependentProperty;
public RequiredIfDepFieldIsNullValidator(string dependentProperty)
{
_dependentProperty = dependentProperty;
}

protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
// get a reference to the property this validation depends upon
var field = validationContext.ObjectType.GetProperty(_dependentProperty);

if (field == null)
{
// field not valid - return an error
return new ValidationResult(string.Format(CultureInfo.CurrentCulture, "Unknown property {0}", new[] { _dependentProperty }));
}

// get the value of the dependent property
var otherPropertyValue = field.GetValue(validationContext.ObjectInstance, null);

// trim spaces and convert dependent value to uppercase to support case senstive comparison
if (otherPropertyValue != null && otherPropertyValue is string)
{
otherPropertyValue = (otherPropertyValue as string).Length == 0 ? null : (otherPropertyValue as string).ToUpper();
}

// trim spaces and convert TargetValue to uppercase to support case senstive comparison
if (targetValue != null && targetValue is string)
{
targetValue = (targetValue as string).Length == 0 ? null : (targetValue as string).ToUpper();
}

// compare the value against the target value
if (otherPropertyValue == null && targetValue == null)
{
// validation failed - return an error
return new ValidationResult(string.Format(CultureInfo.CurrentCulture,
                                        FormatErrorMessage(validationContext.DisplayName), new[] { _dependentProperty }));
}
// validation success - return success
return ValidatioResult.Success;
}
}
We need to create or make changes to Model(s) and Controller(s) as below to implement the created custom validation.

POCO / Model: Create / Modify Address class to apply the custom validation on Country property as below to define Country is required when City is not provided.
public class Address {
public int AddressId {get; set; }

public string Line1 { get; set; }

public string Line2 { get; set; }

public string City { get; set; }

public string State { get; set; }

public string Zip { get; set; }

[RequiredIfDepFieldIsNullValidator("City", ErrorMessage = "Country is Required")]
public string Country { get; set; }
}
Controller: The controller for Address entity with a POST method will look like below.

Here, before persisting the changes in database, I am forcing process to re-validate Modal State by calling "this.Validate()", which will help the process to identify and throw an error upfront instead of making a call to DB.
public class AddressController : ApiController
{
[ResponseType(typeof(Address))]
public IHttpActionResult PostAddress(Address address)
{
// use below to force validation before doing anything
this.Validate(address);

if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
db.Address.Add(address);
db.SaveChanges();

return CreatedAtRoute("DefaultApi", new { id = address.AddressId }, address);
}
}
Testing: Below TestMethods will help testing the post method to check for the expected validation error:

Here, The first test method will represent a case forcing this.Validate() method and second without forcing this.Validate() in controller's POST method.
// Forcing this.Validate() - this case Controller will thow the error
[TestMethod]
public void RequiredCountryWhenCityIsNullTest()
{
Address address = new Address { Line1 = "200 E Main St", Line2 = "Apt B", State = "VA" };
AddressController addressController = new AddressController();
AddressController.Request = new HttpRequestMessage();
AddressController.Request.Properties["MS_HttpConfiguration"] = new HttpConfiguration();

addressController.PostAddress(address);

Assert.IsFalse(addressController.ModelState.IsValid);
Assert.IsTrue(addressController.ModelState.Count == 1, "Country is Required");
}
// Without forcing this.Validate() - this case DB will throw the error
[TestMethod]
public void RequiredCountryWhenCityIsNullTest()
{
Address address = new Address { Line1 = "200 E Main St", Line2 = "Apt B", State = "VA" };
try
{
AddressController addressController = new AddressController();
AddressController.Request = new HttpRequestMessage();
AddressController.Request.Properties["MS_HttpConfiguration"] = new HttpConfiguration();

addressController.PostAddress(address);
}
catch (System.Data.Entity.Validation.DbEntityValidationException dbEx)
{
foreach (var validationErrors in dbEx.EntityValidationErrors)
{
foreach (var validationError in validationErrors.ValidationErrors)
{
Assert.AreEqual("Country is Required", validationError.ErrorMessage);
}
}
}
}
With this I am concluding the illustration. Feel free to share your feedback.

Happy Programming !!!

No comments:

Post a Comment