Skip to main content
Başlangıç verisi üç ayrı kaynaktan gelir:
  1. DevDataSeeder — config’ten okunan bootstrap admin’leri (hosted service).
  2. DistrictSeeder — 973 ilçe, SeedData/Districts.csv dosyasından (hosted service).
  3. Fluent HasData — Role, Permission ve RolePermission gibi referans verileri (migration ile kalıcı).
Üçü de AddInfrastructureEFCore içinde kaydedilir:
services.AddHostedService<MigrationService>();   // önce migrate
services.AddHostedService<DevDataSeeder>();      // bootstrap admin
services.AddHostedService<DistrictSeeder>();     // ilçeler

DevDataSeeder — bootstrap admin

Yönetim paneli (admin realm’ı) davet bazlı çalışır: kullanıcı DB’de yoksa Keycloak login’i reddedilir. Yeni bir deployment’ın kilitlenmemesi için bu seeder en az bir SuperAdmin’i (ilk davet edeni) yazar. Seed edilecek liste config’ten okunur:
{
  "SeedWork": {
    "BootstrapAdmins": [
      {
        "Email": "admin@example.com",
        "FullName": "İlk Yönetici",
        "Roles": [ "SuperAdmin" ]
      }
    ]
  }
}
Çekirdek mantık (idempotent, factory + role atama):
public async Task StartAsync(CancellationToken cancellationToken)
{
    var seeds = _configuration
        .GetSection("SeedWork:BootstrapAdmins")
        .Get<List<BootstrapAdminConfig>>() ?? new();

    if (seeds.Count == 0)
    {
        _logger.LogDebug("[Seeder] SeedWork:BootstrapAdmins boş — atlandı.");
        return;                                  // no-op — yeni deployment için güvenli default
    }

    await using var scope = _serviceProvider.CreateAsyncScope();
    var userRepo = scope.ServiceProvider.GetRequiredService<IRepository<User>>();
    var userFact = scope.ServiceProvider.GetRequiredService<IUserFactory>();
    var uow      = scope.ServiceProvider.GetRequiredService<IUnitOfWork>();

    foreach (var seed in seeds)
    {
        var email    = new Email(seed.Email);
        var existing = await userRepo.FirstOrDefaultAsync(new UserByEmailSpecification(email), cancellationToken);
        if (existing is not null) continue;       // idempotent — zaten varsa atla

        var user = await userFact.CreateWithEmailAsync(new FullName(seed.FullName), email, cancellationToken);
        user.ActivateOnExternalLogin();           // Status = Active

        foreach (var roleName in seed.Roles ?? Array.Empty<string>())
        {
            var role = Enumeration.GetAll<Role>()
                .FirstOrDefault(r => string.Equals(r.Name, roleName, StringComparison.OrdinalIgnoreCase));
            if (role is null) continue;
            user.AssignRole(role.Id, tenantId: null, assignedBy: SystemUserId);
        }

        await userRepo.AddAsync(user, cancellationToken);
    }

    await uow.SaveEntitiesAsync(cancellationToken);
}
Davranış özeti:
  • Liste boşsa no-op. Hiç kullanıcı yaratılmaz.
  • İdempotent. Email zaten DB’deyse atlanır.
  • Aktif yazılır. ActivateOnExternalLogin() ile Status = Active; ilk login’de ek adım kalmaz.
  • Factory + AssignRole. Kullanıcı IUserFactory ile üretilir, roller Role enumeration’dan eşleştirilir.
Dev test admin’leri (cihan.delipinar@diyanet.gov.tr, safa.goksel@diyanet.gov.tr) artık hardcoded değil; appsettings.Development.json içindeki SeedWork:BootstrapAdmins listesinde tanımlıdır.

DistrictSeeder — 973 ilçe

İlçeler CSV’den okunur ve DB’de olmayanlar eklenir. Tüm ortamlarda çalışır (City enumeration referans bütünlüğü için zorunlu) ve idempotenttir:
var existingIds = await db.Districts.Select(d => d.Id).ToListAsync(cancellationToken);
var existingSet = new HashSet<int>(existingIds);

