Если Вы читаете данную статью, значит, скорее всего, Вы в курсе что такое JSON и картинка ниже Вам знакома. Но в любом случае советую посетить эту страничку, если Вы там еще не были, а так же перед прочтением желательно ознакомиться с общими принципами работы с нотацией JSON на языке C#, например по этой ссылке.



Хочу отметить, что данная заметка не претендует на какую-то полноту раскрытия темы. Цель данного текста – структурировать и сохранить те наработки, которые я использовал при работе с библиотекой Newtonsoft.Json.

Постановка задачи


По большому счету, в рамках статьи не так важно каким образом были получены исходные данные, для парсинга, однако данное пояснение наверняка облегчит восприятие материала. Итак, основная задача состоит в том, чтобы реализовать функции API криптобиржи EXMO. Для простоты будем в основном работать с Публичным интерфейсом (Public API). В данном случае запрос информации будет выглядеть как обычный адрес странички сайта. Для отправки запросов используется класс HttpClient пространства имен System.Net.Http:

HttpClient httpClient = new HttpClient();
string request = "https://api.exmo.com/v1/trades/?pair=BTC_USD";
HttpResponseMessage response = 
    (await httpClient.GetAsync(request)).EnsureSuccessStatusCode();
string responseBody = await response.Content.ReadAsStringAsync();

Результат работы данной программы можно увидеть пройдя по ссылке https://api.exmo.com/v1/trades/?pair=BTC_USD. Если загрузить эти данные в JSON C# Class Generator, нам будет предложена следующая структура класса для десериализации:

    public class BTCUSD
    {
        public int trade_id { get; set; }
        public string type { get; set; }
        public string quantity { get; set; }
        public string price { get; set; }
        public string amount { get; set; }
        public int date { get; set; }
    }
    public class RootObject
    {
        public List<BTCUSD> BTC_USD { get; set; }
    }

Надо отметить, что сама функция “trades” криптобиржи позволяет запрашивать информацию сразу по нескольким валютным парам. И в случае запроса

string request = "https://api.exmo.com/v1/trades/?pair=BTC_USD,ETH_USD";

Структура класса будет выглядеть уже следующим образом:

    public class BTCUSD
    {
        public int trade_id { get; set; }
        public string type { get; set; }
        public string quantity { get; set; }
        public string price { get; set; }
        public string amount { get; set; }
        public int date { get; set; }
    }
    public class ETHUSD
    {
        public int trade_id { get; set; }
        public string type { get; set; }
        public string quantity { get; set; }
        public string price { get; set; }
        public string amount { get; set; }
        public int date { get; set; }
    }
    public class RootObject
    {
        public List<BTCUSD> BTC_USD { get; set; }
        public List<ETHUSD> ETH_USD { get; set; }
    }

Очевидно, что для каждой комбинации валютных пар классы не подготовишь, а значит нужно как-то выкручиваться.

