(caveat: this may or may not work in normal ASP.NET, I haven’t tried it yet)
“Validation” is a wonderfully ambiguous term. Usually, in simple examples, it’s restricted to ensuring that required fields are present, that dates are in the correct format, and so on. All of these only require access to the object being validated, which can be very easily done using data annotations:
using System; using System.ComponentModel.DataAnnotations; public class ValidateMe { [Required] [StringLength(50)] public string ImARequiredField { get; set; } }
You can take this a step further if, for example, you want to run rules on the data in the object by implementing IValidatableObject
. An example:
using System; using System.ComponentModel.DataAnnotations; public class ValidateMe : IValidatableObject { [StringLength(50)] public string ImARequiredField { get; set; } public bool TheFieldIsRequired { get; set; } public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) { if (TheFieldIsRequired) { yield return new ValidationResult("The field is required", new string[] { nameof(ImARequiredField) }); } } }
Great! Now we can wire this up in our controller:
public IActionResult DoStuff(ValidateMe model) { if (ModelState.IsValid) { // do stuff! } else { return View(model); } }
Fantastic! But wait, what’s this? Oh dear, it’s the Real World(TM) come to burst our bubble.
Let’s take a more realistic example. Let’s say that we have a model that needs to set up a new user account. In our system, email addresses must be unique, and so we need to validate this before saving the user. We want to have a nice, clean, de-coupled architecture with injected dependencies, but where can we inject the dependencies into our validation system? Without them, we’d end up like this:
public IActionResult AddUser(AddUserModel model, [FromServices] IEmailValidator emailValidator) { if (!emailValidator.IsEmailAvailable(model.Email)) { ModelState.AddModelError("Email", "Email address is taken"); } if (ModelState.IsValid) { // do stuff! } else { return View(model); } }
Well, that’s ugly – it’s not terrible, but it’s bloating our controller, and, it turns out, we don’t need to do it at all, because enter ValidationContext
!
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) { // magic happens here! var emailValidator = validationContext.GetService(typeof(IEmailValidator)); // now I has dependency, I can use ftw! if (!emailValidator.IsEmailAvailable(Email)) { yield return new ValidationResult("Email address is taken", new string[] { nameof(Email) }); } }
Without doing anything special, ASP.NET is clever enough to pass its IServiceProvider
into ValidationContext
, and so we now have access to any dependencies that our application needs. The only slight downside is that the Validate
method isn’t async, and so we’ll need to wrap any async calls in Task.Run(...)
calls, but that’s a small price to pay for keeping our controllers slim.