I'm trying to create a Web API using Asp.NET Core that exposes routes to start and cancel long downloads of large files. The server should be able to handle multiple downloads at the same time.
- The download is performed using
WebClient.DownloadFileAsync
in order to have a short response time and returning adownloadId
for later use. The instance of the WebClient is stored as value in a static dictionary whose correspondingkey
is of naturally thedownloadId
. - The download should be canceled using
WebClient.CancelAsync
on the instance of the client retrieved by accessing the value of the dictionary corresponding to thedownloadId
key.
The following code works perfectly when the download reaches its completion without being canceled; the AsyncCompletedEventHandler (OnDownloadFileCompleted
in that case) is properly invoked.
PROBLEM: When invoking WebClient.CancelAsync
, the file keeps on downloading and OnDownloadFileCompleted
is not invoked right away. The WebClient seems to wait until the download finishes before invoking the handler. In both case however, the property AsyncCompletedEventArgs.Canceled
is properly set (e.g to true
if WebClient.CancelAsync
was indeed invoked.
- Am I missing something?
- Is there a better/smarter pattern to handle multiple downloads in a WebAPI?
Any help would much appreciated!
DownloadController.cs
[Route ("api/download")]
public class DownloadController {
private readonly DownloadService service;
public DownloadController (DownloadService service) {
this.service = service;
}
[Route ("start")]
[HttpPost]
public string Start ([FromForm] string fileUrl) => this.service.StartDownload (fileUrl);
[Route ("cancel")]
[HttpPost]
public void Cancel ([FromForm] string downloadId) => this.service.CancelDownload (downloadId);
}
DownloadService.cs
public class DownloadService {
public string DOWNLOAD_FOLDER { get => "C:\\tmp"; }
public static Dictionary<string, WebClient> DownloadClients = new Dictionary<string, WebClient> ();
public string StartDownload (string fileUrl) {
var downloadId = Guid.NewGuid ().ToString ("N");
DownloadClients[downloadId] = new WebClient ();
DownloadClients[downloadId].DownloadFileCompleted += OnDownloadFileCompleted;
DownloadClients[downloadId].DownloadFileAsync (new Uri (fileUrl), Path.Combine (DOWNLOAD_FOLDER, downloadId), downloadId);
return downloadId;
}
public void CancelDownload (string downloadId) {
if (DownloadClients.TryGetValue (downloadId, out WebClient client)) {
client.CancelAsync ();
}
}
private void OnDownloadFileCompleted (object sender, AsyncCompletedEventArgs e) {
var downloadId = e.UserState.ToString ();
if (!e.Cancelled) {
Debug.WriteLine ("Completed");
} else {
Debug.WriteLine ("Cancelled"); //will only be reached when the file finishes downloading
}
if (DownloadClients.ContainsKey (downloadId)) {
DownloadClients[downloadId].Dispose ();
DownloadClients.Remove (downloadId);
}
}
}
I was able to replicate what you saw: CancelAsync
does not actually cancel the download.
Using HttpClient
, you can get the stream and save it to a file using CopyToAsync
, accepts a CancellationToken
. Cancelling the token stops the download immediately.
Here is the DownloadService
class that I modified to use HttpClient
.
public class DownloadService {
public string DOWNLOAD_FOLDER {
get => "C:\\tmp";
}
public static readonly ConcurrentDictionary<string, Download> Downloads = new ConcurrentDictionary<string, Download>();
public async Task<string> StartDownload(string fileUrl) {
var downloadId = Guid.NewGuid().ToString("N");
Downloads[downloadId] = new Download(fileUrl);
await Downloads[downloadId].Start(Path.Combine(DOWNLOAD_FOLDER, downloadId));
return downloadId;
}
public void CancelDownload(string downloadId) {
if (Downloads.TryRemove(downloadId, out var download)) {
download.Cancel();
}
}
This uses a Download
class that looks like this:
public class Download {
private static readonly HttpClient Client = new HttpClient();
private readonly string _fileUrl;
private readonly CancellationTokenSource _tokenSource = new CancellationTokenSource();
private Task _copyTask;
private Stream _responseStream;
private Stream _fileStream;
public Download(string fileUrl) {
_fileUrl = fileUrl;
}
public async Task Start(string saveTo) {
var response = await Client.GetAsync(_fileUrl, HttpCompletionOption.ResponseHeadersRead);
_responseStream = await response.Content.ReadAsStreamAsync();
_fileStream = File.Create(saveTo);
_copyTask = _responseStream.CopyToAsync(_fileStream, 81920, _tokenSource.Token).ContinueWith(task => {
if (task.IsCanceled) return;
_responseStream.Dispose();
_fileStream.Dispose();
});
}
public void Cancel() {
_tokenSource.Cancel();
_responseStream.Dispose();
_fileStream.Dispose();
}
}
You will still have some work to do to remove successfully-completed downloads from your Downloads
list, but I'll leave that with you.
来源:https://stackoverflow.com/questions/48187976/webclient-cancelasync-file-still-downloading