Как делать не нужно.
Удивительно, но хороших статей по работе с нотацией JSON в русскоязычном интернете найти практически невозможно. Как следствие, те реализации EXMO API, которые можно найти на GitHub`е так или иначе содержат манипуляции с исходной структурой данных с использованием split`ов, trim`ов и тому подобных непотребств.

Надо признать, что я то же сначала изобрел велосипед и реализовал парсинг самостоятельно. Несмотря на то, что код абсолютно рабочий, это отличный пример как НЕЛЬЗЯ делать.


LinkedList<OrderInform> loi = new LinkedList<OrderInform>();
try
{
  string[] json = answer.Split(new char[] { '{', '}' });
  foreach (var item in json)
  {
    if (item.Length > 20)
    {
      string[] trade = item.Split(new char[] { ',' });
      if (trade.Length > 5)
      {
        string t_id = trade[0].Split(new char[] { ':' })[1].Trim('"');
        string t_type = trade[1].Split(new char[] { ':' })[1].Trim('"');
        string t_quantity = trade[2].Split(new char[] { ':' })[1].Trim('"')
          .Replace(".", ",");
        string t_price = trade[3].Split(new char[] { ':' })[1].Trim('"')
          .Replace(".", ",");
        string t_amount = trade[4].Split(new char[] { ':' })[1].Trim('"')
          .Replace(".", ",");
        string t_date_time = trade[5].Split(new char[] { ':' })[1].Trim('"');
        loi.AddLast(new OrderInform(pair, Convert.ToInt32(t_id), t_type,
          Convert.ToDouble(t_quantity), Convert.ToDouble(t_price), 
          Convert.ToDouble(t_amount),
          (new DateTime(1970, 1, 1, 0, 0, 0, 0))
          .AddSeconds(Convert.ToInt32(t_date_time))));
      }
    }
  }
  return loi;
} 
catch (Exception ex)
{
  return new LinkedList<OrderInform>();
}


Объекты JSON (класс JObject)


Объект JSON — неупорядоченный набор пар ключ/значение. Объект начинается с '{' (открывающей фигурной скобки) и заканчивается '}' (закрывающей фигурной скобкой). Каждое имя сопровождается: (двоеточием), пары ключ/значение разделяются запятой.



Из структуры, предложенной нам ранее JSON C# Class Generator`ом видно, что для каждой валютной пары у нас есть 6 постоянных полей. Обернем их в отдельный класс и назовем его Order.

    class Order
    {
        public int trade_id { get; set; }
        public string type { get; set; }
        public double quantity { get; set; }
        public double price { get; set; }
        public double amount { get; set; }
        public int date { get; set; }
    }

Для того, чтобы суметь «выцепить» набор этих полей из динамически меняющейся структуры данных биржи, необходимо поподробней разобраться с типами данных доступными в библиотеке newtosoft.json. А точнее в пространстве имен newtonsoft.json.linq.

Для работы с библиотекой ее необходимо установить.
Чтобы установить библиотеку в Visual Studio можно просто выполнить поиск в NuGet.
Нажимаем правой кнопкой мыши на Решение (Solution) в Обозревателе решений (Solution explorer) и выбираем пункт «Управлением пакетами NuGet для решения...» («Mange NuGet Packages for solution...»).



Далее жмем «Обзор» («Browse») и в строке поиска вводим newtosoft.json. Ставим галочку напротив нашего решения и нажимаем «Установить» («Install»).



Пространство имен newtosoft.json.linq, после установки библиотеки, становится доступно для подключения к проекту:

using Newtonsoft.Json.Linq;

Для представления данных в newtosoft.json.linq используется абстрактный класс JToken, от которого наследуются классы JValue (для представления простых значений) и JContainer (для представления структур). Структуры в свою очередь могут представлять из себя JArray (массив), JConstructor (конструктор), JObject (объект), либо JProperty (свойство).

В классах JArray и JObject имеется метод Parse, позволяющей преобразовывать JSON данные из обычной строки в соответствующие объекты. Так, если мы воспользуемся методом JObject.Parse(String) для структуры данных функции trades биржи Exmo:

HttpClient httpClient = new HttpClient();
string request = "https://api.exmo.com/v1/trades/?pair=BTC_USD,ETH_USD";
HttpResponseMessage response = 
    (await httpClient.GetAsync(request)).EnsureSuccessStatusCode();
string responseBody = await response.Content.ReadAsStringAsync();

JObject jObject = JObject.Parse(responseBody);

мы увидим структуру из двух элементов, каждый из которых представляет пару ключ-значение. Ключом в данном случае будет название валютной пары (тип String), а значением — список сделок (тип JToken):



Теперь списки сделок нам доступны по ключам «BTC_USD» и «USD_ETH». Например сделки по валютной паре «BTC_USD»:



Далее нам только останется перевести полученные данные в удобный для работы тип, например List<Order>, используя метод ToObject<>():

HttpClient httpClient = new HttpClient();
string request = "https://api.exmo.com/v1/trades/?pair=BTC_USD,ETH_USD";
HttpResponseMessage response = 
    (await httpClient.GetAsync(request)).EnsureSuccessStatusCode();
string responseBody = await response.Content.ReadAsStringAsync();
JObject jObject = JObject.Parse(responseBody);

JToken list = jObject["BTC_USD"];
List<Order> trades = list.ToObject<List<Order>>();

либо всю структуру преобразовать в тип Dictionary<string, List<Order>>:

