3.13.2013

ModelState.IsValid always returning False for RegularExpression ValidationAttribute for a File Upload in MVC 4

I posted a question in stackoverflow and ended up answering it on my own. Some people gave me clues along the way so I'm thankful. I just want to do a recap of the solution that way I have it in my notes...

I was having issues with model validation using RegularExpression as indicated in my class below, the property AttachmentTrace is a file attachment that is uploaded during form post.
public class Certificate {
    [Required]
    // TODO:  Wow looks like there's a problem with using regex in MVC 4, this does not work!
    [RegularExpression(@"^.*\.(xlsx|xls|XLSX|XLS)$", ErrorMessage = "Only Excel files (*.xls, *.xlsx) files are accepted")]
    public string AttachmentTrace { get; set; }
}
All the while I thought there's something wrong with my Regex. But after looking closely on this issue, I found out that what is being validated on the server side is the string "System.Web.HttpPostedFileWrapper", and not the actual filename. That's the reason why ModelState.IsValid returns a false everytime, no matter how right the regex is. But I cannot simply switch the property type of string to HttpPostedFileBase in my model, because I'm using EF code-first migration, which will result into an unpleasant error message when adding a migration.

So the solution to this is to employ a ViewModel, instead of the Entity directly:
public class CertificateViewModel {
    // .. other properties
    [Required]
    [FileTypes("xls,xlsx")]
    public HttpPostedFileBase AttachmentTrace { get; set; }
}
The next step is to create a custom ValidationAttribute for the FileTypes:
public class FileTypesAttribute : ValidationAttribute {
    private readonly List _types;

    public FileTypesAttribute(string types) {
        _types = types.Split(',').ToList();
    }

    public override bool IsValid(object value) {
        if (value == null) return true;
        var postedFile = value as HttpPostedFileBase;
        var fileExt = System.IO.Path.GetExtension(postedFile.FileName).Substring(1);
        return _types.Contains(fileExt, StringComparer.OrdinalIgnoreCase);
    }

    public override string FormatErrorMessage(string name) {
        return string.Format("Invalid file type. Only {0} are supported.", String.Join(", ", _types));
    }
}
In the controller Action, use the ViewModel instead of the Entity, then map the ViewModel back to the Entity via AutoMapper:
public ActionResult Create(CertificateViewModel certificate, HttpPostedFileBase attachmentTrace, HttpPostedFileBase attachmentEmail) {
        if (ModelState.IsValid) {
            // Let's use AutoMapper to map the ViewModel back to our Certificate Entity
            // We also need to create a converter for type HttpPostedFileBase -> string
            Mapper.CreateMap().ConvertUsing(new HttpPostedFileBaseTypeConverter());
            Mapper.CreateMap();
            Certificate myCert = Mapper.Map(certificate);
            // other code ...
        }
        return View(myCert);
    }
For the AutoMapper, create a TypeConverter for HttpPostedFileBase:
public class HttpPostedFileBaseTypeConverter : ITypeConverter {

    public string Convert(ResolutionContext context) {
        var fileBase = context.SourceValue as HttpPostedFileBase;
        if (fileBase != null) {
            return fileBase.FileName;
        }
        return null;
    }
}
I know, it's a lot of work, but this will ensure the file extension validation will be done correctly. But wait, this is not yet complete, I need to also make sure that the client side validation will work for this and that will be the topic of my next post.

No comments:

Post a Comment