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.AspNetCoreTemel 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: -10Validation 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ı XValidatorolarak 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.