File Upload APIs
Profile photos, invoices, CSV imports — sooner or later your API must accept files. Web API 2 reads multipart uploads with a provider class, and you must validate everything about the file.
On this page · 6 min · Advanced
On this page
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 6 min
- Difficulty
- Advanced
- Updated
- Oct 28, 2026
Related tutorials
Learning objectives#
- Accept multipart/form-data uploads
- Save files with MultipartFormDataStreamProvider
- Validate size and extension and generate safe names
- Stream a file download with the right headers
Explanation#
Browsers and apps send files as multipart/form-data: several parts, each with headers, some containing files and some ordinary form fields.
Check Request.Content.IsMimeMultipartContent(), then await Request.Content.ReadAsMultipartAsync(new MultipartFormDataStreamProvider(folder)). Files land in temporary names; provider.FormData holds the other fields.
Never trust the client’s file name or Content-Type. Check the extension against an allow-list, check the size, and store the file under a new GUID name outside the web root (App_Data).
IIS limits request size: maxRequestLength (in KB) and maxAllowedContentLength (in bytes) in web.config. Raise them deliberately and enforce your own lower limit in code.
For downloads, return an HttpResponseMessage with StreamContent, a Content-Type and a Content-Disposition: attachment header. Use Path.GetFileName on the requested name to block ../ path traversal.
For large or numerous files, store them in Azure Blob Storage or similar rather than on the web server’s disk.
Syntax#
<!-- Web.config: allow uploads up to 10 MB -->
<system.web>
<httpRuntime targetFramework="4.8" maxRequestLength="10240" /> <!-- KB -->
</system.web>
<system.webServer>
<security>
<requestFiltering>
<requestLimits maxAllowedContentLength="10485760" /> <!-- bytes -->
</requestFiltering>
</security>
</system.webServer>
Code example: Upload and download endpoints#
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using System.Web;
using System.Web.Hosting;
using System.Web.Http;
[RoutePrefix("api/files")]
public class FilesController : ApiController
{
private static readonly string[] AllowedExtensions = { ".jpg", ".jpeg", ".png", ".pdf" };
private const long MaxBytes = 5 * 1024 * 1024; // 5 MB
private static string UploadRoot => HostingEnvironment.MapPath("~/App_Data/uploads");
[HttpPost, Route("")]
public async Task<IHttpActionResult> Upload()
{
if (!Request.Content.IsMimeMultipartContent())
return StatusCode(HttpStatusCode.UnsupportedMediaType);
Directory.CreateDirectory(UploadRoot);
var provider = new MultipartFormDataStreamProvider(UploadRoot);
await Request.Content.ReadAsMultipartAsync(provider);
var saved = new List<object>();
foreach (var file in provider.FileData)
{
string original = Path.GetFileName(file.Headers.ContentDisposition.FileName.Trim('"'));
string extension = Path.GetExtension(original).ToLowerInvariant();
long size = new FileInfo(file.LocalFileName).Length;
if (!AllowedExtensions.Contains(extension) || size > MaxBytes)
{
File.Delete(file.LocalFileName);
return BadRequest(original + " is not allowed (jpg, png or pdf up to 5 MB).");
}
string storedName = Guid.NewGuid().ToString("N") + extension;
File.Move(file.LocalFileName, Path.Combine(UploadRoot, storedName));
saved.Add(new { original, storedName, size });
}
string description = provider.FormData["description"];
return Ok(new { description, files = saved });
}
[HttpGet, Route("{name}")]
public HttpResponseMessage Download(string name)
{
string safeName = Path.GetFileName(name); // blocks ../../web.config
string path = Path.Combine(UploadRoot, safeName);
if (!File.Exists(path)) return Request.CreateResponse(HttpStatusCode.NotFound);
var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StreamContent(File.OpenRead(path)),
};
response.Content.Headers.ContentType = new MediaTypeHeaderValue(MimeMapping.GetMimeMapping(safeName));
response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment") { FileName = safeName };
return response;
}
}
POST /api/files Content-Type: multipart/form-data
description=Invoice October
file=@invoice.pdf (240 KB)
→ 200 {"description":"Invoice October","files":[{"original":"invoice.pdf","storedName":"9b1e...c4.pdf","size":245760}]}
POST /api/files file=@setup.exe → 400 "setup.exe is not allowed (jpg, png or pdf up to 5 MB)."
GET /api/files/9b1e...c4.pdf → 200 Content-Type: application/pdf
Content-Disposition: attachment; filename=9b1e...c4.pdfThis example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- The action rejects anything that is not multipart with 415 Unsupported Media Type.
- MultipartFormDataStreamProvider streams each file to disk instead of holding it in memory.
- Each file’s extension and size are checked; rejected files are deleted immediately.
- Accepted files are renamed to a GUID, so user-supplied names never touch the file system.
- provider.FormData gives access to the ordinary form fields such as description.
- Download uses Path.GetFileName to strip directory parts and streams the file with download headers.
Real-world use case#
An HR portal lets employees upload ID documents. Files are validated, renamed, virus-scanned by a background job, then moved to private blob storage; downloads require the employee’s own token or an HR role.
Common mistakes#
A name like ../../web.config or shell.aspx can overwrite files or run code. Generate your own names and store outside the web root.
ReadAsByteArrayAsync on a 500 MB upload can exhaust memory. Stream to disk or blob storage.
IIS may treat /api/files/report.pdf as a static file and return 404. Configure the handler (or pass the name as a query parameter).
Best practices#
- Allow-list extensions and enforce a size limit in code
- Rename files to GUIDs; keep originals only as metadata
- Store outside the web root or in blob storage
- Protect downloads with authorization
Remember this#
multipart/form-data → ReadAsMultipartAsync(provider).
Never trust the name, type or size the client claims.
Download = StreamContent + Content-Disposition: attachment.
Practice exercise#
Your task: Write a helper IsAllowedImage(string fileName, long size) that returns true only for .jpg/.jpeg/.png up to 2 MB, and use it in an upload action for profile photos.
Need a hint?
Show solution
using System.IO;
using System.Linq;
public static class UploadRules
{
private static readonly string[] ImageExtensions = { ".jpg", ".jpeg", ".png" };
public static bool IsAllowedImage(string fileName, long size)
{
string extension = Path.GetExtension(fileName ?? "").ToLowerInvariant();
return ImageExtensions.Contains(extension) && size > 0 && size <= 2 * 1024 * 1024;
}
}
// In the action: if (!UploadRules.IsAllowedImage(original, size)) return BadRequest("Images only, max 2 MB.");
Mini quiz#
Which provider saves multipart files to disk in Web API 2?
Want more? Practise all ASP.NET Web API MCQs · ASP.NET Web API interview questions
My notes#
Saved in this browser.