6. Location Validation — Root Cause and Fix
6.1 The production error
Shared location reference-data validation is not implemented in this codebase.
This appeared for every real tenant self-registration attempt that reached the location-validation step
— i.e. every attempt, since CountryId/RegionId/CityId are required fields. It was raised as a
NotImplementedException from NotImplementedSharedLocationService, a fallback stub in
Shumoul.Framework.MultiTenancy.Api.
6.2 Why the stub existed
// Shumoul.Framework.MultiTenancy.Api/Tenants/NotImplementedSharedLocationService.cs
public class NotImplementedSharedLocationService : ISharedLocationService
{
// every method throws NotImplementedException
}
The stub's job was to let RegisterNewTenantRequestValidator / CreateTenantRequestValidator's DI graph
resolve something for ISharedLocationService when running standalone (e.g.
Shumoul.Framework.MultiTenancy.Host, or a test host) where no real location-validation implementation
exists. The intent was: fail loudly with a clear exception rather than silently skip validation. In
practice, it was resolved instead of the real implementation in the actual production process.
6.3 The real implementation already existed
Shumoul.Infrastructure.Services.SharedLocationService (in Shumoul.Saas.Api) already implemented
ISharedLocationService correctly, querying SharedDbContext.Countries/Regions/Cities — the exact
same tables and context used by RegistrationReferenceService, the service backing the public
Country/Region/City dropdown APIs (see Chapter 4). This ruled out
one of the task's leading hypotheses: the validator was not checking a different database than the
one the dropdowns read from. The dropdowns worked in production because RegistrationReferenceService
was reachable; the validator failed because the wrong ISharedLocationService was being resolved.
6.4 Root cause — two layered DI defects
Defect 1 — explicit registration order. Startup.AddMultiTenancy() registered the stub with plain
services.AddTransient<ISharedLocationService, NotImplementedSharedLocationService>(). Under ASP.NET
Core's "last registration wins" rule, if the host's own real registration ran before this line, the
stub would silently overwrite it. This is the same class of bug documented previously for
ICurrentUserPermissionsService/OnboardingCurrentUserPermissionsService (see
docs/testing/MULTITENANCY_PERMISSION_AUTHORIZATION_FIX_REPORT.md in the BackEnd repo). The fix: use
TryAddTransient, which only registers if nothing is already registered for that interface.
This fix alone was shipped first, live-tested, and proved insufficient — production registration
still threw the identical NotImplementedException after deploying it. That forced a deeper
investigation.
Defect 2 — the real root cause: a blanket cross-assembly DI scan collision. Both
Shumoul.Framework.Application's and Shumoul.Infrastructure's copies of AddDynamicServices() do:
AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(s => s.GetTypes())
.Where(p => transientServiceType.IsAssignableFrom(p))
// ... registers each match via plain AddTransient/AddScoped, not TryAdd
This is a blanket reflection scan over every loaded assembly, not just the host's own. Because
ISharedLocationService extended ITransientService, both implementations — the real
SharedLocationService (host assembly) and the package's own NotImplementedSharedLocationService
fallback — independently satisfied the scan's filter. The scan registers with plain AddTransient, so
whichever implementation the scan happened to enumerate last silently won — entirely independent of,
and before, Startup.AddMultiTenancy()'s own TryAddTransient fallback ever ran. This non-deterministic
double-registration, not the explicit registration line, was the actual cause of the production failure.
6.5 The fix
ISharedLocationService no longer extends ITransientService (or IScopedService/ISingletonService)
— removing it from the blanket scan entirely. Both implementations are now registered explicitly,
exactly once each:
SharedLocationService→ registered explicitly inShumoul.Infrastructure'sServiceCollectionExtensions.AddInfrustracture().NotImplementedSharedLocationService→ registered viaTryAddTransientinShumoul.Framework.MultiTenancy.Api'sStartup.AddMultiTenancy(), as a fallback for hosts that never register a real implementation.
This mirrors ICurrentUserPermissionsService, which never extended any of the auto-registration marker
interfaces and was never subject to this ambiguity.
A regression test guards this permanently:
// SharedLocationServiceRegistrationShould.NeverExtendAnyAutoRegistrationMarkerInterface
Assert.DoesNotContain(typeof(ISharedLocationService).GetInterfaces(),
new[] { typeof(ITransientService), typeof(IScopedService), typeof(ISingletonService) }.Contains);
6.6 What was deliberately not changed
- The registration DTO still requires
CountryId/RegionId/CityIdas GUIDs, not names. Names are not supported anywhere in the current contract, and no locale-dependent name-matching was added — adding one silently would risk ambiguous matches (e.g. two cities named "الرياض" in different regions). - No production data was hardcoded, and no Saudi/Riyadh-only shortcut was added — validation is fully
generic across every country/region/city row in
SharedDbContext. - Invalid location IDs return a clear validation failure (never
NotImplementedException, guarded byRegisterNewTenantRequestValidatorShould.NeverThrow_RegardlessOfLocationValidationOutcome) — they never expose internal implementation details.
6.7 Validation rules
SharedLocationService implements exactly the rules the task required:
| Rule | Method |
|---|---|
| Country exists and is active | CountryExistsAsync |
| Region exists and is active | RegionExistsAsync |
| City exists and is active | CityExistsAsync |
| Region belongs to the selected country | RegionBelongsToCountryAsync |
| City belongs to the selected region | CityBelongsToRegionAsync |
| City belongs to the selected country (defense in depth) | CityBelongsToCountryAsync |
IsActive filtering was added to all six methods during this fix — previously the validator did not
check IsActive at all, while RegistrationReferenceService's dropdowns already did. An ID that was
never offered to the frontend (inactive) could previously still pass validation; it can no longer.
6.8 A note on the dev environment itself
While resolving real IDs for a live dev test, direct SQL inspection of the dev database
(sqlcmd/DatabaseSettings__ConnectionString) and the running Shumoul.Api process's own
SharedDbContext queries returned different Country/Region/City datasets, despite both reporting
the same server/database name. This was never fully explained during this investigation — considered
and ruled out: response caching, a second IRegistrationReferenceService implementation, an EF
query-filter mismatch. Permission counts and pricing data matched exactly between the two connections;
only Countries/Regions/Cities differed. Separately, in the app's own dataset, the literal "الرياض"
(Riyadh) region had zero linked cities — an unrelated, pre-existing data gap, not caused by this fix.
The live dev verification in this guide therefore substitutes "Eastern Province" / "Dammam" for the
user's literal "Riyadh region" / "Riyadh city" scenario. This substitution and the dataset discrepancy
are environment-specific findings for this dev machine — they do not indicate a code defect and do not
require any application change. See Chapter 13 and the final report for this
task for the recommended read-only production checks.
