11namespace Particular . LicensingComponent . WebApi
22{
33 using System . IO . Compression ;
4+ using System . Text ;
45 using System . Text . Json ;
56 using System . Threading ;
67 using Contracts ;
8+ using Microsoft . AspNetCore . Http ;
79 using Microsoft . AspNetCore . Mvc ;
10+ using Microsoft . Net . Http . Headers ;
811 using Particular . LicensingComponent . Report ;
912
1013 [ ApiController ]
@@ -40,12 +43,16 @@ public async Task<ReportGenerationState> CanThroughputReportBeGenerated(Cancella
4043
4144 [ Route ( "report/file" ) ]
4245 [ HttpGet ]
43- public async Task < IActionResult > GetThroughputReportFile ( [ FromQuery ( Name = "spVersion" ) ] string ? spVersion , CancellationToken cancellationToken )
46+ public async Task GetThroughputReportFile ( [ FromQuery ( Name = "spVersion" ) ] string ? spVersion , CancellationToken cancellationToken )
4447 {
4548 var reportStatus = await CanThroughputReportBeGenerated ( cancellationToken ) ;
4649 if ( ! reportStatus . ReportCanBeGenerated )
4750 {
48- return BadRequest ( $ "Report cannot be generated - { reportStatus . Reason } ") ;
51+ HttpContext . Response . StatusCode = StatusCodes . Status400BadRequest ;
52+ HttpContext . Response . ContentType = "text/plain; charset=utf-8" ;
53+
54+ await HttpContext . Response . WriteAsync ( $ "Report cannot be generated – { reportStatus . Reason } ", Encoding . UTF8 , cancellationToken ) ;
55+ return ;
4956 }
5057
5158 var report = await throughputCollector . GenerateThroughputReport (
@@ -55,16 +62,19 @@ public async Task<IActionResult> GetThroughputReportFile([FromQuery(Name = "spVe
5562
5663 var fileName = $ "{ report . ReportData . CustomerName } .throughput-report-{ report . ReportData . EndTime : yyyyMMdd-HHmmss} ";
5764
58- using var memoryStream = new MemoryStream ( ) ;
59- using ( var archive = new ZipArchive ( memoryStream , ZipArchiveMode . Create , true ) )
65+ HttpContext . Response . ContentType = "application/zip" ;
66+ HttpContext . Response . Headers [ HeaderNames . ContentDisposition ] = new ContentDispositionHeaderValue ( "attachment" )
6067 {
61- var entry = archive . CreateEntry ( $ "{ fileName } .json") ;
62- await using var entryStream = entry . Open ( ) ;
63- await JsonSerializer . SerializeAsync ( entryStream , report , SerializationOptions . IndentedWithNoEscaping , cancellationToken ) ;
64- }
68+ FileName = $ "{ fileName } .zip"
69+ } . ToString ( ) ;
6570
66- memoryStream . Position = 0 ;
67- return File ( memoryStream , "application/zip" , fileDownloadName : $ "{ fileName } .zip") ;
71+ // The zip archive is written directly to the response body stream and has to remain open until the response is fully sent.
72+ // This is done for performance reasons to avoid buffering the entire report in memory before sending it.
73+ // The BodyWriter is used as a stream to avoid into synchronous IO operations that would be prevented by the ASP.NET Core pipeline.
74+ using var archive = new ZipArchive ( Response . BodyWriter . AsStream ( ) , ZipArchiveMode . Create , leaveOpen : true ) ;
75+ var entry = archive . CreateEntry ( $ "{ fileName } .json") ;
76+ await using var entryStream = entry . Open ( ) ;
77+ await JsonSerializer . SerializeAsync ( entryStream , report , SerializationOptions . IndentedWithNoEscaping , cancellationToken ) ;
6878 }
6979
7080 [ Route ( "settings/info" ) ]
0 commit comments