ASP.NET Core 9 MVC — Bölüm 3: EF Core 9 ile Veritabanı, Migration, Repository ve Service Pattern
Katmanlı mimari (Core/Infrastructure/Web), EF Core 9 DbContext setup, IEntityTypeConfiguration, migration workflow, UseAsyncSeeding, LINQ best practice'leri (AsNoTracking, Include/ThenInclude, N+1, IQueryable vs IEnumerable), Repository ve Service pattern — .NET 9 ile modern veri erişim katmanı.
Merhaba arkadaşlar, bu makalemizde ASP.NET Core 9 MVC serisinin üçüncü bölümüne geldik ve artık konu veritabanı. EF Core 9 ile modern bir veri erişim katmanı kuracağız — DbContext setup'ından başlayıp migration workflow'una, Repository + Service pattern'lerine, LINQ best practice'lere ve .NET 9 ile gelen EF yeniliklerine kadar. İşe yarar, production-grade bir yapı.
EF Core 9'da Neler Yeni?
.NET 9 ile birlikte EF Core 9 çıktı ve önemli iyileştirmeler getirdi:
- Complex types iyileştirmesi: Value object'leri collection olarak da kullanabiliyoruz.
- Azure Cosmos DB provider yeniden yazıldı — LINQ daha eksiksiz, vector search hazır.
- UseSeeding ve UseAsyncSeeding: Migration bağımlı olmadan veri seed'i daha temiz yol.
- Read-only primitive collection: EF artık IReadOnlyList<int> property'leri primitive collection olarak map edebilir.
- Translate new LINQ operators: IN ile birlikte parametre optimizasyonu.
Bu bölümde bunların çoğunu kullanıyor olacağız.
Projeleri Bölelim — Clean Architecture Katmanları
Şimdiye kadar her şey SemGoksuMvc.Web içindeydi. Artık ayırma zamanı:
dotnet new classlib -n SemGoksuMvc.Core -o src/SemGoksuMvc.Core
dotnet new classlib -n SemGoksuMvc.Infrastructure -o src/SemGoksuMvc.Infrastructure
dotnet sln add src/SemGoksuMvc.Core src/SemGoksuMvc.Infrastructure
cd src/SemGoksuMvc.Infrastructure
dotnet add reference ../SemGoksuMvc.Core
cd ../SemGoksuMvc.Web
dotnet add reference ../SemGoksuMvc.Core ../SemGoksuMvc.Infrastructure
Bağımlılık yönü: Web → Infrastructure → Core. Core hiçbir şeye bağımlı olmuyor, en içte.
Entity'leri Core Katmanına Yazalım
// Core/Entities/BaseEntity.cs
public abstract class BaseEntity
{
public int Id { get; set; }
public DateTime OlusturmaTarihi { get; set; } = DateTime.UtcNow;
public DateTime? GuncellemeTarihi { get; set; }
public bool AktifMi { get; set; } = true;
public bool SilindiMi { get; set; }
}
// Core/Entities/Kategori.cs
public class Kategori : BaseEntity
{
public required string Ad { get; set; }
public required string Slug { get; set; }
public string? Aciklama { get; set; }
public int SiraNo { get; set; }
public ICollection<Urun> Urunler { get; set; } = new List<Urun>();
}
// Core/Entities/Urun.cs
public class Urun : BaseEntity
{
public required string Ad { get; set; }
public required string Slug { get; set; }
public string? Aciklama { get; set; }
public decimal Fiyat { get; set; }
public int StokMiktari { get; set; }
public int KategoriId { get; set; }
public Kategori? Kategori { get; set; }
public ICollection<UrunEtiket> Etiketler { get; set; } = new List<UrunEtiket>();
}
public class UrunEtiket : BaseEntity
{
public required string Ad { get; set; }
public ICollection<Urun> Urunler { get; set; } = new List<Urun>();
}
Visual Studio'dan: Visual Studio tarafinda Tools > NuGet Package Manager > Package Manager Console menusunden PM konsoluna erisiriz. Default project secimine dikkat.
DbContext — EF Core'un Kalbi
dotnet add package Microsoft.EntityFrameworkCore --project src/SemGoksuMvc.Infrastructure
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --project src/SemGoksuMvc.Infrastructure
dotnet add package Microsoft.EntityFrameworkCore.Design --project src/SemGoksuMvc.Web
// Infrastructure/Persistence/AppDbContext.cs
using Microsoft.EntityFrameworkCore;
using SemGoksuMvc.Core.Entities;
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
{
public DbSet<Kategori> Kategoriler => Set<Kategori>();
public DbSet<Urun> Urunler => Set<Urun>();
public DbSet<UrunEtiket> Etiketler => Set<UrunEtiket>();
protected override void OnModelCreating(ModelBuilder b)
{
b.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
// Global filter: soft-delete edilmis kayitlar gelmesin
b.Entity<Kategori>().HasQueryFilter(x => !x.SilindiMi);
b.Entity<Urun>().HasQueryFilter(x => !x.SilindiMi);
b.Entity<UrunEtiket>().HasQueryFilter(x => !x.SilindiMi);
}
public override async Task<int> SaveChangesAsync(CancellationToken ct = default)
{
foreach (var entry in ChangeTracker.Entries<BaseEntity>())
{
if (entry.State == EntityState.Modified)
entry.Entity.GuncellemeTarihi = DateTime.UtcNow;
}
return await base.SaveChangesAsync(ct);
}
}
Entity Configuration — IEntityTypeConfiguration
OnModelCreating içinde her entity'yi konfigüre etmek dosyayı şişirir. Ayrı dosyalar yapalım:
// Infrastructure/Persistence/Configurations/UrunConfig.cs
public class UrunConfig : IEntityTypeConfiguration<Urun>
{
public void Configure(EntityTypeBuilder<Urun> b)
{
b.ToTable("Urunler");
b.HasKey(x => x.Id);
b.Property(x => x.Ad).HasMaxLength(200).IsRequired();
b.Property(x => x.Slug).HasMaxLength(220).IsRequired();
b.HasIndex(x => x.Slug).IsUnique();
b.Property(x => x.Fiyat).HasPrecision(18, 2);
b.HasOne(x => x.Kategori)
.WithMany(k => k.Urunler)
.HasForeignKey(x => x.KategoriId)
.OnDelete(DeleteBehavior.Restrict);
}
}
Bu yaklaşım scalable — 50 entity'li projede her biri ayrı dosyada, düzenli.
DI Kaydı
// Program.cs (Web)
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
Migration Workflow
İlk migration oluşturup DB'yi yaratıyoruz:
cd src/SemGoksuMvc.Web
dotnet ef migrations add IlkMigration -p ../SemGoksuMvc.Infrastructure -s .
dotnet ef database update -p ../SemGoksuMvc.Infrastructure -s .
-p parametresi migration'ların yazılacağı projeyi, -s startup projesini belirtir. Migration dosyaları Infrastructure/Migrations/ altında birikecek.
Şema değişikliği yaptığınızda:
dotnet ef migrations add UrunleriyleBirlikteEtikletErtile -p ... -s .
dotnet ef database update -p ... -s .
Migration'ı geri almak için:
dotnet ef database update IlkMigration -p ... -s . # belirli bir migration'a don
dotnet ef migrations remove -p ... -s . # son migration'i sil
Production'da dotnet ef database update çalıştırmayın — SQL script'i üretip DBA'inizle kontrol ettirin:
dotnet ef migrations script OncekiMigration YeniMigration -o migration.sql
Seed Data — UseSeeding ile
EF Core 9 ile gelen yeni seed API'si çok daha temiz:
builder.Services.AddDbContext<AppDbContext>((sp, options) =>
{
options.UseSqlServer(config.GetConnectionString("Default"));
options.UseAsyncSeeding(async (ctx, _, ct) =>
{
if (!await ctx.Set<Kategori>().AnyAsync(ct))
{
ctx.Set<Kategori>().AddRange(
new Kategori { Ad = "Elektronik", Slug = "elektronik" },
new Kategori { Ad = "Kitap", Slug = "kitap" },
new Kategori { Ad = "Giyim", Slug = "giyim" });
await ctx.SaveChangesAsync(ct);
}
});
});
LINQ Best Practices
EF'nin üretmeyi en çok zorladığı SQL'ler LINQ yazım tarzınızdan doğar. Bilmeniz gereken kurallar:
1. AsNoTracking — Sadece Okuma İşlemlerinde
// Kotu - change tracker'a giriyor, gereksiz bellek
var urunler = await _ctx.Urunler.ToListAsync();
// Iyi - sadece oku, tracking yapma
var urunler = await _ctx.Urunler.AsNoTracking().ToListAsync();
Listeleme, read-only operasyonlarda AsNoTracking kullanın, %30-50'ye varan performans kazancı normaldir.
2. Include / ThenInclude ile Eager Loading
// Eager loading - tek sorguda related data gelir
var urunler = await _ctx.Urunler
.Include(u => u.Kategori)
.Include(u => u.Etiketler)
.AsNoTracking()
.ToListAsync();
// Nested: Kategori'nin altindaki DigerUrunleri de getir
var urun = await _ctx.Urunler
.Include(u => u.Kategori)
.ThenInclude(k => k.Urunler)
.FirstOrDefaultAsync(u => u.Id == id);
Alternatif: Projection — sadece ihtiyacınız olan alanları seçmek çoğu zaman daha iyi:
var urunler = await _ctx.Urunler
.AsNoTracking()
.Select(u => new UrunListeItem(u.Id, u.Ad, u.Kategori!.Ad, u.Fiyat))
.ToListAsync();
3. N+1 Tuzağı
// KOTU - her urun icin ayri sorgu
var urunler = await _ctx.Urunler.ToListAsync();
foreach (var u in urunler)
{
Console.WriteLine(u.Kategori!.Ad); // Lazy loading / N+1
}
// IYI - Include ile tek sorgu
var urunler = await _ctx.Urunler
.Include(u => u.Kategori)
.ToListAsync();
4. IQueryable vs IEnumerable
// IQueryable - sorgu SQL'e cevrilir
IQueryable<Urun> sorgu = _ctx.Urunler;
sorgu = sorgu.Where(u => u.AktifMi);
if (kategoriId.HasValue)
sorgu = sorgu.Where(u => u.KategoriId == kategoriId.Value);
var sonuc = await sorgu.ToListAsync(); // Tek SQL sorgusu
// IEnumerable - tum veri belleye yuklenir sonra filtrelenir (kotu!)
IEnumerable<Urun> tumu = (await _ctx.Urunler.ToListAsync());
var sonuc2 = tumu.Where(u => u.AktifMi).ToList(); // Bellek israfi
Repository Pattern — Gerekli mi?
EF Core zaten bir Unit of Work ve Repository gibi davranır (DbContext = UoW, DbSet = Repository). O yüzden gereksiz Repository katmanı eklemek anti-pattern olarak da görülebilir. Ama büyük projelerde bazı faydaları vardır:
- Test edilebilirlik: Mock'lamak kolaylaşır.
- Query encapsulation: Kompleks sorguları tek yerde toplar.
- DB provider bağımsızlığı: Teoride — pratikte nadir gerekir.
Basit bir generic repository:
// Core/Interfaces/IRepository.cs
public interface IRepository<T> where T : BaseEntity
{
Task<T?> GetirAsync(int id, CancellationToken ct = default);
Task<IReadOnlyList<T>> TumunuGetirAsync(CancellationToken ct = default);
Task EkleAsync(T entity, CancellationToken ct = default);
void Guncelle(T entity);
void Sil(T entity);
}
// Infrastructure/Repositories/Repository.cs
public class Repository<T>(AppDbContext ctx) : IRepository<T> where T : BaseEntity
{
protected readonly AppDbContext _ctx = ctx;
protected DbSet<T> _set => _ctx.Set<T>();
public async Task<T?> GetirAsync(int id, CancellationToken ct = default)
=> await _set.FindAsync(new object[] { id }, ct);
public async Task<IReadOnlyList<T>> TumunuGetirAsync(CancellationToken ct = default)
=> await _set.AsNoTracking().ToListAsync(ct);
public async Task EkleAsync(T entity, CancellationToken ct = default)
=> await _set.AddAsync(entity, ct);
public void Guncelle(T entity) => _set.Update(entity);
public void Sil(T entity) => _set.Remove(entity);
}
Özelleşmiş repository'lerde generic'i inherit edip ekleyebilirsiniz:
public interface IUrunRepository : IRepository<Urun>
{
Task<IReadOnlyList<Urun>> KategoriyeGoreGetirAsync(int katId, CancellationToken ct);
Task<Urun?> SlugIleGetirAsync(string slug, CancellationToken ct);
}
public class UrunRepository(AppDbContext ctx) : Repository<Urun>(ctx), IUrunRepository
{
public async Task<IReadOnlyList<Urun>> KategoriyeGoreGetirAsync(int katId, CancellationToken ct)
=> await _set.AsNoTracking()
.Where(u => u.KategoriId == katId)
.Include(u => u.Kategori)
.OrderByDescending(u => u.OlusturmaTarihi)
.ToListAsync(ct);
public async Task<Urun?> SlugIleGetirAsync(string slug, CancellationToken ct)
=> await _set.Include(u => u.Kategori)
.FirstOrDefaultAsync(u => u.Slug == slug, ct);
}
Service Katmanı — İş Kurallarının Evi
Controller doğrudan repository kullanmasın. Araya bir Service katmanı koyuyoruz — business logic orada:
// Core/Services/IUrunServisi.cs
public interface IUrunServisi
{
Task<IReadOnlyList<UrunListeDto>> SayfalaAsync(int sayfa, int boyut, CancellationToken ct);
Task<UrunDetayDto?> DetayGetirAsync(string slug, CancellationToken ct);
Task<int> EkleAsync(UrunEkleDto dto, CancellationToken ct);
Task GuncelleAsync(int id, UrunGuncelleDto dto, CancellationToken ct);
Task SilAsync(int id, CancellationToken ct);
}
// Infrastructure/Services/UrunServisi.cs
public class UrunServisi(AppDbContext ctx, IUrunRepository repo, ILogger<UrunServisi> log) : IUrunServisi
{
public async Task<IReadOnlyList<UrunListeDto>> SayfalaAsync(int sayfa, int boyut, CancellationToken ct)
{
return await ctx.Urunler
.AsNoTracking()
.OrderByDescending(u => u.OlusturmaTarihi)
.Skip((sayfa - 1) * boyut)
.Take(boyut)
.Select(u => new UrunListeDto(u.Id, u.Slug, u.Ad, u.Fiyat, u.Kategori!.Ad))
.ToListAsync(ct);
}
public async Task<int> EkleAsync(UrunEkleDto dto, CancellationToken ct)
{
// Is kurali: ayni isim altinda baska urun olmasin
var slug = SlugOlustur(dto.Ad);
if (await ctx.Urunler.AnyAsync(u => u.Slug == slug, ct))
throw new InvalidOperationException("Bu urun zaten mevcut");
var urun = new Urun
{
Ad = dto.Ad,
Slug = slug,
Aciklama = dto.Aciklama,
Fiyat = dto.Fiyat,
KategoriId = dto.KategoriId,
StokMiktari = dto.StokMiktari
};
await repo.EkleAsync(urun, ct);
await ctx.SaveChangesAsync(ct);
log.LogInformation("Urun eklendi: {Slug}", slug);
return urun.Id;
}
public async Task SilAsync(int id, CancellationToken ct)
{
var urun = await repo.GetirAsync(id, ct)
?? throw new KeyNotFoundException($"Urun bulunamadi: {id}");
urun.SilindiMi = true; // soft delete
repo.Guncelle(urun);
await ctx.SaveChangesAsync(ct);
}
private static string SlugOlustur(string ad)
=> ad.ToLowerInvariant().Replace(' ', '-');
}
DI Registration
// Program.cs
builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
builder.Services.AddScoped<IUrunRepository, UrunRepository>();
builder.Services.AddScoped<IUrunServisi, UrunServisi>();
Controller Kullanımı
public class UrunlerController(IUrunServisi servis) : Controller
{
public async Task<IActionResult> Index(int sayfa = 1, CancellationToken ct = default)
{
var urunler = await servis.SayfalaAsync(sayfa, 20, ct);
return View(urunler);
}
public async Task<IActionResult> Detay(string slug, CancellationToken ct)
{
var detay = await servis.DetayGetirAsync(slug, ct);
return detay is null ? NotFound() : View(detay);
}
[HttpPost]
public async Task<IActionResult> Ekle(UrunEkleDto dto, CancellationToken ct)
{
if (!ModelState.IsValid) return View(dto);
var id = await servis.EkleAsync(dto, ct);
TempData["Mesaj"] = "Urun eklendi";
return RedirectToAction(nameof(Detay), new { slug = dto.Ad });
}
}
CancellationToken Kültürü
Dikkat ettiniz mi, her async metot CancellationToken alıyor. Bu production-grade uygulamalar için çok önemli — kullanıcı sayfayı kapattığında request düşer, DB'de uzun süren sorgular iptal edilebilir. ASP.NET Core otomatik HttpContext.RequestAborted'u parametre olarak geçer.
Bu Bölümde Ne Yaptık?
Katmanlı mimariye geçtik — Core, Infrastructure, Web. EF Core 9'u kurduk, entity configuration'ları ayrı dosyalarda yazdık, migration workflow'unu öğrendik, seed data ile başlangıç verisi kurduk, LINQ best practice'leri (AsNoTracking, Include, IQueryable vs IEnumerable, N+1) gördük, Repository ve Service pattern ile iş mantığını UI'dan ayırdık.
Bir sonraki bölümde Identity ve Authentication konusunu işliyoruz: Identity setup, custom user, cookie vs JWT, rol tabanlı ve policy-based authorization, claims. Bol kolay gelsin, bir sonraki bölümde görüşürüz!