C# RestSharp Library - Report Progress when loading a large "application/octect-stream"

2.9k Views Asked by At

I am aware that this question has been submitted multiple times, but the last answers are dated and I wonder whether there would be a solution available today? This is not a feature request. I'm rather looking for any workaround that would work.

I'm using RestSharp on a client which talks with my API. The API replies with an "application/octect-stream" which can take up to several minutes to download. Link to the GitHub code here.

public void Download()
{
     if (NewerApp == null) return;

      var client = new RestClient(ServerAddress);
      var request = new RestRequest("/Apps/", Method.POST);
      request.AddParameter("id", CurrentApp.EncryptedId());
      request.AddParameter("action", "download");

      var asyncHandle = client.ExecuteAsync<App>(request, response => {
          HandleResponseToDownloadRequest(response);
      });
}

I would need to report the progress of the reception of "response" to my UI to build a progress bar or something like that. I already know the expected amount of data to be received (through a previous API response), I would just need to know how many bytes were received before the full response is received and parsed.

I don't believe RestSharp currently offers a 'report progress' type of event, right? I can see several approaches:

  1. Use client.DownloadData(request).SaveAs(path);. Maybe the file is created while it is downloaded. I could read the size of the file to report the progress of the download. But my first impression is that the client first downloads the data, then saves the file. In that case, that wouldn't help.
  2. Using a stream to load the response. Evaluate the size of the stream at regular intervals, or everytime the buffer size extends.
  3. Change the type of response from the API to another one (send the data in a JSON, for example?).
  4. Any other option?

What do you think? Anyone has managed to report the progress of the upload?

Can I access the RawBytes of 'response' while ExecuteAsync is still executing? Should I use Execute (without Asyn) instead? Should I use stream and update watch the size of the stream at regular updates?

1

There are 1 best solutions below

0
On BEST ANSWER

I managed to get a progress report, but not using RestSharp. I used System.Net.HttpClient based on kievic's answer here. As kievic highlighted, the key here is that aClient.SendAsync returns once the HTTP headers are received and read, but the content is still loading. Then the content is slowly loaded through a stream in the while loop. RestSharp doesn't seem to enable this, or I couldn't achieve that.

public async Task DownloadAsync(Action<bool> callback)
    {
        if (NewerApp == null) return;

        // Your original code.
        HttpClientHandler aHandler = new HttpClientHandler();
        aHandler.ClientCertificateOptions = ClientCertificateOption.Automatic;
        HttpClient aClient = new HttpClient(aHandler);
        aClient.DefaultRequestHeaders.ExpectContinue = false;
        HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Post, ServerAddress + "/Apps/");
        string content = "id=" + CurrentApp.EncryptedId() + "&action=download";
        message.Content = new StringContent(content);
        HttpResponseMessage response = await aClient.SendAsync(message,
            HttpCompletionOption.ResponseHeadersRead); // Important! ResponseHeadersRead.

        // New code.
        Stream stream = await response.Content.ReadAsStreamAsync();
        MemoryStream memStream = new MemoryStream();

        // Start reading the stream
        var res = stream.CopyToAsync(memStream);

        // While reading the stream
        while (true)
        {
            // Report progress
            this.DownloadedSize = memStream.Length;
            this.Progress = 100.0 * (double)memStream.Length / (double)NewerApp.Filesize;

            // Leave if no new data was read
            if (res.IsCompleted)
            {
                // Report progress one last time
                this.DownloadedSize = memStream.Length;
                this.Progress = 100.0 * (double)memStream.Length / (double)NewerApp.Filesize;

                break;
            }
        }

        // Get the bytes from the memory stream
        byte[] responseContent = new byte[memStream.Length];
        memStream.Position = 0;
        memStream.Read(responseContent, 0, responseContent.Length);

        // Function has ended - return whether the app was donwloaded
        // properly and verified, or not
        callback(HandleResponseToDownloadRequest(responseContent));
    }

Every time this.DownloadedSize and this.Progress are assigned a new value, they fire an event which can be caught by the UI.

        private double progress = 0;
    /// <summary>
    /// Progress of the download of the App. From 0.0 (%) to 100.0 (%)
    /// </summary>
    public double Progress
    {
        get { return progress; }
        set
        {
            // Max / Min
            double val = value;
            if (val > 100.0) val = 100;
            else if (val < 0.0) val = 0.0;

            // Assign value
            if (progress != val)
            {
                progress = val;
                OnProgressReport("Progress");
                OnPropertyChanged("Progress");
            }
        }
    }

    public long downloadedSize = 0;
    /// <summary>
    /// Quantity of bytes downloaded of the app.
    /// Note: there can be more bytes downloaded than advertized because
    /// the quantity of advertize filesize is not encrypted while the
    /// received bytes are encrypted.
    /// TODO: advertize the size of the encrypted file.
    /// </summary>
    public long DownloadedSize
    {
        get
        {
            return downloadedSize;
        }
        set
        {
            if (downloadedSize != value)
            {
                downloadedSize = value;
                OnDownloadedSizeReport("DownloadedSize");
            }
        }
    }

/// <summary>
    /// Fired when the progress of the download of the app file
    /// has updated (more bytes received).
    /// </summary>
    public event PropertyChangedEventHandler ProgressReport;
    protected void OnProgressReport(string name)
    {
        PropertyChangedEventHandler handler = ProgressReport;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(name));
        }
    }

    /// <summary>
    /// Fired when the progress of the download of the app file
    /// has updated (more bytes received).
    /// </summary>
    public event PropertyChangedEventHandler DownloadedSizeReport;
    protected void OnDownloadedSizeReport(string name)
    {
        PropertyChangedEventHandler handler = DownloadedSizeReport;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(name));
        }
    }

I call DownloadAsync like this:

// Start the download - await makes the execution of the method in background
// so that UI can refresh and keep responsive.
// downloaded: bool, true if file properly downloaded
// Updater_AppDownloaded: function called once download has ended (failed or succeeded, either way)
await System.Threading.Tasks.Task.Run(() => 
     Updater.DownloadAsync(downloaded =>     
         Updater_AppDownloaded(downloaded)));