HttpClient httpClient = new HttpClient();
string request = "https://api.exmo.com/v1/trades/?pair=BTC_USD,ETH_USD";
HttpResponseMessage response = 
    (await httpClient.GetAsync(request)).EnsureSuccessStatusCode();
string responseBody = await response.Content.ReadAsStringAsync();
JObject jObject = JObject.Parse(responseBody);

Dictionary<string, List<Order>> trades = new Dictionary<string, List<Order>>();
foreach (KeyValuePair<string, JToken> obj in jObject)
  trades.Add(obj.Key, obj.Value.ToObject<List<Order>>());

Массивы JSON (класс JArray)


Массив JSON — упорядоченная коллекция значений. Массив начинается с '[' (открывающей квадратной скобки) и заканчивается ']' (закрывающей квадратной скобкой). Значения разделены запятой.

Как выглядит массив JSON можно увидеть пройдя по ссылке https://api.exmo.com/v1/currency. Если попытаться преобразовать его в объект JObject, так как мы делали с предыдущим запросом:

HttpClient httpClient = new HttpClient();
string request = "https://api.exmo.com/v1/currency";
HttpResponseMessage response = 
    (await httpClient.GetAsync(request)).EnsureSuccessStatusCode();
string responseBody = await response.Content.ReadAsStringAsync();

JObject jObject = JObject.Parse(responseBody);

мы получим исключение Newtonsoft.Json.JsonReaderException, сообщающее нам, что преобразуемые данные не являются объектом.



Массивы должны быть преобразованы в специальный тип JArray, который, так же как JObject, наследуется от JContainer.

HttpClient httpClient = new HttpClient();
string request = "https://api.exmo.com/v1/currency";
HttpResponseMessage response = 
    (await httpClient.GetAsync(request)).EnsureSuccessStatusCode();
string responseBody = await response.Content.ReadAsStringAsync();
JObject jObject = JObject.Parse(responseBody);
JArray jArray = JArray.Parse(responseBody);

В случае, когда при парсинге, исходная структура заранее неизвестна, либо требуется возможность обработки обоих типов данных — в качестве типа преобразуемого объекта можно указать JContainer, или же JToken, тогда во время парсинга данные будут неявно преобразованы к нужному типу.

HttpClient httpClient = new HttpClient();

string request = "https://api.exmo.com/v1/currency";
HttpResponseMessage response = 
    (await httpClient.GetAsync(request)).EnsureSuccessStatusCode();
string responseBody = await response.Content.ReadAsStringAsync();
JToken jArray = JToken.Parse(responseBody );
//  Неявное преобразование в JArray
List<string> list = jArray.ToObject<List<string>>();

request = "https://api.exmo.com/v1/trades/?pair=BTC_USD,ETH_USD";
HttpResponseMessage response = 
    (await httpClient.GetAsync(request)).EnsureSuccessStatusCode();
string responseBody = await response.Content.ReadAsStringAsync();
JToken jObject = JToken.Parse(responseBody );
//  Неявное преобразование в JObject
Dictionary<string, List<Order>> dict = 
    jObject.ToObject<Dictionary<string, List<Order>>>();

Значения JSON (класс JValue)


Согласно описанию формата JSON, значение может быть строкой в двойных кавычках, числом, true, false, null, объектом или массивом. Эти структуры могут быть вложенными. В реализации newotnsoft.json значением могут быть только объекты простых типов. Попытка преобразовать объект JArray в объект JValue приведет к исключению «System.InvalidCastException».

В качестве ознакомительного материала по работе с конкретными значениями (класс JValue) в библиотеке newtonsoft.json — хорошо подойдут примеры из документации раз и два.

Пример использования для данных с биржи:

            
HttpClient httpClient = new HttpClient();

string request = "https://api.exmo.com/v1/currency";
HttpResponseMessage response = 
    (await httpClient.GetAsync(request)).EnsureSuccessStatusCode();
string responseBody = await response.Content.ReadAsStringAsync();
JArray jArray = JArray.Parse(responseBody );
foreach (JValue value in jArray)
{
    string s = (string)value;
}

