I have a web.api action that accepts multipart/form-data via streaming, so it doesn't have any arguments:
[HttpPost("api/longrunning")]
[Consumes("multipart/form-data")]
public async Task<IActionResult> LongRunningAsync(){
string? boundary = MultipartRequestHelper.GetBoundary(
MediaTypeHeaderValue.Parse(Request.ContentType),
DefaultFormOptions.MultipartBoundaryLengthLimit);
var reader = new MultipartReader(boundary, HttpContext.Request.Body);
MultipartSection? section = await reader.ReadNextSectionAsync(); // <---- exception thrown here if below argument is specified
// imagine the rest of below link is implemented here
// https://learn.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-5.0#upload-large-files-with-streaming
// tldr: it streams incoming files directly to disk, and some form-data in memory, each part is processed
// as a stream, so I can't use attributes like [FormData], because that would put everything in memory
// If I used [FormData] with a poco, and cancellation token as two arguments, it would probably work, but that doesn't support streaming
}
However this way I don't have any cancellation tokens available. So I try to use the default .Net supported cancellation:
public async Task<IActionResult> LongRunningAsync(CancellationToken cancellationToken){
In a normal controller this works fine, however in this type of controller I get an exception because of the Multipartreader:
Unexpected end of Stream, the content may have already been read by another component.
at Microsoft.AspNetCore.WebUtilities.MultipartReaderStream.<ReadAsync>d__36.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at Microsoft.AspNetCore.WebUtilities.StreamHelperExtensions.<DrainAsync>d__3.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at Microsoft.AspNetCore.WebUtilities.MultipartReader.<ReadNextSectionAsync>d__20.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at MyNamespace.MyController.<LongRunningAsync>d__8.MoveNext() in C:\Source\Project\MyController.cs:line 94
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.TaskOfIActionResultExecutor.<Execute>d__0.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Threading.Tasks.ValueTask`1.get_Result()
at System.Runtime.CompilerServices.ValueTaskAwaiter`1.GetResult()
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<<InvokeActionMethodAsync>g__Logged|12_1>d.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<<InvokeNextActionFilterAsync>g__Awaited|10_0>d.MoveNext()
So how can I solve this? Usually when the framework detects the request is cancelled or abandoned, the built-in cancellation token fires. But in this case I can't use that, but I would really like to have the api cancelled requests, so it can trickle down and cancel all applicable operations.
Since in .Net web api the controllers derive from ControllerBase
, they all have a HttpContext
instance property, you can use its RequestAborted
token.
corresponding docs
So something like this:
[HttpPost("api/longrunning")]
[Consumes("multipart/form-data")]
public async Task<IActionResult> LongRunningAsync(){
var cancellationToken = this.HttpContext.RequestAborted;
string? boundary = MultipartRequestHelper.GetBoundary(
MediaTypeHeaderValue.Parse(Request.ContentType),
DefaultFormOptions.MultipartBoundaryLengthLimit);
var reader = new MultipartReader(boundary, HttpContext.Request.Body);
MultipartSection? section = await reader.ReadNextSectionAsync(cancellationToken); // you can already start passing it to methods that support it
// ....
}