Search code examples
c#asp.net-coreasp.net-core-webapimultipartform-datacancellation

How to retrieve cancellation token in web-api action that consumes multipart/* request (.Net 5)


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.


Solution

  • 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
    // ....
    }