Одно из двух, — прошелестел он, — или пациент жив, или он умер. Если он жив — он останется жив или он не останется жив. Если он мёртв — его можно оживить или нельзя оживить.   А.Н. Толстой. "Золотой ключик, или Приключения Буратино"    Недавно в организации, где я тружусь, появилась идея переписать корпоративную систему, которая работает более 20 лет и за этот срок несколько устарела. Если кратко - информационная система в области морских грузовых перевозок и обслуживания судов в порту. В данный момент я занимаюсь НИОКР и хочу поделиться с вами некоторыми проблемами, с которыми может столкнуться разработчик подобной системы и как я планирую их решать. На текущий момент планирую сервер приложений ASP.NET Core под .NET 6. База данных останется старая, так как это даст возможность постепенно переходить на новую систему по мере добавления функционала. Основное клиентское ПО предполагается на WPF под .NET 6. Кроме того, возможны небольшие узкопрофильные клиенты на веб-страницах и мобильных устройствах. Пользователю лень фильтровать запрашиваемые данные, а их довольно много. Заставлять пользователя что-то фильтровать - негуманно и попахивает произволом. В текущей системе такой пользователь сидит грустит, ругает разработчиков, давит на техподдержку. В текущей системе клиент идёт прямо в базу данных через BDE, ничего с этим сделать уже нельзя. В новой системе клиент про базу данных ничего не знает, сервер по его запросу шлет JSON через HTTP. Если делать "в лоб", то сильно мало что поменяется: сервер по запросу клиента будет собирать для отправки коллекцию объектов, загружая их из базы или ещё как-то. Потом всю эту махину он пошлёт в ответном HTTP-пакете, предварительно серилизовав в JSON, там это всё будет в один присест десериализовано в клиентские объекты, которые, наконец-то предстанут пред взором потерявшего надежду пользователя.  А давайте, сервер по запросу начнёт грузить в свою коллекцию, как и раньше, но на клиента отправит не всё, а сколько получится загрузить до истечения таймаута или какое-то фиксированное количество объектов или в результате суперпозиции этих условий. При этом клиенту мы будем сообщать в специальном заголовке, уже всё или нет. А если нет, передавать специальный идентификатор, который клиент должен предъявлять при запросе следующей порции. Таким образом, пользователь быстро увидит результат, возможно, даже сможет использовать. Предположим, у нас есть следующий класс: Данный класс может быть также унаследован с предопределёнными "утилизаторами". В этой статье мы будем использовать класс ChunksPartialLoader, который помещает очередную порцию в List<T>, заменяя предыдущую порцию. Посмотрим, как этим можно воспользоваться. Чтобы испытать, как всё работает, создадим в VisualStudio Studio Community 2022 три проекта. Здесь опишу по возможности кратко, так как все исходники доступны: https://sourceforge.net/p/partialloader/code/ci/v1.1.0/tree/. BigCatsDataContract - библиотека классов, общих для сервера и клиента. Здесь находится класс, описывающий элемент данных, в нашем случае - кошку. Также присутствует класс с некоторыми константами. BigCatsDataServer - проект ASP.NET Core.  В Program.cs Мы обрабатываем два маршрута с параметрами: В классе CatsGenerator расположены метод, генерирующий данные и методы, обрабатывающие HTTP-запросы: BigCatsDataClient - проект WPF. Разметку рассматривать не будем, а в code-behind для простоты размещены методы, запрашивающие список кошек полностью и частями. Код для краткости привожу не полностью, он доступен в исходниках. Запустим сервер и клиент. Будем запрашивать 100000 кошек с задержкой 0.1 миллисекунд на каждую. Сначала нажмём "Получить сразу всё". Как и ожидалось, примерно за 10 секунд получили. В течение этого времени таблица была пуста и пользователь только по часикам мог надеяться, что приложение живое. Теперь попробуем получить частями. Таймаут поставим 200, меньше нет смысла, так как в PartialLoader мы используем ManualResetEventSlim.Wait(...) и меньшую задержку мы не сможем выдержать. Видим, что уже через десятые доли секунды пользователь увидел первые данные.  Попробуем вместо таймаута задать размер партии.  Как видим, всё прошло по плану. Ну и оба параметра: После нескольких попыток удалось подобрать размер партии такой, что timeout и paging сработали примерно поровну - 51% в пользу paging. 1 1803 00:00:00.1110889 1803 00:00:00.1110889 0 2 1971 00:00:00.2389203 3774 00:00:00.3500092 0 3 2070 00:00:00.2179351 5844 00:00:00.5679443 1 4 2070 00:00:00.2198922 7914 00:00:00.7878365 1 5 2070 00:00:00.2197443 9984 00:00:01.0075808 1 6 2061 00:00:00.2236320 12045 00:00:01.2312128 0 7 2055 00:00:00.2192046 14100 00:00:01.4504174 0 8 2026 00:00:00.2243919 16126 00:00:01.6748093 0 9 2038 00:00:00.2201484 18164 00:00:01.8949577 0 10 2070 00:00:00.2219114 20234 00:00:02.1168691 1 11 2046 00:00:00.2223356 22280 00:00:02.3392047 0 12 2070 00:00:00.2228286 24350 00:00:02.5620333 1 13 2025 00:00:00.2215145 26375 00:00:02.7835478 0 14 2048 00:00:00.2218749 28423 00:00:03.0054227 0 15 2070 00:00:00.2216480 30493 00:00:03.2270707 1 16 2069 00:00:00.2219907 32562 00:00:03.4490614 0 17 2069 00:00:00.2219315 34631 00:00:03.6709929 0 18 2070 00:00:00.2228502 36701 00:00:03.8938431 1 19 2065 00:00:00.2218863 38766 00:00:04.1157294 0 20 2070 00:00:00.2228646 40836 00:00:04.3385940 1 21 2067 00:00:00.2219197 42903 00:00:04.5605137 0 22 2069 00:00:00.2212811 44972 00:00:04.7817948 0 23 2070 00:00:00.2211251 47042 00:00:05.0029199 1 24 2028 00:00:00.2230665 49070 00:00:05.2259864 0 25 2057 00:00:00.2223452 51127 00:00:05.4483316 0 26 2070 00:00:00.2231241 53197 00:00:05.6714557 1 27 2070 00:00:00.2201679 55267 00:00:05.8916236 1 28 2070 00:00:00.2206666 57337 00:00:06.1122902 1 29 2070 00:00:00.2230042 59407 00:00:06.3352944 1 30 2057 00:00:00.2217751 61464 00:00:06.5570695 0 31 2064 00:00:00.2229603 63528 00:00:06.7800298 0 32 2070 00:00:00.2216961 65598 00:00:07.0017259 1 33 2070 00:00:00.2220264 67668 00:00:07.2237523 1 34 2070 00:00:00.2227823 69738 00:00:07.4465346 1 35 2070 00:00:00.2218313 71808 00:00:07.6683659 1 36 2070 00:00:00.2249333 73878 00:00:07.8932992 1 37 2070 00:00:00.2199873 75948 00:00:08.1132865 1 38 2070 00:00:00.2228433 78018 00:00:08.3361298 1 39 2070 00:00:00.1097573 80088 00:00:08.4458871 1 40 2068 00:00:00.2390979 82156 00:00:08.6849850 0 41 2070 00:00:00.2215380 84226 00:00:08.9065230 1 42 2070 00:00:00.2231034 86296 00:00:09.1296264 1 43 2070 00:00:00.2226903 88366 00:00:09.3523167 1 44 2064 00:00:00.2218493 90430 00:00:09.5741660 0 45 2038 00:00:00.2219079 92468 00:00:09.7960739 0 46 2054 00:00:00.2234104 94522 00:00:10.0194843 0 47 2070 00:00:00.2202597 96592 00:00:10.2397440 1 48 2048 00:00:00.2221415 98640 00:00:10.4618855 0 49 1360 00:00:00.1118606 100000 00:00:10.5737461 0      0,5102040816 Ну и несколько слов о реализации PartialLoader, использованной для данной демонстрации. При первом вызове LoadAsync(...) запускается задача, которая читает асинхронный Enumerable и записывает их в очередь. Как  первый, так и последующие вызовы LoadAsync() читают данные из этой очереди в течение времени, соответствующего заданным параметрам. Для совместного доступа потоков к очереди используется ManualResetEventSlim. Вначале он сброшен. Когда задача, пишущая в очередь, добавляет объект, она устанавливает ManualResetEventSlim, и происходит чтение из очереди. Когда очередь пустеет, ManualResetEventSlim сбрасывается. Подробнее реализацию можно посмотреть в исходниках. При необходимости загрузки в пользовательский интерфейс большого количества данных, можно сократить время ожидания пользователя выполняя такую загрузку частями и сразу отображая их. NuGet: https://www.nuget.org/packages/Net.Leksi.PartialLoader/