This package allows you to read Form Data values and files. Files streams are not read in binding model process and are provided to be used in user code.
$ dotnet add package Byndyusoft.AspNetCore.Mvc.ModelBinding.FormStreamedDataThis package allows you to read Form Data values and files. Files streams are not read in binding model process and are provided to be used in user code.
Default ASP .NET Core behaviour reads all form contents including files during model binding process. File streams are fully drained to the end and their contents are saved in memory and/or disk before any user custom code.
This behaviour is not convenient when we want to treat files as streams. Possible cases:
This package allows you to use default binding model except for files that will be available to user code later after model binding process.
There are some drawbacks:
It is recommended to use standard behaviour if both conditions are met:
In Benchmarking section you can see that new behaviour is faster in high performance stream processing use cases. It is strongly recommended to measure performance for your use case before using it in production environment.
The implementation is based on Microsoft suggestion with addition:
dotnet add package Byndyusoft.AspNetCore.Mvc.ModelBinding.FormStreamedData
Register model binder for FormStreamedFileCollection type.
services.AddControllers(o => o.AddFormStreamedFileCollectionBinder());
Set SetFormStreamedDataValueProviderAttribute for controller actions where file streams should be read. This will replace default value providers for Form Data with FormStreamedDataValueProvider.
Set FromFormStreamedDataAttribute for action parameters or request object properties that should be bound to form values. To read files' streams use only one property or one parameter of type FormStreamedFileCollection.
Examples:
public class NewRequestDto
{
public string Name { get; set; }
public int Age { get; set; }
public FormStreamedFileCollection StreamedFiles { get; set; }
}
[ApiController]
[Route("[controller]")]
public class FilesController : ControllerBase
{
private readonly FileService _fileService;
public FilesController(FileService fileService)
{
_fileService = fileService;
}
[HttpPost("SaveNew")]
[RequestSizeLimit(long.MaxValue)]
[SetFormStreamedDataValueProvider]
public async Task<ActionResult> SaveNewWay(
[FromFormStreamedData] NewRequestDto requestDto,
CancellationToken cancellationToken)
{
await foreach (var file in requestDto.StreamedFiles.WithCancellation(cancellationToken))
{
await using var stream = file.OpenReadStream();
await _fileService.SaveFileAsync(stream, file.FileName, cancellationToken);
}
return Ok();
}
[HttpPost("SaveNewByParameter")]
[RequestSizeLimit(long.MaxValue)]
[SetFormStreamedDataValueProvider]
public async Task<ActionResult> SaveNewWayByParameter(
FormStreamedFileCollection files,
CancellationToken cancellationToken)
{
await foreach (var file in files.WithCancellation(cancellationToken))
{
await using var stream = file.OpenReadStream();
var filePath = await _fileService.SaveFileAsync(stream, file.FileName, cancellationToken);
}
return Ok();
}
}
BenchmarkNet was used to measure new behaviour performance. 3 use cases were tested:
All three cases were implemented in TestApi project. Performance tests were implemented in (PerformanceTests)[tests/PerformanceTests] project.
There were used three values for TestFileSize parameter:
All these values can be changed in FileGeneratorSetting class.
| Method | TestFileSize | Mean | Error | StdDev | StdErr |
|---|---|---|---|---|---|
| HashOld | Small | 41.65 ms | 0.822 ms | 1.698 ms | 0.236 ms |
| HashNew | Small | 15.39 ms | 0.058 ms | 0.045 ms | 0.013 ms |
| HashOld | Big | 1,259.90 ms | 24.976 ms | 26.724 ms | 6.299 ms |
| HashNew | Big | 485.09 ms | 10.955 ms | 30.899 ms | 3.221 ms |
| HashOld | Large | 10,082.17 ms | 176.127 ms | 164.749 ms | 42.538 ms |
| HashNew | Large | 2,469.44 ms | 44.506 ms | 110.007 ms | 12.964 ms |
| Method | TestFileSize | Mean | Error | StdDev | StdErr |
|---|---|---|---|---|---|
| SaveOld | Small | 24.87 ms | 0.493 ms | 1.102 ms | 0.142 ms |
| SaveNew | Small | 14.91 ms | 0.107 ms | 0.083 ms | 0.024 ms |
| SaveOld | Big | 836.72 ms | 39.259 ms | 113.272 ms | 11.561 ms |
| SaveNew | Big | 501.68 ms | 11.450 ms | 32.669 ms | 3.370 ms |
| SaveOld | Large | 8,952.73 ms | 178.932 ms | 513.388 ms | 52.673 ms |
| SaveNew | Large | 7,913.74 ms | 139.790 ms | 149.574 ms | 35.255 ms |
| Method | TestFileSize | Mean | Error | StdDev | StdErr |
|---|---|---|---|---|---|
| UploadOld | Small | 184.0 ms | 62.66 ms | 41.45 ms | 13.11 ms |
| UploadNew | Small | 446.3 ms | 140.33 ms | 92.82 ms | 29.35 ms |
| UploadOld | Big | 5,068.9 ms | 653.25 ms | 432.08 ms | 136.64 ms |
| UploadNew | Big | 51,659.3 ms | 1,413.90 ms | 935.21 ms | 295.74 ms |
| UploadOld | Large | 26,848.8 ms | 1,585.32 ms | 1,048.59 ms | 331.59 ms |
| UploadNew | Large | 259,196.0 ms | 7,761.91 ms | 5,134.02 ms | 1,623.52 ms |
Api site, performance test console application and Mino S3 server storage were launched on one machine. So these results may not reflect production environment performance. You are welcome to update them if performance is measured in production (or at least preproduction) environment.
To contribute, you will need to setup your local environment, see prerequisites. For the contribution and workflow guide, see package development lifecycle.
Make sure you have installed all of the following prerequisites on your development machine:
srctestsmaster branch