By adding some metadata to your object, either by inheriting from an abstract class that contains that data, writing a composite object, or something similar.
In ASP.NET MVC 1, this would have been done by attaching a partial class
to your entity (In the C# language, classes having the same name with the modifier partial
in front of them are merely treated like one big single class).
So, for example:
public partial class Dinner {
public bool IsValid {
get { return (GetRuleViolations().Count() == 0); }
}
// Return a list of errors that we can iterate over with `foreach`
public IEnumerable<RuleViolation> GetRuleViolations() {
// Validation rules go here; see below for an example.
yield break;
}
partial void OnValidate(ChangeAction action) {
if (!IsValid)
throw new ApplicationException("Rule violations prevent saving");
}
}
A Rule Violation is just an immutable object containing an Error Message and a Property Name:
public class RuleViolation {
public string ErrorMessage { get; private set; }
public string PropertyName { get; private set; }
public RuleViolation(string errorMessage, string propertyName) {
ErrorMessage = errorMessage;
PropertyName = propertyName;
}
}
You would then check for errors like this:
public IEnumerable<RuleViolation> GetRuleViolations() {
if (String.IsNullOrEmpty(Title))
yield return new RuleViolation("Title required","Title");
if (String.IsNullOrEmpty(Description))
yield return new RuleViolation("Description required","Description");
if (String.IsNullOrEmpty(HostedBy))
yield return new RuleViolation("HostedBy required", "HostedBy");
if (String.IsNullOrEmpty(Address))
yield return new RuleViolation("Address required", "Address");
if (String.IsNullOrEmpty(Country))
yield return new RuleViolation("Country required", "Country");
if (String.IsNullOrEmpty(ContactPhone))
yield return new RuleViolation("Phone# required", "ContactPhone");
if (!PhoneValidator.IsValidNumber(ContactPhone, Country))
yield return new RuleViolation("Phone# does not match country", "ContactPhone");
yield break;
}
This arrangement allows you to pass the errors back to a view so that they can be displayed to the user, like so (This is a controller method; it accepts a form POST from a user web form):
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues) {
Dinner dinner = dinnerRepository.GetDinner(id);
try {
// Map form fields to dinner object
UpdateModel(dinner);
// Validation occurs here. Throws exception if validation fails
dinnerRepository.Save();
return RedirectToAction("Details", new { id=dinner.DinnerID });
}
catch {
// Add each rule violation to the View, so that errors can be displayed.
foreach (var issue in dinner.GetRuleViolations()) {
ModelState.AddModelError(issue.PropertyName, issue.ErrorMessage);
}
// Display form with validation errors.
return View(dinner);
}
}
Note that the exception is there only to trigger the validation handling process; there isn't an exception for every possible validation error. Instead, the validation rules are encoded in the GetRuleViolations()
method, and they are all handled at once.
Of course, nowadays, the process has become a bit more refined. In the newer versions of ASP.NET MVC, attributes are used to encode the validation rules. But the principle is still the same: you don't use exceptions to encode individual validation rules. Notice the validation metadata that is encoded in the attributes of the code below (the attributes are those lines of code that are surrounded by brackets):
public class Movie
{
public int ID { get; set; }
[StringLength(60, MinimumLength = 3)]
public string Title { get; set; }
[Display(Name = "Release Date")]
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime ReleaseDate { get; set; }
[RegularExpression(@"^[A-Z]+[a-zA-Z''-'\s]*$")]
[Required]
[StringLength(30)]
public string Genre { get; set; }
[Range(1, 100)]
[DataType(DataType.Currency)]
public decimal Price { get; set; }
[RegularExpression(@"^[A-Z]+[a-zA-Z''-'\s]*$")]
[StringLength(5)]
public string Rating { get; set; }
}