Во второй части статьи я более детально опишу возможности подготовки класса для парсинга данных, а также некоторые особенности работы с типом данных DateTime.

Полный текст программы.
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Net.Http;
using Newtonsoft.Json.Linq;

namespace JSONObjects
{
    class Order
    {
        public int trade_id { get; set; }
        public string type { get; set; }
        public double quantity { get; set; }
        public double price { get; set; }
        public double amount { get; set; }
        public int date { get; set; }
    }
    class Program
    {
        public static async Task Main(string[] args)
        {
            HttpClient httpClient = new HttpClient();
            string request = "https://api.exmo.com/v1/currency";
            HttpResponseMessage response = 
                (await httpClient.GetAsync(request)).EnsureSuccessStatusCode();
            string responseBody = await response.Content.ReadAsStringAsync();

            JToken jArray = JToken.Parse(responseBody);
            //  Неявное преобразование в JArray
            List<string> list = jArray.ToObject<List<string>>();

            request = "https://api.exmo.com/v1/trades/?pair=BTC_USD,ETH_USD";
            response = (await httpClient.GetAsync(request)).EnsureSuccessStatusCode();
            responseBody = await response.Content.ReadAsStringAsync();
            JToken jObject = JToken.Parse(responseBody);
            //  Неявное преобразование в JObject
            Dictionary<string, List<Order>> dict =
                jObject.ToObject<Dictionary<string, List<Order>>>();
        }
    }
}


Наиболее полную и подробную информацию о библиотеке всегда можно почерпнуть из официальной документации.

