C# / .NET // .NETD::03 ORTA
22m READCOMPLETION: 85%ID::CS-201

ENTITY FRAMEWORK CORE: CODE-FIRST VE LINQ

Code-First modelleme, migration yönetimi ve LINQ sorguları

Entity Framework Core, .NET ekosisteminin standart ORM (Object-Relational Mapping) kütüphanesidir. Code-First yaklaşım ile C# sınıflarınızdan veritabanı şeması oluşturulur; migration'lar sayesinde şema değişiklikleri versiyon kontrolüyle yönetilir.

Code-First ile Model Tanımlama

// CSHARP //
// Models/Makale.cs
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
 
namespace BlogApi.Models;
 
public class Makale
{
    public int Id { get; set; }
 
    [Required, MaxLength(300)]
    public string Baslik { get; set; } = string.Empty;
 
    [Required, MaxLength(350)]
    public string Slug { get; set; } = string.Empty;
 
    [Required]
    public string Icerik { get; set; } = string.Empty;
 
    public bool Yayinlandi { get; set; } = false;
 
    public int Gorunumler { get; set; } = 0;
 
    public DateTime OlusturulduAt { get; set; } = DateTime.UtcNow;
 
    // Foreign key
    public int YazarId { get; set; }
 
    // Navigation properties
    [ForeignKey(nameof(YazarId))]
    public Kullanici Yazar { get; set; } = null!;
 
    public ICollection<Etiket> Etiketler { get; set; } = [];
    public ICollection<Yorum> Yorumlar { get; set; } = [];
}
 
public class Kullanici
{
    public int Id { get; set; }
 
    [Required, MaxLength(100)]
    public string Ad { get; set; } = string.Empty;
 
    [Required, MaxLength(255)]
    public string Email { get; set; } = string.Empty;
 
    [Required]
    public string SifreHash { get; set; } = string.Empty;
 
    public string Rol { get; set; } = "okuyucu";
 
    public DateTime OlusturulduAt { get; set; } = DateTime.UtcNow;
 
    public ICollection<Makale> Makaleler { get; set; } = [];
}

DbContext Yapılandırması

// CSHARP //
// Data/AppDbContext.cs
using Microsoft.EntityFrameworkCore;
 
namespace BlogApi.Data;
 
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
{
    public DbSet<Kullanici> Kullanicilar => Set<Kullanici>();
    public DbSet<Makale>    Makaleler   => Set<Makale>();
    public DbSet<Etiket>    Etiketler   => Set<Etiket>();
    public DbSet<Yorum>     Yorumlar    => Set<Yorum>();
 
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Unique constraint
        modelBuilder.Entity<Kullanici>()
            .HasIndex(k => k.Email)
            .IsUnique();
 
        modelBuilder.Entity<Makale>()
            .HasIndex(m => m.Slug)
            .IsUnique();
 
        // Many-to-many: Makale <-> Etiket
        modelBuilder.Entity<Makale>()
            .HasMany(m => m.Etiketler)
            .WithMany(e => e.Makaleler)
            .UsingEntity("MakaleEtiketler");
 
        // Soft delete filter
        modelBuilder.Entity<Makale>()
            .HasQueryFilter(m => !m.Silindi);
 
        // Tablo adlarını özelleştir
        modelBuilder.Entity<Kullanici>().ToTable("kullanicilar");
        modelBuilder.Entity<Makale>().ToTable("makaleler");
 
        // Seed data
        modelBuilder.Entity<Kullanici>().HasData(
            new Kullanici { Id = 1, Ad = "Admin", Email = "admin@blog.com",
                SifreHash = "$2b$12$...", Rol = "admin" }
        );
    }
}
 
// Program.cs
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("Default"),
        o => o.EnableRetryOnFailure(maxRetryCount: 3)));

Migration Yönetimi

// CSHARP //
// Yeni migration — özelleştirme
public partial class GorunumlerSutunuEkle : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.AddColumn<int>(
            name: "Gorunumler",
            table: "makaleler",
            nullable: false,
            defaultValue: 0);
 
        // Var olan verileri güncelle
        migrationBuilder.Sql(
            "UPDATE makaleler SET Gorunumler = 0 WHERE Gorunumler IS NULL");
    }
 
    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropColumn(name: "Gorunumler", table: "makaleler");
    }
}

