Başlangıç verisi üç ayrı kaynaktan gelir:
DevDataSeeder — config’ten okunan bootstrap admin’leri (hosted service).
DistrictSeeder — 973 ilçe, SeedData/Districts.csv dosyasından (hosted service).
- 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ış
| Seeder | Production davranışı |
|---|
DevDataSeeder | SeedWork: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. |
DistrictSeeder | Tüm ortamlarda çalışır (idempotent). İlçeler master data olduğundan prod’da da yüklenmelidir. |
HasData | Migration’ı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ı.