Blazor: Server и WebAssembly одновременно в одном приложении

ASP.NET Core Blazor — это разработанная Microsoft веб-платформа, предназначенная для запуска на стороне клиента в браузере на основе WebAssembly (Blazor WebAssembly) или на стороне сервера в ASP.NET Core (Blazor Server), но две эти модели нельзя использовать одновременно. Подробнее о моделях размещения написано в документации.
В статье я расскажу о том, как
- запустить Server и WebAssembly одновременно в одном приложении,
- переключаться с Server на WebAssembly без перезагрузки приложения,
- реализовать универсальный механизм аутентификации,
- синхронизировать состояние Server и WebAssembly с помощью gRPC.
TL; DR:

Пример доступен на github.
Введение: зачем это нужно
Обе модели размещения имеют свои преимущества и свои недостатки:
Преимущества Blazor Server:
- Небольшой объём загружаемых данных (blazor.server.js без сжатия ~ 250 КБ).
- Быстрая загрузка.
- Отзывчивый UI.
Недостатки Blazor Server:
- Так как изменения DOM рассчитываются на сервере, для отзывчивости UI нужно надёжное и быстрое соединение с сервером.
- В случае разрыва соединения, приложение в браузере перестанет работать.
- В случае перезапуска сервера, приложение в браузере перестанет работать, а состояние интерфейса будет потеряно.
- Сложно масштабировать, так как клиент должен работать только с тем сервером, который хранит его состояние.
Преимущества Blazor WebAssembly
- Нет всех недостатков Blazor Server, так как приложение работает в браузере автономно. Например, можно работать offline, или делать PWA.
Недостатки Blazor WebAssembly
- Неприлично большой размер: 10 — 15 Мб.
- Из-за такого размера от перехода по ссылке до появления интерфейса может пройти 15 — 20 секунд (для первого запуска), что в современном мире уже за гранью допустимого.
Нужно отметить, что пререндеринг доступен для обеих моделей размещения, и здорово улучшает отзывчивость, мы будем его использовать. Но даже со включенным пререндерингом для WebAssembly интерфейс будет оставаться неотзывчивым слишком долго, те же 15 — 20 секунд для первого запуска и 5 — 10 секунд для повторных.
Чтобы объединить преимущества Server и WebAssembly, у меня появилась идея реализовать гибридный режим работы: приложение должно запускаться в режиме Server, а позже переходить в режим WebAssembly незаметно для пользователя, например, во время навигации между страницами.
Далее я расскажу как у меня получилось это реализовать.
Часть 1: запуск Server и WebAssembly одновременно
Начать нужно с размещения WebAssembly приложения в приложении ASP.NET Core и включения Prerendering.
В такой конфигурации запуск Blazor начинается в файле _Host.cshtml с добавления на страницу компонента, который создаст для нас DOM нашего приложения, и загрузки скрипта, делающего наше приложение интерактивным.
Для Server это выглядит так:
А для WebAssembly так:
Поэтому нам ничего не мешает загрузить их одновременно:
Работать это, естественно, не будет. Дело в том, что тег превращается в такой html:
В процессе инициализации приложения, blazor ищет этот фрагмент, а затем заменяет его на DOM приложения. Если скрипты blazor.server.js и blazor.webassembly.js запустить одновременно, они оба будут конкурировать за первый компонент, игнорируя второй.
Этого легко избежать, если начинать запуск blazor.webassembly.js только после того, как blazor.server.js закончил работу, примерно так:
var loadWasmFunction = function () {
// Дождёмся момента, когда blazor.server.js закончит работу
if (srvrApp.innerHTML.indexOf('
