Fullstack Mobil — Bölüm 2: Backend API ve Veritabanı
ASP.NET Core 9 + EF Core ile blog uygulamamızın backend'ini yazıyoruz. Entity modelleri, DbContext, migrations, DTO pattern, tam CRUD controller ve seed data — production-hazır bir API iskeleti.
20 Nisan 2026 6 dk okuma 7 0
Merhaba arkadaşlar, serinin ikinci bölümüne hoş geldiniz. Geçen yazıda iskeleti kurduk, bu yazıda backend'i inşa ediyoruz — EF Core ile veritabanı modelleri, migrations, CRUD endpoint'leri, DTO pattern ve validation. Bu bölümü bitirdiğinizde çalışan bir REST API'niz olacak, Swagger'dan test edebileceksiniz.
Domain Modelimiz: Blog Uygulaması
Yapacağımız uygulama basit bir blog. Üç ana tablo:
- Users — kullanıcılar (auth için, bir sonraki bölümde JWT'de kullanacağız)
- Posts — blog yazıları
- Comments — yorumlar
// Models/User.cs
public class User
{
public int Id { get; set; }
public required string KullaniciAdi { get; set; }
public required string Email { get; set; }
public required string SifreHash { get; set; }
public string Rol { get; set; } = "User";
public DateTime OlusturmaTarihi { get; set; } = DateTime.UtcNow;
public List<Post> Posts { get; set; } = new();
}
// Models/Post.cs
public class Post
{
public int Id { get; set; }
public required string Baslik { get; set; }
public required string Icerik { get; set; }
public string? KapakResmi { get; set; }
public DateTime OlusturmaTarihi { get; set; } = DateTime.UtcNow;
public DateTime? YayinlanmaTarihi { get; set; }
public bool Yayinda { get; set; }
public int YazarId { get; set; }
public User? Yazar { get; set; }
public List<Comment> Comments { get; set; } = new();
}
// Models/Comment.cs
public class Comment
{
public int Id { get; set; }
public required string Icerik { get; set; }
public DateTime OlusturmaTarihi { get; set; } = DateTime.UtcNow;
public int PostId { get; set; }
public Post? Post { get; set; }
public int YazarId { get; set; }
public User? Yazar { get; set; }
}
DbContext'i Kuralım
EF Core paketini ekleyelim:
cd api
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.Tools
Package Manager Console - Add-Migration InitialCreate ve Update-Database calistirildi.
IDE tarafinda: Visual Studio tarafinda Tools > NuGet Package Manager > Package Manager Console'u acip EF Core migration'larini buradan yonetiyoruz. Default project secimini Data katmanina vermeyi unutmayin.
dotnet ef migrations add Initial
dotnet ef database update
LocalDB'de BlogAppDb veritabanı oluşur. SQL Server Management Studio'dan veya VS'nin SQL Server Object Explorer'ından doğrulayabilirsiniz.
DTO Pattern — Neden ve Nasıl
Entity'leri direkt dışarı açmak yaygın bir hata:
- Kullanıcının SifreHash'ini API'de görmek istemezsiniz
- İstemcinin ihtiyacı olmayan alanlar (CreatedBy, UpdatedBy vs.) gereksiz payload
- Entity değişirse API de otomatik değişir — versiyonlama kabusu
Çözüm: DTO (Data Transfer Object). Her entity için girdi ve çıktı DTO'ları:
// DTOs/PostDtos.cs
public record PostListItemDto(int Id, string Baslik, string? KapakResmi, DateTime? YayinlanmaTarihi, string YazarAdi, int YorumSayisi);
public record PostDetailDto(int Id, string Baslik, string Icerik, string? KapakResmi, DateTime? YayinlanmaTarihi, string YazarAdi);
public record PostCreateDto(
[property: Required, StringLength(300, MinimumLength = 3)] string Baslik,
[property: Required, MinLength(10)] string Icerik,
string? KapakResmi);
public record PostUpdateDto(
[property: Required, StringLength(300, MinimumLength = 3)] string Baslik,
[property: Required] string Icerik,
bool Yayinda);
Controller — Tam CRUD
// Controllers/PostsController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
[ApiController]
[Route("api/[controller]")]
public class PostsController(AppDb db) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> Tumu(int page = 1, int pageSize = 20)
{
var query = db.Posts
.Where(p => p.Yayinda)
.OrderByDescending(p => p.YayinlanmaTarihi)
.Select(p => new PostListItemDto(
p.Id, p.Baslik, p.KapakResmi, p.YayinlanmaTarihi,
p.Yazar!.KullaniciAdi,
p.Comments.Count));
var total = await query.CountAsync();
var items = await query.Skip((page - 1) * pageSize).Take(pageSize).ToListAsync();
Response.Headers["X-Total-Count"] = total.ToString();
return Ok(items);
}
[HttpGet("{id:int}")]
public async Task<IActionResult> Getir(int id)
{
var post = await db.Posts
.Include(p => p.Yazar)
.Where(p => p.Id == id)
.Select(p => new PostDetailDto(
p.Id, p.Baslik, p.Icerik, p.KapakResmi,
p.YayinlanmaTarihi, p.Yazar!.KullaniciAdi))
.FirstOrDefaultAsync();
return post is null ? NotFound() : Ok(post);
}
[HttpPost]
public async Task<IActionResult> Olustur([FromBody] PostCreateDto dto)
{
var post = new Post
{
Baslik = dto.Baslik,
Icerik = dto.Icerik,
KapakResmi = dto.KapakResmi,
YazarId = 1, // geçici, JWT bölümünde current user'dan alacağız
Yayinda = false
};
db.Posts.Add(post);
await db.SaveChangesAsync();
return CreatedAtAction(nameof(Getir), new { id = post.Id }, new { id = post.Id });
}
[HttpPut("{id:int}")]
public async Task<IActionResult> Guncelle(int id, [FromBody] PostUpdateDto dto)
{
var post = await db.Posts.FindAsync(id);
if (post is null) return NotFound();
post.Baslik = dto.Baslik;
post.Icerik = dto.Icerik;
if (dto.Yayinda && post.YayinlanmaTarihi is null)
post.YayinlanmaTarihi = DateTime.UtcNow;
post.Yayinda = dto.Yayinda;
await db.SaveChangesAsync();
return NoContent();
}
[HttpDelete("{id:int}")]
public async Task<IActionResult> Sil(int id)
{
var post = await db.Posts.FindAsync(id);
if (post is null) return NotFound();
db.Posts.Remove(post);
await db.SaveChangesAsync();
return NoContent();
}
}
Seed Data — İlk Testler İçin
Uygulama ayağa kalkınca boş bir DB yerine birkaç örnek veri olsun:
// Program.cs içinde app.Run()'dan ÖNCE
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDb>();
db.Database.Migrate();
if (!db.Users.Any())
{
var admin = new User
{
KullaniciAdi = "admin",
Email = "admin@blogapp.com",
SifreHash = "geciciBCryptHashBuradaOlacak",
Rol = "Admin"
};
db.Users.Add(admin);
db.SaveChanges();
db.Posts.AddRange(
new Post { Baslik = "İlk Yazım", Icerik = "Merhaba blog!", YazarId = admin.Id, Yayinda = true, YayinlanmaTarihi = DateTime.UtcNow },
new Post { Baslik = "API'ye Giriş", Icerik = "Bugün API yazıyoruz", YazarId = admin.Id, Yayinda = true, YayinlanmaTarihi = DateTime.UtcNow });
db.SaveChanges();
}
}
Swagger'dan Test dotnet run çalıştırıp https://localhost:5000/swagger adresine gidin:
- GET /api/posts — listeyi almak
- GET /api/posts/1 — tek yazıyı almak
- POST /api/posts — yeni oluşturmak (Try it out)
Hepsi çalışıyorsa backend'imiz hazır.
Dikkat Edilmesi Gereken Şeyler
- N+1 problemi: Include() veya .Select() projection ile önleyin
- Pagination: Büyük listeler için şart, hep Skip/Take + X-Total-Count
- Transaction: Birden fazla tablo değişiyorsa db.Database.BeginTransaction()
- Soft delete: Kalıcı silme yerine IsDeleted flag (kullanıcı verisinde özellikle)
Özet
Bu bölümde EF Core ile modelleri, migrations, DTO pattern ve tam CRUD API'yi yazdık. Elimizde artık production-hazır bir backend var.
Bir sonraki bölümde bu API'ye JWT authentication ekleyeceğiz — login, token üretimi, rol kontrolleri, şifre hash'leme. Kolay gelsin, yorumlarda gördüklerinizi bekliyorum...