Sem Göksu
Sem Göksu
Yazılım · Yolculuk · Fenerbahçe
ASP.NET

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

17 Mart 2026 9 dk okuma 8 0

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>();
}
Package Manager Console - Add-Migration InitialCreate ve Update-Database komutlari uygulandi.
Package Manager Console - Add-Migration InitialCreate ve Update-Database komutlari uygulandi.

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!

Paylaş:

Yorumlar (0)

Henüz yorum yok. İlk yorumu sen yap!

Yorum bırak

* Yorumlar moderasyon sonrası yayınlanır. E-posta gizli tutulur.