--- title: Admin assets crash — missing root AssetsFolder seed type: concept tags: [cms, admin, data-seed, mssql, gotcha, null-check] sources: [../sources/iis-host-migration-2026-05-19.md] updated: 2026-05-19 --- # CMS admin assets crash — root AssetsFolder seed `AssetsJsonViewModelBuilder.cs:22` (admin /admin/assets/.../getList) падает NullReferenceException на любом site, где **root row `Folders` (Discriminator='AssetsFolder', LoweredPath='/', OwnerId=SiteId)** отсутствует. Root создаётся lazy (при first asset upload через admin), и sites которые никогда не использовали admin assets UI — без root. Связано: [[cms-server-port-leak-fix]] (нашли при verification после port-leak fix), [[recovery-architecture-snapshot]] (был open issue #8). ## Симптом ``` GET /admin/assets//getList?path= → 500 NullReferenceException [NullReferenceException] MoreThenCms.Admin.ViewModels.Builders.AssetsJsonViewModelBuilder.Build(...) AssetsJsonViewModelBuilder.cs:22 MoreThenCms.Web.Admin.Controllers.AssetsController.GetList(Guid ownerId, String path) AssetsController.cs:46 ``` В `AssetsController.GetList` (line 46): ```csharp var model = _assetsViewModelBuilder.Build( _assetsFoldersService.GetFolderByPath(ExecutionContext, ownerId, path), ...); ``` `GetFolderByPath` (`MoreThenCms\Assets\Services\AssetsFoldersService.cs:47-54`) делает `repo.Find(p => p.LoweredPath == path && p.OwnerId == ownerId)` — возвращает **null** если row нет. `Build` дальше делает `new JValue(model.ParentPath)` без null-check → crash. Frontend код (`AssetsAppFunc.cs:66-86`) делает proper null-check → HTTP 404. Только admin view-model builder упустил. ## Кого затрагивает (snapshot 2026-05-19) После recovery: **15 sites без root** (из ~50). Sites где admin assets когда-либо открывался → root есть. Остальные: - emspb.ru, pilorama98.ru, labtools.pro, kupimknigi.spb.ru, sestech.ru, aquamax.spb.ru, artmone.pro, priemka-kvartiry.ru, profund.spb.ru, ics-artmaterials.com, _voda-indigo.ru - 4 sites с NULL `PrimaryDomain` (legacy/test data) Проверочный запрос: ```sql SELECT s.SiteId, s.PrimaryDomain, CASE WHEN EXISTS (SELECT 1 FROM Folders f WHERE f.OwnerId = s.SiteId AND f.LoweredPath = '/' AND f.Discriminator = 'AssetsFolder') THEN 'OK' ELSE 'NO ROOT' END AS RootStatus FROM Sites s ORDER BY RootStatus, s.PrimaryDomain; ``` ## Fix (применён 2026-05-19) DB seed — один INSERT для всех missing roots, идемпотентный (`WHERE NOT EXISTS`): ```sql DECLARE @CreatedById uniqueidentifier = 'E4CC416B-5D13-4757-8EFE-03CBEA15B18C'; -- existing admin user DECLARE @Inserted TABLE (FolderId uniqueidentifier, OwnerId uniqueidentifier); INSERT INTO Folders (FolderId, OwnerId, Path, LoweredPath, DateCreated, UtcDateCreated, CreatedById, Discriminator) OUTPUT INSERTED.FolderId, INSERTED.OwnerId INTO @Inserted SELECT NEWID(), s.SiteId, '/', '/', SYSDATETIME(), SYSUTCDATETIME(), @CreatedById, 'AssetsFolder' FROM Sites s WHERE NOT EXISTS (SELECT 1 FROM Folders f WHERE f.OwnerId = s.SiteId AND f.LoweredPath = '/' AND f.Discriminator = 'AssetsFolder'); SELECT FolderId, OwnerId FROM @Inserted; -- save for atomic revert ``` 15 rows вставлены. Inserted FolderId/OwnerId сохранены в `.tasks/cms-admin-assets-root-folders-seed.inserted-rows.txt` для atomic revert (`DELETE FROM Folders WHERE FolderId IN (...)`). После: `/admin/assets//getList?path=` возвращает HTTP 302 → /login для unauth (normal auth path), вместо 500. Authorized users видят empty assets list. ## Долгосрочный TODO (не сделано — нет рабочего build env) `AssetsJsonViewModelBuilder.Build` должна null-guard'иться — возвращать empty JObject ({parentPath:"", path:"/", folders:[], files:[]}) когда model null. Это **defensive code**, плюс auto-create root в `GetFolderByPath` для root path (как обычно делают админ-репозитории). Требует recompile `MoreThenCms.Admin.dll` — отложено до восстановления полного build pipeline (gitea/build/registry — см. [[vds-kzntsv-bootstrap]] task). ## Возможно затрагивает ещё (open для следующей сессии) `Folders` table — polymorphic: `AssetsFolder`, `ImagesFolder` (OwnerId=ThemeId), `ScriptsFolder` (ThemeId), `StylesheetsFolder` (ThemeId). Аналогичная null-falling логика может быть в admin views для Themes (Stylesheets/Scripts/Images). Не проверено, не reported user'ом. Если будет — same pattern: `SELECT s.SiteId vs Folders root по соответствующему Discriminator` + seed.