Содержание
В шаблонном приложении Blazor WebAssembly мы можем найти по сути «псевдо» сервис для работы с метеоданными. В этой части работы над проектом «Open Ecology» мы создадим свой компонент для загрузки данных из JSON.
Компонент fetchdata
Для начала, изучим как работает уже готовый сервис. Посмотрим, как выглядит компонент fetchdata
из шаблона Blazor WebAssembly. Вот его код:
@page "/fetchdata" @inject HttpClient Http <PageTitle>Weather forecast</PageTitle> <h1>Weather forecast</h1> <p>This component demonstrates fetching data from the server.</p> @if (forecasts == null) { <p><em>Loading...</em></p> } else { <table class="table"> <thead> <tr> <th>Date</th> <th>Temp. (C)</th> <th>Temp. (F)</th> <th>Summary</th> </tr> </thead> <tbody> @foreach (var forecast in forecasts) { <tr> <td>@forecast.Date.ToShortDateString()</td> <td>@forecast.TemperatureC</td> <td>@forecast.TemperatureF</td> <td>@forecast.Summary</td> </tr> } </tbody> </table> } @code { private WeatherForecast[]? forecasts; protected override async Task OnInitializedAsync() { forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json"); } public class WeatherForecast { public DateTime Date { get; set; } public int TemperatureC { get; set; } public string? Summary { get; set; } public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); } }
Здесь мы видим, что у компонента имеется перегруженный метод OnInitializedAsync
внутри которого происходит асинхронная операция загрузки данных из JSON-файла, который располагается по пути «sample-data/weather.json
«. Физически, этот файл лежит по следующему пути на жестком диске:
Полученный массив объектов класса WeatherForecast
в дальнейшем выводится в виде таблицы.
Теперь посмотрим на файл Program.cs:
using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using OpenEcology; var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add<App>("#app"); builder.RootComponents.Add<HeadOutlet>("head::after"); builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); await builder.Build().RunAsync();
Здесь мы видим, что в качестве сервиса был зарегистрирован HttpClient
. AddScoped
означает, что для каждого запроса создается свой объект сервиса. Это означает, что если в течение одного запроса есть несколько обращений к одному сервису, то при всех этих обращениях будет использоваться один и тот же объект сервиса.
Теперь, используя полученную информацию, попробуем реализовать свою загрузку данных из JSON в приложение.
Файл JSON с экологическими данными
Для реализации нашего сервиса воспользуемся json-файлом, скачать который можно по ссылке: https://uonvos.rpn.gov.ru/storage/onv_pub.zip
.
Создадим в нашем проекте папку data
в папке wwwroot
:
Распакуем архив с json в созданную папку:
Откроем этот файл и посмотрим, что там содержится. Так как файл очень большой, то приведу только одну запись из этого файла:
{ "name": "База филиала УТТиСТ, Надымский район, п. Ямбург", "number": "71-0289-001197-П", "registry_type": "Regional", "registry_category": "3", "inclusion_date": "2016-12-30T14:06:36+03:00", "org_full_name": "Общество с ограниченной ответственностью Газпром добыча Ямбург", "value_co2": 0.25172, "facts": [ { "air": [ { "annual_value": 0.0000418522, "code": "0703", "name": "Бенз/а/пирен (3,4-Бензпирен)" }, { "annual_value": 0.153754, "code": "0616", "name": "Диметилбензол (Ксилол) (смесь изомеров о-, м-, п-)" }, { "annual_value": 0.00003175588, "code": "0405", "name": "Пентан" }, { "annual_value": 0.822589, "code": "2908", "name": "Пыль неорганическая, содержащая 70-20% двуокиси кремния (шамот, цемент, пыль цементного производства - глина, глинистый сланец, доменный шлак, песок, клинкер, зола кремнезем и др.)" }, { "annual_value": 0.763400004, "code": "2930", "name": "Пыль абразивная (Корунд белый; Монокорунд)" } ] } ] }
и таких объектов в файле на 600 Мб. Сложность парсинга этого файла заключается в том, что все эти объекты содержатся в файле в виде отдельных JSON-объектов, не объединенных в массив. Каждая строка файла — это отдельный объект JSON. То есть, в данном случае, десериализация JSON «из коробки» не сработает. Во-вторых, в файле присутствуют экранированные строки вида //"
, что также не позволит распарсить JSON без дополнительных проблем. Но, попробуем 🙂
Парсинг очень-очень большого JSON в C#
Работа с JSON в C#: сериализация и десериализация объектов
Работа с JSON в C#: сериализация производных классов
Работа с JSON в C#: применение невалидного JSON в C#
Как создать класс C# из JSON?
JSON DOM в .NET 6
Создадим новый компонент Razor с названием «AirData» — этот компонент мы будем использовать для вывода информации о выбросах в атмосферный воздух от предприятий. Как мы уже определили выше, в файле содержится JSON, который мы не можем просто «скормить» в JsonSerializer и на выходе получить массив объектов. Поэтому организуем парсинг следующим образом:
- Считываем файл в
Stream
- Читаем полученный поток данных по-строчно
- Каждую строку чистим от лишних символов, которые могут помешать десериализации объекта
- Десериализуем объект
Здесь следует сделать небольшое отступление и пояснить несколько моментов.
Во-первых, так как размер файла достаточно большой, то считать весь файл в строку string
не получится — получим ошибку OutOfMemory. Именно поэтому мы будем использовать поток Stream
.
Во-вторых, перед десериализацией Json мы должны убедиться, что Json валидный. Если попытаться десериализировать вот такую строку Акционерное общество \\"Пермский мукомольный завод\\""
, то получим ошибку десериализации следующего вида:
В C# символ \
используется для экранирования, поэтому правильной для десериализации будет строка Акционерное общество \"Пермский мукомольный завод\""
в которой кавычки экранированы одним обратным слэшем.
Создаем необходимые классы
Первое, что нам необходимо сделать — это создать необходимые классы в которые будет десериализироваться наш JSON. Создадим в проекте папку Models и в этой папке создадим новый файл, который назовем Pollutant.cs
.
Всё содержимое файла удаляем за исключением пространства имен. То есть файл должен выглядеть вот так:
namespace OpenEcology.Models { }
Теперь копируем одну строку из json-файла в буфер обмена. Я взял самую первую строку, предварительно заменив в ней \\
на \
.
Ставим курсор в файле между {
и }
и в меню Visual Studio выбираем «Правка —> Специальная вставка —> Вставить JSON как классы». Если всё сделано правильно и в буфере обмена находится валидный JSON, то получим все необходимые классы для работы. Мой файл принял следующий вид:
namespace OpenEcology.Models { public class Rootobject { public string name { get; set; } public string number { get; set; } public string registry_type { get; set; } public string registry_category { get; set; } public DateTime inclusion_date { get; set; } public string org_full_name { get; set; } public float value_co2 { get; set; } public Fact[] facts { get; set; } } public class Fact { public Air[] air { get; set; } } public class Air { public float annual_value { get; set; } public string code { get; set; } public string name { get; set; } } }
Учитывая, что в JSON вместо чисел может находиться значение null
, я немного отредактировал свойства классов. Приведу только изменения:
public class Rootobject { <--тут прочие свойства--> public float? value_co2 { get; set; } } public class Air { public float? annual_value { get; set; } <--тут прочие свойства--> }
Необходимые для работы с JSON классы созданы. Можно переходить к разработке компонента Razor.
Компонент Razor для десериализации большого JSON
Напишем следующий код для нашего компонента AirData
:
@page "/Air_data" @inject HttpClient Http @using OpenEcology.Models @using System.Text.Json @using System.Text.RegularExpressions <h3>air_data</h3> @if (update) { <p>Размер файла @data.Length байт</p> } <p>Получено @count элементов json</p> @code { private int count; //счётчик полученных элементов private Stream data; //данные файла bool update = false; protected override async Task OnInitializedAsync() { data = await Http.GetStreamAsync("data/onv_pub.json"); update = true; await InvokeAsync(StateHasChanged); await Parse(); } private async Task Parse() { using (StreamReader streamReader = new(data)) { while (true) { string temp = await streamReader.ReadLineAsync(); if (temp == null) break; temp = Regex.Unescape(temp); count++; await Task.Delay(1); await InvokeAsync(StateHasChanged); Rootobject rootobject = JsonSerializer.Deserialize<Rootobject>(temp); } } } }
Рассмотрим код компонента подробнее.
Во-первых, мы подключили необходимые пространства имен для дальнейшей работы:
@using OpenEcology.Models @using System.Text.Json @using System.Text.RegularExpressions
В перегруженной версии метода OnInitializedAsync()
мы считываем данные файл в поток Stream
и переключаем флаг update
, чтобы показать на экране строку о том, сколько байт было прочитано из файла. После этого, мы запускаем асинхронный метод Parse()
в котором и происходит разбор и десериализация JSON.
В методе Parse() мы выполняем следующие действия:
во-первых, мы создаем StreamReader
, чтобы иметь возможность удобного построчного чтения данных из потока. Делаем мы это по причине указанной выше — JSON файл не содержит ни запятых для отделения одного JSON-объекта от другого, ни, собственно, массива. Поэтому приходится вот так изгаляться, чтобы получить строку с очередным JSON-объектом.
во-вторых, в цикле while
мы проверяем считалась ли строка и, если считалась, то применяем к ней метод Regex.Unescape()
, который преобразует все escape-последовательности обратно в символы. Таким образом, мы освобождаемся от проблем с двойными бэкслэшами в тексте.
в-третьих, обновляем счётчик прочтенных элементов. В Blazor WebAssembly это делается несколько странным, на первый взгляд, способом:
await Task.Delay(1); await InvokeAsync(StateHasChanged);
мы делаем задержку на 1 мс и после этого просим перерисовать страницу компонента. Дело в том, что Blazor WebAssembly работает в одном потоке, что является ограничениями браузеров и, если не делать подобной задержки, что счётчик просто не обновится, пока наш цикл не закончит работу.
и, наконец, в-четвертых, мы десериализуем строку Json в класс C#.
Можно запустить приложение и убедиться, что файл считывается, а счётчик отсчитывает строки:
Понятно, что каждый раз каждому пользователю парсить JSON-файл на 600+ Мб, чтобы посмотреть несколько строк в таблице — совсем не серьезно. Именно поэтому мы пока никак не работаем с полученным JSON. В следующей части мы продолжим работу над компонентом AirData
и сохраним все наши объекты в базе данных, чтобы иметь возможность удобного постраничного доступа к ним.
Итого
Сегодня мы начали разрабатывать свой первый серьезный компонент для приложения «Open Ecology» и попутно разобрались с тем как десериализовать большие JSON (более 600 Мб), которые, к тому же, не являются валидными. В следующей части мы научимся создавать БД для приложения, записывать и считывать из базы данные.