Seed the default party at tenant provisioning
Downstream modules (Invoicing, Subscriptions, Payments, Tax, CustomerBalance)
resolve the billing identity for a tenant via IDefaultPartyResolver, which
looks up the host-scoped Party carrying the reserved
PartyExternalProviderNames.Tenant external mapping. That party has to
exist before any of them can issue an invoice, charge a card, or run a tax
calculation against the tenant.
Granit.Parties.MultiTenancy automates the creation: it ships a
Wolverine-discovered handler subscribing to TenantCreatedEvent that calls
IDefaultPartySeeder.SeedForTenantAsync. The seeder is idempotent —
Wolverine’s at-least-once delivery is safe.
Opt in
Section titled “Opt in”Add the module package to your bundle:
dotnet add package Granit.Parties.MultiTenancyThe base Granit.Parties package stays free of any Granit.MultiTenancy
dependency — apps that do not run a multi-tenant stack do not pay for it. The
seeder itself is registered by Granit.Parties.EntityFrameworkCore regardless.
What it does
Section titled “What it does”When Granit.MultiTenancy raises TenantCreatedEvent(tenantId, name, identifier):
- The handler resolves
IDefaultPartySeederfrom DI. - The seeder asks
IDefaultPartyResolverwhether a party already exists fortenantId(via the reservedtenantexternal mapping). If yes, it returns the existing one — replays of the same event are no-ops. - Otherwise it creates a host-scoped (
TenantId == null)Companyparty named after the tenant, attaches thePartyExternalProviderNames.Tenant = tenantId.ToString()external mapping, and persists it with the multi-tenant query filter disabled (the handler typically runs in the freshly-created tenant’s scope).
Backfill for pre-existing tenants
Section titled “Backfill for pre-existing tenants”Apps that already have tenants when adopting Granit.Parties should call the
seeder once per existing tenant during their own migration script. The framework
does not ship a SQL backfill — that work belongs in the consuming app’s
migration runner alongside the rest of its data setup. Pseudo-code:
public async Task BackfillAsync(IServiceProvider sp, CancellationToken ct){ var seeder = sp.GetRequiredService<IDefaultPartySeeder>(); var tenants = sp.GetRequiredService<ITenantReader>(); foreach (var t in await tenants.ListAsync(ct)) { await seeder.SeedForTenantAsync(t.Id, t.Name, cancellationToken: ct); }}Customising the seeded party
Section titled “Customising the seeded party”Apps that need to seed extra fields (taxId, billing address, language…) should
register their own subscriber for TenantCreatedEvent after the framework
handler — read the just-created party via IPartyReader.GetByIdAsync,
mutate it through the aggregate’s methods (UpdateContact, AddAddress, …),
and persist with IPartyWriter.UpdateAsync. The seeder only ships the
identity-level baseline so it stays composable.