From 1c1218d89dd35b4eff6eeee90c3480132d846d56 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Fri, 23 May 2025 13:23:45 +0200 Subject: [PATCH 01/30] init --- .../file-uploads/samples/9.x/.gitignore | 1 + .../Controllers/FileController.cs | 69 +++++++++++++++++++ .../FileManagerSample.csproj | 9 +++ .../samples/9.x/FileManagerSample/Program.cs | 26 +++++++ .../appsettings.Development.json | 8 +++ .../9.x/FileManagerSample/appsettings.json | 9 +++ 6 files changed, 122 insertions(+) create mode 100644 aspnetcore/mvc/models/file-uploads/samples/9.x/.gitignore create mode 100644 aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Controllers/FileController.cs create mode 100644 aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/FileManagerSample.csproj create mode 100644 aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Program.cs create mode 100644 aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/appsettings.Development.json create mode 100644 aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/appsettings.json diff --git a/aspnetcore/mvc/models/file-uploads/samples/9.x/.gitignore b/aspnetcore/mvc/models/file-uploads/samples/9.x/.gitignore new file mode 100644 index 000000000000..77a6daefc17e --- /dev/null +++ b/aspnetcore/mvc/models/file-uploads/samples/9.x/.gitignore @@ -0,0 +1 @@ +**/*.dat diff --git a/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Controllers/FileController.cs b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Controllers/FileController.cs new file mode 100644 index 000000000000..0ee674a5c201 --- /dev/null +++ b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Controllers/FileController.cs @@ -0,0 +1,69 @@ +using Microsoft.AspNetCore.Mvc; + +namespace FileManagerSample.Controllers +{ + [ApiController] + [Route("[controller]")] + public class FileController : ControllerBase + { + private readonly ILogger _logger; + + public FileController(ILogger logger) + { + _logger = logger; + } + + [HttpPost] + [Route(nameof(Upload))] + public async Task Upload() + { + try + { + // Validate if the request has a body + if (!Request.HasFormContentType) + { + return BadRequest("The request does not contain a valid form."); + } + + var bodyReader = Request.BodyReader; + + // Initialize streaming variables + long totalBytesRead = 0; + const int bufferSize = 16 * 1024; // 16 KB buffer size + + while (true) + { + // Read from the body in chunks + var readResult = await bodyReader.ReadAsync(); + + // Get the buffer containing the data + var buffer = readResult.Buffer; + + // Process the buffer data (streaming logic here) + foreach (var memory in buffer) + { + // Add your custom logic here for processing the streamed data + totalBytesRead += memory.Length; + } + + // Mark the buffer as processed + bodyReader.AdvanceTo(buffer.End); + + // Break the loop if there's no more data to read + if (readResult.IsCompleted) + { + break; + } + } + + _logger.LogInformation("File upload completed. Total bytes read: {TotalBytesRead} bytes.", totalBytesRead); + return Ok(new { Message = "File uploaded successfully.", BytesProcessed = totalBytesRead }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during file upload"); + return StatusCode(500, "An error occurred while uploading the file."); + } + } + } +} diff --git a/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/FileManagerSample.csproj b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/FileManagerSample.csproj new file mode 100644 index 000000000000..6568b3dcfb4b --- /dev/null +++ b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/FileManagerSample.csproj @@ -0,0 +1,9 @@ + + + + net9.0 + enable + enable + + + diff --git a/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Program.cs b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Program.cs new file mode 100644 index 000000000000..e1db655407cc --- /dev/null +++ b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Program.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Http.Features; + +var builder = WebApplication.CreateBuilder(args); + +// Configure Kestrel to allow large files +builder.WebHost.ConfigureKestrel(options => +{ + options.Limits.MaxRequestBodySize = 6L * 1024 * 1024 * 1024; // 6 GB + + // timeout settings + options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(10); // Extend timeout for large file uploads + options.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(10); +}); + +// Configure FormOptions to allow large multipart body +builder.Services.Configure(options => +{ + options.MultipartBodyLengthLimit = 6L * 1024 * 1024 * 1024; // 6 GB +}); + +builder.Services.AddControllers(); + +var app = builder.Build(); + +app.MapControllers(); +app.Run(); diff --git a/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/appsettings.Development.json b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/appsettings.Development.json new file mode 100644 index 000000000000..0c208ae9181e --- /dev/null +++ b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/appsettings.json b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/appsettings.json new file mode 100644 index 000000000000..10f68b8c8b4f --- /dev/null +++ b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} From 6509db12d9cc24a14a8dba25c42ff394b58cfa9c Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Fri, 23 May 2025 13:39:11 +0200 Subject: [PATCH 02/30] init file stream --- .../Controllers/FileController.cs | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Controllers/FileController.cs b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Controllers/FileController.cs index 0ee674a5c201..2c69334e519b 100644 --- a/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Controllers/FileController.cs +++ b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Controllers/FileController.cs @@ -19,21 +19,35 @@ public async Task Upload() { try { - // Validate if the request has a body if (!Request.HasFormContentType) { return BadRequest("The request does not contain a valid form."); } + _ = Request.Headers.TryGetValue("Content-Type", out var contentType); + _logger.LogInformation("Content-Type: {ContentType}", contentType!); var bodyReader = Request.BodyReader; - // Initialize streaming variables long totalBytesRead = 0; - const int bufferSize = 16 * 1024; // 16 KB buffer size + const int bufferSize = 16 * 1024 * 1024; // 16 MB buffer size + + string targetFilePath = Path.Combine(Directory.GetCurrentDirectory(), "file-upload.dat"); + if (System.IO.File.Exists(targetFilePath)) + { + System.IO.File.Delete(targetFilePath); + _logger.LogDebug("Removed existing output file: {path}", targetFilePath); + } + + using FileStream outputFileStream = new FileStream( + path: targetFilePath, + mode: FileMode.OpenOrCreate, + access: FileAccess.Write, + share: FileShare.ReadWrite, + bufferSize: bufferSize, + useAsync: true); while (true) { - // Read from the body in chunks var readResult = await bodyReader.ReadAsync(); // Get the buffer containing the data From e7ed63be59617ae8edddfe37b56bb1721721d3dc Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Fri, 23 May 2025 14:05:16 +0200 Subject: [PATCH 03/30] save --- .../file-uploads/samples/9.x/.gitignore | 1 + .../Controllers/FileController.cs | 79 +++++++++++++++++-- 2 files changed, 72 insertions(+), 8 deletions(-) diff --git a/aspnetcore/mvc/models/file-uploads/samples/9.x/.gitignore b/aspnetcore/mvc/models/file-uploads/samples/9.x/.gitignore index 77a6daefc17e..bf018662343c 100644 --- a/aspnetcore/mvc/models/file-uploads/samples/9.x/.gitignore +++ b/aspnetcore/mvc/models/file-uploads/samples/9.x/.gitignore @@ -1 +1,2 @@ **/*.dat +**/*.txt diff --git a/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Controllers/FileController.cs b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Controllers/FileController.cs index 2c69334e519b..a1e45bf1055d 100644 --- a/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Controllers/FileController.cs +++ b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Controllers/FileController.cs @@ -6,6 +6,10 @@ namespace FileManagerSample.Controllers [Route("[controller]")] public class FileController : ControllerBase { + private const string UploadFilePath = "file-upload.dat"; + // private const string OriginalFilePath = "/* replace with your file path */"; + private const string OriginalFilePath = @"D:\.other\big-files\bigfile.dat"; + private readonly ILogger _logger; public FileController(ILogger logger) @@ -13,6 +17,39 @@ public FileController(ILogger logger) _logger = logger; } + [HttpPost] + [Route(nameof(Compare))] + public IActionResult Compare() + { + try + { + string targetFilePath = Path.Combine(Directory.GetCurrentDirectory(), UploadFilePath); + if (!System.IO.File.Exists(targetFilePath)) + { + return NotFound($"File not found: {targetFilePath}"); + } + if (!System.IO.File.Exists(OriginalFilePath)) + { + return NotFound($"File not found: {OriginalFilePath}"); + } + + bool areFilesEqual = CompareFiles(targetFilePath, OriginalFilePath); + if (areFilesEqual) + { + return Ok(new { Message = "The files are identical." }); + } + else + { + return Ok(new { Message = "The files are different." }); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during file comparison"); + return StatusCode(500, "An error occurred while comparing the files."); + } + } + [HttpPost] [Route(nameof(Upload))] public async Task Upload() @@ -31,7 +68,7 @@ public async Task Upload() long totalBytesRead = 0; const int bufferSize = 16 * 1024 * 1024; // 16 MB buffer size - string targetFilePath = Path.Combine(Directory.GetCurrentDirectory(), "file-upload.dat"); + string targetFilePath = Path.Combine(Directory.GetCurrentDirectory(), UploadFilePath); if (System.IO.File.Exists(targetFilePath)) { System.IO.File.Delete(targetFilePath); @@ -49,21 +86,15 @@ public async Task Upload() while (true) { var readResult = await bodyReader.ReadAsync(); - - // Get the buffer containing the data var buffer = readResult.Buffer; - // Process the buffer data (streaming logic here) foreach (var memory in buffer) { - // Add your custom logic here for processing the streamed data + await outputFileStream.WriteAsync(memory, default); totalBytesRead += memory.Length; } - // Mark the buffer as processed bodyReader.AdvanceTo(buffer.End); - - // Break the loop if there's no more data to read if (readResult.IsCompleted) { break; @@ -79,5 +110,37 @@ public async Task Upload() return StatusCode(500, "An error occurred while uploading the file."); } } + + private bool CompareFiles(string filePath1, string filePath2) + { + var fileInfo1 = new FileInfo(filePath1); + var fileInfo2 = new FileInfo(filePath2); + + if (fileInfo1.Length != fileInfo2.Length) + { + return false; + } + + using (FileStream fs1 = System.IO.File.OpenRead(filePath1)) + using (FileStream fs2 = System.IO.File.OpenRead(filePath2)) + { + byte[] buffer1 = new byte[8192]; // 8 KB buffer + byte[] buffer2 = new byte[8192]; + int bytesRead1, bytesRead2; + + do + { + bytesRead1 = fs1.Read(buffer1, 0, buffer1.Length); + bytesRead2 = fs2.Read(buffer2, 0, buffer2.Length); + + if (bytesRead1 != bytesRead2 || !buffer1.SequenceEqual(buffer2)) + { + return false; + } + } while (bytesRead1 > 0 && bytesRead2 > 0); + } + + return true; + } } } From 281c969e00759d81426bb441df950fa839ed5fee Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Fri, 23 May 2025 14:06:58 +0200 Subject: [PATCH 04/30] minor --- .../FileManagerSample/Controllers/FileController.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Controllers/FileController.cs b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Controllers/FileController.cs index a1e45bf1055d..13ad6dd5f699 100644 --- a/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Controllers/FileController.cs +++ b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Controllers/FileController.cs @@ -52,7 +52,7 @@ public IActionResult Compare() [HttpPost] [Route(nameof(Upload))] - public async Task Upload() + public async Task Upload(CancellationToken cancellationToken) { try { @@ -85,12 +85,12 @@ public async Task Upload() while (true) { - var readResult = await bodyReader.ReadAsync(); + var readResult = await bodyReader.ReadAsync(cancellationToken); var buffer = readResult.Buffer; foreach (var memory in buffer) { - await outputFileStream.WriteAsync(memory, default); + await outputFileStream.WriteAsync(memory, cancellationToken); totalBytesRead += memory.Length; } @@ -121,8 +121,8 @@ private bool CompareFiles(string filePath1, string filePath2) return false; } - using (FileStream fs1 = System.IO.File.OpenRead(filePath1)) - using (FileStream fs2 = System.IO.File.OpenRead(filePath2)) + using (var fs1 = System.IO.File.OpenRead(filePath1)) + using (var fs2 = System.IO.File.OpenRead(filePath2)) { byte[] buffer1 = new byte[8192]; // 8 KB buffer byte[] buffer2 = new byte[8192]; From 881fb5552ca4264d99e3e5c063d37e5684d0784a Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Fri, 23 May 2025 14:19:02 +0200 Subject: [PATCH 05/30] rollback --- .../Controllers/FileController.cs | 86 +++---------------- 1 file changed, 13 insertions(+), 73 deletions(-) diff --git a/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Controllers/FileController.cs b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Controllers/FileController.cs index 13ad6dd5f699..e53388c7af78 100644 --- a/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Controllers/FileController.cs +++ b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Controllers/FileController.cs @@ -6,9 +6,8 @@ namespace FileManagerSample.Controllers [Route("[controller]")] public class FileController : ControllerBase { + private const int BufferSize = 16 * 1024 * 1024; // 16 MB buffer size private const string UploadFilePath = "file-upload.dat"; - // private const string OriginalFilePath = "/* replace with your file path */"; - private const string OriginalFilePath = @"D:\.other\big-files\bigfile.dat"; private readonly ILogger _logger; @@ -18,41 +17,8 @@ public FileController(ILogger logger) } [HttpPost] - [Route(nameof(Compare))] - public IActionResult Compare() - { - try - { - string targetFilePath = Path.Combine(Directory.GetCurrentDirectory(), UploadFilePath); - if (!System.IO.File.Exists(targetFilePath)) - { - return NotFound($"File not found: {targetFilePath}"); - } - if (!System.IO.File.Exists(OriginalFilePath)) - { - return NotFound($"File not found: {OriginalFilePath}"); - } - - bool areFilesEqual = CompareFiles(targetFilePath, OriginalFilePath); - if (areFilesEqual) - { - return Ok(new { Message = "The files are identical." }); - } - else - { - return Ok(new { Message = "The files are different." }); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error during file comparison"); - return StatusCode(500, "An error occurred while comparing the files."); - } - } - - [HttpPost] - [Route(nameof(Upload))] - public async Task Upload(CancellationToken cancellationToken) + [Route(nameof(UploadPipeReader))] + public async Task UploadPipeReader() { try { @@ -66,7 +32,7 @@ public async Task Upload(CancellationToken cancellationToken) var bodyReader = Request.BodyReader; long totalBytesRead = 0; - const int bufferSize = 16 * 1024 * 1024; // 16 MB buffer size + string targetFilePath = Path.Combine(Directory.GetCurrentDirectory(), UploadFilePath); if (System.IO.File.Exists(targetFilePath)) @@ -80,21 +46,27 @@ public async Task Upload(CancellationToken cancellationToken) mode: FileMode.OpenOrCreate, access: FileAccess.Write, share: FileShare.ReadWrite, - bufferSize: bufferSize, + bufferSize: BufferSize, useAsync: true); while (true) { - var readResult = await bodyReader.ReadAsync(cancellationToken); + var readResult = await bodyReader.ReadAsync(); + + // Get the buffer containing the data var buffer = readResult.Buffer; + // Process the buffer data (streaming logic here) foreach (var memory in buffer) { - await outputFileStream.WriteAsync(memory, cancellationToken); + await outputFileStream.WriteAsync(memory, default); totalBytesRead += memory.Length; } + // Mark the buffer as processed bodyReader.AdvanceTo(buffer.End); + + // Break the loop if there's no more data to read if (readResult.IsCompleted) { break; @@ -110,37 +82,5 @@ public async Task Upload(CancellationToken cancellationToken) return StatusCode(500, "An error occurred while uploading the file."); } } - - private bool CompareFiles(string filePath1, string filePath2) - { - var fileInfo1 = new FileInfo(filePath1); - var fileInfo2 = new FileInfo(filePath2); - - if (fileInfo1.Length != fileInfo2.Length) - { - return false; - } - - using (var fs1 = System.IO.File.OpenRead(filePath1)) - using (var fs2 = System.IO.File.OpenRead(filePath2)) - { - byte[] buffer1 = new byte[8192]; // 8 KB buffer - byte[] buffer2 = new byte[8192]; - int bytesRead1, bytesRead2; - - do - { - bytesRead1 = fs1.Read(buffer1, 0, buffer1.Length); - bytesRead2 = fs2.Read(buffer2, 0, buffer2.Length); - - if (bytesRead1 != bytesRead2 || !buffer1.SequenceEqual(buffer2)) - { - return false; - } - } while (bytesRead1 > 0 && bytesRead2 > 0); - } - - return true; - } } } From 63aa7a871119c7449eb5dc8a3c1d948d6090fa0d Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Fri, 23 May 2025 15:06:49 +0200 Subject: [PATCH 06/30] no cts --- .../Controllers/FileController.cs | 80 ++++++++++++++++++- 1 file changed, 78 insertions(+), 2 deletions(-) diff --git a/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Controllers/FileController.cs b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Controllers/FileController.cs index e53388c7af78..5ea52ca3e121 100644 --- a/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Controllers/FileController.cs +++ b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Controllers/FileController.cs @@ -1,4 +1,7 @@ +using System.Threading; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Net.Http.Headers; namespace FileManagerSample.Controllers { @@ -16,6 +19,79 @@ public FileController(ILogger logger) _logger = logger; } + [HttpPost] + [Route(nameof(UploadMultipartReader))] + public async Task UploadMultipartReader() + { + try + { + if (!Request.ContentType?.StartsWith("multipart/form-data") ?? true) + { + return BadRequest("The request does not contain valid multipart form data."); + } + + // Extract the boundary from the Content-Type header + var boundary = Microsoft.Net.Http.Headers.HeaderUtilities.RemoveQuotes( + Microsoft.Net.Http.Headers.MediaTypeHeaderValue.Parse(Request.ContentType).Boundary).Value; + + if (string.IsNullOrWhiteSpace(boundary)) + { + return BadRequest("Missing boundary in multipart form data."); + } + + var reader = new MultipartReader(boundary, Request.Body); + + // Process each section in the multipart body + MultipartSection section; + long totalBytesRead = 0; + string targetFilePath = Path.Combine(Directory.GetCurrentDirectory(), UploadFilePath); + + if (System.IO.File.Exists(targetFilePath)) + { + System.IO.File.Delete(targetFilePath); + _logger.LogDebug($"Removed existing output file: {targetFilePath}"); + } + + using FileStream outputFileStream = new FileStream( + path: targetFilePath, + mode: FileMode.Create, + access: FileAccess.Write, + share: FileShare.None, + bufferSize: 16 * 1024 * 1024, // 16 MB buffer + useAsync: true); + + while ((section = await reader.ReadNextSectionAsync()) != null) + { + // Check if the section is a file + var contentDisposition = section.GetContentDispositionHeader(); + if (contentDisposition != null && contentDisposition.IsFileDisposition()) + { + _logger.LogInformation($"Processing file: {contentDisposition.FileName.Value}"); + + // Write the file content to the target file + await section.Body.CopyToAsync(outputFileStream); + totalBytesRead += section.Body.Length; + } + else if (contentDisposition != null && contentDisposition.IsFormDisposition()) + { + // Handle metadata (form fields) + string key = contentDisposition.Name.Value; + using var streamReader = new StreamReader(section.Body); + string value = await streamReader.ReadToEndAsync(); + _logger.LogInformation($"Received metadata: {key} = {value}"); + } + } + + _logger.LogInformation($"File upload completed. Total bytes read: {totalBytesRead} bytes."); + return Ok(new { Message = "File uploaded successfully.", BytesProcessed = totalBytesRead }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during file upload"); + return StatusCode(500, "An error occurred while uploading the file."); + } + } + [HttpPost] [Route(nameof(UploadPipeReader))] public async Task UploadPipeReader() @@ -45,7 +121,7 @@ public async Task UploadPipeReader() path: targetFilePath, mode: FileMode.OpenOrCreate, access: FileAccess.Write, - share: FileShare.ReadWrite, + share: FileShare.None, bufferSize: BufferSize, useAsync: true); @@ -59,7 +135,7 @@ public async Task UploadPipeReader() // Process the buffer data (streaming logic here) foreach (var memory in buffer) { - await outputFileStream.WriteAsync(memory, default); + await outputFileStream.WriteAsync(memory); totalBytesRead += memory.Length; } From 1f75b85b5269dfaea648567ebff699946c1511ae Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Fri, 23 May 2025 15:34:16 +0200 Subject: [PATCH 07/30] sample app --- .../9.x/FileManager/FileManager.csproj | 10 +++ .../samples/9.x/FileManager/Program.cs | 61 +++++++++++++++++++ .../Controllers/FileController.cs | 19 ++---- 3 files changed, 76 insertions(+), 14 deletions(-) create mode 100644 aspnetcore/mvc/models/file-uploads/samples/9.x/FileManager/FileManager.csproj create mode 100644 aspnetcore/mvc/models/file-uploads/samples/9.x/FileManager/Program.cs diff --git a/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManager/FileManager.csproj b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManager/FileManager.csproj new file mode 100644 index 000000000000..fd4bd08da298 --- /dev/null +++ b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManager/FileManager.csproj @@ -0,0 +1,10 @@ + + + + Exe + net9.0 + enable + enable + + + diff --git a/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManager/Program.cs b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManager/Program.cs new file mode 100644 index 000000000000..ce3fbb83b315 --- /dev/null +++ b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManager/Program.cs @@ -0,0 +1,61 @@ +/* + This sample allows to compare "big files" byte by byte which FileManagerSample demonstrates how to use in aspnetcore. +*/ + +string targetFilePath = @"D:\code\AspNetCore.Docs\aspnetcore\mvc\models\file-uploads\samples\9.x\FileManagerSample\file-upload.dat"; +// string originalFilePath = @"D:\.other\big-files\bigfile.dat"; +string originalFilePath = @"D:\.other\big-files\bigfile.dat"; + +try +{ + if (!File.Exists(targetFilePath)) + { + Console.WriteLine($"Error: File '{targetFilePath}' does not exist."); + return; + } + if (!File.Exists(originalFilePath)) + { + Console.WriteLine($"Error: File '{originalFilePath}' does not exist."); + return; + } + + using var file1Stream = File.OpenRead(targetFilePath); + using var file2Stream = File.OpenRead(originalFilePath); + + if (file1Stream.Length != file2Stream.Length) + { + Console.WriteLine("Files are different: Their sizes do not match."); + return; + } + + const int bufferSize = 8192; // 8 KB buffer + byte[] buffer1 = new byte[bufferSize]; + byte[] buffer2 = new byte[bufferSize]; + + int bytesRead1, bytesRead2; + long position = 0; + int chunksProcessed = 0; + + while ((bytesRead1 = file1Stream.Read(buffer1, 0, bufferSize)) > 0 && + (bytesRead2 = file2Stream.Read(buffer2, 0, bufferSize)) > 0) + { + for (int i = 0; i < bytesRead1; i++) + { + if (buffer1[i] != buffer2[i]) + { + Console.WriteLine($"Files are different: Mismatch at byte position {position + i}"); + return; + } + } + + position += bytesRead1; + Console.WriteLine($"Validated {++chunksProcessed}th {bufferSize} bytes"); + } + + Console.WriteLine("----"); + Console.WriteLine("Files are identical."); +} +catch (Exception ex) +{ + Console.WriteLine($"An error occurred: {ex.Message}"); +} diff --git a/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Controllers/FileController.cs b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Controllers/FileController.cs index 5ea52ca3e121..02502885bbe8 100644 --- a/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Controllers/FileController.cs +++ b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Controllers/FileController.cs @@ -9,8 +9,8 @@ namespace FileManagerSample.Controllers [Route("[controller]")] public class FileController : ControllerBase { - private const int BufferSize = 16 * 1024 * 1024; // 16 MB buffer size - private const string UploadFilePath = "file-upload.dat"; + public const int BufferSize = 16 * 1024 * 1024; // 16 MB buffer size + public const string UploadFilePath = "file-upload.dat"; private readonly ILogger _logger; @@ -30,10 +30,7 @@ public async Task UploadMultipartReader() return BadRequest("The request does not contain valid multipart form data."); } - // Extract the boundary from the Content-Type header - var boundary = Microsoft.Net.Http.Headers.HeaderUtilities.RemoveQuotes( - Microsoft.Net.Http.Headers.MediaTypeHeaderValue.Parse(Request.ContentType).Boundary).Value; - + var boundary = HeaderUtilities.RemoveQuotes(MediaTypeHeaderValue.Parse(Request.ContentType).Boundary).Value; if (string.IsNullOrWhiteSpace(boundary)) { return BadRequest("Missing boundary in multipart form data."); @@ -42,7 +39,7 @@ public async Task UploadMultipartReader() var reader = new MultipartReader(boundary, Request.Body); // Process each section in the multipart body - MultipartSection section; + MultipartSection? section; long totalBytesRead = 0; string targetFilePath = Path.Combine(Directory.GetCurrentDirectory(), UploadFilePath); @@ -75,7 +72,7 @@ public async Task UploadMultipartReader() else if (contentDisposition != null && contentDisposition.IsFormDisposition()) { // Handle metadata (form fields) - string key = contentDisposition.Name.Value; + string key = contentDisposition.Name.Value!; using var streamReader = new StreamReader(section.Body); string value = await streamReader.ReadToEndAsync(); _logger.LogInformation($"Received metadata: {key} = {value}"); @@ -128,21 +125,15 @@ public async Task UploadPipeReader() while (true) { var readResult = await bodyReader.ReadAsync(); - - // Get the buffer containing the data var buffer = readResult.Buffer; - // Process the buffer data (streaming logic here) foreach (var memory in buffer) { await outputFileStream.WriteAsync(memory); totalBytesRead += memory.Length; } - - // Mark the buffer as processed bodyReader.AdvanceTo(buffer.End); - // Break the loop if there's no more data to read if (readResult.IsCompleted) { break; From 0d3f9112dd2efb174cf4ed073c5ed616ab9acca5 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Fri, 23 May 2025 15:42:29 +0200 Subject: [PATCH 08/30] add description --- .../samples/9.x/FileManagerSample/Program.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Program.cs b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Program.cs index e1db655407cc..9456e3deedb0 100644 --- a/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Program.cs +++ b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Program.cs @@ -1,10 +1,16 @@ +using System.Drawing; using Microsoft.AspNetCore.Http.Features; +using static System.Runtime.InteropServices.JavaScript.JSType; var builder = WebApplication.CreateBuilder(args); -// Configure Kestrel to allow large files +// allow large files builder.WebHost.ConfigureKestrel(options => { + // if not present, will throw similar exception: + // fail: FileManagerSample.Controllers.FileController[0] + // Error during file upload + // Microsoft.AspNetCore.Server.Kestrel.Core.BadHttpRequestException: Request body too large. The max request body size is 30000000 bytes. options.Limits.MaxRequestBodySize = 6L * 1024 * 1024 * 1024; // 6 GB // timeout settings @@ -12,12 +18,6 @@ options.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(10); }); -// Configure FormOptions to allow large multipart body -builder.Services.Configure(options => -{ - options.MultipartBodyLengthLimit = 6L * 1024 * 1024 * 1024; // 6 GB -}); - builder.Services.AddControllers(); var app = builder.Build(); From 00a9426a97c6763430f25974915d74ec29a442c5 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Fri, 23 May 2025 17:38:25 +0200 Subject: [PATCH 09/30] prettify --- .../file-uploads/samples/9.x/FileManager/Program.cs | 3 ++- .../FileManagerSample/Controllers/FileController.cs | 2 +- .../samples/9.x/FileManagerSample/Program.cs | 11 ++--------- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManager/Program.cs b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManager/Program.cs index ce3fbb83b315..98b33a7e7289 100644 --- a/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManager/Program.cs +++ b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManager/Program.cs @@ -1,5 +1,6 @@ /* - This sample allows to compare "big files" byte by byte which FileManagerSample demonstrates how to use in aspnetcore. + This sample allows to compare "big files" byte by byte which FileManagerSample emits. + Look at FileManagerSample to understand how working with "big files" can be done in aspnetcore. */ string targetFilePath = @"D:\code\AspNetCore.Docs\aspnetcore\mvc\models\file-uploads\samples\9.x\FileManagerSample\file-upload.dat"; diff --git a/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Controllers/FileController.cs b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Controllers/FileController.cs index 02502885bbe8..19481572d9e5 100644 --- a/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Controllers/FileController.cs +++ b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Controllers/FileController.cs @@ -54,7 +54,7 @@ public async Task UploadMultipartReader() mode: FileMode.Create, access: FileAccess.Write, share: FileShare.None, - bufferSize: 16 * 1024 * 1024, // 16 MB buffer + bufferSize: BufferSize, useAsync: true); while ((section = await reader.ReadNextSectionAsync()) != null) diff --git a/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Program.cs b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Program.cs index 9456e3deedb0..19f5f4d5d041 100644 --- a/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Program.cs +++ b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Program.cs @@ -1,20 +1,13 @@ -using System.Drawing; -using Microsoft.AspNetCore.Http.Features; -using static System.Runtime.InteropServices.JavaScript.JSType; - var builder = WebApplication.CreateBuilder(args); -// allow large files builder.WebHost.ConfigureKestrel(options => { // if not present, will throw similar exception: - // fail: FileManagerSample.Controllers.FileController[0] - // Error during file upload // Microsoft.AspNetCore.Server.Kestrel.Core.BadHttpRequestException: Request body too large. The max request body size is 30000000 bytes. options.Limits.MaxRequestBodySize = 6L * 1024 * 1024 * 1024; // 6 GB - // timeout settings - options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(10); // Extend timeout for large file uploads + // optional: timeout settings + options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(10); options.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(10); }); From c5646d0f8f41f787eee9d8ac455e9a8f9425a1da Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Mon, 26 May 2025 16:22:03 +0200 Subject: [PATCH 10/30] minimal impl as well --- .../9.x/FileManager/FileManager.csproj | 10 -- .../samples/9.x/FileManager/Program.cs | 62 ------- .../Controllers/FileController.cs | 156 +++++------------- .../FileManagerSample.csproj | 2 +- .../samples/9.x/FileManagerSample/Program.cs | 54 +++++- .../Services/FileManagerService.cs | 108 ++++++++++++ 6 files changed, 202 insertions(+), 190 deletions(-) delete mode 100644 aspnetcore/mvc/models/file-uploads/samples/9.x/FileManager/FileManager.csproj delete mode 100644 aspnetcore/mvc/models/file-uploads/samples/9.x/FileManager/Program.cs create mode 100644 aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Services/FileManagerService.cs diff --git a/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManager/FileManager.csproj b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManager/FileManager.csproj deleted file mode 100644 index fd4bd08da298..000000000000 --- a/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManager/FileManager.csproj +++ /dev/null @@ -1,10 +0,0 @@ - - - - Exe - net9.0 - enable - enable - - - diff --git a/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManager/Program.cs b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManager/Program.cs deleted file mode 100644 index 98b33a7e7289..000000000000 --- a/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManager/Program.cs +++ /dev/null @@ -1,62 +0,0 @@ -/* - This sample allows to compare "big files" byte by byte which FileManagerSample emits. - Look at FileManagerSample to understand how working with "big files" can be done in aspnetcore. -*/ - -string targetFilePath = @"D:\code\AspNetCore.Docs\aspnetcore\mvc\models\file-uploads\samples\9.x\FileManagerSample\file-upload.dat"; -// string originalFilePath = @"D:\.other\big-files\bigfile.dat"; -string originalFilePath = @"D:\.other\big-files\bigfile.dat"; - -try -{ - if (!File.Exists(targetFilePath)) - { - Console.WriteLine($"Error: File '{targetFilePath}' does not exist."); - return; - } - if (!File.Exists(originalFilePath)) - { - Console.WriteLine($"Error: File '{originalFilePath}' does not exist."); - return; - } - - using var file1Stream = File.OpenRead(targetFilePath); - using var file2Stream = File.OpenRead(originalFilePath); - - if (file1Stream.Length != file2Stream.Length) - { - Console.WriteLine("Files are different: Their sizes do not match."); - return; - } - - const int bufferSize = 8192; // 8 KB buffer - byte[] buffer1 = new byte[bufferSize]; - byte[] buffer2 = new byte[bufferSize]; - - int bytesRead1, bytesRead2; - long position = 0; - int chunksProcessed = 0; - - while ((bytesRead1 = file1Stream.Read(buffer1, 0, bufferSize)) > 0 && - (bytesRead2 = file2Stream.Read(buffer2, 0, bufferSize)) > 0) - { - for (int i = 0; i < bytesRead1; i++) - { - if (buffer1[i] != buffer2[i]) - { - Console.WriteLine($"Files are different: Mismatch at byte position {position + i}"); - return; - } - } - - position += bytesRead1; - Console.WriteLine($"Validated {++chunksProcessed}th {bufferSize} bytes"); - } - - Console.WriteLine("----"); - Console.WriteLine("Files are identical."); -} -catch (Exception ex) -{ - Console.WriteLine($"An error occurred: {ex.Message}"); -} diff --git a/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Controllers/FileController.cs b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Controllers/FileController.cs index 19481572d9e5..f55c7655bb9f 100644 --- a/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Controllers/FileController.cs +++ b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Controllers/FileController.cs @@ -1,153 +1,77 @@ -using System.Threading; +using FileManagerSample.Services; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.WebUtilities; using Microsoft.Net.Http.Headers; namespace FileManagerSample.Controllers { [ApiController] - [Route("[controller]")] + [Route("controller")] public class FileController : ControllerBase { public const int BufferSize = 16 * 1024 * 1024; // 16 MB buffer size public const string UploadFilePath = "file-upload.dat"; private readonly ILogger _logger; + private readonly FileManagerService _fileManager; - public FileController(ILogger logger) + public FileController( + ILogger logger, + FileManagerService fileManager) { _logger = logger; + _fileManager = fileManager; } [HttpPost] - [Route(nameof(UploadMultipartReader))] + [Route("multipart")] public async Task UploadMultipartReader() { - try + if (!Request.ContentType?.StartsWith("multipart/form-data") ?? true) { - if (!Request.ContentType?.StartsWith("multipart/form-data") ?? true) - { - return BadRequest("The request does not contain valid multipart form data."); - } - - var boundary = HeaderUtilities.RemoveQuotes(MediaTypeHeaderValue.Parse(Request.ContentType).Boundary).Value; - if (string.IsNullOrWhiteSpace(boundary)) - { - return BadRequest("Missing boundary in multipart form data."); - } - - var reader = new MultipartReader(boundary, Request.Body); - - // Process each section in the multipart body - MultipartSection? section; - long totalBytesRead = 0; - string targetFilePath = Path.Combine(Directory.GetCurrentDirectory(), UploadFilePath); - - if (System.IO.File.Exists(targetFilePath)) - { - System.IO.File.Delete(targetFilePath); - _logger.LogDebug($"Removed existing output file: {targetFilePath}"); - } - - using FileStream outputFileStream = new FileStream( - path: targetFilePath, - mode: FileMode.Create, - access: FileAccess.Write, - share: FileShare.None, - bufferSize: BufferSize, - useAsync: true); - - while ((section = await reader.ReadNextSectionAsync()) != null) - { - // Check if the section is a file - var contentDisposition = section.GetContentDispositionHeader(); - if (contentDisposition != null && contentDisposition.IsFileDisposition()) - { - _logger.LogInformation($"Processing file: {contentDisposition.FileName.Value}"); - - // Write the file content to the target file - await section.Body.CopyToAsync(outputFileStream); - totalBytesRead += section.Body.Length; - } - else if (contentDisposition != null && contentDisposition.IsFormDisposition()) - { - // Handle metadata (form fields) - string key = contentDisposition.Name.Value!; - using var streamReader = new StreamReader(section.Body); - string value = await streamReader.ReadToEndAsync(); - _logger.LogInformation($"Received metadata: {key} = {value}"); - } - } - - _logger.LogInformation($"File upload completed. Total bytes read: {totalBytesRead} bytes."); - return Ok(new { Message = "File uploaded successfully.", BytesProcessed = totalBytesRead }); + return BadRequest("The request does not contain valid multipart form data."); } - catch (Exception ex) + + var boundary = HeaderUtilities.RemoveQuotes(MediaTypeHeaderValue.Parse(Request.ContentType).Boundary).Value; + if (string.IsNullOrWhiteSpace(boundary)) { - _logger.LogError(ex, "Error during file upload"); - return StatusCode(500, "An error occurred while uploading the file."); + return BadRequest("Missing boundary in multipart form data."); } + + var cancellationToken = HttpContext.RequestAborted; + var filePath = await _fileManager.SaveViaMultipartReaderAsync(boundary, Request.Body, cancellationToken); + return Ok("Saved file at " + filePath); } [HttpPost] - [Route(nameof(UploadPipeReader))] + [Route("pipe")] public async Task UploadPipeReader() { - try + if (!Request.HasFormContentType) { - if (!Request.HasFormContentType) - { - return BadRequest("The request does not contain a valid form."); - } - _ = Request.Headers.TryGetValue("Content-Type", out var contentType); - _logger.LogInformation("Content-Type: {ContentType}", contentType!); - - var bodyReader = Request.BodyReader; - - long totalBytesRead = 0; - - - string targetFilePath = Path.Combine(Directory.GetCurrentDirectory(), UploadFilePath); - if (System.IO.File.Exists(targetFilePath)) - { - System.IO.File.Delete(targetFilePath); - _logger.LogDebug("Removed existing output file: {path}", targetFilePath); - } - - using FileStream outputFileStream = new FileStream( - path: targetFilePath, - mode: FileMode.OpenOrCreate, - access: FileAccess.Write, - share: FileShare.None, - bufferSize: BufferSize, - useAsync: true); - - while (true) - { - var readResult = await bodyReader.ReadAsync(); - var buffer = readResult.Buffer; - - foreach (var memory in buffer) - { - await outputFileStream.WriteAsync(memory); - totalBytesRead += memory.Length; - } - bodyReader.AdvanceTo(buffer.End); + return BadRequest("The request does not contain a valid form."); + } - if (readResult.IsCompleted) - { - break; - } - } + var cancellationToken = HttpContext.RequestAborted; + var filePath = await _fileManager.SaveViaPipeReaderAsync(Request.BodyReader, cancellationToken); + return Ok("Saved file at " + filePath); + } - _logger.LogInformation("File upload completed. Total bytes read: {TotalBytesRead} bytes.", totalBytesRead); - return Ok(new { Message = "File uploaded successfully.", BytesProcessed = totalBytesRead }); - } - catch (Exception ex) + [HttpPost] + [Route("form")] + public async Task ReadForms() + { + if (!Request.HasFormContentType) { - _logger.LogError(ex, "Error during file upload"); - return StatusCode(500, "An error occurred while uploading the file."); + return BadRequest("The request does not contain a valid form."); } + + var cancellationToken = HttpContext.RequestAborted; + var formFeature = Request.HttpContext.Features.GetRequiredFeature(); + await formFeature.ReadFormAsync(cancellationToken); + + var filePath = Request.Form.Files.First().FileName; + return Ok("Saved file at " + filePath); } } } diff --git a/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/FileManagerSample.csproj b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/FileManagerSample.csproj index 6568b3dcfb4b..537fbc9e5f12 100644 --- a/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/FileManagerSample.csproj +++ b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/FileManagerSample.csproj @@ -1,4 +1,4 @@ - + net9.0 diff --git a/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Program.cs b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Program.cs index 19f5f4d5d041..e38ec7a26ae0 100644 --- a/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Program.cs +++ b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Program.cs @@ -1,4 +1,9 @@ +using FileManagerSample.Services; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Net.Http.Headers; + var builder = WebApplication.CreateBuilder(args); +builder.Services.AddSingleton(); builder.WebHost.ConfigureKestrel(options => { @@ -11,9 +16,56 @@ options.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(10); }); +builder.Services.Configure(options => +{ + options.MultipartBodyLengthLimit = 10L * 1024 * 1024 * 1024; // 10 GB +}); + builder.Services.AddControllers(); var app = builder.Build(); - app.MapControllers(); + +app.MapPost("minimal/multipart", async (FileManagerService fileManager, HttpRequest request, CancellationToken cancellationToken) => +{ + if (!request.ContentType?.StartsWith("multipart/form-data") ?? true) + { + return Results.BadRequest("The request does not contain valid multipart form data."); + } + + var boundary = HeaderUtilities.RemoveQuotes(MediaTypeHeaderValue.Parse(request.ContentType).Boundary).Value; + if (string.IsNullOrWhiteSpace(boundary)) + { + return Results.BadRequest("Missing boundary in multipart form data."); + } + + var filePath = await fileManager.SaveViaMultipartReaderAsync(boundary, request.Body, cancellationToken); + return Results.Ok("Saved file at " + filePath); +}); + +app.MapPost("minimal/pipe", async (FileManagerService fileManager, HttpRequest request, CancellationToken cancellationToken) => +{ + if (!request.HasFormContentType) + { + return Results.BadRequest("The request does not contain a valid form."); + } + + var filePath = await fileManager.SaveViaPipeReaderAsync(request.BodyReader, cancellationToken); + return Results.Ok("Saved file at " + filePath); +}); + +app.MapPost("minimal/form", async (HttpRequest request, CancellationToken cancellationToken) => +{ + if (!request.HasFormContentType) + { + return Results.BadRequest("The request does not contain a valid form."); + } + + var formFeature = request.HttpContext.Features.GetRequiredFeature(); + await formFeature.ReadFormAsync(cancellationToken); + + var filePath = request.Form.Files.First().FileName; + return Results.Ok("Saved file at " + filePath); +}); + app.Run(); diff --git a/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Services/FileManagerService.cs b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Services/FileManagerService.cs new file mode 100644 index 000000000000..acc25cee59e3 --- /dev/null +++ b/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample/Services/FileManagerService.cs @@ -0,0 +1,108 @@ +using System.IO.Pipelines; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Net.Http.Headers; + +namespace FileManagerSample.Services +{ + public class FileManagerService + { + private const int BufferSize = 16 * 1024 * 1024; // 16 MB buffer size + private const string UploadFilePath = "file-upload.dat"; + + private readonly ILogger _logger; + + public FileManagerService(ILogger logger) + { + _logger = logger; + } + + public async Task SaveViaMultipartReaderAsync(string boundary, Stream contentStream, CancellationToken cancellationToken) + { + string targetFilePath = Path.Combine(Directory.GetCurrentDirectory(), UploadFilePath); + CheckAndRemoveLocalFile(targetFilePath); + + using FileStream outputFileStream = new FileStream( + path: targetFilePath, + mode: FileMode.Create, + access: FileAccess.Write, + share: FileShare.None, + bufferSize: BufferSize, + useAsync: true); + + var reader = new MultipartReader(boundary, contentStream); + MultipartSection? section; + long totalBytesRead = 0; + + // Process each section in the multipart body + while ((section = await reader.ReadNextSectionAsync(cancellationToken)) != null) + { + // Check if the section is a file + var contentDisposition = section.GetContentDispositionHeader(); + if (contentDisposition != null && contentDisposition.IsFileDisposition()) + { + _logger.LogInformation($"Processing file: {contentDisposition.FileName.Value}"); + + // Write the file content to the target file + await section.Body.CopyToAsync(outputFileStream, cancellationToken); + totalBytesRead += section.Body.Length; + } + else if (contentDisposition != null && contentDisposition.IsFormDisposition()) + { + // Handle metadata (form fields) + string key = contentDisposition.Name.Value!; + using var streamReader = new StreamReader(section.Body); + string value = await streamReader.ReadToEndAsync(cancellationToken); + _logger.LogInformation($"Received metadata: {key} = {value}"); + } + } + + _logger.LogInformation($"File upload completed (via multipart). Total bytes read: {totalBytesRead} bytes."); + return targetFilePath; + } + + private void CheckAndRemoveLocalFile(string filePath) + { + if (File.Exists(filePath)) + { + File.Delete(filePath); + _logger.LogDebug($"Removed existing output file: {filePath}"); + } + } + + public async Task SaveViaPipeReaderAsync(PipeReader contentReader, CancellationToken cancellationToken) + { + string targetFilePath = Path.Combine(Directory.GetCurrentDirectory(), UploadFilePath); + CheckAndRemoveLocalFile(targetFilePath); + long totalBytesRead = 0; + + using FileStream outputFileStream = new FileStream( + path: targetFilePath, + mode: FileMode.OpenOrCreate, + access: FileAccess.Write, + share: FileShare.None, + bufferSize: BufferSize, + useAsync: true); + + while (true) + { + var readResult = await contentReader.ReadAsync(); + var buffer = readResult.Buffer; + + foreach (var memory in buffer) + { + await outputFileStream.WriteAsync(memory); + totalBytesRead += memory.Length; + } + contentReader.AdvanceTo(buffer.End); + + if (readResult.IsCompleted) + { + break; + } + } + + _logger.LogInformation($"File upload completed (via pipeReader). Total bytes read: {totalBytesRead} bytes."); + return targetFilePath; + } + } +} From b209bd0376e863dd94a2ead44b078c7f18ea6072 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Mon, 26 May 2025 18:36:52 +0200 Subject: [PATCH 11/30] explanations to sample --- aspnetcore/mvc/models/file-uploads.md | 38 +++++++++++++++++++ .../file-uploads/samples/9.x/.gitignore | 1 - 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/aspnetcore/mvc/models/file-uploads.md b/aspnetcore/mvc/models/file-uploads.md index f73c88f9426a..7e5b366d1f54 100644 --- a/aspnetcore/mvc/models/file-uploads.md +++ b/aspnetcore/mvc/models/file-uploads.md @@ -415,6 +415,44 @@ The preceding example is similar to a scenario demonstrated in the sample app: ### Upload large files with streaming +For scenarios where large file uploads are required, streaming uploads allow you to process incoming multipart form data directly, without buffering the entire file in memory or on disk via model binding. This technique is especially important for files that could exceed server or framework buffering thresholds. + +> [!TIP] +> In latest .NET support of the advanced streaming upload solutions use `MultipartReader` (specifically designed for parsing `multipart` data), `IFormFeature` (a wrapper over `MultipartReader`) and even `IPipeReader` for the most precise control over the request body processing. + +[Sample application 9.x](./file-uploads/samples/9.x/) demonstrates a toy-scenario of server receiving a file with the incoming request and streaming the data directly to the file on the disk. Example supports a robust cancellation via the HTTP request's cancellation token. + +[MultipartReader](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.webutilities.multipartreader) is the essential piece of ASP.NET Core's type system specifically designed to read the files from incoming requests. The following snippet shows how to process a request and stream body of the request (a file) into the `outputStream` (can be a `FileStream` or any other stream): +```csharp +// read boundary from `Content-Type` header +var boundary = HeaderUtilities.RemoveQuotes(MediaTypeHeaderValue.Parse(request.ContentType).Boundary).Value; + +// read via `MultipartReader` and stream data to other location +MultipartSection? section; +var reader = new MultipartReader(boundary, Request.Body); +while ((section = await reader.ReadNextSectionAsync(cancellationToken)) != null) +{ + await section.Body.CopyToAsync(outputStream, cancellationToken); +} +``` + +[IFormFeature](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.http.features.iformfeature) is a wrapper over the `MultipartReader` allowing to now write the manual request body parse by yourself, but simply using it to read the form using [ReadFormAsync(CancellationToken)](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.http.features.iformfeature.readformasync#microsoft-aspnetcore-http-features-iformfeature-readformasync(system-threading-cancellationtoken)). Snippet below shows how to get the `IFormFeature`, request to read form (with awaiting) and to simply access the file from the built under the hood collection: +```csharp +// get the IFormFeature and invoke reading the form +var formFeature = Request.HttpContext.Features.GetRequiredFeature(); +await formFeature.ReadFormAsync(cancellationToken); + +// access file - in this case the only one +var filePath = Request.Form.Files.First().FileName; +return Results.Ok("Saved file at " + filePath); +``` + +For more advanced scenarios one can write the parsing code manually. For that purpose there is a type [HttpRequest.BodyReader](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.http.httprequest.bodyreader) which has access to reading the raw bytes of the http request. It is built on top of the [System.IO.Pipelines](https://learn.microsoft.com/aspnet/core/fundamentals/middleware/request-response). Sample app also has the endpoint handler for using `IPipeReader` for both minimal APIs and controllers. + +> [!WARNING] +> **Model binding quirk:** +> When using controller actions, adding any model-bound parameter (for example, a `CancellationToken`, a `string`, or a custom model) causes ASP.NET Core's model binding pipeline to eagerly read and buffer the entire form body before your handler executes. This can defeat streaming and cause performance or memory problems with large uploads. For true streaming, use only infrastructure parameters in your method signature and access the request data manually. To get cancellation token bind to the request you can use [HttpContext.RequestAborted](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.http.httpcontext.requestaborted). + The [3.1 example](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Pages/StreamedSingleFileUploadDb.cshtml) demonstrates how to use JavaScript to stream a file to a controller action. The file's antiforgery token is generated using a custom filter attribute and passed to the client HTTP headers instead of in the request body. Because the action method processes the uploaded data directly, form model binding is disabled by another custom filter. Within the action, the form's contents are read using a `MultipartReader`, which reads each individual `MultipartSection`, processing the file or storing the contents as appropriate. After the multipart sections are read, the action performs its own model binding. The initial page response loads the form and saves an antiforgery token in a cookie (via the `GenerateAntiforgeryTokenCookieAttribute` attribute). The attribute uses ASP.NET Core's built-in [antiforgery support](xref:security/anti-request-forgery) to set a cookie with a request token: diff --git a/aspnetcore/mvc/models/file-uploads/samples/9.x/.gitignore b/aspnetcore/mvc/models/file-uploads/samples/9.x/.gitignore index bf018662343c..77a6daefc17e 100644 --- a/aspnetcore/mvc/models/file-uploads/samples/9.x/.gitignore +++ b/aspnetcore/mvc/models/file-uploads/samples/9.x/.gitignore @@ -1,2 +1 @@ **/*.dat -**/*.txt From 5dbaf93f0aba819f9a7b11a628e51831c9c49c7b Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Mon, 26 May 2025 18:41:42 +0200 Subject: [PATCH 12/30] AI help --- aspnetcore/mvc/models/file-uploads.md | 28 +++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/aspnetcore/mvc/models/file-uploads.md b/aspnetcore/mvc/models/file-uploads.md index 7e5b366d1f54..2146f52bda32 100644 --- a/aspnetcore/mvc/models/file-uploads.md +++ b/aspnetcore/mvc/models/file-uploads.md @@ -418,40 +418,44 @@ The preceding example is similar to a scenario demonstrated in the sample app: For scenarios where large file uploads are required, streaming uploads allow you to process incoming multipart form data directly, without buffering the entire file in memory or on disk via model binding. This technique is especially important for files that could exceed server or framework buffering thresholds. > [!TIP] -> In latest .NET support of the advanced streaming upload solutions use `MultipartReader` (specifically designed for parsing `multipart` data), `IFormFeature` (a wrapper over `MultipartReader`) and even `IPipeReader` for the most precise control over the request body processing. +> In modern .NET versions, streaming solutions are available using `MultipartReader` (designed for parsing `multipart` data), `IFormFeature` (a wrapper over `MultipartReader`), and even `IPipeReader` for the most precise control over request body processing. -[Sample application 9.x](./file-uploads/samples/9.x/) demonstrates a toy-scenario of server receiving a file with the incoming request and streaming the data directly to the file on the disk. Example supports a robust cancellation via the HTTP request's cancellation token. +The [sample application for 9.x](./file-uploads/samples/9.x/) demonstrates how a server can receive a file and stream the data directly to disk, supporting robust cancellation via the HTTP request's cancellation token. -[MultipartReader](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.webutilities.multipartreader) is the essential piece of ASP.NET Core's type system specifically designed to read the files from incoming requests. The following snippet shows how to process a request and stream body of the request (a file) into the `outputStream` (can be a `FileStream` or any other stream): +[`MultipartReader`](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.webutilities.multipartreader) is an ASP.NET Core utility for reading files from incoming requests. The following snippet shows how to process a request and stream the file into an `outputStream` (such as a `FileStream`): ```csharp -// read boundary from `Content-Type` header +// Read the boundary from the Content-Type header var boundary = HeaderUtilities.RemoveQuotes(MediaTypeHeaderValue.Parse(request.ContentType).Boundary).Value; -// read via `MultipartReader` and stream data to other location -MultipartSection? section; +// Use MultipartReader to stream data to a destination var reader = new MultipartReader(boundary, Request.Body); +MultipartSection? section; while ((section = await reader.ReadNextSectionAsync(cancellationToken)) != null) { - await section.Body.CopyToAsync(outputStream, cancellationToken); + var contentDisposition = section.GetContentDispositionHeader(); + if (contentDisposition != null && contentDisposition.IsFileDisposition()) + { + await section.Body.CopyToAsync(outputStream, cancellationToken); + } } ``` -[IFormFeature](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.http.features.iformfeature) is a wrapper over the `MultipartReader` allowing to now write the manual request body parse by yourself, but simply using it to read the form using [ReadFormAsync(CancellationToken)](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.http.features.iformfeature.readformasync#microsoft-aspnetcore-http-features-iformfeature-readformasync(system-threading-cancellationtoken)). Snippet below shows how to get the `IFormFeature`, request to read form (with awaiting) and to simply access the file from the built under the hood collection: +[`IFormFeature`](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.http.features.iformfeature) is a wrapper around `MultipartReader` that lets you avoid writing manual request body parsing code. You can use its [`ReadFormAsync(CancellationToken)`](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.http.features.iformfeature.readformasync#microsoft-aspnetcore-http-features-iformfeature-readformasync(system-threading-cancellationtoken)) method to populate the request's form data, then access uploaded files from the built-in collection: ```csharp -// get the IFormFeature and invoke reading the form +// Get the IFormFeature and read the form var formFeature = Request.HttpContext.Features.GetRequiredFeature(); await formFeature.ReadFormAsync(cancellationToken); -// access file - in this case the only one +// Access the uploaded file (example: first file) var filePath = Request.Form.Files.First().FileName; return Results.Ok("Saved file at " + filePath); ``` -For more advanced scenarios one can write the parsing code manually. For that purpose there is a type [HttpRequest.BodyReader](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.http.httprequest.bodyreader) which has access to reading the raw bytes of the http request. It is built on top of the [System.IO.Pipelines](https://learn.microsoft.com/aspnet/core/fundamentals/middleware/request-response). Sample app also has the endpoint handler for using `IPipeReader` for both minimal APIs and controllers. +For more advanced scenarios, you can manually parse the raw request body using [`HttpRequest.BodyReader`](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.http.httprequest.bodyreader), which exposes an [`IPipeReader`](https://learn.microsoft.com/aspnet/core/fundamentals/middleware/request-response) for low-level, high-performance streaming. The sample application includes endpoint handlers that use `IPipeReader` in both minimal APIs and controllers. > [!WARNING] > **Model binding quirk:** -> When using controller actions, adding any model-bound parameter (for example, a `CancellationToken`, a `string`, or a custom model) causes ASP.NET Core's model binding pipeline to eagerly read and buffer the entire form body before your handler executes. This can defeat streaming and cause performance or memory problems with large uploads. For true streaming, use only infrastructure parameters in your method signature and access the request data manually. To get cancellation token bind to the request you can use [HttpContext.RequestAborted](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.http.httpcontext.requestaborted). +> In controller actions, adding any model-bound parameter (such as a `CancellationToken`, `string`, or custom model) causes ASP.NET Core's model binding pipeline to eagerly read and buffer the entire form body before your handler executes. This defeats streaming and can cause performance or memory issues with large uploads. For true streaming, use only infrastructure parameters in your method signature and access request data manually. To get a cancellation token tied to the HTTP request, use [`HttpContext.RequestAborted`](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.http.httpcontext.requestaborted). The [3.1 example](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Pages/StreamedSingleFileUploadDb.cshtml) demonstrates how to use JavaScript to stream a file to a controller action. The file's antiforgery token is generated using a custom filter attribute and passed to the client HTTP headers instead of in the request body. Because the action method processes the uploaded data directly, form model binding is disabled by another custom filter. Within the action, the form's contents are read using a `MultipartReader`, which reads each individual `MultipartSection`, processing the file or storing the contents as appropriate. After the multipart sections are read, the action performs its own model binding. From d29dca31e16f69c11ded8dd74d95b2f4ec366b40 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Mon, 26 May 2025 18:46:24 +0200 Subject: [PATCH 13/30] fix links --- aspnetcore/mvc/models/file-uploads.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/aspnetcore/mvc/models/file-uploads.md b/aspnetcore/mvc/models/file-uploads.md index 2146f52bda32..70a636ff90bd 100644 --- a/aspnetcore/mvc/models/file-uploads.md +++ b/aspnetcore/mvc/models/file-uploads.md @@ -420,9 +420,9 @@ For scenarios where large file uploads are required, streaming uploads allow you > [!TIP] > In modern .NET versions, streaming solutions are available using `MultipartReader` (designed for parsing `multipart` data), `IFormFeature` (a wrapper over `MultipartReader`), and even `IPipeReader` for the most precise control over request body processing. -The [sample application for 9.x](./file-uploads/samples/9.x/) demonstrates how a server can receive a file and stream the data directly to disk, supporting robust cancellation via the HTTP request's cancellation token. +The [sample application for 9.x](file-uploads/samples/9.x) demonstrates how a server can receive a file and stream the data directly to disk, supporting robust cancellation via the HTTP request's cancellation token. -[`MultipartReader`](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.webutilities.multipartreader) is an ASP.NET Core utility for reading files from incoming requests. The following snippet shows how to process a request and stream the file into an `outputStream` (such as a `FileStream`): +[`MultipartReader`](/dotnet/api/microsoft.aspnetcore.webutilities.multipartreader) is an ASP.NET Core utility for reading files from incoming requests. The following snippet shows how to process a request and stream the file into an `outputStream` (such as a `FileStream`): ```csharp // Read the boundary from the Content-Type header var boundary = HeaderUtilities.RemoveQuotes(MediaTypeHeaderValue.Parse(request.ContentType).Boundary).Value; @@ -440,7 +440,7 @@ while ((section = await reader.ReadNextSectionAsync(cancellationToken)) != null) } ``` -[`IFormFeature`](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.http.features.iformfeature) is a wrapper around `MultipartReader` that lets you avoid writing manual request body parsing code. You can use its [`ReadFormAsync(CancellationToken)`](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.http.features.iformfeature.readformasync#microsoft-aspnetcore-http-features-iformfeature-readformasync(system-threading-cancellationtoken)) method to populate the request's form data, then access uploaded files from the built-in collection: +[`IFormFeature`](/dotnet/api/microsoft.aspnetcore.http.features.iformfeature) is a wrapper around `MultipartReader` that lets you avoid writing manual request body parsing code. You can use its [`ReadFormAsync(CancellationToken)`](/dotnet/api/microsoft.aspnetcore.http.features.iformfeature.readformasync#microsoft-aspnetcore-http-features-iformfeature-readformasync(system-threading-cancellationtoken)) method to populate the request's form data, then access uploaded files from the built-in collection: ```csharp // Get the IFormFeature and read the form var formFeature = Request.HttpContext.Features.GetRequiredFeature(); @@ -451,11 +451,11 @@ var filePath = Request.Form.Files.First().FileName; return Results.Ok("Saved file at " + filePath); ``` -For more advanced scenarios, you can manually parse the raw request body using [`HttpRequest.BodyReader`](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.http.httprequest.bodyreader), which exposes an [`IPipeReader`](https://learn.microsoft.com/aspnet/core/fundamentals/middleware/request-response) for low-level, high-performance streaming. The sample application includes endpoint handlers that use `IPipeReader` in both minimal APIs and controllers. +For more advanced scenarios, you can manually parse the raw request body using [`HttpRequest.BodyReader`](/dotnet/api/microsoft.aspnetcore.http.httprequest.bodyreader), which exposes an [`IPipeReader`](/aspnet/core/fundamentals/middleware/request-response) for low-level, high-performance streaming. The sample application includes endpoint handlers that use `IPipeReader` in both minimal APIs and controllers. > [!WARNING] > **Model binding quirk:** -> In controller actions, adding any model-bound parameter (such as a `CancellationToken`, `string`, or custom model) causes ASP.NET Core's model binding pipeline to eagerly read and buffer the entire form body before your handler executes. This defeats streaming and can cause performance or memory issues with large uploads. For true streaming, use only infrastructure parameters in your method signature and access request data manually. To get a cancellation token tied to the HTTP request, use [`HttpContext.RequestAborted`](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.http.httpcontext.requestaborted). +> In controller actions, adding any model-bound parameter (such as a `CancellationToken`, `string`, or custom model) causes ASP.NET Core's model binding pipeline to eagerly read and buffer the entire form body before your handler executes. This defeats streaming and can cause performance or memory issues with large uploads. For true streaming, use only infrastructure parameters in your method signature and access request data manually. To get a cancellation token tied to the HTTP request, use [`HttpContext.RequestAborted`](/dotnet/api/microsoft.aspnetcore.http.httpcontext.requestaborted). The [3.1 example](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Pages/StreamedSingleFileUploadDb.cshtml) demonstrates how to use JavaScript to stream a file to a controller action. The file's antiforgery token is generated using a custom filter attribute and passed to the client HTTP headers instead of in the request body. Because the action method processes the uploaded data directly, form model binding is disabled by another custom filter. Within the action, the form's contents are read using a `MultipartReader`, which reads each individual `MultipartSection`, processing the file or storing the contents as appropriate. After the multipart sections are read, the action performs its own model binding. From bc2a547e71f090d50f8d6cbd7c9b9c9090d4cb94 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Mon, 26 May 2025 18:50:48 +0200 Subject: [PATCH 14/30] remove unneeded tip --- aspnetcore/mvc/models/file-uploads.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/aspnetcore/mvc/models/file-uploads.md b/aspnetcore/mvc/models/file-uploads.md index 70a636ff90bd..3dbbea853194 100644 --- a/aspnetcore/mvc/models/file-uploads.md +++ b/aspnetcore/mvc/models/file-uploads.md @@ -417,10 +417,7 @@ The preceding example is similar to a scenario demonstrated in the sample app: For scenarios where large file uploads are required, streaming uploads allow you to process incoming multipart form data directly, without buffering the entire file in memory or on disk via model binding. This technique is especially important for files that could exceed server or framework buffering thresholds. -> [!TIP] -> In modern .NET versions, streaming solutions are available using `MultipartReader` (designed for parsing `multipart` data), `IFormFeature` (a wrapper over `MultipartReader`), and even `IPipeReader` for the most precise control over request body processing. - -The [sample application for 9.x](file-uploads/samples/9.x) demonstrates how a server can receive a file and stream the data directly to disk, supporting robust cancellation via the HTTP request's cancellation token. +The [sample application for 9.x](file-uploads/samples/9.x) demonstrates how a server can receive a file and stream the data directly to disk, supporting robust cancellation via the HTTP request's cancellation token. There are several approaches. [`MultipartReader`](/dotnet/api/microsoft.aspnetcore.webutilities.multipartreader) is an ASP.NET Core utility for reading files from incoming requests. The following snippet shows how to process a request and stream the file into an `outputStream` (such as a `FileStream`): ```csharp From 9c1965abd8f10928f33c21ff66de6fdc0604e181 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Thu, 29 May 2025 21:07:38 +0200 Subject: [PATCH 15/30] tmp remove docs --- aspnetcore/mvc/models/file-uploads.md | 39 --------------------------- 1 file changed, 39 deletions(-) diff --git a/aspnetcore/mvc/models/file-uploads.md b/aspnetcore/mvc/models/file-uploads.md index 3dbbea853194..f73c88f9426a 100644 --- a/aspnetcore/mvc/models/file-uploads.md +++ b/aspnetcore/mvc/models/file-uploads.md @@ -415,45 +415,6 @@ The preceding example is similar to a scenario demonstrated in the sample app: ### Upload large files with streaming -For scenarios where large file uploads are required, streaming uploads allow you to process incoming multipart form data directly, without buffering the entire file in memory or on disk via model binding. This technique is especially important for files that could exceed server or framework buffering thresholds. - -The [sample application for 9.x](file-uploads/samples/9.x) demonstrates how a server can receive a file and stream the data directly to disk, supporting robust cancellation via the HTTP request's cancellation token. There are several approaches. - -[`MultipartReader`](/dotnet/api/microsoft.aspnetcore.webutilities.multipartreader) is an ASP.NET Core utility for reading files from incoming requests. The following snippet shows how to process a request and stream the file into an `outputStream` (such as a `FileStream`): -```csharp -// Read the boundary from the Content-Type header -var boundary = HeaderUtilities.RemoveQuotes(MediaTypeHeaderValue.Parse(request.ContentType).Boundary).Value; - -// Use MultipartReader to stream data to a destination -var reader = new MultipartReader(boundary, Request.Body); -MultipartSection? section; -while ((section = await reader.ReadNextSectionAsync(cancellationToken)) != null) -{ - var contentDisposition = section.GetContentDispositionHeader(); - if (contentDisposition != null && contentDisposition.IsFileDisposition()) - { - await section.Body.CopyToAsync(outputStream, cancellationToken); - } -} -``` - -[`IFormFeature`](/dotnet/api/microsoft.aspnetcore.http.features.iformfeature) is a wrapper around `MultipartReader` that lets you avoid writing manual request body parsing code. You can use its [`ReadFormAsync(CancellationToken)`](/dotnet/api/microsoft.aspnetcore.http.features.iformfeature.readformasync#microsoft-aspnetcore-http-features-iformfeature-readformasync(system-threading-cancellationtoken)) method to populate the request's form data, then access uploaded files from the built-in collection: -```csharp -// Get the IFormFeature and read the form -var formFeature = Request.HttpContext.Features.GetRequiredFeature(); -await formFeature.ReadFormAsync(cancellationToken); - -// Access the uploaded file (example: first file) -var filePath = Request.Form.Files.First().FileName; -return Results.Ok("Saved file at " + filePath); -``` - -For more advanced scenarios, you can manually parse the raw request body using [`HttpRequest.BodyReader`](/dotnet/api/microsoft.aspnetcore.http.httprequest.bodyreader), which exposes an [`IPipeReader`](/aspnet/core/fundamentals/middleware/request-response) for low-level, high-performance streaming. The sample application includes endpoint handlers that use `IPipeReader` in both minimal APIs and controllers. - -> [!WARNING] -> **Model binding quirk:** -> In controller actions, adding any model-bound parameter (such as a `CancellationToken`, `string`, or custom model) causes ASP.NET Core's model binding pipeline to eagerly read and buffer the entire form body before your handler executes. This defeats streaming and can cause performance or memory issues with large uploads. For true streaming, use only infrastructure parameters in your method signature and access request data manually. To get a cancellation token tied to the HTTP request, use [`HttpContext.RequestAborted`](/dotnet/api/microsoft.aspnetcore.http.httpcontext.requestaborted). - The [3.1 example](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Pages/StreamedSingleFileUploadDb.cshtml) demonstrates how to use JavaScript to stream a file to a controller action. The file's antiforgery token is generated using a custom filter attribute and passed to the client HTTP headers instead of in the request body. Because the action method processes the uploaded data directly, form model binding is disabled by another custom filter. Within the action, the form's contents are read using a `MultipartReader`, which reads each individual `MultipartSection`, processing the file or storing the contents as appropriate. After the multipart sections are read, the action performs its own model binding. The initial page response loads the form and saves an antiforgery token in a cookie (via the `GenerateAntiforgeryTokenCookieAttribute` attribute). The attribute uses ASP.NET Core's built-in [antiforgery support](xref:security/anti-request-forgery) to set a cookie with a request token: From 74a09e202f1079d83ecca6a8544061faaaa00ade Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Fri, 30 May 2025 10:24:02 +0200 Subject: [PATCH 16/30] docs for file uploads --- aspnetcore/mvc/models/file-uploads.md | 39 +++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/aspnetcore/mvc/models/file-uploads.md b/aspnetcore/mvc/models/file-uploads.md index f73c88f9426a..3dbbea853194 100644 --- a/aspnetcore/mvc/models/file-uploads.md +++ b/aspnetcore/mvc/models/file-uploads.md @@ -415,6 +415,45 @@ The preceding example is similar to a scenario demonstrated in the sample app: ### Upload large files with streaming +For scenarios where large file uploads are required, streaming uploads allow you to process incoming multipart form data directly, without buffering the entire file in memory or on disk via model binding. This technique is especially important for files that could exceed server or framework buffering thresholds. + +The [sample application for 9.x](file-uploads/samples/9.x) demonstrates how a server can receive a file and stream the data directly to disk, supporting robust cancellation via the HTTP request's cancellation token. There are several approaches. + +[`MultipartReader`](/dotnet/api/microsoft.aspnetcore.webutilities.multipartreader) is an ASP.NET Core utility for reading files from incoming requests. The following snippet shows how to process a request and stream the file into an `outputStream` (such as a `FileStream`): +```csharp +// Read the boundary from the Content-Type header +var boundary = HeaderUtilities.RemoveQuotes(MediaTypeHeaderValue.Parse(request.ContentType).Boundary).Value; + +// Use MultipartReader to stream data to a destination +var reader = new MultipartReader(boundary, Request.Body); +MultipartSection? section; +while ((section = await reader.ReadNextSectionAsync(cancellationToken)) != null) +{ + var contentDisposition = section.GetContentDispositionHeader(); + if (contentDisposition != null && contentDisposition.IsFileDisposition()) + { + await section.Body.CopyToAsync(outputStream, cancellationToken); + } +} +``` + +[`IFormFeature`](/dotnet/api/microsoft.aspnetcore.http.features.iformfeature) is a wrapper around `MultipartReader` that lets you avoid writing manual request body parsing code. You can use its [`ReadFormAsync(CancellationToken)`](/dotnet/api/microsoft.aspnetcore.http.features.iformfeature.readformasync#microsoft-aspnetcore-http-features-iformfeature-readformasync(system-threading-cancellationtoken)) method to populate the request's form data, then access uploaded files from the built-in collection: +```csharp +// Get the IFormFeature and read the form +var formFeature = Request.HttpContext.Features.GetRequiredFeature(); +await formFeature.ReadFormAsync(cancellationToken); + +// Access the uploaded file (example: first file) +var filePath = Request.Form.Files.First().FileName; +return Results.Ok("Saved file at " + filePath); +``` + +For more advanced scenarios, you can manually parse the raw request body using [`HttpRequest.BodyReader`](/dotnet/api/microsoft.aspnetcore.http.httprequest.bodyreader), which exposes an [`IPipeReader`](/aspnet/core/fundamentals/middleware/request-response) for low-level, high-performance streaming. The sample application includes endpoint handlers that use `IPipeReader` in both minimal APIs and controllers. + +> [!WARNING] +> **Model binding quirk:** +> In controller actions, adding any model-bound parameter (such as a `CancellationToken`, `string`, or custom model) causes ASP.NET Core's model binding pipeline to eagerly read and buffer the entire form body before your handler executes. This defeats streaming and can cause performance or memory issues with large uploads. For true streaming, use only infrastructure parameters in your method signature and access request data manually. To get a cancellation token tied to the HTTP request, use [`HttpContext.RequestAborted`](/dotnet/api/microsoft.aspnetcore.http.httpcontext.requestaborted). + The [3.1 example](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Pages/StreamedSingleFileUploadDb.cshtml) demonstrates how to use JavaScript to stream a file to a controller action. The file's antiforgery token is generated using a custom filter attribute and passed to the client HTTP headers instead of in the request body. Because the action method processes the uploaded data directly, form model binding is disabled by another custom filter. Within the action, the form's contents are read using a `MultipartReader`, which reads each individual `MultipartSection`, processing the file or storing the contents as appropriate. After the multipart sections are read, the action performs its own model binding. The initial page response loads the form and saves an antiforgery token in a cookie (via the `GenerateAntiforgeryTokenCookieAttribute` attribute). The attribute uses ASP.NET Core's built-in [antiforgery support](xref:security/anti-request-forgery) to set a cookie with a request token: From 82fd5a85319e290ff23ed7ad7a83b9612b077589 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Thu, 5 Jun 2025 19:29:34 +0200 Subject: [PATCH 17/30] no warning --- aspnetcore/mvc/models/file-uploads.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/aspnetcore/mvc/models/file-uploads.md b/aspnetcore/mvc/models/file-uploads.md index 8eabd3428118..0cb03e6a1290 100644 --- a/aspnetcore/mvc/models/file-uploads.md +++ b/aspnetcore/mvc/models/file-uploads.md @@ -450,10 +450,6 @@ return Results.Ok("Saved file at " + filePath); For more advanced scenarios, you can manually parse the raw request body using [`HttpRequest.BodyReader`](/dotnet/api/microsoft.aspnetcore.http.httprequest.bodyreader), which exposes an [`IPipeReader`](/aspnet/core/fundamentals/middleware/request-response) for low-level, high-performance streaming. The sample application includes endpoint handlers that use `IPipeReader` in both minimal APIs and controllers. -> [!WARNING] -> **Model binding quirk:** -> In controller actions, adding any model-bound parameter (such as a `CancellationToken`, `string`, or custom model) causes ASP.NET Core's model binding pipeline to eagerly read and buffer the entire form body before your handler executes. This defeats streaming and can cause performance or memory issues with large uploads. For true streaming, use only infrastructure parameters in your method signature and access request data manually. To get a cancellation token tied to the HTTP request, use [`HttpContext.RequestAborted`](/dotnet/api/microsoft.aspnetcore.http.httpcontext.requestaborted). - The [3.1 example](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Pages/StreamedSingleFileUploadDb.cshtml) demonstrates how to use JavaScript to stream a file to a controller action. The file's antiforgery token is generated using a custom filter attribute and passed to the client HTTP headers instead of in the request body. Because the action method processes the uploaded data directly, form model binding is disabled by another custom filter. Within the action, the form's contents are read using a `MultipartReader`, which reads each individual `MultipartSection`, processing the file or storing the contents as appropriate. After the multipart sections are read, the action performs its own model binding. The initial page response loads the form and saves an antiforgery token in a cookie (via the `GenerateAntiforgeryTokenCookieAttribute` attribute). The attribute uses ASP.NET Core's built-in [antiforgery support](xref:security/anti-request-forgery) to set a cookie with a request token: From 3eab678304db972b9e22775479bee60a20e76235 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Thu, 5 Jun 2025 19:46:39 +0200 Subject: [PATCH 18/30] fix link --- aspnetcore/mvc/models/file-uploads.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aspnetcore/mvc/models/file-uploads.md b/aspnetcore/mvc/models/file-uploads.md index 0cb03e6a1290..9897b5c3655a 100644 --- a/aspnetcore/mvc/models/file-uploads.md +++ b/aspnetcore/mvc/models/file-uploads.md @@ -417,7 +417,7 @@ The preceding example is similar to a scenario demonstrated in the sample app: For scenarios where large file uploads are required, streaming uploads allow you to process incoming multipart form data directly, without buffering the entire file in memory or on disk via model binding. This technique is especially important for files that could exceed server or framework buffering thresholds. -The [sample application for 9.x](file-uploads/samples/9.x) demonstrates how a server can receive a file and stream the data directly to disk, supporting robust cancellation via the HTTP request's cancellation token. There are several approaches. +The [sample application for 9.x](file-uploads/samples/9.x/FileManagerSample/Program.cs) demonstrates how a server can receive a file and stream the data directly to disk, supporting robust cancellation via the HTTP request's cancellation token. There are several approaches. [`MultipartReader`](/dotnet/api/microsoft.aspnetcore.webutilities.multipartreader) is an ASP.NET Core utility for reading files from incoming requests. The following snippet shows how to process a request and stream the file into an `outputStream` (such as a `FileStream`): ```csharp From 0754d910bc666a25f9b5ccdcebabb55bb4ba78a6 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Thu, 5 Jun 2025 19:53:27 +0200 Subject: [PATCH 19/30] github link --- aspnetcore/mvc/models/file-uploads.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aspnetcore/mvc/models/file-uploads.md b/aspnetcore/mvc/models/file-uploads.md index 9897b5c3655a..e361669554ec 100644 --- a/aspnetcore/mvc/models/file-uploads.md +++ b/aspnetcore/mvc/models/file-uploads.md @@ -417,7 +417,7 @@ The preceding example is similar to a scenario demonstrated in the sample app: For scenarios where large file uploads are required, streaming uploads allow you to process incoming multipart form data directly, without buffering the entire file in memory or on disk via model binding. This technique is especially important for files that could exceed server or framework buffering thresholds. -The [sample application for 9.x](file-uploads/samples/9.x/FileManagerSample/Program.cs) demonstrates how a server can receive a file and stream the data directly to disk, supporting robust cancellation via the HTTP request's cancellation token. There are several approaches. +The [sample application for 9.x](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample) demonstrates how a server can receive a file and stream the data directly to disk, supporting robust cancellation via the HTTP request's cancellation token. There are several approaches. [`MultipartReader`](/dotnet/api/microsoft.aspnetcore.webutilities.multipartreader) is an ASP.NET Core utility for reading files from incoming requests. The following snippet shows how to process a request and stream the file into an `outputStream` (such as a `FileStream`): ```csharp From 89617c11199912acf23541aa0e6e04fc9f6771f8 Mon Sep 17 00:00:00 2001 From: Korolev Dmitry Date: Fri, 6 Jun 2025 14:00:40 +0200 Subject: [PATCH 20/30] Update aspnetcore/mvc/models/file-uploads.md Co-authored-by: Luke Latham <1622880+guardrex@users.noreply.github.com> --- aspnetcore/mvc/models/file-uploads.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aspnetcore/mvc/models/file-uploads.md b/aspnetcore/mvc/models/file-uploads.md index e361669554ec..76a6bc9c99fb 100644 --- a/aspnetcore/mvc/models/file-uploads.md +++ b/aspnetcore/mvc/models/file-uploads.md @@ -415,7 +415,7 @@ The preceding example is similar to a scenario demonstrated in the sample app: ### Upload large files with streaming -For scenarios where large file uploads are required, streaming uploads allow you to process incoming multipart form data directly, without buffering the entire file in memory or on disk via model binding. This technique is especially important for files that could exceed server or framework buffering thresholds. +For scenarios where large file uploads are required, streaming uploads allow you to process incoming multipart form data directly without buffering the entire file in memory or on disk via model binding. This technique is especially important for files that could exceed server or framework buffering thresholds. The [sample application for 9.x](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample) demonstrates how a server can receive a file and stream the data directly to disk, supporting robust cancellation via the HTTP request's cancellation token. There are several approaches. From 964deb12bd0761e955bc047af8c4b20ab65d02dc Mon Sep 17 00:00:00 2001 From: Korolev Dmitry Date: Fri, 6 Jun 2025 14:01:00 +0200 Subject: [PATCH 21/30] Update aspnetcore/mvc/models/file-uploads.md Co-authored-by: Luke Latham <1622880+guardrex@users.noreply.github.com> --- aspnetcore/mvc/models/file-uploads.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aspnetcore/mvc/models/file-uploads.md b/aspnetcore/mvc/models/file-uploads.md index 76a6bc9c99fb..ba1746f7fa0f 100644 --- a/aspnetcore/mvc/models/file-uploads.md +++ b/aspnetcore/mvc/models/file-uploads.md @@ -417,7 +417,7 @@ The preceding example is similar to a scenario demonstrated in the sample app: For scenarios where large file uploads are required, streaming uploads allow you to process incoming multipart form data directly without buffering the entire file in memory or on disk via model binding. This technique is especially important for files that could exceed server or framework buffering thresholds. -The [sample application for 9.x](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample) demonstrates how a server can receive a file and stream the data directly to disk, supporting robust cancellation via the HTTP request's cancellation token. There are several approaches. +The [sample application for 9.x](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample) demonstrates how a server can receive a file and stream the data directly to disk, supporting robust cancellation via the HTTP request's cancellation token. [`MultipartReader`](/dotnet/api/microsoft.aspnetcore.webutilities.multipartreader) is an ASP.NET Core utility for reading files from incoming requests. The following snippet shows how to process a request and stream the file into an `outputStream` (such as a `FileStream`): ```csharp From 5208804b5f5c2d5b03dc08fc440af339c840492e Mon Sep 17 00:00:00 2001 From: Korolev Dmitry Date: Fri, 6 Jun 2025 14:04:53 +0200 Subject: [PATCH 22/30] Update aspnetcore/mvc/models/file-uploads.md Co-authored-by: Luke Latham <1622880+guardrex@users.noreply.github.com> --- aspnetcore/mvc/models/file-uploads.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aspnetcore/mvc/models/file-uploads.md b/aspnetcore/mvc/models/file-uploads.md index ba1746f7fa0f..1cb61af423aa 100644 --- a/aspnetcore/mvc/models/file-uploads.md +++ b/aspnetcore/mvc/models/file-uploads.md @@ -419,7 +419,8 @@ For scenarios where large file uploads are required, streaming uploads allow you The [sample application for 9.x](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/file-uploads/samples/9.x/FileManagerSample) demonstrates how a server can receive a file and stream the data directly to disk, supporting robust cancellation via the HTTP request's cancellation token. -[`MultipartReader`](/dotnet/api/microsoft.aspnetcore.webutilities.multipartreader) is an ASP.NET Core utility for reading files from incoming requests. The following snippet shows how to process a request and stream the file into an `outputStream` (such as a `FileStream`): + is an ASP.NET Core utility for reading files from incoming requests. The following snippet shows how to process a request and stream the file into an `outputStream` (such as a ): + ```csharp // Read the boundary from the Content-Type header var boundary = HeaderUtilities.RemoveQuotes(MediaTypeHeaderValue.Parse(request.ContentType).Boundary).Value; From c9ecf5cade2382732c69185867c4f0f63f09f8c9 Mon Sep 17 00:00:00 2001 From: Korolev Dmitry Date: Fri, 6 Jun 2025 14:05:16 +0200 Subject: [PATCH 23/30] Update aspnetcore/mvc/models/file-uploads.md Co-authored-by: Luke Latham <1622880+guardrex@users.noreply.github.com> --- aspnetcore/mvc/models/file-uploads.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aspnetcore/mvc/models/file-uploads.md b/aspnetcore/mvc/models/file-uploads.md index 1cb61af423aa..6ae5f9cfe01d 100644 --- a/aspnetcore/mvc/models/file-uploads.md +++ b/aspnetcore/mvc/models/file-uploads.md @@ -423,7 +423,8 @@ The [sample application for 9.x](https://github.com/dotnet/AspNetCore.Docs/tree/ ```csharp // Read the boundary from the Content-Type header -var boundary = HeaderUtilities.RemoveQuotes(MediaTypeHeaderValue.Parse(request.ContentType).Boundary).Value; +var boundary = HeaderUtilities.RemoveQuotes( + MediaTypeHeaderValue.Parse(request.ContentType).Boundary).Value; // Use MultipartReader to stream data to a destination var reader = new MultipartReader(boundary, Request.Body); From 026986be6aea7a54d4a180aa029bc87a97105ed1 Mon Sep 17 00:00:00 2001 From: Korolev Dmitry Date: Fri, 6 Jun 2025 14:05:45 +0200 Subject: [PATCH 24/30] Update aspnetcore/mvc/models/file-uploads.md Co-authored-by: Luke Latham <1622880+guardrex@users.noreply.github.com> --- aspnetcore/mvc/models/file-uploads.md | 1 + 1 file changed, 1 insertion(+) diff --git a/aspnetcore/mvc/models/file-uploads.md b/aspnetcore/mvc/models/file-uploads.md index 6ae5f9cfe01d..437c195a9b9f 100644 --- a/aspnetcore/mvc/models/file-uploads.md +++ b/aspnetcore/mvc/models/file-uploads.md @@ -429,6 +429,7 @@ var boundary = HeaderUtilities.RemoveQuotes( // Use MultipartReader to stream data to a destination var reader = new MultipartReader(boundary, Request.Body); MultipartSection? section; + while ((section = await reader.ReadNextSectionAsync(cancellationToken)) != null) { var contentDisposition = section.GetContentDispositionHeader(); From 7ada23548277663f4a6d39a4fb9990718b9a990e Mon Sep 17 00:00:00 2001 From: Korolev Dmitry Date: Fri, 6 Jun 2025 14:06:22 +0200 Subject: [PATCH 25/30] Update aspnetcore/mvc/models/file-uploads.md Co-authored-by: Luke Latham <1622880+guardrex@users.noreply.github.com> --- aspnetcore/mvc/models/file-uploads.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aspnetcore/mvc/models/file-uploads.md b/aspnetcore/mvc/models/file-uploads.md index 437c195a9b9f..4dfb52f3d557 100644 --- a/aspnetcore/mvc/models/file-uploads.md +++ b/aspnetcore/mvc/models/file-uploads.md @@ -440,7 +440,7 @@ while ((section = await reader.ReadNextSectionAsync(cancellationToken)) != null) } ``` -[`IFormFeature`](/dotnet/api/microsoft.aspnetcore.http.features.iformfeature) is a wrapper around `MultipartReader` that lets you avoid writing manual request body parsing code. You can use its [`ReadFormAsync(CancellationToken)`](/dotnet/api/microsoft.aspnetcore.http.features.iformfeature.readformasync#microsoft-aspnetcore-http-features-iformfeature-readformasync(system-threading-cancellationtoken)) method to populate the request's form data, then access uploaded files from the built-in collection: + is a wrapper around that doesn't require you to write manual request body parsing code. You can use its method to populate the request's form data, then access uploaded files from the built-in collection: ```csharp // Get the IFormFeature and read the form var formFeature = Request.HttpContext.Features.GetRequiredFeature(); From 6770449bfbaf895e95b823a884a35dc7b9266933 Mon Sep 17 00:00:00 2001 From: Korolev Dmitry Date: Fri, 6 Jun 2025 14:06:30 +0200 Subject: [PATCH 26/30] Update aspnetcore/mvc/models/file-uploads.md Co-authored-by: Luke Latham <1622880+guardrex@users.noreply.github.com> --- aspnetcore/mvc/models/file-uploads.md | 1 + 1 file changed, 1 insertion(+) diff --git a/aspnetcore/mvc/models/file-uploads.md b/aspnetcore/mvc/models/file-uploads.md index 4dfb52f3d557..32f7b7edccaa 100644 --- a/aspnetcore/mvc/models/file-uploads.md +++ b/aspnetcore/mvc/models/file-uploads.md @@ -448,6 +448,7 @@ await formFeature.ReadFormAsync(cancellationToken); // Access the uploaded file (example: first file) var filePath = Request.Form.Files.First().FileName; + return Results.Ok("Saved file at " + filePath); ``` From 99e68e142fd1396d3a6da3438d890034533b6f2e Mon Sep 17 00:00:00 2001 From: Korolev Dmitry Date: Fri, 6 Jun 2025 14:06:38 +0200 Subject: [PATCH 27/30] Update aspnetcore/mvc/models/file-uploads.md Co-authored-by: Luke Latham <1622880+guardrex@users.noreply.github.com> --- aspnetcore/mvc/models/file-uploads.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aspnetcore/mvc/models/file-uploads.md b/aspnetcore/mvc/models/file-uploads.md index 32f7b7edccaa..d8931f0ceee6 100644 --- a/aspnetcore/mvc/models/file-uploads.md +++ b/aspnetcore/mvc/models/file-uploads.md @@ -452,7 +452,7 @@ var filePath = Request.Form.Files.First().FileName; return Results.Ok("Saved file at " + filePath); ``` -For more advanced scenarios, you can manually parse the raw request body using [`HttpRequest.BodyReader`](/dotnet/api/microsoft.aspnetcore.http.httprequest.bodyreader), which exposes an [`IPipeReader`](/aspnet/core/fundamentals/middleware/request-response) for low-level, high-performance streaming. The sample application includes endpoint handlers that use `IPipeReader` in both minimal APIs and controllers. +For more advanced scenarios, you can manually parse the raw request body using , which exposes an [`IPipeReader`](/aspnet/core/fundamentals/middleware/request-response) for low-level, high-performance streaming. The sample app includes endpoint handlers that use `IPipeReader` in both minimal APIs and controllers. The [3.1 example](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Pages/StreamedSingleFileUploadDb.cshtml) demonstrates how to use JavaScript to stream a file to a controller action. The file's antiforgery token is generated using a custom filter attribute and passed to the client HTTP headers instead of in the request body. Because the action method processes the uploaded data directly, form model binding is disabled by another custom filter. Within the action, the form's contents are read using a `MultipartReader`, which reads each individual `MultipartSection`, processing the file or storing the contents as appropriate. After the multipart sections are read, the action performs its own model binding. From 0028878640347e7346f6170a9749f7bb1dbf9333 Mon Sep 17 00:00:00 2001 From: Korolev Dmitry Date: Fri, 6 Jun 2025 14:06:48 +0200 Subject: [PATCH 28/30] Update aspnetcore/mvc/models/file-uploads.md Co-authored-by: Luke Latham <1622880+guardrex@users.noreply.github.com> --- aspnetcore/mvc/models/file-uploads.md | 1 + 1 file changed, 1 insertion(+) diff --git a/aspnetcore/mvc/models/file-uploads.md b/aspnetcore/mvc/models/file-uploads.md index d8931f0ceee6..7803a5c0416c 100644 --- a/aspnetcore/mvc/models/file-uploads.md +++ b/aspnetcore/mvc/models/file-uploads.md @@ -433,6 +433,7 @@ MultipartSection? section; while ((section = await reader.ReadNextSectionAsync(cancellationToken)) != null) { var contentDisposition = section.GetContentDispositionHeader(); + if (contentDisposition != null && contentDisposition.IsFileDisposition()) { await section.Body.CopyToAsync(outputStream, cancellationToken); From 0f18f394e02335e429ab4167b9727e1cabd32368 Mon Sep 17 00:00:00 2001 From: Korolev Dmitry Date: Fri, 6 Jun 2025 14:07:03 +0200 Subject: [PATCH 29/30] Update aspnetcore/mvc/models/file-uploads.md Co-authored-by: Luke Latham <1622880+guardrex@users.noreply.github.com> --- aspnetcore/mvc/models/file-uploads.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aspnetcore/mvc/models/file-uploads.md b/aspnetcore/mvc/models/file-uploads.md index 7803a5c0416c..26df270528fb 100644 --- a/aspnetcore/mvc/models/file-uploads.md +++ b/aspnetcore/mvc/models/file-uploads.md @@ -450,7 +450,7 @@ await formFeature.ReadFormAsync(cancellationToken); // Access the uploaded file (example: first file) var filePath = Request.Form.Files.First().FileName; -return Results.Ok("Saved file at " + filePath); +return Results.Ok($"Saved file at {filePath}"); ``` For more advanced scenarios, you can manually parse the raw request body using , which exposes an [`IPipeReader`](/aspnet/core/fundamentals/middleware/request-response) for low-level, high-performance streaming. The sample app includes endpoint handlers that use `IPipeReader` in both minimal APIs and controllers. From c9748e2bfcfb7321cab7fd4a584ff7f31dd72e07 Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Fri, 6 Jun 2025 08:12:39 -0400 Subject: [PATCH 30/30] Apply suggestions from code review --- aspnetcore/mvc/models/file-uploads.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aspnetcore/mvc/models/file-uploads.md b/aspnetcore/mvc/models/file-uploads.md index 26df270528fb..f228c95b15de 100644 --- a/aspnetcore/mvc/models/file-uploads.md +++ b/aspnetcore/mvc/models/file-uploads.md @@ -442,6 +442,7 @@ while ((section = await reader.ReadNextSectionAsync(cancellationToken)) != null) ``` is a wrapper around that doesn't require you to write manual request body parsing code. You can use its method to populate the request's form data, then access uploaded files from the built-in collection: + ```csharp // Get the IFormFeature and read the form var formFeature = Request.HttpContext.Features.GetRequiredFeature(); @@ -453,7 +454,7 @@ var filePath = Request.Form.Files.First().FileName; return Results.Ok($"Saved file at {filePath}"); ``` -For more advanced scenarios, you can manually parse the raw request body using , which exposes an [`IPipeReader`](/aspnet/core/fundamentals/middleware/request-response) for low-level, high-performance streaming. The sample app includes endpoint handlers that use `IPipeReader` in both minimal APIs and controllers. +For advanced scenarios, manually parse the raw request body using , which exposes an [`IPipeReader`](/aspnet/core/fundamentals/middleware/request-response) for low-level, high-performance streaming. The sample app includes endpoint handlers that use `IPipeReader` in both minimal APIs and controllers. The [3.1 example](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Pages/StreamedSingleFileUploadDb.cshtml) demonstrates how to use JavaScript to stream a file to a controller action. The file's antiforgery token is generated using a custom filter attribute and passed to the client HTTP headers instead of in the request body. Because the action method processes the uploaded data directly, form model binding is disabled by another custom filter. Within the action, the form's contents are read using a `MultipartReader`, which reads each individual `MultipartSection`, processing the file or storing the contents as appropriate. After the multipart sections are read, the action performs its own model binding.