70

I have a Create action that takes an entity object and a HttpPostedFileBase image. The image does not belong to the entity model.

I can save the entity object in the database and the file in disk, but I am not sure how to validate these business rules:

  • Image is required
  • Content type must be "image/png"
  • Must not exceed 1MB
Serdar
  • 1,416
  • 2
  • 17
  • 43
  • You can check this for multiple validations: http://www.codingfusion.com/Post/validate-uploaded-file-in-ASP-NET-MVC – MaxPayne Aug 01 '23 at 03:56

5 Answers5

140

A custom validation attribute is one way to go:

public class ValidateFileAttribute : RequiredAttribute
{
    public override bool IsValid(object value)
    {
        var file = value as HttpPostedFileBase;
        if (file == null)
        {
            return false;
        }

        if (file.ContentLength > 1 * 1024 * 1024)
        {
            return false;
        }

        try
        {
            using (var img = Image.FromStream(file.InputStream))
            {
                return img.RawFormat.Equals(ImageFormat.Png);
            }
        }
        catch { }
        return false;
    }
}

and then apply on your model:

public class MyViewModel
{
    [ValidateFile(ErrorMessage = "Please select a PNG image smaller than 1MB")]
    public HttpPostedFileBase File { get; set; }
}

The controller might look like this:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        var model = new MyViewModel();
        return View(model);
    }

    [HttpPost]
    public ActionResult Index(MyViewModel model)
    {
        if (!ModelState.IsValid)
        {
            return View(model);
        }

        // The uploaded image corresponds to our business rules => process it

        var fileName = Path.GetFileName(model.File.FileName);
        var path = Path.Combine(Server.MapPath("~/App_Data"), fileName);
        model.File.SaveAs(path);

        return Content("Thanks for uploading", "text/plain");
    }
}

and the view:

@model MyViewModel

@using (Html.BeginForm("Index", "Home", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
    @Html.LabelFor(x => x.File)
    <input type="file" name="@Html.NameFor(x => x.File)" id="@Html.IdFor(x => x.File)" />
    @Html.ValidationMessageFor(x => x.File)
    <input type="submit" value="upload" />
}
Trent
  • 335
  • 2
  • 8
Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • What would be the equivalent of `Image.FromStream` for PDFs? – escist Nov 06 '12 at 07:21
  • 3
    @escist, there isn't. The .NET framework doesn't support the PDF format. You will have to use a third party library if you want to manipulate PDF files. `iTextSharp` is a popular open source library. There are also many other commercial libraries. If you don't want to use a third party library you could look at the first bytes of the stream to determine whether it is a valid PDF. Bear in mind though that there are different versions of the PDF format and if you want an universal solution you have to take into account them. – Darin Dimitrov Nov 06 '12 at 08:57
  • @DarinDimitrov is this the most paranoid validation? can any more checks being added? – Serdar Feb 11 '13 at 20:05
  • The only problem with this approach is that it only fires if your uploaded file doesn't exist (as it inherits from RequiredAttribute.) You need to then inherit from ValidationAttribute instead of RequiredAttribute – Evonet May 26 '13 at 22:22
  • When is the file ever valid? It doesn't return true at any point as far as I can see. – Ian Warburton Jun 25 '13 at 12:44
  • @IanWarburton, didn't you see the following line: `return img.RawFormat.Equals(ImageFormat.Png);`? – Darin Dimitrov Jun 25 '13 at 13:20
  • This will not work since the line 'var file = value as HttpPostedFileBase;' in method isValid will return false as value is of type array HttpPostedFileBase[]; @Darin Dimitrov – GotaloveCode Aug 26 '14 at 09:07
  • There are several things wrong with this answer, but people want to knee-jerk reject edits which fix the problems, so hopefully everyone reading this answer figures them out. – Jakotheshadows Sep 02 '16 at 15:24
  • 1
    @DarinDimitrov Do you happen to know whether it works for client side validation? – Pyrejkee Oct 22 '18 at 12:49
9

Based on Darin Dimitrov's answer which I have found very helpful, I have an adapted version which allows checks for multiple file types, which is what I was initially looking for.

public override bool IsValid(object value)
    {
        bool isValid = false;
        var file = value as HttpPostedFileBase;

        if (file == null || file.ContentLength > 1 * 1024 * 1024)
        {
            return isValid;
        }

        if (IsFileTypeValid(file))
        {
            isValid = true;
        }

        return isValid;
    }

    private bool IsFileTypeValid(HttpPostedFileBase file)
    {
        bool isValid = false;

        try
        {
            using (var img = Image.FromStream(file.InputStream))
            {
                if (IsOneOfValidFormats(img.RawFormat))
                {
                    isValid = true;
                } 
            }
        }
        catch 
        {
            //Image is invalid
        }
        return isValid;
    }

    private bool IsOneOfValidFormats(ImageFormat rawFormat)
    {
        List<ImageFormat> formats = getValidFormats();

        foreach (ImageFormat format in formats)
        {
            if(rawFormat.Equals(format))
            {
                return true;
            }
        }
        return false;
    }

    private List<ImageFormat> getValidFormats()
    {
        List<ImageFormat> formats = new List<ImageFormat>();
        formats.Add(ImageFormat.Png);
        formats.Add(ImageFormat.Jpeg);
        formats.Add(ImageFormat.Gif);
        //add types here
        return formats;
    }
}
Elizabeth Hamlet
  • 170
  • 3
  • 10
  • This will not work since the line 'var file = value as HttpPostedFileBase;' in method isValid will return false as value is of type array HttpPostedFileBase[]; @Elizabeth Hamlet – GotaloveCode Aug 26 '14 at 09:09
  • Help please. In IsValid function, I get null for value and thus null for file as well. Following lines are from my ViewModel: [ValidateFile(ErrorMessage = "Please select an image file")] [DataType(DataType.Upload)] [DisplayName("File")] public HttpPostedFileBase FileAttachment { get; set; } Thanks. – corix010 Feb 06 '15 at 17:53
  • Never mind got it working. I don't have the FileAttachment property in [Bind(Include = "...")] in my controller. – corix010 Feb 06 '15 at 20:17
