4. Seed Architecture and the SeedHistory Gotcha
4.1 The gotcha that shaped every seeder in this guide
BackEnd's original seeders — SubscriptionPackageSeeder, SubscriptionPlanSeeder,
SubscriptionFeatureSeeder (the BackEnd copy), PlanPackagesSeeder, Starter/Basic/ AdvancedPackageFeaturesSeeder, SubscriptionFeatureReportActionSeeder — are all wrapped in a shared
ExecuteSeedIfNeededAsync(successSeedsHistory, seedKey, seedAction) helper:
if (successSeedsHistory.Any(q => q.SeedKey == seedKey)) return;
This means each of those seeders' entire body runs at most once, ever, per environment — recorded
in TenantDbContext.SeedsHistory. Adding new rows to their JSON files and redeploying does nothing
in an already-seeded dev/staging/production database; the seed key is already recorded, so the body
never runs again to pick up the new rows. SubscriptionPackageSeeder additionally double-guards with
if (!_db.Packages.Any()) inside the body itself.
The four Shumoul packages therefore could not be added by editing those existing JSON files — every
new seeder in this guide is a brand-new class, with its own never-before-seen SeedKey identity, and
no SeedHistory gate at all, following the pattern already established by the newer
SubscriptionFeatureSettingsGroupSeeder (no gate, runs every startup, a pure ComputeSeedPlan static
method decides insert vs. skip vs. update from plain in-memory collections).
4.2 The five new seeders
All live in Shumoul.Framework.MultiTenancy.Api (Packages: 25–29):
| Seeder | Priority | Embedded JSON | Identity |
|---|---|---|---|
ShumoulSubscriptionPackageSeeder | 25 | Packages/ShumoulSubscriptionPackages.json | exact SubscriptionPackage.FName (no Code column exists — see Chapter 3) |
ShumoulPlanPackageSeeder | 26 | — (computed from the 4 packages × every active SubscriptionPlan) | (PlanId, PackageId) pair |
ShumoulPackageFeatureSeeder | 27 | Packages/ShumoulPackageFeatures.json | (Package.FName, Feature.Key) pair |
ShumoulPackageFeatureRuleSeeder | 28 | Packages/ShumoulStarterPackageFeatureRules.json | (SubscriptionPackageFeature.Id, EntityName, ApplyPeriod) triple |
ShumoulFeatureReportActionSeeder | 29 | FeatureReportActions/ShumoulFeatureReportActions.json | (SubscriptionFeatureId, AppReportNameActionId) pair |
Each computes its insert/skip plan via a pure static ComputeSeedPlan method (see
Chapter 11) — no live database or embedded JSON required to unit
test the decision logic. Live-verified: first startup inserted 4 packages, 8 plan-packages, 14
package-features, 5 rules, 21 report-action links; every subsequent startup (confirmed across 3
consecutive re-seed passes during this task's own dev session) inserted 0 new rows and logged
"already exists" for every package — exactly the intended idempotent behavior.
4.3 One additive edit to an existing, already-safe file
FeatureSettingsGroups/SubscriptionFeatureSettingsGroups.json (MultiTenancyApi repo) — unlike the
BackEnd seeders above, SubscriptionFeatureSettingsGroupSeeder has no SeedHistory gate at all and
re-syncs every startup, so it was safe to add one new row directly:
{ "FeatureKey": "Features.AccountManagement", "SettingsGroupKey": "TaxSettings", "IsActive": true, "DisplayOrder": 29 }
This was necessary because no existing finance-only feature granted TaxSettings — both Finance
Starter and Finance Advanced need it for VAT/tax report and settings coverage. No existing row was
changed or removed.
