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

UserModel.cs C#
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

UserValidator.cs C#
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 PowerShell
# 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

1

Model Sınıfı

Models/Product.cs C#
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; }
}
2

Validator Sınıfı

Validators/ProductValidator.cs C#
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);
    }
}
3

Validation Kullanımı

Program.cs C#
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

ConditionalValidator.cs C#
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ı

CustomValidation.cs C#
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

NestedValidation.cs C#
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

1

Startup Configuration

Program.cs C#
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();
2

Controller Kullanımı

ProductController.cs C#
[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");
    }
}
3

Rule Sets Kullanımı

ProductValidatorWithRuleSets.cs C#
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

ProductValidatorTests.cs C#
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ı
Validators/
ProductValidator.cs
UserValidator.cs
AddressValidator.cs
Extensions/
CustomValidators.cs

Performans

Async validation'ı sadece gerektiğinde kullanın
Karmaşık kuralları cache'leyin
RuleSet'leri kullanarak gereksiz validation'lardan kaçının
Validator'ları Singleton olarak kaydedin

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.

Ana Faydalar

Temiz Kod: Validation logic'i model sınıflarından ayrı
Okunabilirlik: Fluent syntax ile doğal dil gibi yazım
Test Edilebilirlik: Unit test yazmak çok kolay
Tekrar Kullanım: Validation logic'i paylaşılabilir
Esneklik: Karmaşık validation senaryoları desteklenir

Sonraki Adımlar

  • Mevcut projelerinizde FluentValidation'ı deneyin
  • Data Annotations'dan FluentValidation'a geçiş yapın
  • Custom validation metodları yazarak pratik yapın
  • ASP.NET Core projelerinde otomatik validation'ı aktifleştirin
  • Unit testler yazarak validation logic'inizi doğrulayın