# IClientSeedDataProvider Template

> Referenced from `steps/step-03b-provider.md` — C# template for client project seed data providers.

---

## Provider Class Template

**Location:** `Infrastructure/Persistence/Seeding/{AppPascalName}SeedDataProvider.cs`

Where `{AppPascalName}` is derived from the application code (e.g., `free-bike` → `FreeBike`).

```csharp
using Microsoft.EntityFrameworkCore;
using SmartStack.Application.Common.Interfaces;
using SmartStack.Domain.Navigation;
using SmartStack.Domain.Platform.Administration.Roles;

namespace {BaseNamespace}.Infrastructure.Persistence.Seeding;

/// <summary>
/// Seeds {AppLabel} navigation, roles, permissions, and role-permission data
/// into the SmartStack Core schema at application startup.
/// Implements <see cref="IClientSeedDataProvider"/> for runtime seeding
/// (no Core migrations required).
/// </summary>
public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
{
    public int Order => 100;

    public async Task SeedNavigationAsync(ICoreDbContext context, CancellationToken ct)
    {
        // --- Application (idempotent — reuse if already exists) ---
        var appEntry = NavigationApplicationSeedData.GetApplicationEntry();
        var existingApp = await context.NavigationApplications
            .FirstOrDefaultAsync(a => a.Code == appEntry.Code, ct);

        NavigationApplication app;
        if (existingApp != null)
        {
            app = existingApp; // Reuse existing for module seeding below
        }
        else
        {
            // v3.46+ signature : Zone removed. New flags isOpen + isPersonal at the END (optional, default false).
            app = NavigationApplication.Create(
                appEntry.Code, appEntry.Label,
                appEntry.Description, appEntry.Icon, appEntry.IconType,
                appEntry.Route, appEntry.DisplayOrder,
                isOpen: appEntry.IsOpen, isPersonal: appEntry.IsPersonal);
            context.NavigationApplications.Add(app);
            await ((DbContext)context).SaveChangesAsync(ct);

            // Application translations (only when creating new application)
            foreach (var t in NavigationApplicationSeedData.GetTranslationEntries())
            {
                context.NavigationTranslations.Add(
                    NavigationTranslation.Create(t.EntityType, app.Id, t.LanguageCode, t.Label, t.Description));
            }
            await ((DbContext)context).SaveChangesAsync(ct);
        }

        // --- Modules (idempotent per-module) ---
        // var mod1Exists = await context.NavigationModules.AnyAsync(
        //     m => m.Code == "{module_code}" && m.ApplicationId == app.Id, ct);
        // if (!mod1Exists) { context.NavigationModules.Add(NavigationModule.Create(...)); }
        // Repeat per module...
        await ((DbContext)context).SaveChangesAsync(ct);

        // Resolve module entities
        // var mod1Entity = await context.NavigationModules.FirstAsync(
        //     m => m.Code == "{module_code}" && m.ApplicationId == app.Id, ct);

        // --- Module translations (IDEMPOTENT — check before inserting) ---
        // CRITICAL: nav_Translations has unique index IX_nav_Translations_EntityType_EntityId_LanguageCode
        // Always guard with AnyAsync to prevent duplicate key errors on re-runs.
        // CRITICAL: Use modEntity.Id (actual DB ID), NOT seed-time GUID from SeedData class.
        // if (!await context.NavigationTranslations.AnyAsync(
        //     t => t.EntityId == mod1Entity.Id
        //          && t.EntityType == NavigationEntityType.Module, ct))
        // { foreach (var t in {Module}NavigationSeedData.GetTranslationEntries())
        //   { context.NavigationTranslations.Add(
        //       NavigationTranslation.Create(t.EntityType, mod1Entity.Id, t.LanguageCode, t.Label, t.Description)); } }
        await ((DbContext)context).SaveChangesAsync(ct);

        // --- Sections (idempotent per-section) ---
        // Check each section before inserting: AnyAsync(s => s.Code == secEntry.Code && s.ModuleId == ...)

        // --- Section translations (IDEMPOTENT — same guard pattern as module translations) ---
        // CRITICAL: secEntry.Id is a seed-time GUID ≠ actual DB ID.
        // Resolve actual section from DB: var actualSection = await context.NavigationSections
        //     .FirstAsync(s => s.Code == secEntry.Code && s.ModuleId == modEntity.Id, ct);
        // if (!await context.NavigationTranslations.AnyAsync(
        //     t => t.EntityId == actualSection.Id && t.EntityType == NavigationEntityType.Section, ct))

        // --- Resources (idempotent — use ACTUAL section IDs from DB) ---
        // CRITICAL: NavigationSection.Create() generates its own random ID.
        // MUST query actual section from DB to get real ID for FK references:
        // var actualSection = await context.NavigationSections
        //     .FirstAsync(s => s.Code == secEntry.Code && s.ModuleId == modEntity.Id, ct);
        // Then use actualSection.Id for NavigationResource.Create() and AnyAsync
    }

    public async Task SeedRolesAsync(ICoreDbContext context, CancellationToken ct)
    {
        // Resolve application from DB by Code — NOT seed-time GUID
        var app = await context.NavigationApplications
            .FirstAsync(a => a.Code == "{app_code}", ct);

        // Check idempotence — scoped by actual DB ApplicationId
        var exists = await context.Roles
            .AnyAsync(r => r.ApplicationId == app.Id, ct);
        if (exists) return;

        // Create application-scoped roles (Admin, Manager, Contributor, Viewer)
        // Use data from ApplicationRolesSeedData.cs
        foreach (var entry in ApplicationRolesSeedData.GetRoleEntries())
        {
            var role = Role.Create(
                name: entry.Name,
                shortName: entry.Code,
                category: RoleCategory.Application,
                description: entry.Description,
                isSystem: entry.IsSystem,
                applicationId: app.Id,
                code: entry.Code);
            context.Roles.Add(role);
        }
        await ((DbContext)context).SaveChangesAsync(ct);
    }

    public async Task SeedPermissionsAsync(ICoreDbContext context, CancellationToken ct)
    {
        // Check idempotence
        var exists = await context.Permissions
            .AnyAsync(p => p.Path == "{full_path}.*", ct);
        if (exists) return;

        // Retrieve modules by Code for FK resolution
        // Use data from {Module}PermissionSeedData.cs
        // Create via Permission.CreateForModule(...) and Permission.CreateWildcard(...)
        // ...
        await ((DbContext)context).SaveChangesAsync(ct);
    }

    public async Task SeedRolePermissionsAsync(ICoreDbContext context, CancellationToken ct)
    {
        // Check idempotence
        var exists = await context.RolePermissions
            .AnyAsync(rp => rp.Permission!.Path.StartsWith("{full_path}."), ct);
        if (exists) return;

        // Retrieve existing roles and created permissions
        // Use data from {Module}RolePermissionSeedData.cs
        // Create via RolePermission.Create(roleId, permissionId, "system")
        // ...
        await ((DbContext)context).SaveChangesAsync(ct);
    }

    // Private helper methods fed by SeedData classes
    // ...
}
```

