Showing posts with label DataAnnotations. Show all posts
Showing posts with label DataAnnotations. Show all posts

Wednesday, February 23, 2011

How to unit test your DataAnnotations attributes

Earlier on the Twitter, I helped a pal solve a problem they were having with unit testing their DataAnnotations implementation.  He compelled me to blog my solution, and I agree -- what a great subject!

So let's take a pretty common use-case: validating an email address.  Email addresses are a slippery fish, so I'll make a DataAnnotations validator to make sure that an input's value looks like it's probably in a valid email address format.

I start with a regex validator that I think I pulled off of Scott Guthrie's blog:

public class RegexAttribute: ValidationAttribute
{
  public string Pattern { get; set; }
  public RegexOptions Options { get; set; }

  public RegexAttribute(string pattern)
      : this(pattern, RegexOptions.None) {}

  public RegexAttribute(string pattern, RegexOptions options)
  {
    Pattern = pattern;
    Options = options;
  }

  public override bool IsValid(object value)
  {
    //null or empty is valid, let Required handle null/empty
    var str = (string)value;
    return string.IsNullOrEmpty(str) || 
      new Regex(Pattern, Options).IsMatch(str);
  }

}


Simple enough, let's extend it and make an Email validator:

public class EmailAttribute : RegexAttribute
{
  public EmailAttribute() : 
    base(@"^(([^<>()[\]\\.,;:\s@\""]+(\.[^<>()[\]\\.,;:\s@\""]+)*)|(\"".+\""))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$", 
    RegexOptions.None) {}
}


Pretty simple.  I bet I could get away with putting it all on one line since no one actually reads regular expressions.

Now here's the magic part.  I'm going to use Reflection to grab all the attributes that are of the type ValidationAttribute and see if those attributes are valid.  If they aren't, I'll make a list of ErrorInfo objects.  I call it my ValidationBuddy.

public static class ValidationBuddy
{
  public static IList<ErrorInfo> GetTypeErrors(object instance)
  {
    var attributes = TypeDescriptor.GetAttributes(instance)
      .OfType<ValidationAttribute>();
      return (from attribute in attributes
              let result = attribute.GetValidationResult(instance, 
          new ValidationContext(instance, null, null))
              where result != null
              select new ErrorInfo(instance.GetType().ToString(), 
          attribute.FormatErrorMessage(string.Empty), 
          instance))
      .ToList();
  }

  public static IList<ErrorInfo> GetPropertyErrors(object instance)
  {
    return (from prop in TypeDescriptor.GetProperties(instance)
      .Cast<PropertyDescriptor>()
        from attribute in prop.Attributes
          .OfType<ValidationAttribute>()
        where !attribute.IsValid(prop.GetValue(instance))
        select new ErrorInfo(prop.Name, 
          attribute.FormatErrorMessage(prop.DisplayName), instance))
         .ToList();
  }

  public static IList<ErrorInfo> GetErrors(object instance)
  {
    var list = GetPropertyErrors(instance);

    foreach (var error in GetTypeErrors(instance))
      list.Add(error);

    return list;
  }
}


The ErrorInfo class is pretty simple, too:

public class ErrorInfo
{
  public ErrorInfo(){}
  public ErrorInfo(string propertyName, string errorMessage, object instance)
  {
    PropertyName = propertyName;
    ErrorMessage = errorMessage;
    Instance = instance;
  }

  public string PropertyName { get; set; }
  public string ErrorMessage { get; set; }
  public object Instance { get; set; }
}


And finally, a single unit test:

[TestMethod]
public void TestEmailValidator05()
{
  // Arrange
  var model = GetModel();
  model.EmailAddress = "uncommonTLD@domain.travel";

  // Act
  var errors = ValidationBuddy.GetErrors(model);

  // Assert
  Assert.IsTrue(!errors.Any());
}


In my actual test class, I have quite a few more tests, but this should get you started in writing your own DataAnnotations unit tests.

Monday, February 14, 2011

Validating an uploaded image file using ASP.NET MVC and DataAnnotations

I recently wrote a feature for a small MVC app that required me to allow a user to upload an image to my web server. This got me wondering "how does a web server know if the file I uploaded is an image file?" There are a few issues to think through:
  • Not every operating system implements file name conventions the same way. Rather, we shouldn't assume that we know anything about the format of the file name we'll receive, it's just a string -- and it may be an empty string.
  • We can't assume that the browser used to upload the file will accurately identify the content type of the uploaded file.
  • The user may upload an image type that they recognize, but our web server does not recognize as an image.
Since this is an ASP.NET MVC app, I started by identifying the Model I want to deal with. Here's my UploadedFileModel:
    public class UploadedFileModel
    {
        [Required, ImageType]
        public HttpPostedFileBase Image { get; set; }
    }
Pretty simple, just one property, nothing too fancy. If you're familiar with DataAnnotations, you'll recognize the Required validator right away. This just checks to see if the property is null when the model is bound to my action.

The other thing one might notice is the ImageType attribute. This is where I put my secret sauce. Take a look:
  public class ImageTypeAttribute : ValidationAttribute
  {
    public override bool IsValid(object value)
    {
      // don't duplicate the Required validator
      if (value == null)
        return true;

      // is it the type we expect?
      if (!(value is HttpPostedFileBase))
        return false;

      // We got this far, is it a recognizable image type?
      var fileStream = ((HttpPostedFileBase) value).InputStream;
      try
      {
        var image = Image.FromStream(fileStream);
      }
      catch
      {
        return false;
      }
      return true;
    }

    public override string FormatErrorMessage(string name)
    {
      return string.Format("{0} is not a valid image format.", name);
    }
  }
At this point, I'm effectively asking the web server if it recognizes whether the posted file as an image type. Yes? Hooray! No? We'll tell the user that it didn't look like a valid image type to us.

Once all that is wired up, we can let the action just do action stuff:
    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Insert(UploadedFileModel viewModel)
    {
      if (!ModelState.IsValid)
        return View("Create");

      var model = ImageUtilities.SaveImage(viewModel.Image);

      if (model == null)
      {
        ModelState.AddModelError("Image",
            "File could not be saved, this could happen if the client did not have permission to save the file to disk.");
        return View("Create");
      }

      repository.Add(model);
      return RedirectToAction("Details", new {id = model.Name});
    }
And that's pretty much it. I tried to take advantage of as much default behavior as I could, and I ended up with a validator that I can reuse.