How to validate uploaded file in ASP.NET MVC?

asp.net MvcSecurity

asp.net Mvc Problem Overview


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

asp.net Mvc Solutions


Solution 1 - asp.net Mvc

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" />
}

Solution 2 - asp.net Mvc

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

Solution 3 - asp.net Mvc

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][1] [1]: http://codeanalyze.com/Articles/Details/6/MVC-5-Validate-Posted-Files-view-models-viewmodels

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();
    } 
    //....
    //...
}

Solution 4 - asp.net Mvc

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
}

Solution 5 - asp.net Mvc

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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questionuser386167View Question on Stackoverflow
Solution 1 - asp.net MvcDarin DimitrovView Answer on Stackoverflow
Solution 2 - asp.net MvcElizabeth HamletView Answer on Stackoverflow
Solution 3 - asp.net MvcJeff DView Answer on Stackoverflow
Solution 4 - asp.net MvcDavid LevinView Answer on Stackoverflow
Solution 5 - asp.net MvcBonyTView Answer on Stackoverflow