如何在使用 multipart/* 请求的 web-api 操作中检索取消令牌 (.Net 5)

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

我有一个 web.api 动作,它通过流式传输接受 multipart/form-data,所以它没有任何参数:

[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://docs.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
}

但是这样一来我就没有任何可用的取消令牌了。 所以我尝试使用 default .Net 支持的取消:

public async Task<IActionResult> LongRunningAsync(CancellationToken cancellationToken){

在普通控制器中,这工作正常,但是在这种类型的控制器中,由于 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()

那我该如何解决呢?通常当框架检测到请求被取消或放弃时,内置的取消令牌会触发。但在这种情况下我不能使用它,但我真的很想 api 取消请求,这样它就可以逐渐减少并取消所有适用的操作。

由于在 .Net web api 中,控制器派生自 ControllerBase,它们都有一个 HttpContext 实例 属性,您可以使用它的 RequestAborted令牌。 corresponding docs

所以像这样:

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