Model validation: Gelen veriyi Data Annotations ve FluentValidation ile doğrulamak
Gelen veriyi doğrulamayı işliyoruz: Data Annotations ile hızlı kural tanımlama, [ApiController]'ın otomatik doğrulaması ve karmaşık kurallar için FluentValidation. Neyi ne zaman seçmeli?
Önceki yazıda giriş DTO'ları tanımladık; istemciden hangi alanları beklediğimizi söyledik. Ama bir şeyi beklemek, o şeyin doğru geldiğini garanti etmez. İstemci bize boş bir isim, geçersiz bir e-posta ya da beş karakterlik bir şifre gönderebilir. İşte tam bu noktada model validation devreye girer. Bugün gelen veriyi kapıda karşılayıp geçerli olup olmadığını nasıl kontrol edeceğimizi konuşuyoruz.
Neden Doğrulama Şart?
Basit bir kural var: dışarıdan gelen hiçbir veriye güvenme. İstemci tarafında ne kadar kontrol koyarsan koy, sonuçta senin API'ne ham bir istek ulaşabilir; kötü niyetli biri ya da hatalı bir uygulama, kurallarına uymayan veri gönderebilir. Sunucu tarafındaki doğrulama, uygulamanın son ve en güvenilir savunma hattıdır.
Doğrulamayı bir binanın giriş güvenliği gibi düşün. İçeri girmek isteyen herkes önce kontrol noktasından geçer; kimliği eksik olan, kuralı ihlal eden içeri alınmaz. Böylece binanın içindeki iş mantığın, hep temiz ve güvenilir veriyle çalışır. Kontrolü kapıda yaparsan, içerideki kodun her yerine "acaba bu veri geçerli mi?" kontrolleri serpiştirmek zorunda kalmazsın.
Data Annotations: Hızlı ve Pratik Yol
.NET'te doğrulamanın en hızlı yolu, DTO'nun özelliklerinin üstüne attribute'lar yerleştirmek. Bunlara Data Annotations diyoruz. Önceki yazıdaki CreateUserDto'ya birkaç kural ekleyelim:
public class CreateUserDto
{
[Required]
[StringLength(50, MinimumLength = 2)]
public string Name { get; set; }
[Required]
[EmailAddress]
public string Email { get; set; }
[Required]
[MinLength(8)]
public string Password { get; set; }
}
Her attribute bir kural söylüyor. [Required] alanın boş bırakılamayacağını, [StringLength] uzunluk sınırlarını, [EmailAddress] değerin geçerli bir e-posta formatında olması gerektiğini, [MinLength] ise en az kaç karakter olacağını belirtiyor. Kodun kendisi neredeyse bir kurallar listesi gibi okunuyor: "isim zorunlu, 2 ile 50 karakter arası; e-posta zorunlu ve geçerli formatta; şifre zorunlu, en az 8 karakter."
[ApiController] İşin Yarısını Zaten Yapıyor
Şimdi işin en güzel kısmı. Dördüncü yazıda controller'ımızın başına koyduğumuz [ApiController] attribute'unu hatırlıyor musun? İşte o, gelen veri bu kurallara uymadığında otomatik olarak devreye girer. Sen action metodunun içinde tek bir kontrol yazmasan bile, kurallara uymayan bir istek geldiğinde .NET otomatik olarak 400 Bad Request döner ve hangi alanın neden geçersiz olduğunu açıklayan düzgün bir hata cevabı üretir.
Yani senin metodun şöyle sade kalabiliyor:
[HttpPost]
public IActionResult Create(CreateUserDto dto)
{
// Buraya ulaştıysak, veri zaten geçerli demektir.
// Doğrulama kontrolünü elle yazmamıza gerek yok.
return Ok($"Oluşturulan kullanıcı: {dto.Name}");
}
Metodun içine dikkat et: tek bir doğrulama satırı yok. Çünkü kod bu noktaya ulaştıysa, gelen verinin tüm kurallardan geçtiğinden emin olabilirsin. Geçmeseydi, .NET metodu hiç çalıştırmadan isteği geri çevirirdi. Bu, kodunu hem temiz hem de güvenli tutan çok değerli bir kolaylık.
Data Annotations Nerede Yetersiz Kalıyor?
Data Annotations basit kurallar için harika. Ama işler karmaşıklaştığında sınırlarına çarparsın. Diyelim ki "şifre, kullanıcının adını içermemeli" ya da "başlangıç tarihi bitiş tarihinden önce olmalı" gibi, birden çok alana bakan veya iş mantığı gerektiren bir kural yazmak istiyorsun. Bunları attribute'larla ifade etmek ya çok zor ya da imkânsız hale gelir.
Bir diğer sıkıntı da kuralların DTO sınıfının içine gömülü olması. Kural sayısı arttıkça DTO'n attribute'larla dolup taşar ve okunması zorlaşır. İşte bu noktada devreye daha güçlü bir yaklaşım giriyor: FluentValidation.
FluentValidation: Karmaşık Kurallar İçin Güçlü Yol
FluentValidation, .NET'in kendisiyle gelmeyen ama toplulukta çok sevilen bir kütüphane. Doğrulama kurallarını DTO'nun içine değil, ayrı bir sınıfta tanımlamana izin verir. Böylece DTO'n sade kalır, kurallar da kendi evinde yaşar. Aynı kuralları FluentValidation ile şöyle yazarız:
public class CreateUserDtoValidator : AbstractValidator
{
public CreateUserDtoValidator()
{
RuleFor(x => x.Name)
.NotEmpty()
.Length(2, 50);
RuleFor(x => x.Email)
.NotEmpty()
.EmailAddress();
RuleFor(x => x.Password)
.NotEmpty()
.MinimumLength(8);
}
}
Okunuşuna dikkat et: neredeyse İngilizce bir cümle gibi. "Name için: boş olmasın, 2 ile 50 karakter arasında olsun." Bu akıcı (fluent) yazım, kütüphaneye adını veren şey. Asıl gücü ise karmaşık senaryolarda ortaya çıkıyor; birden çok alana bakan, koşullu çalışan ya da veritabanına danışması gereken kuralları burada rahatça ifade edebilirsin. Kurallar arttıkça bu ayrı sınıf yapısı, kodunu düzenli tutar.
Peki Hangisini Seçmeliyim?
İkisi de geçerli araçlar ve seçim çoğunlukla projenin karmaşıklığına bağlı. Kuralların basitse (zorunlu alan, uzunluk, format kontrolü gibi), Data Annotations tamamen yeterli; ekstra bir kütüphaneye gerek kalmadan hızlıca yazarsın. Kuralların karmaşıklaşmaya, alanlar arası mantık içermeye ya da DTO'yu attribute'larla boğmaya başladığında ise FluentValidation'a geçmek kodunu çok daha temiz tutar.
Pratik bir tavsiye: küçük projede ya da öğrenme aşamasındaysan Data Annotations ile başla, çünkü hem .NET'in içinde hazır gelir hem de kavramı en hızlı böyle oturtursun. Proje büyüyüp kuralların ciddileştiğinde FluentValidation'ı devreye alırsın. İkisini aynı projede bir arada kullanmanda da bir sakınca yok.
Küçük Bir Deneme
Elindeki CreateUserDto'ya yukarıdaki Data Annotations kurallarını ekle. Sonra bir API test aracıyla önce geçerli bir istek gönder ve metodun çalıştığını gör. Ardından bilerek kuralları ihlal et: ismi boş bırak, geçersiz bir e-posta yolla, kısa bir şifre gönder. [ApiController]'ın senin için otomatik ürettiği 400 cevabına ve hangi alanların neden reddedildiğini açıklayan mesajlara dikkatlice bak. Bu deney, doğrulamanın kapıda nasıl çalıştığını en net şekilde gösterir.
Bir sonraki yazıda Entity Framework Core'a giriş yapacağız: DbContext nedir, veritabanıyla nasıl konuşuruz ve ilk migration'ı nasıl oluştururuz. Şimdiye kadar hep bellekteki sabit listelerle çalıştık; artık verimizi gerçek bir veritabanında saklamanın zamanı geliyor.