Комментарии (40)


  1. lair
    22.12.2019 16:16
    +2

    А зачем вам промежуточный JObject, если можно прекрасно десериализовывать сразу в нужный тип? Не говоря уже о том, что не надо считывать ответ в строку, можно делать десериализацию из потока, да и WebClient тоже использовать не нужно?


    Проще говоря, весь нужный код сводится к


    await (await httpClient.GetAsync(uri)).EnsureSuccessStatusCode().Content.ReadAsAsync<T>();


    1. VokiLook Автор
      24.12.2019 09:23

      JObject, также как и промежуточные строки мне нужны для понимания происходящего. Как уже справедливо было замечено в комментариях — я пока далек от .Net, да и от программирования в целом. Но у меня есть цель с ними сблизиться или даже породниться. Мне сейчас очень не хватает подобных статей, где, возможно, с точки зрения языка все не очень красиво, но зато абсолютно понятно. Мне потребовалось несколько часов чтобы осознать и воспроизвести Вашу строчку кода. И я, с Вашего позволения, использую ее во второй части статьи, но не здесь.
      Я заменил webClient на httpClient, в остальном же я, пожалуй, оставлю все как есть.


      1. mayorovp
        24.12.2019 09:36

        Если вам требуется JObject "для понимания происходящего" — значит у вас неправильное понимание происходящего...


        1. VokiLook Автор
          24.12.2019 10:24

          Это не так.


          1. lair
            24.12.2019 12:12
            +1

            Скажите, вы думаете, что при десериализации объекта из потока (через JsonSerializer.Deserialize) где-то фигурирует JObject?


            1. VokiLook Автор
              24.12.2019 12:46

              Нет, я не думаю, что при десериализации через JsonSerializer.Deserialize происходит формирование промежуточных объектов. Насколько я понимаю, они нужны для сериализации и мое их использование не совсем по назначению.


              1. mayorovp
                24.12.2019 12:58

                И для сериализации они тоже вовсе не обязательны.


                1. VokiLook Автор
                  24.12.2019 13:32

                  А для чего они обязательны?


                  1. mayorovp
                    24.12.2019 13:36

                    Они нужны для работы с нетипизированными данными.


                  1. lair
                    24.12.2019 14:20

                    Ни для чего. С ними удобнее разбирать не- и слабо-типизованные данные, но даже это можно делать без них.


              1. lair
                24.12.2019 14:19

                Насколько я понимаю, они нужны для сериализации

                Нет, не нужны.


                мое их использование не совсем по назначению.

                Именно. Что возвращает нас к идее о том, что понимать происходящее через JObject, который нигде для этого "происходящего" не используется — странно.


                1. VokiLook Автор
                  24.12.2019 15:01

                  Возможно. Если Вы сможете предложить что-нибудь более подходящее для объяснения что такое объект, массив и значение JSON — буду признателен.


                  1. lair
                    24.12.2019 15:04
                    +1

                    Спецификация JSON, очевидно.


                    Но вообще, эти понятия имеют вполне прямые соответствия в .net — словарь, массив и примитивное значение.


                    1. VokiLook Автор
                      24.12.2019 15:14

                      Для меня было совсем не очевидно как сопоставить набор символов, который представляет из себя объект JSON и соответствующие им конструкции .Net.


                      1. lair
                        24.12.2019 15:17

                        Так и сопоставить: вот такая запись эквивалентна вот такой конструкции, вот такая — вот такой. Промежуточные конструкции Newtonsoft.Json.Linq здесь лишние.


                        1. VokiLook Автор
                          24.12.2019 15:55

                          Что такое 2 + 2? Это когда берем 2 яблока и добавляем еще 2 яблока. Да, яблоки тут лишние, но так понятней.


                          1. lair
                            24.12.2019 15:56

                            Плохая аналогия подобна котенку с дверцей.


      1. lair
        24.12.2019 12:11

        JObject, также как и промежуточные строки мне нужны для понимания происходящего.

        Так вы определитесь, ваша статья — она про работу с библиотекой или про то, как вы понимаете происходящее? Потому что работать с этой библиотекой так, как вы показываете, не надо.


        1. VokiLook Автор
          24.12.2019 13:31
          -2

          Зачем вообще писать:

          Никогда не угадаешь, кто прочитает ваш пост. Часто это те, кто разбирается в вопросе на голову лучше вас. Фидбэк от таких людей — возможно, самое ценное, что вы приобретете на Хабре.


          1. lair
            24.12.2019 14:20

            Мой вопрос не в том, что вы приобретаете от этого поста. Мой вопрос в том, что аудитория хабра от него приобретает.


            1. VokiLook Автор
              24.12.2019 14:55

              Я уже написал, что мне не хватало именно такой статьи. И я верю, что я такой не один. Возможно, делать так, как описано в статье и не нужно но это работает, а значит может быть где-то полезно.


              1. lair
                24.12.2019 15:01

                но это работает, а значит может быть где-то полезно.

                Это глубоко ошибочное мнение. Не все, что "работает", может быть полезно — до тех пор, пока вы не понимаете весьма конкретно, что именно "работает" и как.


  1. alex-khv
    22.12.2019 18:37

    Да, рекомендуют использовать потоки. Чтобы избежать фрагментации памяти.
    www.newtonsoft.com/json/help/html/Performance.htm

    Еще удобно генерировать DTO классы из json с помощью сервиса app.quicktype.io


  1. Severus1992
    22.12.2019 22:15
    +1

    Не мой взгляд, главной рекомендацией является отказ от WebClient и Newtonsoft.Json в сторону HttpClient и System.Text.Json/Utf8JsonSerializer


  1. SektorLP
    22.12.2019 22:15
    -3

    Вместо DTO можно использовать dynamic:


    string json = "{\"name\": \"Johny\"}";
    dynamic obj = Newtonsoft.Json.JsonConvert.DeserializeObject<dynamic>(json);
    Console.WriteLine($"Name = {obj.name}");


    1. lair
      23.12.2019 00:17

      А зачем?


  1. justmara
    22.12.2019 22:53

    Сходил по ссылкам и хотел, было, написать, что ещё 4 года назад API этого сайтика был кем-то реализован на C# и притом уже с HttpClient и async'ами, но потом посмотрел в тот код и… Пожалуй, лучше похвалить автора этой статьи за попытку. Довольно наивную (выглядит, будто сильно далёкий от .net человек попытался что-то написать по статьям десятилетней давности), но зато честно самостоятельную.
    По сути:


    1. async/await нужен. уже даже public static async Task Main(string[] args) можно
    2. WebClient забыть в пользу HttpClient (по большей части из-за п1, впрочем).
    3. Промежуточные JObject для парсинга в объект не нужны. Его назначение как раз наоборот — не собирая конечную модель работать просто с json-деревом. Дада, тот самый JsonConvert.DeserializeObject<>(), на который уже выше указали
    4. JSON — не протокол. Это разметка, нотация, формат если хочешь.
    5. Ну и да, System.Text.Json ещё далековат от полноценной продакш-реди замены newtonsoft'у, но всё ж для таких простых вещей можно и на него посмотреть (кстати, есть ещё и JsonReader — тоже классная штука).


    1. VokiLook Автор
      23.12.2019 14:45

      Спасибо, добрый человек, за науку. WebClient обязательно переделаю на HttpClient и добавлю async/await.
      Что же касается промежуточных string, JObject и т.д., то они были нужны мне для понимания внутренних процессов. Все же статья не о том, как написать максимально компактный код.
      JsonConvert.DeserializeObject<>() будет во второй части.


  1. Doomer3D
    23.12.2019 12:13

    В дополнение к прочим комментариям добавлю, что ресурсы нужно освобождать.
    В вашем случае, это объект WebClient, т.е. нужно было обернуть его создание в конструкцию using или (для последних версий C#) написать

    using var wb = new WebClient();


    В общем, статья больше похожа на вредный совет, как писать не нужно.


    1. force
      23.12.2019 19:39

      А HttpClient не нужно. Лучше его статиком сделать. У Microsoft просто очень логичное API :)


      1. mayorovp
        24.12.2019 09:36

        Вообще-то нужно, если только вы не собираетесь ограничивать количество одновременных запросов до одного.


        1. force
          24.12.2019 10:33

          Тогда стоит использовать фабрику, но ни в коем случае не обычный диспозабельный HttpClient, иначе у вас будут утекать ресурсы, с весьма неприятными последствиями. Если что, в Microsoft после нескольких пинков поправили документацию, так что там теперь это указано.


          1. mayorovp
            24.12.2019 10:37

            А можно подробнее про утечку ресурсов? Откуда они вообще возьмутся, чтобы утекать?


            1. force
              24.12.2019 11:04

              В документации как бы расписано теперь

              HttpClient is intended to be instantiated once and re-used throughout the life of an application. Instantiating an HttpClient class for every request will exhaust the number of sockets available under heavy loads.


              Ну и про один запрос оттуда же
              The HttpClient class instance acts as a session to send HTTP requests. An HttpClient instance is a collection of settings applied to all requests executed by that instance. In addition, every HttpClient instance uses its own connection pool, isolating its requests from requests executed by other HttpClient instances.


              В реальности, мне писали, что получали SocketException, у нас было веселее — приложение чем дольше работало, тем больше процессора использовало. Т.е. после пары недель работы 100%. А при подключении к нему отладчика — всё сбрасывалось и опять типа норма. Приятного анализа, блин.

              Да, и работает это именно под Core 2.2 и выше. В более старых версиях — используется другая реализация, а в большом .NET — тот же HttpWebRequest, который не страдает этой проблемой.


            1. DieSlogan
              24.12.2019 12:20

              Еще подробная статья была на хабре: habr.com/ru/post/424873


              1. mayorovp
                24.12.2019 12:33

                Да, я её читал, но именно утечки ресурсов я там не увидел, только длинные TIME_WAIT.


                1. force
                  24.12.2019 13:13
                  -1

                  Там автор больше напирает на .NET Framework и Windows, там утечки нет, потому что под капотом у HttpClient находится банальный HttpWebRequest, со своими тараканами, но без утечек. Утечки именно появились в .NET Core 2.2, где сделали свою реализацию работы с HTTP (быстрее, выше, сильнее). Поэтому многие очень удивились проблемам, когда просто обновили фреймворк, не меняя ни строчки в приложении и получили подобное поведение.


          1. lair
            24.12.2019 12:19

            (del, перепутал)


  1. DieSlogan
    24.12.2019 13:16

    Очень прошу вас использовать в названиях соглашения по именам для C#. Для публичных свойств класса это PascalCase.
    Для биндинга свойств удобно использовать [JsonProperty(PropertyName = «jsonBindingName»)]


    1. VokiLook Автор
      24.12.2019 13:33
      -1

      Про это будет рассказано во второй части.