Skip to content

DNN: duplicate EAV zones can be created for the same portal on concurrent first access #3763

Description

@tvatavuk

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:

  1. Ensure PortalSettings has no TsDynDataZoneId row for portal 0.
  2. Make sure 2sxc is installed and the first 2sxc/EAV site context access has not initialized the portal zone yet.
  3. 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.
  4. 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.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions