Summary
A fresh DNN/2sxc instance can create two EAV zones for the same DNN portal when the first portal ZoneId lookup is hit concurrently before PortalSettings.TsDynDataZoneId exists.
Observed on local test instance:
- DNN site:
2sxc-dnn1030-t01
- 2sxc package:
21.7.0 (DesktopModules.Version = 21.07.00.00)
- Portal:
PortalID = 0, PortalName = My Website
Database evidence
TsDynDataZone contains two zones for the same portal:
ZoneId Name
1 Default
2 My Website (Portal 0)
3 My Website (Portal 0)
4 s1 (Portal 1)
The DNN portal setting points only to zone 2:
PortalID SettingName SettingValue CreatedOnDate
0 TsDynDataZoneId 2 2026-04-29 17:27:33
1 TsDynDataZoneId 4 2026-04-29 18:25:39
Zone/app ordering strongly suggests concurrent zone creation:
ZoneId Name AppIds
1 Default 1
2 My Website (Portal 0) 3,5,6,...,28
3 My Website (Portal 0) 2,4
4 s1 (Portal 1) 29,30,31
Zone 3 has only the default and primary apps (AppId 2 and 4), while zone 2 has default/primary plus the installed apps. The app IDs are interleaved: one request created zone 2, another request created zone 3 and its default/primary apps, then the zone 2 request continued and the final portal setting ended up pointing to zone 2.
Log evidence
The issue happened shortly after install/startup:
2026-04-29 17:22:12: package install logged for ToSic.Sxc.Dnn
2026-04-29 17:25:05, 17:25:23, 17:25:38: APPLICATION_START events
2026-04-29 17:25:27: several TaskCanceledException entries for persona bar API calls
2026-04-29 17:26:48 through 17:27:15: repeated DNN ClientResourceController errors on multiple thread IDs (T:48, T:62, T:94, T:65, T:6)
2026-04-29 17:27:33: TsDynDataZoneId setting is created for portal 0
The DNN logs do not contain a direct 2sxc zone creation trace, but the timing and multiple active threads match the race window.
Suspected root cause
The intended protection is in DnnZoneMapper.GetZoneId:
Src/Dnn/ToSic.Sxc.Dnn.Core/Dnn/Run/DnnZoneMapper.cs
GetZoneId checks PortalSettings.TsDynDataZoneId
- if missing, it calls
_zoneCreateLocker.Call(...)
CreateNewZone creates the EAV zone and then calls PortalController.UpdatePortalSetting(...)
However, _zoneCreateLocker is an instance field:
private readonly TryLockTryDo _zoneCreateLocker = new();
and the mapper is registered as transient:
services.TryAddTransient<IZoneMapper, DnnZoneMapper>();
So concurrent requests can receive different DnnZoneMapper instances, each with its own lock object. The double-check lock only protects within one mapper instance, not across concurrent first requests.
This was reproduced with an isolated concurrency demo using the same lock semantics:
transient locks => results=[1,2], generated=2, finalSetting=2
shared lock => results=[1,1], generated=1, finalSetting=1
Reproduction scenario
Use a fresh DNN/2sxc instance or a throwaway copy of a DB/site:
- Ensure
PortalSettings has no TsDynDataZoneId row for portal 0.
- Make sure 2sxc is installed and the first 2sxc/EAV site context access has not initialized the portal zone yet.
- Trigger multiple concurrent requests that cause
DnnSite.ZoneId / DnnZoneMapper.GetZoneId(0) to resolve, for example concurrent first page/API/admin requests during or just after DNN startup.
- Query:
SELECT * FROM dbo.TsDynDataZone;
SELECT * FROM dbo.PortalSettings WHERE SettingName = 'TsDynDataZoneId';
SELECT AppId, ZoneId, Name FROM dbo.TsDynDataApp ORDER BY AppId;
Expected: exactly one zone named <PortalName> (Portal 0) and one portal setting pointing to it.
Actual: multiple zones with the same portal name can be created; one is referenced by PortalSettings, the others are orphaned but contain default/primary app rows.
Suggested fix direction
The lock must be shared across DnnZoneMapper instances and ideally keyed by siteId, or the creation needs a DB-backed transactional guard. A minimal fix could be a shared/static lock dictionary by portal ID or a singleton lock service, with the existing double-check preserved inside the shared lock.
Also consider a defensive recovery path if a duplicate already exists: prefer the zone referenced by PortalSettings.TsDynDataZoneId and detect/report orphan zones with the same generated portal name.
Summary
A fresh DNN/2sxc instance can create two EAV zones for the same DNN portal when the first portal
ZoneIdlookup is hit concurrently beforePortalSettings.TsDynDataZoneIdexists.Observed on local test instance:
2sxc-dnn1030-t0121.7.0(DesktopModules.Version = 21.07.00.00)PortalID = 0,PortalName = My WebsiteDatabase evidence
TsDynDataZonecontains two zones for the same portal:The DNN portal setting points only to zone 2:
Zone/app ordering strongly suggests concurrent zone creation:
Zone 3 has only the default and primary apps (
AppId2 and 4), while zone 2 has default/primary plus the installed apps. The app IDs are interleaved: one request created zone 2, another request created zone 3 and its default/primary apps, then the zone 2 request continued and the final portal setting ended up pointing to zone 2.Log evidence
The issue happened shortly after install/startup:
2026-04-29 17:22:12: package install logged forToSic.Sxc.Dnn2026-04-29 17:25:05,17:25:23,17:25:38:APPLICATION_STARTevents2026-04-29 17:25:27: severalTaskCanceledExceptionentries for persona bar API calls2026-04-29 17:26:48through17:27:15: repeated DNNClientResourceControllererrors on multiple thread IDs (T:48,T:62,T:94,T:65,T:6)2026-04-29 17:27:33:TsDynDataZoneIdsetting is created for portal 0The DNN logs do not contain a direct 2sxc zone creation trace, but the timing and multiple active threads match the race window.
Suspected root cause
The intended protection is in
DnnZoneMapper.GetZoneId:Src/Dnn/ToSic.Sxc.Dnn.Core/Dnn/Run/DnnZoneMapper.csGetZoneIdchecksPortalSettings.TsDynDataZoneId_zoneCreateLocker.Call(...)CreateNewZonecreates the EAV zone and then callsPortalController.UpdatePortalSetting(...)However,
_zoneCreateLockeris an instance field:and the mapper is registered as transient:
So concurrent requests can receive different
DnnZoneMapperinstances, each with its own lock object. The double-check lock only protects within one mapper instance, not across concurrent first requests.This was reproduced with an isolated concurrency demo using the same lock semantics:
Reproduction scenario
Use a fresh DNN/2sxc instance or a throwaway copy of a DB/site:
PortalSettingshas noTsDynDataZoneIdrow for portal 0.DnnSite.ZoneId/DnnZoneMapper.GetZoneId(0)to resolve, for example concurrent first page/API/admin requests during or just after DNN startup.Expected: exactly one zone named
<PortalName> (Portal 0)and one portal setting pointing to it.Actual: multiple zones with the same portal name can be created; one is referenced by
PortalSettings, the others are orphaned but contain default/primary app rows.Suggested fix direction
The lock must be shared across
DnnZoneMapperinstances and ideally keyed bysiteId, or the creation needs a DB-backed transactional guard. A minimal fix could be a shared/static lock dictionary by portal ID or a singleton lock service, with the existing double-check preserved inside the shared lock.Also consider a defensive recovery path if a duplicate already exists: prefer the zone referenced by
PortalSettings.TsDynDataZoneIdand detect/report orphan zones with the same generated portal name.