Search code examples
jsonvb.netwinformswebclient

How to read and process multiple JSON API responses asynchronously?


I'm reading JSON responses from the Binance Api, from this link

I need to get some of the data out of it and this is the code I'm using:

Imports System.Net
Imports Newtonsoft.Json
Imports System.Collections.Generic

Public Class Form1
    Private wc As New WebClient()
    Private wc1 As New WebClient()
    Private wc2 As New WebClient()
    Private Async Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick
        Dim btc = Await wc.DownloadStringTaskAsync("https://api.binance.com/api/v1/ticker/24hr?symbol=BTCEUR")
        Dim doge = Await wc1.DownloadStringTaskAsync("https://api.binance.com/api/v1/ticker/24hr?symbol=DOGEEUR")
        Dim bnb = Await wc2.DownloadStringTaskAsync("https://api.binance.com/api/v1/ticker/24hr?symbol=BNBEUR")

        Dim d = JsonConvert.DeserializeObject(Of Dictionary(Of String, String))(btc)
        Dim d1 = JsonConvert.DeserializeObject(Of Dictionary(Of String, String))(doge)
        Dim d2 = JsonConvert.DeserializeObject(Of Dictionary(Of String, String))(bnb)

        Label1.Text = "PRICE " + d("lastPrice")
        Label2.Text = "24H CHANGE " + d("priceChange")
        Label3.Text = "24H CHANGE % " + d("priceChangePercent")
        Label4.Text = "HIGH 24H " + d("highPrice")
        Label5.Text = "LOW 24H " + d("lowPrice")
        Label6.Text = "PRICE " + d1("lastPrice")
        Label7.Text = "24H CHANGE " + d1("priceChange")
        Label8.Text = "24H CHANGE % " + d1("priceChangePercent")
        Label9.Text = "HIGH 24H " + d1("highPrice")
        Label10.Text = "LOW 24H " + d1("lowPrice")
        Label11.Text = "PRICE " + d2("lastPrice")
        Label12.Text = "24H CHANGE " + d2("priceChange")
        Label13.Text = "24H CHANGE % " + d2("priceChangePercent")
        Label14.Text = "HIGH 24H " + d2("highPrice")
        Label15.Text = "LOW 24H " + d2("lowPrice")
    End Sub

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        Timer1.Start()
    End Sub
End Class

This code is working perfectly, the Timer.Intrval is set at 1000ms, but after a while I'm getting an exception:

System.NotSupportedException: WebClient does not support concurrent I/O operations

in the line:

Dim bnb = Await wc2.DownloadStringTaskAsync("https://api.binance.com/api/v1/ticker/24hr?symbol=BNBEUR")

How can I solve it? It doesn't seems wrong cause I'm using 3 different WebClients objects to do that.

Also, how can I just display just 2 decimals after the comma ?


