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

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions