Activities of "kberntsen"

Issue reported against the framework in gh repo

https://github.com/abpframework/abp/issues/26208

Bug: Global settings unique index doesn't prevent duplicates when ProviderKey is NULL (SQL Server)

Summary

SettingManagementDbContextModelBuilderExtensions.ConfigureSettingManagement defines a unique index on AbpSettings(Name, ProviderName, ProviderKey):

b.HasIndex(x => new { x.Name, x.ProviderName, x.ProviderKey }).IsUnique(true);

For tenant-scoped settings ( ProviderKey = <tenantId> ), this correctly prevents duplicate
rows. For global/host-scoped settings ( ProviderName = "G" ,  ProviderKey = NULL ), the
constraint silently fails to prevent duplicates on SQL Server, because SQL Server (like most
RDBMS unique-index implementations) treats each  NULL  as distinct from every other  NULL  for
uniqueness purposes — i.e.  (Name='Foo', ProviderName='G', ProviderKey=NULL)  and a second
identical row do not violate the unique constraint.

 SettingManager.SetGlobalAsync / SetForTenantAsync  performs a check-then-insert rather than an
atomic upsert. Under concurrent calls (e.g. two near-simultaneous requests that both persist the
same global setting before either commit), this produces two rows with the same
 (Name, ProviderName, ProviderKey=NULL)  and no DB-level guard stops it.

Impact

Once two duplicate global setting rows exist, every subsequent read of that setting throws,
because  SettingManagementStore.SetCacheItemsAsync  builds the settings cache via:

settingValues.ToDictionary(x => x.Name, x => x.Value)

 ToDictionary  throws  ArgumentException: An item with the same key has already been added  on
the duplicate  Name . This is a hard, unrecoverable 500 on every request that reads that
setting's cache — in our case it broke  /Account/Login  for the entire application (through
 ExternalProviderSettingsHelper.GetAllForHostAsync()  →  SettingManager.GetOrNullAsync  →
 SettingManagementStore.GetCacheItemAsync ), until the duplicate row was manually deleted from
the database.

Repro

1. Trigger two concurrent calls to  ISettingManager.SetGlobalAsync("MyModule.MySetting", value) 
for a setting that does not yet have a global row (e.g. two app-service requests racing on
first save, or a distributed deployment with two pods handling near-simultaneous requests).
2. Both calls read "no existing row", then both insert.
3. Query  AbpSettings  — two rows exist with identical  Name / ProviderName='G' / ProviderKey=NULL .
4. Any subsequent call to read that setting (directly or via any code path that populates the
settings cache for that provider/providerKey) throws  ArgumentException  and surfaces as a 500.

Environment

• ABP Framework (Volo.Abp.SettingManagement / Volo.Abp.SettingManagement.EntityFrameworkCore) 10.6.0
• SQL Server (Azure SQL), EF Core provider
• .NET 10

Suggested Fix

Add a filtered unique index for the null- ProviderKey  case (SQL Server  HasFilter ), in addition
to the existing index, e.g.:

b.HasIndex(x => new { x.Name, x.ProviderName })
    .IsUnique()
    .HasFilter("[ProviderKey] IS NULL");

Alternatively/additionally,  SettingManager.SetGlobalAsync / SetForTenantAsync  could perform an
atomic upsert (e.g.  MERGE , or catch the unique-constraint violation and retry as an update)
rather than check-then-insert, to close the race condition at its source rather than only at the
DB-constraint level.

Workaround (applied)

Manually deleted the duplicate row from  AbpSettings  to unblock login. Adding an app-level
filtered unique index migration (scoped to our own DbContext) as defense-in-depth until this is
fixed upstream or we adopt the fix ourselves.
Showing 1 to 1 of 1 entries
Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.8.0-preview. Updated on September 23, 2026, 09:57
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.