---

## DI Registration

Modify `Infrastructure/DependencyInjection.cs` of the client project:

```csharp
// Add using:
using SmartStack.Application.Common.Interfaces;

// In the registration method:
services.AddScoped<IClientSeedDataProvider, {AppPascalName}SeedDataProvider>();
```

---

## Critical Rules

1. **Factory methods mandatory**: `NavigationApplication.Create(zone, ...)`, `NavigationModule.Create(...)`, `Role.Create(name, shortName, category, ...)`, `Permission.CreateForModule(...)`, `RolePermission.Create(...)` — NEVER `new Entity()`
2. **Idempotence**: Each Seed method checks existence before inserting. **Translations require explicit `AnyAsync` guard** — unique index `IX_nav_Translations_EntityType_EntityId_LanguageCode` causes crashes on duplicate inserts
3. **Translation EntityId**: ALWAYS use actual DB ID (e.g. `app.Id`, `mod1Entity.Id`, `actualSection.Id`), NEVER seed-time GUID from SeedData classes
4. **Translation guard pattern**: `if (!await context.NavigationTranslations.AnyAsync(t => t.EntityId == {actualDbId} && t.EntityType == ..., ct))` — MANDATORY before every translation batch insert
5. **SaveChangesAsync per group**: Navigation → save → Roles → save → Permissions → save → RolePermissions → save
6. **Execution order**: `SeedRolesAsync()` MUST be called BEFORE `SeedRolePermissionsAsync()` (roles must exist before mapping)
7. **Random GUIDs**: ALWAYS use `Guid.NewGuid()` — resolve FKs by Code lookup, not fixed IDs
8. **Resolve FK by Code**: Parent entities (applications, modules, roles) are found by `Code` from DB, not by seed-time GUID
9. **Order property**: Use `100` as default. If multiple providers exist, they run in Order sequence
