18. AppSettings Group-Aware Key Lookup
18.1 Purpose and scope
Closes the gap tracked in
Chapter 17 §17.13: GetSettingValueAsync<T>(string key)/SetSettingValueAsync<T>(string key, T value) resolved an AppSetting row by Key alone, with no
GroupName filter. This phase makes that lookup group-aware and safe — no package, schema, business
behavior, or MultiTenancyApi change was involved; this is a BackEnd-only correctness fix.
18.2 Root cause
AppSettingService/TenantSettingService's key-only overloads queried
_repository.Query<AppSetting>().FirstOrDefaultAsync(q => q.Key == key) — no GroupName in the predicate at
all. If the same Key string existed in more than one group (a realistic risk: generic names like
Enabled/IsActive/DefaultValue are exactly the kind of property name likely to be reused across
unrelated settings groups), this silently returned whichever row the query happened to return first — never
an error, never a warning, just a possibly-wrong value or a write landing on the wrong group's row. Tenant
isolation itself was never at risk (the query is still correctly scoped to the current tenant by
ApplicationDbContext's blanket IsMultiTenant() convention) — this was a cross-group, not
cross-tenant, ambiguity.
18.3 Fix
-
New group-aware overloads on
AppSettingService/TenantSettingService(and, forTenantSettingService, on its real, activeITenantSettingServiceinterface —IAppSettingService's own key-only/group-aware methods are not part of its interface contract, matching how the pre-existing key-only overload already wasn't):Task<T> GetSettingValueAsync<T>(string groupName, string key, CancellationToken ct = default, bool returnDefaultOnException = false);Task SetSettingValueAsync<T>(string groupName, string key, T value, CancellationToken ct = default);Both scope the query/upsert to
(GroupName, Key), andSetstampsGroupNameon newly-created rows (the legacy key-onlySetnever did, and still doesn't — unchanged to avoid altering existing behavior). -
The legacy key-only overloads now fail fast instead of guessing: a new private
ResolveUnambiguousMatch(key, matches)helper loads every row matching the key, and throws anInvalidOperationExceptionnaming every distinctGroupNamefound the moment more than one exists — before any value is read or any write is applied. It never silently picks the first row. When the key is genuinely unique for the tenant (the common case), behavior is unchanged. -
Cache keys stay consistent with Chapter 17's
IAppSettingCacheKeyBuilder— the group-aware overload reuses the existingBuildScopedGroupKey(groupName, "key", settingKey)method (already present, no builder change needed) to produceAppSettings:{TenantId}:{GroupName}:key:{Key}; the legacy key-only overload is unchanged,AppSettings:{TenantId}:single:{Key}.
18.4 Callers
No active production caller of either overload existed before this phase (the only two call sites,
ProductService.cs and ShoppingCartService.cs, were already commented-out dead code — left untouched, not
part of this fix's scope). This is a forward-looking safety fix: any future caller that knows its
GroupName should use the new overload; the legacy key-only overload remains available for genuinely
tenant-wide-unique ad-hoc keys, now safely.
18.5 Tests
Shumoul.Application.Tests/ServicesTests/AppSettingServiceGroupAwareKeyTests.cs (7 tests):
- Key exists in exactly one group — legacy key-only lookup still works unchanged.
- Key exists in two groups — legacy key-only
GetthrowsInvalidOperationExceptionnaming both groups. - Key exists in two groups — legacy key-only
Setthrows the same way, and neither row is mutated. - Group-aware
Getreturns the correct group's value even when the key is ambiguous globally. - Group-aware
Setupdates only the intended group's row; the other group's row is untouched. - Group-aware
Seton a brand-new key creates the row withGroupNamestamped. - Tenant isolation from Chapter 17 remains intact for the group-aware overload (two tenants, one shared
IEasyCachingProvider, no cross-contamination).
18.6 Build and test verification
| Command | Result |
|---|---|
dotnet build (full solution) | 0 errors, 0 new warnings |
dotnet test (targeted: new suite) | 7/7 pass |
dotnet test (full solution) | 837 total (830 + 7 new), 35 failed — identical pre-existing baseline, 0 new failures |
Shumoul.Framework.MultiTenancy.Api package: unchanged. No MultiTenancyApi or frontend file was touched.
18.7 Compatibility notes
- Fully backward compatible for the common case (a key unique to one group) — no behavior change.
- Breaking in the narrow, intended sense: if a genuinely ambiguous key-only call exists anywhere not yet
discovered (none found active in this codebase), it will now throw
InvalidOperationExceptioninstead of silently returning a possibly-wrong value. This is the explicit goal of the fix, not a regression — a caller hitting this exception should switch to the group-aware overload.