LINQ ile Sorgulama

// CSHARP //
// Repository pattern
public class MakaleRepository(AppDbContext db) : IMakaleRepository
{
    // Temel sorgu
    public async Task<List<Makale>> ListeleAsync(int sayfa, int boyut)
    {
        return await db.Makaleler
            .Include(m => m.Yazar)
            .Include(m => m.Etiketler)
            .Where(m => m.Yayinlandi)
            .OrderByDescending(m => m.OlusturulduAt)
            .Skip((sayfa - 1) * boyut)
            .Take(boyut)
            .AsNoTracking()        // Okuma için tracking gerekmez
            .ToListAsync();
    }
 
    // Projection ile sadece gerekli alanlar
    public async Task<List<MakaleOnizleme>> OnizlemeleriGetirAsync()
    {
        return await db.Makaleler
            .Where(m => m.Yayinlandi)
            .Select(m => new MakaleOnizleme(
                m.Id,
                m.Baslik,
                m.Slug,
                m.Icerik.Substring(0, Math.Min(160, m.Icerik.Length)),
                m.OlusturulduAt,
                m.Yazar.Ad
            ))
            .OrderByDescending(m => m.OlusturulduAt)
            .ToListAsync();
    }
 
    // İlişkili veri ile birlikte getir
    public async Task<Makale?> SlugIleGetirAsync(string slug)
    {
        return await db.Makaleler
            .Include(m => m.Yazar)
            .Include(m => m.Etiketler)
            .Include(m => m.Yorumlar.OrderBy(y => y.OlusturulduAt))
                .ThenInclude(y => y.Yazar)
            .FirstOrDefaultAsync(m => m.Slug == slug && m.Yayinlandi);
    }
 
    // Aggregate sorgular
    public async Task<Dictionary<string, int>> EtiketIstatistikleriAsync()
    {
        return await db.Etiketler
            .Select(e => new { e.Isim, MakaleSayisi = e.Makaleler.Count(m => m.Yayinlandi) })
            .OrderByDescending(x => x.MakaleSayisi)
            .ToDictionaryAsync(x => x.Isim, x => x.MakaleSayisi);
    }
}

Transaction ve Toplu İşlemler

// CSHARP //
// Transaction kullanımı
public async Task<Makale> SiparisTamamlaAsync(MakaleOlusturDto dto, int yazarId)
{
    await using var transaction = await db.Database.BeginTransactionAsync();
    try
    {
        var makale = new Makale
        {
            Baslik  = dto.Baslik,
            Slug    = SlugOlustur(dto.Baslik),
            Icerik  = dto.Icerik,
            YazarId = yazarId,
        };
        db.Makaleler.Add(makale);
        await db.SaveChangesAsync();
 
        // Etiketleri ekle
        foreach (var etiketIsim in dto.Etiketler)
        {
            var etiket = await db.Etiketler.FirstOrDefaultAsync(e => e.Isim == etiketIsim)
                ?? db.Etiketler.Add(new Etiket { Isim = etiketIsim }).Entity;
            makale.Etiketler.Add(etiket);
        }
        await db.SaveChangesAsync();
 
        await transaction.CommitAsync();
        return makale;
    }
    catch
    {
        await transaction.RollbackAsync();
        throw;
    }
}
 
// EF Core 7+ ExecuteUpdate / ExecuteDelete — toplu işlem
await db.Makaleler
    .Where(m => m.OlusturulduAt < DateTime.UtcNow.AddYears(-2) && !m.Yayinlandi)
    .ExecuteDeleteAsync();  // Toplu silme — tek SQL, object tracking yok
 
await db.Makaleler
    .Where(m => m.YazarId == yazarId)
    .ExecuteUpdateAsync(s => s.SetProperty(m => m.Gorunumler, 0));

Sonuç

Entity Framework Core ile Code-First modelleme, migration yönetimi ve LINQ sorguları .NET backend geliştirmenin temel araçlarıdır. AsNoTracking, projection ve toplu işlemler ile performanslı sorgular yazabilirsiniz. Bir sonraki derste ASP.NET Core ile SignalR kullanarak gerçek zamanlı bildirimler geliştireceğiz.