.NET Core – FluentValidation İle Validasyon Kontrolü
Merhaba,
Çağımızda platform fark etmeksizin geliştirilen ve insanlığa hizmet sağlayan hiçbir yazılım eskisi gibi işlevselliğini sadece sunumdan ibaret tutmamakta, iyi kötü kullanıcıyla aktif bir etkileşime girerek kullanıcı merkezli bir çalışma süreci yaşamaktadır. Bu süreçte kullanıcıdan elde edilen hiçbir veri yazılım açısından direkt iş kuralı olarak benimsenmemekte ve gerekli validation kontrollerinden geçerek onaylandıktan sonra iş sürecinin bir parçası olmaktadırlar.
İşte bu yaşam döngüsünde iş sürecinin parçası olacak verileri doğrulamak için belli başlı validasyonel işleyişe sahip algoritmalar veya attributelar ve hatta özel kütüphaneler tercih edilip kullanılabilmektedir. Bizler bu içeriğimizde .NET Core uygulamalarında FluentValidation kütüphanesi ile nasıl veri doğrulaması yapıldığını inceleyecek ve daha da önemlisi diğer validasyon yöntemlerinden ayıran ana özellikleride ayriyetten vurgulayarak ilgili kütüphanenin bizlere getirisi üzerine farkındalık oluşturacağız.
Validasyon, nesneleri iş kurallarına sokabilmek için uygunluk durumunun değerlendirilip kontrol edilmesidir.
İlk olarak makalemizde detaya girmeden önce validasyon örneklendirmesi yapacağımız sınıfımızı tasarlayarak hazır bulunalım.
class Customer { public int Id { get; set; } public string Name { get; set; } public string Surname { get; set; } public DateTime BornDate { get; set; } }
Öncelikle bildiğimiz validasyon yapılarını ele alarak başlayalım…
If Kontrolü
Validasyon deyince akla ilk olarak veri doğrulama gelmekte ve bunun için genellikle tercih edilen yöntem(spagetti kodlarda 🙂) if kontrolleri olmaktadır.
static void Main(string[] args) { bool CustomValidator(Customer customer) { if (!string.IsNullOrEmpty(customer.Name) && !string.IsNullOrEmpty(customer.Surname)) return true; return false; } Customer customer = new Customer() { Name = "Gençay", BornDate = new DateTime(1992, 05, 09) }; if (CustomValidator(customer)) Console.WriteLine("Nesne onaylandı."); else Console.WriteLine("Lütfen gerekli alanları doldurunuz."); }
Yukarıdaki kod bloğunu incelerseniz eğer “CustomValidator” isminde tanımlanmış olan local function içerisinde parametreden gelen Customer nesnesi validate edilmekte ve duruma göre geriye true ya da false değeri döndürmektedir.
Validasyonel yapılanma gerektiren senaryolarda if kontrolü görüldüğü üzere oldukça kısır ve yüzeysel bir kontrol yapmamızı sağlamaktadır. Yapılan kontrol neticesinde validasyonun geçersiz olduğu bir durumun hangi alandan kaynaklandığına dair bir mesaj almak şu tasarımda pekte mümkün görünmemektedir. Tabi ki de alan bazlı alınan hatalar ayıklanabilir lakin mevcut tasarımda karmaşık ve lüzumsuz işlemler deryasında kulaç atmaktan başka pekte çaremizin olmayacağından dolayı anlayacağınız o detaya hiç temas etmeden bu örnekle yetinerek konuyu noktalıyorum.
Data Annotation
System.ComponentModel.DataAnnotations namespace’i altında bulunan Data Annotation’lar genellikle Entity Framework(Core) ile tercih edilmektedirler ve entity model üzerinde gerekli validasyonel ayarların attributelar aracılığıyla yapılmasını sağlayarak işimizi oldukça kolaylaştırmaktadırlar.
class Customer { public int Id { get; set; } [Required(ErrorMessage = "Lütfen adınızı boş geçmeyiniz")] public string Name { get; set; } [Required(ErrorMessage = "Lütfen soyadınızı boş geçmeyiniz.")] [StringLength(15, ErrorMessage = "Lütfen soyadınızı 3 ile 15 karakter arasında yazınız.", MinimumLength = 3)] public string Surname { get; set; } [Required(ErrorMessage = "Lütfen doğum tarihinizi boş geçmeyiniz.")] public DateTime BornDate { get; set; } }
Yukarıdaki Customer modelini incelerseniz eğer alanlara gerekli validasyonlar uygulanmakta ve artık alan bazlı hata mesajları daha konforlu bir şekilde tasarlanmış bulunmaktadır.
static void Main(string[] args) { Customer customer = new Customer() { Surname = "Sebepsizboşyereayrılanoğulları", BornDate = new DateTime(1992, 05, 09) }; ValidationContext validationContext = new System.ComponentModel.DataAnnotations.ValidationContext(customer); List<ValidationResult> results = new List<ValidationResult>(); bool isValid = Validator.TryValidateObject(customer, validationContext, results, true); if (!isValid) foreach (ValidationResult result in results) Console.WriteLine(result.ErrorMessage); }
Kullanımı ise yukarıdaki gibi biraz kompleks gözüksede en azından validate edilmeyen alanların bilgilerine erişilebilmekte ve hata mesajlarıyla kullanıcı ilgili alana odaklı bir şekilde uyarılmaktadır.
Peki Neden FluentValidation Kullanmalıyız?
Yukarıda ele almış olduğumuz iki yöntemden if kontrolünün çöp olduğu konusunda sanıyorum ki hemfikiriz 🙂 Lakin Data Annotationlarında yapısal ve niteliksel olarak çok kullanışlı görüldüklerini itiraf edelim 🙂 Amaa her göze güzel, ele kolay gelenin esasında prensiplere uygun olup olmadığını iyi değerlendirmekte fayda var…
Data annotationlar yapısal olarak evet kullanışlı ve pratik olabilirler. Ama işlevsel olarak SOLID prensiplerine aykırı bir yazım stiline sahiptirler. Bunun için yukarıda oluşturduğumuz Customer modelini aşağıya tekrar alıp incelersek eğer;
class Customer { public int Id { get; set; } [Required(ErrorMessage = "Lütfen adınızı boş geçmeyiniz")] public string Name { get; set; } [Required(ErrorMessage = "Lütfen soyadınızı boş geçmeyiniz.")] [StringLength(15, ErrorMessage = "Lütfen soyadınızı 3 ile 15 karakter arasında yazınız.", MinimumLength = 3)] public string Surname { get; set; } [Required(ErrorMessage = "Lütfen doğum tarihinizi boş geçmeyiniz.")] public DateTime BornDate { get; set; } }
görüldüğü üzere Customer sadece bir entity olarak tasarlanan modeldir. İşlevi sadece bu olması gereken Customer sınıfı içerisinde ayriyetten data annotation sayesinde validasyon ayarlaması yapılmıştır. Buda SOLID prensiplerinden Tek Sorumluluk Prensibi(Single Responsibility Principle – SRP)‘ne aykırıdır!
İşte bu durumda prensibe uygun validasyonel yapılanma inşa edebilmek için tüm sorumluluğu tek bir sınıfın üstleneceği bir stratejiyi benimsememiz gerekmektedir ve bunun içinde en kullanışlı tasarıma sahip kütüphane olan FluentValidation’ı tercih etmekteyiz.
Entity Framework(Core) ile tercih edilen Data Annotation validasyonları Tek Sorumluluk Prensibi(Single Responsibility Principle – SRP)‘ne aykırıdır!
FluentValidation İle Validasyon İşlemleri
dotnet add package FluentValidation.AspNetCore --version 8.6.2
FluentValidation kütüphanesini kullanabilmek için öncelikle ya Nuget’ten ya da yukarıdaki .NET CLI komutu aracılığıyla projenize dahil etmeniz gerekmektedir.
Yapısal olarak Fluent Interface tasarım desenini benimseyen bu validasyon yönteminde tüm validasyonel sorumluluk tek bir sınıf tarafından üstlenilmekte ve detaylı bir doğrulama işlemi gerçekleştirilmektedir.
Bunun için “AbstractValidator” abstract classından türeyen bir sınıf oluşturmamız ve aşağıdaki gibi üzerinde işlem yapmamız yeterlidir. Ben sınıfın adını “CustomValidator” olarak belirliyorum.
class CustomerValidator : AbstractValidator<Customer> { public CustomerValidator() { RuleFor(c => c.Name).NotEmpty().WithMessage("Lütfen adınızı boş geçmeyiniz."); RuleFor(c => c.Surname).NotEmpty().When(c => c.Id != 1).WithMessage("Lütfen soyadı boş geçmeyiniz."); RuleFor(c => c.BornDate).Must(d => d.Year <= 2000).WithMessage("Lütfen doğum tarihi 2000'den büyük olanları girmeyiniz."); } }
Görüldüğü üzere oluşturduğumuz “CustomerValidator” sınıfının constructorı içerisinde ‘RuleFor’ metotlarıyla validasyon kurallarını alanlara hususi belirlemekte ve 6. satırda olduğu gibi nesneden nesneye göre değişkenlik gösteren doğrulamalar yapabildiğimiz gibi 7. satırda da olduğu gibi direkt olarak bir şarta göre doğrulama gerçekleştirebilmekte ve tüm bunların yanında hata mahiyetindeki mesajları ekleyebilmekteyiz.
static void Main(string[] args) { Customer customer1 = new Customer { Id = 1, BornDate = new DateTime(1992, 9, 5), Name = "Gençay" }; Customer customer2 = new Customer { Id = 2, BornDate = new DateTime(2005, 9, 5), Name = "Hilmi" }; Customer customer3 = new Customer { Id = 3, BornDate = new DateTime(1980, 9, 5), Name = "Hüseyin", Surname = "Kıllıbacak" }; Customer customer4 = new Customer { Id = 4, BornDate = new DateTime(1980, 9, 5), Surname = "Güllüboya" }; void HasError(Customer customer, ValidationResult result) { Console.WriteLine($"Id : {customer.Id} | Name : {customer.Name} | Surname : {customer.Surname}"); if (!result.IsValid) ((List<FluentValidation.Results.ValidationFailure>)result.Errors).ForEach(e => Console.WriteLine(e.ErrorMessage)); else Console.WriteLine($"{customer.Id} numaralı customer başarılı."); Console.WriteLine("\n*******************\n"); } CustomerValidator customerValidator = new CustomerValidator(); HasError(customer1, customerValidator.Validate(customer1)); HasError(customer2, customerValidator.Validate(customer2)); HasError(customer3, customerValidator.Validate(customer3)); HasError(customer4, customerValidator.Validate(customer4)); }
Yukarıdaki kod bloğunu incelerseniz eğer local function olarak tanımlanmış olan ‘HasError’ metodu içerisinde gerekli validate durumu incelenmekte ve ekrana hangi alan doğrulanmadıysa ona dair mesaj yazılmaktadır. Burada asıl üzerine durulması gereken nokta artık Customer modelimizde sadece tanımlama yapılmakta, bununla birlikte validasyon işlemlerinin sorumluluğu başka bir sınıf olan “CustomerValidator” sınıfı tarafından üstlenilmiş olmaktadır.
İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İyi çalışmalar…
Gençay bey merhaba,
Kısa bir sorum olacak yardımcı olursanız çok sevinirim. Bu yapıyı kullanmaya çalışıyorum lakin istediğim sonucu elde edemiyorum.
UserValidator:AbstractValidator ilgili sınıfım ve propertilerim var, validasyonlarımda sıkıntı yok.
şeklinde bir filter da yazdım Result yapımın içinde ki dictionary e set ediyorum key value olarak validationErrors içinde gelmesi gerekli ve son olarak startupım
fakat yanlış yolluyorum verileri filterıma giriyor sorun yok diyor çıkıyor. RegisterValidatorsFromAssemblyContaining yaptığımda ise bu sefer filterıma girmiyor direk oluşturduğum validatora giriyor bu sefer response istediğim şekilde dönmüyor, sorun nerde anlayamadım. yardımcı olur musunuz ?
Merhaba,
Onion Architecture’da CQRS + MediatR Pattern + AutoMapper + Fluent Validation başlıklı yazımdaki Fluent Validation Konfigürasyonu kısmı okumanızı ve uygulamanızı öneririm.
Özellikle probleminiz ilgili alandaki
SuppressModelStateInvalidFilter
konfigürasyonuyla alakalı olabilir.Sevgiler…
Hocam benim anlayamadigim bir konu var DataAnnotation kullanimi yanlis ve Solide aykiri evet ama ozaman CodeFirst yaklasimi ile Tablolarimi olusturuken sinirlamalari nasil koyacagim sinirlamadan kastim mesela isim varchar uzunlugu 50 karakter
EF Core’da Fluent API’a göz atmanızı tavsiye ederim.