Skip to main content
Version: Latest

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/Citiesthe 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 in Shumoul.Infrastructure's ServiceCollectionExtensions.AddInfrustracture().
  • NotImplementedSharedLocationService → registered via TryAddTransient in Shumoul.Framework.MultiTenancy.Api's Startup.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/CityId as 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 by RegisterNewTenantRequestValidatorShould.NeverThrow_RegardlessOfLocationValidationOutcome) — they never expose internal implementation details.

6.7 Validation rules

SharedLocationService implements exactly the rules the task required:

RuleMethod
Country exists and is activeCountryExistsAsync
Region exists and is activeRegionExistsAsync
City exists and is activeCityExistsAsync
Region belongs to the selected countryRegionBelongsToCountryAsync
City belongs to the selected regionCityBelongsToRegionAsync
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.