I am running a Task, which copies from one stream to another. This works without problems, including progress reporting. But i cant cancel the task. If i fire the CancellationToken, the copy progress runs till its completion, then the task is cancelled, but this is of course to late. Here is my actual code
private async Task Download(Uri uriToWork, CancellationToken cts)
{
HttpClient httpClient = new HttpClient();
HttpRequestMessage requestAction = new HttpRequestMessage();
requestAction.Method = new HttpMethod("GET");
requestAction.RequestUri = uriToWork;
HttpResponseMessage httpResponseContent = await httpClient.SendRequestAsync(requestAction, HttpCompletionOption.ResponseHeadersRead);
using (Stream streamToRead = (await httpResponseContent.Content.ReadAsInputStreamAsync()).AsStreamForRead())
{
string fileToWrite = Path.GetTempFileName();
using (Stream streamToWrite = File.Open(fileToWrite, FileMode.Create))
{
await httpResponseContent.Content.WriteToStreamAsync(streamToWrite.AsOutputStream()).AsTask(cts, progressDownload);
await streamToWrite.FlushAsync();
//streamToWrite.Dispose();
}
await streamToRead.FlushAsync();
//streamToRead.Dispose();
}
httpClient.Dispose();
}
Can someone help me please, or can explain, why it does not work?
Is it this operation that continues until it completes ?
await httpResponseContent.Content.WriteToStreamAsync(streamToWrite.AsOutputStream()).AsTask(cts, progressDownload);
Or is it this one ?
await streamToWrite.FlushAsync();
I think the latter needs probably to have the CancellationToken as well:
await streamToWrite.FlushAsync(cts);
Unfortunately I cannot answer why this cancel does not occur. However, a solution that consists in writing the Stream in chunks may help.
Here is something very dirty that works:
private async Task Download(Uri uriToWork, CancellationToken cts) {
using(HttpClient httpClient = new HttpClient()) {
HttpRequestMessage requestAction = new HttpRequestMessage();
requestAction.Method = new HttpMethod("GET");
requestAction.RequestUri = uriToWork;
HttpResponseMessage httpResponseContent = await httpClient.SendRequestAsync(requestAction, HttpCompletionOption.ResponseHeadersRead);
string fileToWrite = Path.GetTempFileName();
using(Stream streamToWrite = File.Open(fileToWrite, FileMode.Create)) {
// Disposes streamToWrite to force any write operation to fail
cts.Register(() => streamToWrite.Dispose());
try {
await httpResponseContent.Content.WriteToStreamAsync(streamToWrite.AsOutputStream()).AsTask(cts, p);
}
catch(TaskCanceledException) {
return; // "gracefully" exit when the token is cancelled
}
await streamToWrite.FlushAsync();
}
}
}
I enclosed the httpClient in a using so a return disposes it properly.
I removed the streamToRead which was not used at all
Now here is the horror: I added a delegate that executes when the token is cancelled: it disposes streamToWrite while it is written to (ughhhh), which triggers an TaskCancelledException when WriteToStreamAsync cannot longer write in this disposed stream.
Please dont throw a puke bag at me yet, I am not experienced enough in this "Universal" Framework which looks very different as the usual one.
Here is a chunked stream solution that looks more acceptable. I shortened a bit the original code and added the IProgress as a parameter.
async Task Download(Uri uriToWork, CancellationToken cts, IProgress<int> progress) {
using(HttpClient httpClient = new HttpClient()) {
var chunkSize = 1024;
var buffer = new byte[chunkSize];
int count = 0;
string fileToWrite = Path.GetTempFileName();
using(var inputStream = await httpClient.GetInputStreamAsync(uriToWork)) {
using(var streamToRead = inputStream.AsStreamForRead()) {
using(Stream streamToWrite = File.OpenWrite(fileToWrite)) {
int size;
while((size = await streamToRead.ReadAsync(buffer, 0, chunkSize, cts).ConfigureAwait(false)) > 0) {
count += size;
await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => progress.Report(count));
// progress.Report(count);
await streamToWrite.WriteAsync(buffer, 0, size, cts).ConfigureAwait(false);
}
}
}
}
}
}
The blocking operation is most probably not WriteToStreamAsync() but FlushAsync(), so #Larry's assumption should be right, the FlushAsync method needs the cancellation token as well.
Related
Here is some of my code:
var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(30));
response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, tokenSource.Token);
response.EnsureSuccessStatusCode();
using (var readStream = await response.Content.ReadAsStreamAsync())
{
var buffer = new byte[4096];
var length = 0;
while ((length = await readStream.ReadAsync(buffer, 0, buffer.Length, tokenSource.Token)) != 0)
{
//...
if (waitTime)
await Task.Delay(waitTime, tokenSource.Token);
}
}
Can I use the CancellationToken like that? Or is that the correct way to write it?
Yes, it's normal to create a token source once and then have a tree of methods take the token and use it for cancellation.
I am fetching data from an online server and this process takes about 2 minutes, I would like to show progress for this process.
Here is my code:
int ProgressBarValue{get; set;} ;
var context = await client.GetAsync(url);
if (context != null)
{
var content = await context.Content.ReadAsStringAsync();
var res = JsonConvert.DeserializeObject<Candles>(content);
}
On the XAML:
<ProgressBar Grid.Column="2" Grid.ColumnSpan="1" Height="21"
Width="512" Margin="0,0,11,0" Value="{Binding ProgressBarValue}"></ProgressBar>
The API of HttpClient doesn't accept a callback to report progress or to get the length of the HTTP response in advance. To know the response's length in advance is mandatory in order to be able to calculate the progress percentage.
For this reason I recommend to configure the ProgressBar to show continuous progress feedback:
<!-- Show continuous progress -->
<ProgressBar IsIndeterminate="True" />
If you want to show progress e.g., a bytes read feedback label, you have to manually read the content of the response body.
The following example reports the progress in bytes read and supports optional cancellation:
// Property should be a DependencyProperty (when on a control like Window)
// or raise the INotifyPropertyChanged.PropertyChanged event
public double ProgressValue { get; set; }
public async Task GetJsonResponse(string url)
{
string jsonResponseText = await DownloadFromUrlWithProgressAsync(url, CancellationToken.None);
if (string.IsNullOrWhiteSpace(jsonResponseText))
{
return;
}
var candles = JsonConvert.DeserializeObject<Candles>(jsonResponseText);
}
private async Task<string> DownloadFromUrlWithProgressAsync(
string url,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
this.ProgressValue = 0;
using (var client = new HttpClient())
{
// Only read headers. Content is read later (manually)
using (HttpResponseMessage response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken))
{
// Report progress safely from the background thread to the UI thread
// by using Progress<T>
IProgress<double> progressReporter =
new Progress<double>(progress => this.ProgressValue = progress);
// Read from content stream with optional cancellation support
var responseContent = await Task.Run(
() => ReadResponseContentAsync(response, progressReporter, cancellationToken),
cancellationToken);
return responseContent;
}
}
}
private async Task<string> ReadResponseContentAsync(
HttpResponseMessage response,
IProgress<double> progressReporter,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var responseBuilder = new StringBuilder();
using (Stream responseContentStream = await response.Content.ReadAsStreamAsync())
{
cancellationToken.ThrowIfCancellationRequested();
using (var streamReader = new StreamReader(responseContentStream))
{
while (!streamReader.EndOfStream)
{
cancellationToken.ThrowIfCancellationRequested();
// Choose a buffer size
var buffer = new char[4096];
await streamReader.ReadAsync(buffer, 0, buffer.Length);
responseBuilder.Append(buffer);
// Report progress in bytes read (automatically posts to the UI thread)
double progress = responseBuilder.Length;
progressReporter.Report(progress);
}
return responseBuilder.ToString();
}
}
}
How to make such piece of code run asynchronously with synchronous System.Xml.Serialization.XmlSerializer.Deserialize method which forces me to use Result all the way long?
...
await GetContent(url)
...
private async Task<Node> GetContent (string url)
{
var response = _httpClient.GetAsync(url).Result;
var ser = new XmlSerializer(typeof(Node));
retVal = (Node)ser.Deserialize(response.Content.ReadAsStreamAsync().Result);
}
The method is already async so there's no reason to use .Result. Just use await and remember to close the stream, otherwise the server connection remains open:
private async Task<Node> GetContent (string url)
{
var response = await _httpClient.GetAsync(url);
//**IMPORTANT** Ensure the stream is closed
using(var stream= await response.Content.ReadAsStreamAsync())
{
var ser = new XmlSerializer(typeof(Node));
var retVal = (Node)ser.Deserialize(stream);
return retVal;
}
}
I am having a simple middleware which fetches the body of the request and store it in a string. It is reading fine the stream, but the issue is it wont call my controller which called just after I read the stream and throw the error
A non-empty request body is required
. Below is my code.
public async Task Invoke(HttpContext httpContext)
{
var timer = Stopwatch.StartNew();
ReadBodyFromHttpContext(httpContext);
await _next(httpContext);
timer.Stop();
}
private string ReadBodyFromHttpContext(HttpContext httpContext)
{
return await new StreamReader(httpContext.Request.Body).ReadToEndAsync();
}
You need to convert HttpContext.Request.Body from a forward only memory stream to a seekable stream, shown below.
// Enable seeking
context.Request.EnableBuffering();
// Read the stream as text
var bodyAsText = await new System.IO.StreamReader(context.Request.Body).ReadToEndAsync();
// Set the position of the stream to 0 to enable rereading
context.Request.Body.Position = 0;
when it comes to capturing the body of an HTTP request and/or response, this is no trivial effort. In ASP .NET Core, the body is a stream – once you consume it (for logging, in this case), it’s gone, rendering the rest of the pipeline useless.
Ref:http://www.palador.com/2017/05/24/logging-the-body-of-http-request-and-response-in-asp-net-core/
public async Task Invoke(HttpContext httpContext)
{
var timer = Stopwatch.StartNew();
string bodyAsText = await new StreamReader(httpContext.Request.Body).ReadToEndAsync();
var injectedRequestStream = new MemoryStream();
var bytesToWrite = Encoding.UTF8.GetBytes(bodyAsText);
injectedRequestStream.Write(bytesToWrite, 0, bytesToWrite.Length);
injectedRequestStream.Seek(0, SeekOrigin.Begin);
httpContext.Request.Body = injectedRequestStream;
await _next(httpContext);
timer.Stop();
}
Few things are crucial here:
enable buffering
last flag leaveOpen in StreamReader
reset request body stream position (SeekOrigin.Begin)
public void UseMyMiddleware(IApplicationBuilder app)
{
app.Use(async (context, next) =>
{
context.Request.EnableBuffering();
using (var reader = new StreamReader(context.Request.Body, Encoding.UTF8, false, 1024, true))
{
var body = await reader.ReadToEndAsync();
context.Request.Body.Seek(0, SeekOrigin.Begin);
}
await next.Invoke();
});
}
using (var mem = new MemoryStream())
using (var reader = new StreamReader(mem))
{
Request.Body.CopyTo(mem);
var body = reader.ReadToEnd();
//and this you can reset the position of the stream.
mem.Seek(0, SeekOrigin.Begin);
body = reader.ReadToEnd();
}
Here you are can read how it works. https://gunnarpeipman.com/aspnet-core-request-body/
You can try this
public async Task Invoke(HttpContext context)
{
var request = context.Request;
request.EnableBuffering();
var buffer = new byte[Convert.ToInt32(request.ContentLength)];
await request.Body.ReadAsync(buffer, 0, buffer.Length);
var requestContent = Encoding.UTF8.GetString(buffer);
request.Body.Position = 0; //rewinding the stream to 0
}
I have the following method, on a windows-store project, to upload a file
public async Task<Boolean> UploadFileStreamService(Stream binaries, String fileName, String filePath)
{
try
{
filePath = Uri.EscapeDataString(filePath);
using (var httpClient = new HttpClient { BaseAddress = Constants.baseAddress })
{
var content = new StreamContent(binaries);
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", App.Current.Resources["token"] as string);
App.Current.Resources["TaskUpload"] = true;
using (var response = await httpClient.PostAsync("file?fileName=" + filePath, content))
{
string responseData = await response.Content.ReadAsStringAsync();
if (responseData.Contains("errorCode"))
throw new Exception("Exception: " + responseData);
else
{
JsonObject jObj = new JsonObject();
JsonObject.TryParse(responseData, out jObj);
if (jObj.ContainsKey("fileId"))
{
if (jObj["fileId"].ValueType != JsonValueType.Null)
{
App.Current.Resources["NewVersionDoc"] = jObj["fileId"].GetString();
}
}
}
return true;
}
}
}
catch (Exception e)
{
...
}
}
And on the app.xaml.cs i have on the constructor:
NetworkInformation.NetworkStatusChanged +=
NetworkInformation_NetworkStatusChanged; // Listen to connectivity changes
And on that method i check for the connection changes.
What i would like to know is how to stop a upload task when i detect that network change ( from having internet to not having).
You can use cancellation tokens. You need CancellationTokenSource:
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
Then pass token to your UploadFileStreamService method (use _cts.Token to get token):
public async Task<Boolean> UploadFileStreamService(Stream binaries, String fileName, String filePath, CancellationToken ct)
And use another overload of PostAsync which accepts token (note - also use overloads that accept tokens for all other async methods where possible, for example for ReadAsStringAsync):
using (var response = await httpClient.PostAsync("file?fileName=" + filePath, content, ct))
Then when you found network connection is lost, cancel with:
_cts.Cancel();
Note that this will throw OperationCancelledException on PostAsync call, which you may (or may not) want to handle somehow.