Search code examples
regexasp.net-mvc-4automappermodel-validationcustom-validators

ModelState.IsValid is always false for RegularExpression ValidationAttribute in MVC 4


In my class, I have a property for a file attachment like so...

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; }
}

I don't see anything wrong with my regex, but I always get ModelState.IsValid false. This seems pretty trivial and simple regex, am I missing something? Do I need to write my own custom validation?

I'm populating AttachmentTrace via a regular input of type file:

<div class="editor-label">
    @Html.LabelFor(model => model.AttachmentTrace)
</div>
<div class="editor-field">
    @Html.TextBoxFor(model => model.AttachmentTrace, new { type = "file" })
    @Html.ValidationMessageFor(model => model.AttachmentTrace)
</div>

The action method is just a regular action:

public ActionResult Create(Certificate certificate, HttpPostedFileBase attachmentTrace, HttpPostedFileBase attachmentEmail)
    {
        if (ModelState.IsValid)
        {
            // code ...
        }
        return View(certificate);
    }

Solution

  • Ok, here's the solution I found. I'm sure there are other solutions out there. First a little background, because my application uses EF code-first migration, specifying a HttpPostedFileBase property type in my model, produces this error when adding migration:

    One or more validation errors were detected during model generation: System.Data.Entity.Edm.EdmEntityType: : EntityType 'HttpPostedFileBase' has no key defined. Define the key for this EntityType. \tSystem.Data.Entity.Edm.EdmEntitySet: EntityType: EntitySet 'HttpPostedFileBases' is based on type 'HttpPostedFileBase' that has no keys defined.

    So I really had to stick with using a string type for the AttachmentTrace property.

    The solution is to employ a ViewModel class like this:

    public class CertificateViewModel {
        // .. other properties
        [Required]
        [FileTypes("xls,xlsx")]
        public HttpPostedFileBase AttachmentTrace { get; set; }
    }
    

    Then create a FileTypesAttribute like so, I borrowed this code from this excellent post.

    public class FileTypesAttribute : ValidationAttribute {
        private readonly List<string> _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, I needed to make a change to use the ViewModel instead, then map it back to my Entity using AutoMapper (which is excellent by the way):

    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<HttpPostedFileBase, string>().ConvertUsing(new HttpPostedFileBaseTypeConverter());
                Mapper.CreateMap<CreateCertificateViewModel, Certificate>();
                Certificate myCert = Mapper.Map<CreateCertificateViewModel, Certificate>(certificate);
                // other code ...
            }
            return View(myCert);
        }
    

    For the AutoMapper, I created my own TypeConverter for the HttpPostedFileBase as follows:

    public class HttpPostedFileBaseTypeConverter : ITypeConverter<HttpPostedFileBase, string> {
    
        public string Convert(ResolutionContext context) {
            var fileBase = context.SourceValue as HttpPostedFileBase;
            if (fileBase != null) {
                return fileBase.FileName;
            }
            return null;
        }
    }
    

    That's it. Hope this helps out others who may have this same issue.