Solution

  • Since you have all async method to call, I suggest to move the API requests to an async method that, when initialized, keeps sending requests to the API - with a delay between calls - until the CancellationToken passed to the method signals that its time to quit.
    I'm passing a Progress<T> delegate to the method, which is responsible to update the UI when the Tasks started by the aysnc method return their results.

    The delegate of course executes in the UI Thread (here; anyway, the Thread that created and initialized it).

    You can run this method from any other method / event handler that can be aysnc. Here, for example, the Click handler of a button. You can also start it from the Form.Load handler. Or whatever else.

    I've decide to deserialize the JSON responses to a class model, since some values need to be converted to different types to make sense. As the Date/Time values returned, which are expressed in Unix (milliseconds) notation. So I'm using a custom UnixDateTimeConverter to convert the Date/Time values to DateTimeOffset structures.

    Imports System.Net
    Imports System.Net.Http
    Imports System.Threading
    Imports System.Threading.Tasks
    Imports Newtonsoft.Json
    Imports Newtonsoft.Json.Converters
    
    Private ctsBinance As CancellationTokenSource = Nothing
    
    Private Async Sub SomeButton_Click(sender As Object, e As EventArgs) Handles SomeButton.Click
        ctsBinance = New CancellationTokenSource()
    
        Dim progressReport = New Progress(Of BinanceResponseRoot())(AddressOf BinanceProgress)
        Try
            ' Pass the Pogress<T> delegate, the delay in ms and the CancellationToken
            Await DownLoadBinanceData(progressReport, 1000, ctsBinance.Token)
        Catch tcEx As TaskCanceledException
            Console.WriteLine("Tasks canceled")
        Finally
            ctsBinance.Dispose()
        End Try
    End Sub
    
    Private Sub BinanceProgress(results As BinanceResponseRoot())
        Console.WriteLine("PRICE " & results(0).LastPrice.ToString("N2"))
        Console.WriteLine("24H CHANGE " & results(0).PriceChange.ToString("N2"))
        Console.WriteLine("24H CHANGE % " & results(0).PriceChangePercent.ToString("N2"))
        Console.WriteLine("HIGH 24H " & results(0).HighPrice.ToString("N2"))
        Console.WriteLine("LOW 24H " & results(0).LowPrice.ToString("N2"))
        Console.WriteLine("PRICE " & results(1).LastPrice.ToString("N2"))
        Console.WriteLine("24H CHANGE " & results(1).PriceChange.ToString("N2"))
        Console.WriteLine("24H CHANGE % " & results(1).PriceChangePercent.ToString("N2"))
        Console.WriteLine("HIGH 24H " & results(1).HighPrice.ToString("N2"))
        Console.WriteLine("LOW 24H " & results(1).LowPrice.ToString("N2"))
        Console.WriteLine("PRICE " & results(1).LastPrice.ToString("N2"))
        Console.WriteLine("24H CHANGE " & results(2).PriceChange.ToString("N2"))
        Console.WriteLine("24H CHANGE % " & results(2).PriceChangePercent.ToString("N2"))
        Console.WriteLine("HIGH 24H " & results(2).HighPrice.ToString("N2"))
        Console.WriteLine("LOW 24H " & results(2).LowPrice.ToString("N2"))
    End Sub
    

    To cancel the execution of the Tasks, call the Cancel() method of the CancellationTokenSource. If the Tasks are not canceled before the Form / Window closes, call it when the Form / Window is closing, handling that event.

     ctsBinance?.Cancel()
     ctsBinance = Nothing
    

    The worker method:

    The method keeps running queries to the API in parallel until a cancellation is requested, calling the Cancel() method of the CancellationTokenSource.

    I'm using a static HttpClient to send the API requests, since this is more likely its kind of job (no custom initialization, it uses all defaults: you may need to initialize a HttpClientHandler in some contexts, as specific Security Protocols).
    All HttpClient.GetAsStringAsync() Tasks are added to a List(Of Task), then all Tasks are executed calling Task.WhenAll().

    When all Tasks return, the API responses are deserialized to the BinanceResponseRoot model and the Progress<T> delegate is called to update the UI with the information received.

    Private Shared binanceClient As New HttpClient()
    
    Public Async Function DownLoadBinanceData(progress As IProgress(Of BinanceResponseRoot()), 
        delay As Integer, token As CancellationToken) As Task
    
        While Not token.IsCancellationRequested
            Dim tasks As New List(Of Task(Of String))({
                binanceClient.GetStringAsync("https://api.binance.com/api/v1/ticker/24hr?symbol=BTCEUR"),
                binanceClient.GetStringAsync("https://api.binance.com/api/v1/ticker/24hr?symbol=DOGEEUR"),
                binanceClient.GetStringAsync("https://api.binance.com/api/v1/ticker/24hr?symbol=BNBEUR")
            })
    
            Await Task.WhenAll(tasks)
    
            Dim btcEur = JsonConvert.DeserializeObject(Of BinanceResponseRoot)(tasks(0).Result)
            Dim dogeEur = JsonConvert.DeserializeObject(Of BinanceResponseRoot)(tasks(1).Result)
            Dim bnbEur = JsonConvert.DeserializeObject(Of BinanceResponseRoot)(tasks(2).Result)
    
            progress.Report({btcEur, dogeEur, bnbEur})
    
            Await Task.Delay(delay, token)
        End While
    End Function
    

    Class Model to convert that JSON data to the corresponding .Net Type values:

    Public Class BinanceResponseRoot
        Public Property Symbol As String
        Public Property PriceChange As Decimal
        Public Property PriceChangePercent As Decimal
        Public Property WeightedAvgPrice As Decimal
        Public Property PrevClosePrice As Decimal
        Public Property LastPrice As Decimal
        Public Property LastQty As Decimal
        Public Property BidPrice As Decimal
        Public Property BidQty As Decimal
        Public Property AskPrice As Decimal
        Public Property AskQty As Decimal
        Public Property OpenPrice As Decimal
        Public Property HighPrice As Decimal
        Public Property LowPrice As Decimal
        Public Property Volume As Decimal
        Public Property QuoteVolume As Decimal
        <JsonConverter(GetType(BinanceDateConverter))>
        Public Property OpenTime As DateTimeOffset
        <JsonConverter(GetType(BinanceDateConverter))>
        Public Property CloseTime As DateTimeOffset
        Public Property FirstId As Long
        Public Property LastId As Long
        Public Property Count As Long
    End Class
    
    Friend Class BinanceDateConverter
        Inherits UnixDateTimeConverter
    
        Public Overrides Function CanConvert(t As Type) As Boolean
            Return t = GetType(Long) OrElse t = GetType(Long?)
        End Function
    
        Public Overrides Function ReadJson(reader As JsonReader, t As Type, existingValue As Object, serializer As JsonSerializer) As Object
            Dim uxDT As Long? = serializer.Deserialize(Of Long?)(reader)
            Return DateTimeOffset.FromUnixTimeMilliseconds(uxDT.Value)
        End Function
        Public Overrides Sub WriteJson(writer As JsonWriter, value As Object, serializer As JsonSerializer)
            Dim dtmo = DirectCast(value, DateTimeOffset)
            If dtmo <> DateTimeOffset.MinValue Then
                serializer.Serialize(writer, CType(DirectCast(value, DateTimeOffset).ToUnixTimeMilliseconds(), ULong))
            Else
                MyBase.WriteJson(writer, Nothing, serializer)
            End If
        End Sub
    End Class