Skip to main content
Version: Latest

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):

SeederPriorityEmbedded JSONIdentity
ShumoulSubscriptionPackageSeeder25Packages/ShumoulSubscriptionPackages.jsonexact SubscriptionPackage.FName (no Code column exists — see Chapter 3)
ShumoulPlanPackageSeeder26— (computed from the 4 packages × every active SubscriptionPlan)(PlanId, PackageId) pair
ShumoulPackageFeatureSeeder27Packages/ShumoulPackageFeatures.json(Package.FName, Feature.Key) pair
ShumoulPackageFeatureRuleSeeder28Packages/ShumoulStarterPackageFeatureRules.json(SubscriptionPackageFeature.Id, EntityName, ApplyPeriod) triple
ShumoulFeatureReportActionSeeder29FeatureReportActions/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.