Как это сделано
Стек и решения
Не список модных слов, а состав этой сборки с объяснением, почему выбрано именно так. Версии заданы централизованно, одним файлом на всё решение.
Слои
Сверху вниз, от браузера к машине.
- Браузер
- Blazor: статическая отрисовка, серверные острова и WebAssembly там, где нужна отзывчивость
- Приложения
- ASP.NET Core на .NET 10 — три сервиса под одним оркестратором
- Вход
- OpenIddict: OAuth 2.0 и OpenID Connect, свои наборы учётных записей
- Данные
- MongoDB: документы, TTL-индексы, файлы в GridFS
- Эксплуатация
- Контейнеры, nginx с TLS, снимки базы по расписанию
Платформа и выполнение
- .NET 10 и ASP.NET Core
Один целевой фреймворк на все проекты решения, заданный в корневом файле свойств. Никакого «почти обновлено»: обновляется всё сразу или ничего.
- .NET Aspire
Оркестратор разработки: одной командой поднимает сервер идентификации, конструктор и этот сайт, раздаёт им строки подключения и адреса. В продакшене топология повторена вручную — compose написан как исходник, а не сгенерирован, потому что половины продакшена (nginx, TLS, снимки базы) в модели оркестратора нет и быть не может.
- Blazor: сервер и WebAssembly
Один компонентный слой на оба режима, и режим выбирается под задачу. Страницы, которые публикует конструктор, собираются на сервере и приезжают в браузер обычным HTML — без Blazor-рантайма вовсе. Редактор — WebAssembly, потому что перетаскивание блока и ползунок отступа меняют документ десятки раз в секунду, и на серверном соединении каждое такое изменение было бы обращением по сети.
- OpenTelemetry
Логи, метрики и трассировки собираются одинаково во всех приложениях и уходят по OTLP. Общий слой умолчаний ставит это вместе с проверками готовности и поиском сервисов — новый сервис получает наблюдаемость фактом подключения библиотеки.
Данные и вход
- MongoDB
Документная модель ложится на то, что здесь хранится: страница — дерево блоков, карточка товара — открытый набор характеристик, которые придумал администратор магазина. Сроком жизни входов занимается TTL-индекс самой базы, а не фоновая задача приложения, которая останавливается вместе с приложением.
- OpenIddict и ASP.NET Core Identity
Свой сервер OAuth 2.0 и OpenID Connect с учётными записями поверх MongoDB: вход, двухфакторная проверка, согласие, самообслуживание и админ-API. Всё остальное аутентифицируется через него, включая этот сайт.
- Свои наборы учётных записей
Те, кто делает сайты, и покупатели магазинов живут в разных наборах: один общий и свой у каждого сайта. Один и тот же адрес регистрируется в ста магазинах независимо, а покупатель одного магазина не существует для другого. Разделение проведено в ключах поиска — то есть невыразимо, а не проверяемо.
- Ключи защиты данных переживают рестарт
Кольцо, которым подписаны cookie входов и токены форм, лежит там, где его найдут после обновления образа: у сервера идентификации и конструктора — в MongoDB, у этого приложения — в отдельном томе. По умолчанию оно легло бы в профиль пользователя, а в контейнере профиль исчезает вместе с контейнером: это выглядит как «после обновления всех разлогинило», без единой ошибки в журнале.
Граница браузера
- Backend-for-frontend вместо токена в браузере
Страница ходит по своему же origin с одной cookie в несколько сотен байт, а тикет с токенами лежит в базе. Токен, отданный браузеру, лежит там, куда дотягивается любой внедрённый скрипт, и живёт ровно столько, сколько его никто не украл; вход в базе можно отозвать одной записью.
- YARP
Взят не как шлюз, а ради прямой пересылки запроса: потоковые тела, hop-by-hop заголовки, HTTP/2 и передача отмены там уже сделаны верно, и переписывать это руками означало бы сделать то же самое хуже.
- Санитайзер на входе
Авторские HTML и Markdown чистятся один раз — при сохранении, а не при каждом показе. Чистка при чтении означает, что о ней должен помнить каждый путь чтения: публичный рендерер, экспорт, предпросмотр, карта сайта. Один из них однажды не вспомнит, и именно он станет уязвимостью.
- SkiaSharp для картинок
Фотография приезжает из учётной системы такой, какой её сохранил товаровед, а в сетке показывается на 280 пикселях — уменьшенные копии делаются по запросу и кэшируются. Библиотека выбрана не по технической причине: раздельная лицензия ImageSharp превращает коммерческое использование выше порога выручки в денежное обязательство, а Skia — MIT.
Сборка и эксплуатация
- Централизованные версии пакетов
Версия каждого пакета названа один раз на всё решение, и рядом с ней написано, почему именно она. Два проекта не могут разъехаться по версии одной библиотеки, а вопрос «что у нас стоит» решается открытием одного файла.
- Контейнеры, nginx и TLS
Продакшен — одна машина: nginx с TLS и автопродлением сертификата, образы приложений и MongoDB со снимками по расписанию. Наружу открыты только 80 и 443; у базы порт на внешнем интерфейсе не «закрыт файрволом», а не существует.
- Автотесты как условие сборки
xUnit, Shouldly и bunit — последний потому, что две поломки разметки дошли до пользователя, не уронив ни одного теста логики: сломано было то, что рисуется. Тесты изоляции идут против настоящей MongoDB, а не против заглушки, — заглушка согласилась бы с любым ожиданием и не доказала бы ни уникального индекса, ни фильтра по сайту.