ASP.NET

Upload files to a server folder and download them again in ASP.NET (C#)

ASP.NET · · Published 2026-10-15

Accepting files from users is one of the riskiest features a web application can have. Done carelessly, an upload form lets anyone place a script on your server and run it. Done properly, it is still short: limit the size, check the type, never trust the client's file name, store files where the web server will not execute them, and serve them back through your own code.

C# code checking the extension, saving under a generated name and streaming the file back with TransmitFile
The upload and download steps in a few lines.

Configuration: size limits

Two settings cap the request size, and the smaller one wins:

<configuration>
  <system.web>
    <!-- kilobytes; default 4096 (4 MB) -->
    <httpRuntime targetFramework="4.8" maxRequestLength="10240" />
  </system.web>
  <system.webServer>
    <security>
      <requestFiltering>
        <!-- bytes; default 30000000 (about 28.6 MB) -->
        <requestLimits maxAllowedContentLength="10485760" />
      </requestFiltering>
    </security>
  </system.webServer>
</configuration>

Exceeding maxRequestLength raises "Maximum request length exceeded"; exceeding IIS's maxAllowedContentLength returns a 404.13 error before ASP.NET even runs. Keep both slightly above the largest file you accept, and check the size in code to give a friendly message.

The page

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Files.aspx.cs" Inherits="Demo.Files" %>
<!DOCTYPE html>
<html>
<head runat="server"><title>Files</title></head>
<body>
<form id="form1" runat="server">
    <asp:FileUpload ID="fuFile" runat="server" accept=".pdf,.png,.jpg,.jpeg,.txt" />
    <asp:Button ID="btnUpload" runat="server" Text="Upload" OnClick="btnUpload_Click" />
    <asp:Label ID="lblMessage" runat="server" />

    <asp:GridView ID="gvFiles" runat="server" AutoGenerateColumns="false" OnRowCommand="gvFiles_RowCommand"
        EmptyDataText="No files uploaded yet.">
        <Columns>
            <asp:BoundField DataField="OriginalName" HeaderText="File" />
            <asp:BoundField DataField="SizeKb" HeaderText="Size (KB)" DataFormatString="{0:N0}" />
            <asp:BoundField DataField="Uploaded" HeaderText="Uploaded (UTC)" DataFormatString="{0:yyyy-MM-dd HH:mm}" />
            <asp:ButtonField CommandName="Download" Text="Download" />
        </Columns>
    </asp:GridView>
</form>
</body>
</html>

The accept attribute filters the browser's file picker. It is a convenience: a user can still pick any file, and a script can post anything. The server makes the real decision.

The code-behind

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace Demo
{
    public partial class Files : Page
    {
        private const int MaxBytes = 10 * 1024 * 1024;
        private static readonly HashSet<string> Allowed =
            new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".pdf", ".png", ".jpg", ".jpeg", ".txt" };

        // App_Data is never served directly by ASP.NET
        private string StoreDir => Server.MapPath("~/App_Data/uploads");

        protected void Page_Load(object sender, EventArgs e)
        {
            Directory.CreateDirectory(StoreDir);
            if (!IsPostBack) BindFiles();
        }

        protected void btnUpload_Click(object sender, EventArgs e)
        {
            if (!fuFile.HasFile)
            {
                lblMessage.Text = "Choose a file first.";
                return;
            }

            HttpPostedFile posted = fuFile.PostedFile;
            string originalName = Path.GetFileName(posted.FileName); // older IE sent the full client path
            string ext = Path.GetExtension(originalName);

            if (!Allowed.Contains(ext))
            {
                lblMessage.Text = "Only PDF, PNG, JPEG and text files are accepted.";
                return;
            }
            if (posted.ContentLength == 0 || posted.ContentLength > MaxBytes)
            {
                lblMessage.Text = "Files must be between 1 byte and 10 MB.";
                return;
            }

            // Never use the client's name on disk: generate one, keep the original as metadata.
            string storedName = Guid.NewGuid().ToString("N") + ext.ToLowerInvariant();
            posted.SaveAs(Path.Combine(StoreDir, storedName));
            File.WriteAllText(Path.Combine(StoreDir, storedName + ".name"), originalName);

            lblMessage.Text = "Uploaded " + Server.HtmlEncode(originalName) + ".";
            BindFiles();
        }

        private void BindFiles()
        {
            var files = new DirectoryInfo(StoreDir).GetFiles()
                .Where(f => Allowed.Contains(f.Extension))
                .OrderByDescending(f => f.CreationTimeUtc)
                .Select(f => new
                {
                    Stored = f.Name,
                    OriginalName = ReadOriginalName(f),
                    SizeKb = Math.Max(1, f.Length / 1024),
                    Uploaded = f.CreationTimeUtc
                })
                .ToList();

            gvFiles.DataKeyNames = new[] { "Stored" };
            gvFiles.DataSource = files;
            gvFiles.DataBind();
        }

        private static string ReadOriginalName(FileInfo f)
        {
            string meta = f.FullName + ".name";
            return File.Exists(meta) ? File.ReadAllText(meta) : f.Name;
        }

        protected void gvFiles_RowCommand(object sender, GridViewCommandEventArgs e)
        {
            if (e.CommandName != "Download") return;

            int index = Convert.ToInt32(e.CommandArgument);
            string stored = (string)gvFiles.DataKeys[index].Value;

            // The key comes back from view state, but validate it anyway.
            if (stored.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0 || !Allowed.Contains(Path.GetExtension(stored)))
                return;

            string fullPath = Path.Combine(StoreDir, stored);
            if (!File.Exists(fullPath)) return;

            string downloadName = ReadOriginalName(new FileInfo(fullPath));
            Response.Clear();
            Response.ContentType = MimeMapping.GetMimeMapping(downloadName);
            Response.AddHeader("Content-Disposition",
                "attachment; filename*=UTF-8''" + Uri.EscapeDataString(downloadName));
            Response.AddHeader("X-Content-Type-Options", "nosniff");
            Response.TransmitFile(fullPath);
            Response.Flush();
            Response.SuppressContent = true;
            Context.ApplicationInstance.CompleteRequest();
        }
    }
}

Why each precaution matters

Checking the extension does not prove the content. If you accept images, you can additionally decode them (for example with System.Drawing.Image.FromStream inside a try/catch, or a maintained imaging library) and reject files that are not valid images. For files other people will download, consider antivirus scanning on the server.

Several files at once

Since .NET 4.5, FileUpload supports multiple selection:

<asp:FileUpload ID="fuFiles" runat="server" AllowMultiple="true" />
foreach (HttpPostedFile file in fuFiles.PostedFiles)
{
    // apply the same checks to each file
}

Remember that maxRequestLength applies to the whole request, so the total of all files must fit.

UpdatePanel and uploads

A FileUpload does not work inside an asynchronous postback: the browser cannot send files through the partial-page request that UpdatePanel uses. Register the upload button as a PostBackTrigger so it performs a full postback.

Related

Official documentation