var toAdd = new List<District>();
using var reader = new StreamReader(csvPath);     // SeedData/Districts.csv
// header atlanır; her satır: id,cityId,name
while ((line = await reader.ReadLineAsync(cancellationToken)) is not null)
{
    // ... parse ...
    if (existingSet.Contains(id)) continue;       // idempotent
    toAdd.Add(new District(id, cityId, name));
}

if (toAdd.Count == 0)
{
    _logger.LogInformation("[DistrictSeeder] Tüm ilçeler zaten yüklü ({Total} kayıt).", existingIds.Count);
    return;
}

await db.Districts.AddRangeAsync(toAdd, cancellationToken);
await db.SaveChangesAsync(cancellationToken);     // AuditInterceptor SystemUserId atar
CSV iki konumdan aranır (önce AppContext.BaseDirectory, sonra çalışma dizini):
Path.Combine(AppContext.BaseDirectory,        "SeedData", "Districts.csv"),
Path.Combine(Directory.GetCurrentDirectory(), "SeedData", "Districts.csv"),
Dosya bulunamazsa uyarı loglanıp seeder atlanır (exception fırlatmaz).

Fluent HasData — referans verisi

Enumeration türevi roller ve varsayılan rol-izin matrisi IEntityTypeConfiguration içindeki HasData ile migration’a gömülür (kalıcı, versiyonlu). Role seed’i:
builder.HasData(
    new { Id = Role.SuperAdmin.Id, Name = Role.SuperAdmin.Name },
    new { Id = Role.Admin.Id,      Name = Role.Admin.Name      },
    new { Id = Role.Staff.Id,      Name = Role.Staff.Name      },
    new { Id = Role.ReadOnly.Id,   Name = Role.ReadOnly.Name   }
);
RolePermission seed’i deterministik Guid ile üretilir (migration tutarlılığı için sabit Id’ler şart):
// Deterministik Guid: rolId üç bloğa, son blok 12 hane = permission Id.
private static Guid Seed(int roleId, int permissionId) =>
    new Guid($"a{roleId:D7}-{roleId:D4}-{roleId:D4}-{roleId:D4}-{permissionId:D12}");

private static readonly DateTime SeedDate = new(2026, 5, 18, 0, 0, 0, DateTimeKind.Utc);

builder.HasData(
    new { Id = Seed(Role.SuperAdmin.Id, Permission.UsersRead.Id),
          RoleId = Role.SuperAdmin.Id, PermissionId = Permission.UsersRead.Id,
          CreatedAt = (DateTime?)SeedDate },
    // ... varsayılan SuperAdmin × Admin × Staff × ReadOnly matrisi
);
HasData değerleri deterministik olmalı (DateTime.Now, Guid.NewGuid() gibi runtime değer KULLANILMAZ) — aksi halde her migration üretiminde model snapshot değişir ve gereksiz migration oluşur. Bu yüzden SeedDate sabittir ve Guid’ler Seed(...) ile hesaplanır.

Prod’da davranış

SeederProduction davranışı
DevDataSeederSeedWork:BootstrapAdmins doluysa çalışır, boşsa no-op. Prod’da genelde appsettings.Production.json ya da env-file (SeedWork__BootstrapAdmins__0__Email=...) ile en az bir SuperAdmin verilir; boş bırakılırsa hiç user yaratılmaz, DBA manuel ekler.
DistrictSeederTüm ortamlarda çalışır (idempotent). İlçeler master data olduğundan prod’da da yüklenmelidir.
HasDataMigration’ın parçası — her ortamda MigrateAsync ile uygulanır.
DevDataSeeder adı yanıltıcı olabilir: production’da otomatik olarak kapanan bir şey değildir; davranışını tamamen SeedWork:BootstrapAdmins config’i belirler. Listeyi boş bırakmak prod için güvenli default’tur.

İlgili

Migrations

MigrationService ve seed’lerin çalışma sırası.

DbContext & Repository

IUnitOfWork.SaveEntitiesAsync ve repository kullanımı.

Audit Interceptor

Seed sırasında SystemActor.Id’nin audit alanlarına yazılması.

Data Genel Bakış

Katman rolü ve klasör yapısı.