4

Here is a way to do it using viewmodel, take a look at whole code here

Asp.Net MVC file validation for size and type Create a viewmodel as shown below with FileSize and FileTypes

public class ValidateFiles
{
    [FileSize(10240)]
    [FileTypes("doc,docx,xlsx")]
    public HttpPostedFileBase File { get; set; }
}

Create custom attributes

public class FileSizeAttribute : ValidationAttribute
{
    private readonly int _maxSize;

    public FileSizeAttribute(int maxSize)
    {
        _maxSize = maxSize;
    }
    //.....
    //.....
}



public class FileTypesAttribute : ValidationAttribute
{
    private readonly List<string> _types;

    public FileTypesAttribute(string types)
    {
        _types = types.Split(',').ToList();
    } 
    //....
    //...
}
Jeff D
  • 349
  • 3
  • 4
  • 1
    That's works for me but on form submit the validation works only after going to the server, do you know how to make it work on client side only so it will work exactly like [Required] attribute? – Alex Feb 27 '18 at 22:23
1

And file length validation in asp.net core:

public async Task<IActionResult> MyAction()
{
    var form = await Request.ReadFormAsync();
    if (form.Files != null && form.Files.Count == 1)
    {
        var file = form.Files[0];
        if (file.Length > 1 * 1024 * 1024)
        {
            ModelState.AddModelError(String.Empty, "Maximum file size is 1 Mb.");
        }
    }
    // action code goes here
}
David Levin
  • 6,573
  • 5
  • 48
  • 80
-1

You may want to consider saving the image to database also:

  using (MemoryStream mstream = new MemoryStream())
                {
                    if (context.Request.Browser.Browser == "IE")
                        context.Request.Files[0].InputStream.CopyTo(mstream);
                    else
                        context.Request.InputStream.CopyTo(mstream);

                    if (ValidateIcon(mstream))
                    {
                        Icon icon = new Icon() { ImageData = mstream.ToArray(), MimeType = context.Request.ContentType };
                        this.iconRepository.SaveOrUpdate(icon);
                    }
                }

I use this with NHibernate - entity defined:

 public Icon(int id, byte[] imageData, string mimeType)
    {
        this.Id = id;
        this.ImageData = imageData;
        this.MimeType = mimeType;
    }

    public virtual byte[] ImageData { get; set; }

    public virtual string MimeType { get; set; }

Then you can return the image as a FileContentResult:

 public FileContentResult GetIcon(int? iconId)
    {
        try
        {
            if (!iconId.HasValue) return null;

            Icon icon = this.iconRepository.Get(iconId.Value);

            return File(icon.ImageData, icon.MimeType);
        }
        catch (Exception ex)
        {
            Log.ErrorFormat("ImageController: GetIcon Critical Error: {0}", ex);
            return null;
        }
    }

Note that this is using ajax submit. Easier to access the data stream otherwise.

BonyT
  • 10,750
  • 5
  • 31
  • 52