FluentValidation, .NET uygulamalarında veri doğrulama işlemlerini daha okunabilir, test edilebilir ve yönetilebilir hale getiren güçlü bir kütüphanedir. Bu yazıda FluentValidation'ın temellerinden ileri seviye kullanımına kadar tüm detayları öğreneceksiniz.
FluentValidation Nedir?
Modern Validation Library
FluentValidation, .NET için fluent interface kullanarak veri doğrulama kuralları tanımlamanızı sağlayan açık kaynak bir kütüphanedir.
Geleneksel Validation Sorunları
Data Annotations Sınırları
Attribute'lar ile karmaşık kurallar yazmak zor
[Range(18, 65, ErrorMessage = "Yaş 18-65 arası olmalı")]
Karışık Sorumluluklar
Model sınıfları hem veri hem validation logic içerir
Class içinde hem property hem validation
Test Zorluğu
Attribute'ları unit test etmek karmaşık
Validation logic'i test etmek zor
Tekrar Kullanım
Validation kurallarını farklı modellerde kullanmak zor
Email validation her yerde tekrar
FluentValidation Avantajları
Data Annotations İle
public class User
{
[Required(ErrorMessage = "Ad zorunludur")]
[StringLength(50, MinimumLength = 2,
ErrorMessage = "Ad 2-50 karakter arası olmalı")]
public string FirstName { get; set; }
[Required(ErrorMessage = "Email zorunludur")]
[EmailAddress(ErrorMessage = "Geçerli email giriniz")]
public string Email { get; set; }
[Range(18, 100, ErrorMessage = "Yaş 18-100 arası olmalı")]
public int Age { get; set; }
[RegularExpression(@"^\+90[0-9]{10}$",
ErrorMessage = "Türkiye telefon formatı: +905xxxxxxxxx")]
public string Phone { get; set; }
}
Sorunlar
- Model karmaşık görünüyor
- Validation logic model içinde
- Test etmek zor
- Tekrar kullanım düşük
FluentValidation İle
public class UserValidator : AbstractValidator<User>
{
public UserValidator()
{
RuleFor(x => x.FirstName)
.NotEmpty().WithMessage("Ad zorunludur")
.Length(2, 50).WithMessage("Ad 2-50 karakter arası olmalı");
RuleFor(x => x.Email)
.NotEmpty().WithMessage("Email zorunludur")
.EmailAddress().WithMessage("Geçerli email giriniz");
RuleFor(x => x.Age)
.InclusiveBetween(18, 100)
.WithMessage("Yaş 18-100 arası olmalı");
RuleFor(x => x.Phone)
.Matches(@"^\+90[0-9]{10}$")
.WithMessage("Türkiye telefon formatı: +905xxxxxxxxx");
}
}
Avantajlar
- Temiz ve okunabilir kod
- Ayrı validation sınıfı
- Kolay unit test
- Yüksek tekrar kullanım
Kurulum ve Temel Kullanım
NuGet Paketi Kurulumu
# Package Manager Console
Install-Package FluentValidation
# .NET CLI
dotnet add package FluentValidation
# ASP.NET Core için ek paket
Install-Package FluentValidation.AspNetCore
Temel Kullanım
Model Sınıfı
public class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
public string Description { get; set; }
public string Category { get; set; }
public int Stock { get; set; }
public DateTime ReleaseDate { get; set; }
}
Validator Sınıfı
using FluentValidation;
public class ProductValidator : AbstractValidator<Product>
{
public ProductValidator()
{
RuleFor(product => product.Name)
.NotEmpty().WithMessage("Ürün adı zorunludur")
.MaximumLength(100).WithMessage("Ürün adı 100 karakterden fazla olamaz");
RuleFor(product => product.Price)
.GreaterThan(0).WithMessage("Fiyat 0'dan büyük olmalıdır")
.LessThan(1000000).WithMessage("Fiyat 1,000,000'dan küçük olmalıdır");
RuleFor(product => product.Description)
.NotEmpty().WithMessage("Açıklama zorunludur")
.MinimumLength(10).WithMessage("Açıklama en az 10 karakter olmalıdır");
RuleFor(product => product.Category)
.NotEmpty().WithMessage("Kategori seçilmelidir")
.Must(BeAValidCategory).WithMessage("Geçersiz kategori");
RuleFor(product => product.Stock)
.GreaterThanOrEqualTo(0).WithMessage("Stok negatif olamaz");
RuleFor(product => product.ReleaseDate)
.LessThanOrEqualTo(DateTime.Now).WithMessage("Çıkış tarihi gelecekte olamaz");
}
private bool BeAValidCategory(string category)
{
var validCategories = new[] { "Elektronik", "Giyim", "Kitap", "Oyuncak" };
return validCategories.Contains(category);
}
}
Validation Kullanımı
var product = new Product
{
Name = "",
Price = -10,
Description = "Kısa",
Category = "Geçersiz",
Stock = -5,
ReleaseDate = DateTime.Now.AddDays(1)
};
var validator = new ProductValidator();
var validationResult = validator.Validate(product);
if (!validationResult.IsValid)
{
foreach (var error in validationResult.Errors)
{
Console.WriteLine($"Hata: {error.ErrorMessage}");
Console.WriteLine($"Property: {error.PropertyName}");
Console.WriteLine($"Değer: {error.AttemptedValue}");
}
}
// Çıktı:
// Hata: Ürün adı zorunludur
// Property: Name
// Değer:
// Hata: Fiyat 0'dan büyük olmalıdır
// Property: Price
// Değer: -10
Validation Kuralları
Temel Kurallar
.NotEmpty()
Boş olmamalı
.NotNull()
Null olmamalı
.Equal("değer")
Belirli değere eşit olmalı
.NotEqual("değer")
Belirli değere eşit olmamalı
String Kuralları
.Length(min, max)
Karakter uzunluğu
.MinimumLength(5)
Minimum uzunluk
.MaximumLength(100)
Maximum uzunluk
.Matches(regex)
Regex pattern
.EmailAddress()
Email formatı
Sayısal Kurallar
.GreaterThan(0)
Büyüktür
.GreaterThanOrEqualTo(18)
Büyük eşittir
.LessThan(100)
Küçüktür
.InclusiveBetween(18, 65)
Aralık (dahil)
.ExclusiveBetween(0, 100)
Aralık (hariç)
Koleksiyon Kuralları
.Must(predicate)
Özel koşul
.IsInEnum()
Enum değeri
.IsEnumName(typeof(Enum))
Enum ismi
.Custom(context => { })
Özel validation
İleri Seviye Özellikler
Koşullu Validation
public class UserValidator : AbstractValidator<User>
{
public UserValidator()
{
// Yaş 18'den büyükse ehliyet numarası zorunlu
RuleFor(x => x.LicenseNumber)
.NotEmpty()
.When(x => x.Age >= 18)
.WithMessage("18 yaş üstü için ehliyet numarası zorunludur");
// Email veya telefon en az birisi dolu olmalı
RuleFor(x => x.Email)
.NotEmpty()
.When(x => string.IsNullOrEmpty(x.Phone))
.WithMessage("Email veya telefon en az birisi girilmelidir");
RuleFor(x => x.Phone)
.NotEmpty()
.When(x => string.IsNullOrEmpty(x.Email))
.WithMessage("Email veya telefon en az birisi girilmelidir");
// Unless kullanımı (değilse)
RuleFor(x => x.StudentId)
.NotEmpty()
.Unless(x => x.UserType == UserType.Teacher)
.WithMessage("Öğrenci için öğrenci numarası zorunludur");
}
}
Özel Validation Metodları
public class OrderValidator : AbstractValidator<Order>
{
public OrderValidator()
{
RuleFor(x => x.CustomerEmail)
.Must(BeAValidDomain)
.WithMessage("Sadece şirket email adresleri kabul edilir");
RuleFor(x => x.CreditCardNumber)
.Must(BeAValidCreditCard)
.WithMessage("Geçersiz kredi kartı numarası");
RuleFor(x => x.DeliveryDate)
.Must(BeAWorkDay)
.WithMessage("Teslimat tarihi iş günü olmalıdır");
// Async validation
RuleFor(x => x.ProductCode)
.MustAsync(BeUniqueProductCode)
.WithMessage("Bu ürün kodu zaten kullanımda");
}
private bool BeAValidDomain(string email)
{
if (string.IsNullOrEmpty(email)) return false;
var allowedDomains = new[] { "company.com", "business.net" };
var domain = email.Split('@').LastOrDefault();
return allowedDomains.Contains(domain);
}
private bool BeAValidCreditCard(string cardNumber)
{
if (string.IsNullOrEmpty(cardNumber)) return false;
// Luhn algoritması ile kredi kartı doğrulama
cardNumber = cardNumber.Replace(" ", "").Replace("-", "");
if (cardNumber.Length != 16) return false;
// Basit Luhn check implementasyonu
return IsValidLuhn(cardNumber);
}
private bool BeAWorkDay(DateTime date)
{
return date.DayOfWeek != DayOfWeek.Saturday &&
date.DayOfWeek != DayOfWeek.Sunday;
}
private async Task<bool> BeUniqueProductCode(string productCode,
CancellationToken cancellationToken)
{
// Veritabanında kontrol et
// Bu örnekte sadece simülasyon
await Task.Delay(100, cancellationToken);
var existingCodes = new[] { "PROD001", "PROD002", "PROD003" };
return !existingCodes.Contains(productCode);
}
private bool IsValidLuhn(string cardNumber)
{
// Luhn algoritması implementasyonu
// Gerçek projede NuGet paketi kullanın
return true; // Basitleştirilmiş
}
}
Nested Object Validation
public class Address
{
public string Street { get; set; }
public string City { get; set; }
public string ZipCode { get; set; }
public string Country { get; set; }
}
public class Customer
{
public string Name { get; set; }
public string Email { get; set; }
public Address HomeAddress { get; set; }
public Address WorkAddress { get; set; }
public List<Address> DeliveryAddresses { get; set; }
}
public class AddressValidator : AbstractValidator<Address>
{
public AddressValidator()
{
RuleFor(x => x.Street)
.NotEmpty().WithMessage("Sokak bilgisi zorunludur")
.MinimumLength(5).WithMessage("Sokak bilgisi en az 5 karakter olmalıdır");
RuleFor(x => x.City)
.NotEmpty().WithMessage("Şehir bilgisi zorunludur");
RuleFor(x => x.ZipCode)
.NotEmpty().WithMessage("Posta kodu zorunludur")
.Matches(@"^\d{5}$").WithMessage("Posta kodu 5 haneli sayı olmalıdır");
RuleFor(x => x.Country)
.NotEmpty().WithMessage("Ülke bilgisi zorunludur");
}
}
public class CustomerValidator : AbstractValidator<Customer>
{
public CustomerValidator()
{
RuleFor(x => x.Name)
.NotEmpty().WithMessage("Müşteri adı zorunludur");
RuleFor(x => x.Email)
.NotEmpty().WithMessage("Email zorunludur")
.EmailAddress().WithMessage("Geçerli email giriniz");
// Nested object validation
RuleFor(x => x.HomeAddress)
.NotNull().WithMessage("Ev adresi zorunludur")
.SetValidator(new AddressValidator());
// Optional nested object
RuleFor(x => x.WorkAddress)
.SetValidator(new AddressValidator())
.When(x => x.WorkAddress != null);
// Collection validation
RuleForEach(x => x.DeliveryAddresses)
.SetValidator(new AddressValidator())
.When(x => x.DeliveryAddresses != null);
// Collection kuralları
RuleFor(x => x.DeliveryAddresses)
.Must(addresses => addresses == null || addresses.Count <= 5)
.WithMessage("En fazla 5 teslimat adresi ekleyebilirsiniz");
}
}
ASP.NET Core Entegrasyonu
Startup Configuration
using FluentValidation;
using FluentValidation.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
// FluentValidation ekle
builder.Services.AddFluentValidationAutoValidation();
builder.Services.AddFluentValidationClientsideAdapters();
// Validator'ları kaydet
builder.Services.AddValidatorsFromAssemblyContaining<ProductValidator>();
// Veya tek tek kayıt
builder.Services.AddScoped<IValidator<Product>, ProductValidator>();
builder.Services.AddScoped<IValidator<User>, UserValidator>();
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
app.Run();
Controller Kullanımı
[ApiController]
[Route("api/[controller]")]
public class ProductController : ControllerBase
{
private readonly IValidator<Product> _validator;
public ProductController(IValidator<Product> validator)
{
_validator = validator;
}
[HttpPost]
public async Task<IActionResult> CreateProduct([FromBody] Product product)
{
// Otomatik validation - ModelState kontrol edilir
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
// Manuel validation
var validationResult = await _validator.ValidateAsync(product);
if (!validationResult.IsValid)
{
// FluentValidation hatalarını ModelState'e ekle
validationResult.AddToModelState(ModelState);
return BadRequest(ModelState);
}
// İş mantığı
// ...
return Ok("Ürün başarıyla oluşturuldu");
}
[HttpPut("{id}")]
public async Task<IActionResult> UpdateProduct(int id, [FromBody] Product product)
{
// Özel validation scenario
var validationResult = await _validator.ValidateAsync(product, options =>
{
options.IncludeRuleSets("Update"); // Sadece Update kuralları
options.IncludeProperties(x => x.Name, x => x.Price); // Sadece belirli property'ler
});
if (!validationResult.IsValid)
{
return BadRequest(validationResult.Errors.Select(e => new
{
Property = e.PropertyName,
Error = e.ErrorMessage,
AttemptedValue = e.AttemptedValue
}));
}
return Ok("Ürün güncellendi");
}
}
Rule Sets Kullanımı
public class ProductValidator : AbstractValidator<Product>
{
public ProductValidator()
{
// Genel kurallar (her zaman çalışır)
RuleFor(x => x.Name)
.NotEmpty().WithMessage("Ürün adı zorunludur");
// Create işlemi için özel kurallar
RuleSet("Create", () =>
{
RuleFor(x => x.Price)
.GreaterThan(0).WithMessage("Yeni ürün fiyatı 0'dan büyük olmalıdır");
RuleFor(x => x.Stock)
.GreaterThanOrEqualTo(1).WithMessage("Yeni ürün için stok girilmelidir");
});
// Update işlemi için özel kurallar
RuleSet("Update", () =>
{
RuleFor(x => x.Price)
.GreaterThanOrEqualTo(0).WithMessage("Güncelleme sırasında fiyat negatif olamaz");
// Güncelleme sırasında stok 0 olabilir
RuleFor(x => x.Stock)
.GreaterThanOrEqualTo(0).WithMessage("Stok negatif olamaz");
});
// Özel durum kuralları
RuleSet("Discount", () =>
{
RuleFor(x => x.Price)
.LessThan(x => x.OriginalPrice * 0.9m)
.WithMessage("İndirim fiyatı orijinal fiyatın en az %10'u olmalıdır");
});
}
}
Unit Testing
FluentValidation Test Avantajları
Kolay Test
Validator sınıfları bağımsız olarak test edilebilir
Detaylı Kontrol
Her kural ayrı ayrı test edilebilir
Hızlı Çalışma
Unit testler çok hızlı çalışır
Test Örnekleri
using FluentValidation.TestHelper;
using Xunit;
public class ProductValidatorTests
{
private readonly ProductValidator _validator;
public ProductValidatorTests()
{
_validator = new ProductValidator();
}
[Fact]
public void Should_Have_Error_When_Name_Is_Empty()
{
// Arrange
var product = new Product { Name = "" };
// Act & Assert
var result = _validator.TestValidate(product);
result.ShouldHaveValidationErrorFor(x => x.Name)
.WithErrorMessage("Ürün adı zorunludur");
}
[Fact]
public void Should_Not_Have_Error_When_Name_Is_Valid()
{
// Arrange
var product = new Product { Name = "Geçerli Ürün Adı" };
// Act & Assert
var result = _validator.TestValidate(product);
result.ShouldNotHaveValidationErrorFor(x => x.Name);
}
[Theory]
[InlineData(-1)]
[InlineData(0)]
[InlineData(1000001)]
public void Should_Have_Error_When_Price_Is_Invalid(decimal price)
{
// Arrange
var product = new Product { Price = price };
// Act & Assert
var result = _validator.TestValidate(product);
result.ShouldHaveValidationErrorFor(x => x.Price);
}
[Theory]
[InlineData(0.01)]
[InlineData(100)]
[InlineData(999999)]
public void Should_Not_Have_Error_When_Price_Is_Valid(decimal price)
{
// Arrange
var product = new Product { Price = price };
// Act & Assert
var result = _validator.TestValidate(product);
result.ShouldNotHaveValidationErrorFor(x => x.Price);
}
[Fact]
public void Should_Have_Error_When_Category_Is_Invalid()
{
// Arrange
var product = new Product { Category = "GeçersizKategori" };
// Act & Assert
var result = _validator.TestValidate(product);
result.ShouldHaveValidationErrorFor(x => x.Category)
.WithErrorMessage("Geçersiz kategori");
}
[Fact]
public void Should_Have_Multiple_Errors_When_Product_Is_Invalid()
{
// Arrange
var product = new Product
{
Name = "",
Price = -10,
Description = "Kısa",
Category = "Geçersiz",
Stock = -1
};
// Act
var result = _validator.TestValidate(product);
// Assert
result.ShouldHaveValidationErrorFor(x => x.Name);
result.ShouldHaveValidationErrorFor(x => x.Price);
result.ShouldHaveValidationErrorFor(x => x.Description);
result.ShouldHaveValidationErrorFor(x => x.Category);
result.ShouldHaveValidationErrorFor(x => x.Stock);
}
[Fact]
public void Should_Pass_All_Validations_When_Product_Is_Valid()
{
// Arrange
var product = new Product
{
Name = "Geçerli Ürün",
Price = 100,
Description = "Bu geçerli bir açıklamadır",
Category = "Elektronik",
Stock = 10,
ReleaseDate = DateTime.Now.AddDays(-1)
};
// Act
var result = _validator.TestValidate(product);
// Assert
result.ShouldNotHaveAnyValidationErrors();
}
[Fact]
public void Should_Validate_Only_Create_Rules_When_RuleSet_Is_Create()
{
// Arrange
var product = new Product
{
Name = "Test",
Price = 0, // Create için geçersiz, Update için geçerli
Stock = 0 // Create için geçersiz, Update için geçerli
};
// Act
var result = _validator.TestValidate(product, options =>
{
options.IncludeRuleSets("Create");
});
// Assert
result.ShouldHaveValidationErrorFor(x => x.Price);
result.ShouldHaveValidationErrorFor(x => x.Stock);
}
}
İpuçları ve En İyi Pratikler
İsimlendirme
Yapın
- Validator sınıflarını
XValidator
olarak adlandırın - Açık ve anlaşılır hata mesajları yazın
- Türkçe karakter kullanmayın
Yapmayın
- Generic hata mesajları kullanmayın
- Çok uzun hata mesajları yazmayın
- Teknik detayları kullanıcıya göstermeyin
Organizasyon
Klasör Yapısı
Performans
Yaygın Kullanım Desenleri
Kullanıcı Kayıt Validasyonu
RuleFor(x => x.Email)
.NotEmpty().WithMessage("Email zorunludur")
.EmailAddress().WithMessage("Geçerli email giriniz")
.MustAsync(BeUniqueEmail).WithMessage("Bu email zaten kullanımda");
RuleFor(x => x.Password)
.NotEmpty().WithMessage("Şifre zorunludur")
.MinimumLength(8).WithMessage("Şifre en az 8 karakter olmalıdır")
.Matches("[A-Z]").WithMessage("En az bir büyük harf")
.Matches("[a-z]").WithMessage("En az bir küçük harf")
.Matches("[0-9]").WithMessage("En az bir rakam");
Ödeme Validasyonu
RuleFor(x => x.CardNumber)
.NotEmpty().WithMessage("Kart numarası zorunludur")
.CreditCard().WithMessage("Geçersiz kart numarası");
RuleFor(x => x.ExpiryDate)
.NotEmpty().WithMessage("Son kullanma tarihi zorunludur")
.Must(BeValidExpiryDate).WithMessage("Kartın süresi dolmuş");
RuleFor(x => x.CVV)
.NotEmpty().WithMessage("CVV zorunludur")
.Length(3, 4).WithMessage("CVV 3-4 haneli olmalıdır");
Dosya Upload Validasyonu
RuleFor(x => x.File)
.NotNull().WithMessage("Dosya seçilmelidir")
.Must(BeValidFileSize).WithMessage("Dosya boyutu max 5MB olabilir")
.Must(BeValidFileType).WithMessage("Sadece JPG, PNG, PDF dosyaları");
private bool BeValidFileSize(IFormFile file)
{
return file?.Length <= 5 * 1024 * 1024; // 5MB
}
private bool BeValidFileType(IFormFile file)
{
var allowedTypes = new[] { ".jpg", ".png", ".pdf" };
var extension = Path.GetExtension(file?.FileName)?.ToLower();
return allowedTypes.Contains(extension);
}
Sonuç
FluentValidation, .NET projelerinde temiz, okunabilir ve test edilebilir validation logic'i yazmanın en etkili yollarından biridir. Geleneksel Data Annotations'a göre çok daha esnek ve güçlü özellikler sunar.