Skip to content

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.

Add the module package to your bundle:

Terminal window
dotnet add package Granit.Parties.MultiTenancy

The 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.

When Granit.MultiTenancy raises TenantCreatedEvent(tenantId, name, identifier):

  1. The handler resolves IDefaultPartySeeder from DI.
  2. The seeder asks IDefaultPartyResolver whether a party already exists for tenantId (via the reserved tenant external mapping). If yes, it returns the existing one — replays of the same event are no-ops.
  3. Otherwise it creates a host-scoped (TenantId == null) Company party named after the tenant, attaches the PartyExternalProviderNames.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).

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);
}
}

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.