.Net 6.0 Üzerinde Validation Factory Yaratmak Part 1
Selamlar,
Bu makalede, bir projede ortak kullanılabilecek validation ve operationları, modellerin propertyleri üzerindeki işaretlemelere göre üretip, topluca işleteceğiz. Ve çıkan sonuçları hep beraber inceleyeceğiz.
Öncelikle gelin, olayı en başından ele alalım:
Validators/IValidator: Esas amaç, girilen input bir alanın kontrol edilmesidir. O zaman gelin bir interface yaratalım ve istenen value bir değere göre, Validate() methodunu çalıştıralım. Geriye bool(true veya false), Exception(hata) döndüren bir Tuple bekleyelim. Unutulmamalıdır ki, bu değerler hata yakalamak haricinde, referance boyutunda değerleri değiştirmek, (Encrypt, Decrypt ya da Hash) gibi methodlar ile içeriği gizlemek ya da göstermek için de kullanılabilirler. Aşağıda, bu IValidator interface’inin beklediği parametreler, detaylıca anlatılmıştır.
- “T value“=> Sınıfa ait, kontrol edilmek amaçlı işaretlenmiş property’nin değeri.
- “int? param” => Null olabilen, ilgili property’nin işaretlendiği attribute’e ait integer parametredir. Bir Attribute, parametre ala da bilir almaya da.
- “string source” => İlgili property’nin, string adıdır.
- “PropertyInfo? pi” => İlgili property’nin referance’ına ulaşmak amacı ile gönderilen nullable değerdir. Amaç “Encrypt, Decrypt ve Hash” işlemlerinde, ilgili propertynin referance değerine ulaşmaktır. Böylece, orjinal değeri değiştirlebilecek yani encrypt ya da decrypt yapılabilecektir.
- “object? model” => İlgili propertynin referance değerini değiştirmek, yani “Encrypt, Decrypt veya Hash“lemek için gönderilen, object valuedur.
1 2 3 4 5 6 7 8 9 10 |
using System; using System.Reflection; namespace ValidationFactory.Validators { public interface IValidator<T> { List<(bool, Exception)> Validate(T value, int? param, string source, PropertyInfo? pi, object? model); } } |
Validators/DefaultValidator: Belirtilen işaretlemeye uygun Validator bulunamaz ise, hata alınmaması amacı ile default bir validator yaratılıp, bunun geriye dönülmesi sağlanmıştır.
1 2 3 4 5 6 7 8 9 10 |
namespace ValidationFactory.Validators { public record DefaultValidator<T> : IValidator<T> { public List<(bool, Exception)> Validate(T value,int? type,string source,System.Reflection.PropertyInfo pi,object model) { return new List<(bool, Exception)>(); } } } |
Validators/DateValidator: Tarih alanlarının yaş ya da zaman kontrolü amacı ile, belirtilen minimum yıldan büyük olmasına bakılır. Aksi durumda, “errorList” hata listesine ilgili error eklenerek geriye dönülür. Bu Validator’da tek hata dönülse de, bazı validatorlarda birden fazla hata dönüldüğü için, global tuple errorList (“List<bool,Execption>()“) şeklinde tanımlanmıştır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
namespace ValidationFactory.Validators { public record DateValidator<T>() : IValidator<T> { public List<(bool, Exception)> Validate(T value, int? minYear, string source, System.Reflection.PropertyInfo pi,object model) { var errorList = new List<(bool, Exception)>(); if (!DateTime.TryParse(value.ToString(), out DateTime temp)) { throw new ArgumentException("T must be proper System.DateTime."); } string stringValue = value.ToString(); // Check minYear if ((DateTime.Parse(stringValue)).Year < minYear) { Console.WriteLine("Time is too old. Wrong Date!"); errorList.Add((false, new Exception($"Time is too old. Wrong Date! Year Must Bigger Than > {minYear}") { Source = source })); } if (errorList.Count == 0) Console.WriteLine("All tests succesful"); return errorList; } } } |
Validators/StringValidator: Bu validator’da, string bir text’in 3 farklı kontrolü yapılmaktadır.
- 1. Kontrol “if (stringValue.Length > max)” girilen text, belli bir karakterden fazla olamaz.
- 2. Kontrol “(!(!stringValue.Equals(stringValue.ToLower())))” girilen text’in içinde, en az bir karakterin büyük harf olması gerekmektedir.
- 3. Kontrol { “!”, “@”, “#”, “$”, “%”, “^”, “&”, “*”, “(“, “)”, “-” } girilen text’in içinde, ilgili karakterlerden hiçbiri geçmemelidir.
Bu 3 hata, bir tuple errorList (“List<bool,Execption>()“)’e doldurulmakta ve geri dönülmektedir. Böylece tüm kontrollerin, tek bir sonuç altında geriye dönülmesi sağlanmaktadır.
if (errorList.Count == 0): Eğer hiç hata yok ise “”All tests succesful” şeklinde bir mesaj konsola’a basılmaktadır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
namespace ValidationFactory.Validators { public record StringValidator<T>() : IValidator<T> { public List<(bool, Exception)> Validate(T value, int? max, string source, System.Reflection.PropertyInfo pi,object model) { var errorList = new List<(bool, Exception)>(); if (!typeof(T).IsValueType && typeof(T) != typeof(String)) { throw new ArgumentException("T must be a value type or System.String."); } string stringValue = value.ToString(); List<string> invalidChars = new List<string>() { "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "-" }; // Check for length //1. Control if (stringValue.Length > max) { Console.WriteLine("String too Long"); errorList.Add((false, new Exception($"String too Long. Text Must Shorter Than < {max}") { Source = source })); } //2. Control if (!(!stringValue.Equals(stringValue.ToLower()))) { //Check for min 1 uppercase Console.WriteLine("Requres at least one uppercase"); errorList.Add((false, new Exception("Requres at least one uppercase") { Source = source })); } //Iterate your list of invalids and check if input has one //3. Control foreach (string s in invalidChars) { if (stringValue.Contains(s)) { Console.WriteLine("String contains invalid character: " + s); Exception exception = new Exception("String contains invalid character: " + s) { Source=source}; errorList.Add((false, exception)); //pi.SetValue(model, "Bora Kasmer"); break; } } if (errorList.Count == 0) Console.WriteLine("All tests succesful"); return errorList; } } } |
Validators/EmailValidator: Bu validatorda, işler biraz farklılaşmaktadır. Burada girilen bir email’in validasyonu, girilen parametreye göre farklı bussineslarda kontrol edilmektedir. Bu farklı bussines seçimi aslında, hemen bir sonraki adımda tanımlanan “enum EmailValidateType” bir “int? validateType” ile yapılmaktadır. Bu enum içerisindeki (Syntax, Goverment, Education ve Gmail) senaryolarının kontrolü yapılmıştır.
- “case EmailValidateType.Syntax“: Standart geçerli bir email kontrolünün yapıldığı bir senaryodur.
- “case EmailValidateType.Gmail:” Girilen mailin, gmail olup olmadığı kontrol edilir. Değil ise, en başta yaratılan “errorList“‘e eklenir.
- case EmailValidateType.Government: Girilen mailin, bir devlet kurumuna ait olup olmadığı kontrol edilir.
- “case EmailValidateType.Education:” Girilen mailin, bir üniversite email’i olup olmadığı kontrol edilmektedir.
Not: Burada aslında “single responsibility” biraz bozulmuş gibi görünse de, ortak amaç email kontrolü altında farklı bussinesların koşturulmasıdır. Kod okunaklığı açısından bu yol tercih edilmiştir. Aksi takdirde herbir senaryo için, farklı bir attribute’un yaratılması gerekecek, bu da test, devops ve debug gibi süreçleri daha da zorlaştıracaktır. Kısacası, benzer işleri ortak bir grup altında toplamak, bazen iş hayatında normal karşılanabilmektedir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
using System.Text.RegularExpressions; using ValidationFactory.Attributes; namespace ValidationFactory.Validators { public record EmailValidator<T>() : IValidator<T> { public List<(bool, Exception)> Validate(T value, int? validateType,string source, System.Reflection.PropertyInfo pi,object model) { var errorList = new List<(bool, Exception)>(); if (!typeof(T).IsValueType && typeof(T) != typeof(String)) { throw new ArgumentException("T must be a value type or System.String."); } string emailValue = value.ToString(); if (validateType != null) { switch ((EmailValidateType)validateType) { case EmailValidateType.Syntax: { Regex mailRegex = new Regex(@"^([\w\.\-]+)@([\w\-]+)((\.(\w){2,3})+)$"); // Check for length if (!mailRegex.Match(emailValue).Success) { Console.WriteLine("Email is not correct"); errorList.Add((false, new Exception("Email is not correct") { Source = source })); } break; } case EmailValidateType.Gmail: { Regex mailRegex = new Regex(@"^([\w\.\-]+)@(gmail)\.(com)$"); // Check for length if (!mailRegex.Match(emailValue).Success) { Console.WriteLine("Email is not gmail"); errorList.Add((false, new Exception("Email is not gmail!") { Source = source })); } break; } case EmailValidateType.Government: { Regex mailRegex = new Regex(@"^([\w\.\-]+)@(gov)\.(tr)$"); // Check for length if (!mailRegex.Match(emailValue).Success) { Console.WriteLine("Email is not government email"); errorList.Add((false, new Exception("Email is not government email!") { Source = source })); } break; } case EmailValidateType.Education: { Regex mailRegex = new Regex(@"^([\w\.\-]+)@(edu)\.(tr)$"); // Check for length if (!mailRegex.Match(emailValue).Success) { Console.WriteLine("Email is not educational email"); errorList.Add((false, new Exception("Email is not educatinal email!") { Source = source })); } break; } } } if (errorList.Count == 0) Console.WriteLine("All tests succesful"); return errorList; } } } |
Email Bussines Validate Enum: Mail kontrolü için ilgili bussines seçimi, aşağıda tanımlanmış Enum paramtrelerine göre yapılmaktadır.
1 2 3 4 5 6 7 8 |
public enum EmailValidateType { Syntax=1, Education=2, Government=3, Gmail=4, Hotmail=5 } |
Validators/EncryptValidator: Bu validator’da amaç, text alanın kontrolü değil içeriğin “2 way encryption” dediğimiz, geri çevirilebilir şifrelenmesidir. Bu yüzden ilgili property’nin PropertyInfo’su => “System.Reflection.PropertyInfo pi” alınmıştır. Bu PropertyInfo ile, ilgili alanın değeri değiştirilebilecektir. Ayrıca “object model“, propertyleri kontrol edilen sınıfın ta kendisidir.
- “if (!typeof(T).IsValueType && typeof(T) != typeof(String))” : T value değerin, “ValueType” ve “string” olup olmadığı kontrol edilir.
- “using (Encryption en = new()) { pi.SetValue(model, en.EncryptText(stringValue)); }”: İlgili property değerinin şifrelenip setlendiği kısımdır.
- string stringValue = value.ToString(): Kontrol edilen sınıfın, işaretli property değeridir.
- en.EncryptText(stringValue): EncryptText(), bir sonraki adımda anlatılacak olan çift yönlü yani geri dönülebilir şifreleme methodudur.
- pi.SetValue(): İlgili property değerinin setlenmesi için PropertyInfo class’ının SetValue() methodu kullanılarak, var olan data güvenlik amaçlı şifreli hali ile değiştirilir.
Kısaca bu validator, kontrol amaçlı değil, var olan property’nin referance değerinin değiştirilmesi amacı ile kullanılmıştır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
using ValidationFactory.Security; namespace ValidationFactory.Validators { public record EncryptValidator<T>() : IValidator<T> { public List<(bool, Exception)> Validate(T value, int? max, string source, System.Reflection.PropertyInfo pi,object model) { var errorList = new List<(bool, Exception)>(); if (!typeof(T).IsValueType && typeof(T) != typeof(String)) { throw new ArgumentException("T must be a value type or System.String."); } string stringValue = value.ToString(); // Encrypted to the Value using (Encryption en = new()) { pi.SetValue(model, en.EncryptText(stringValue)); } if (errorList.Count == 0) Console.WriteLine("All tests succesful"); return errorList; } } } |
Validators/HashValidator: Bu validator’da amaç, text alanın kontrolü değil içeriğin “One way encryption” dediğimiz, yani geri çevrilemiyecek şekilde şifrelenmesidir. Burada EncryptionValidator’dan farklı olarak, bir “SaltKey”‘e ihtiyaç duyulmaktadır. Normalde tek bir SaltKey kullanılsa da, bu örnekde her HashKey için farklı bir SaltKey kullanılmış ve HashKey’in içine, ilgili SaltKey gömülmüştür.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
using ValidationFactory.Security; namespace ValidationFactory.Validators { public record HashValidator<T>() : IValidator<T> { public List<(bool, Exception)> Validate(T value, int? max, string source, System.Reflection.PropertyInfo pi,object model) { var errorList = new List<(bool, Exception)>(); if (!typeof(T).IsValueType && typeof(T) != typeof(String)) { throw new ArgumentException("T must be a value type or System.String."); } string stringValue = value.ToString(); // Hash to the Value using (Encryption en = new()) { pi.SetValue(model, en.HashCreate(stringValue,en.GenerateSalt())); } if (errorList.Count == 0) Console.WriteLine("All tests succesful"); return errorList; } } } |
Security/IEncryption: İşaretlenen propertyleri tek ve çift yönlü şifrelemek için kullan sınıfın genel işlevlerinin gösterildiği Interface, aşağıdaki gibidir.
- EncryptText: Çift yönlü verilen text’in, şifrelenmesini sağlar.
- DecryptText: Verilen encrypt text’in, deşifre edilmesini sağlar.
- HashCreate: Tek yönlü verilen textin, şifrelenmesini sağlar. Kısaca decrypt edilemez.
- ValidateHash: Girilen okunur bir text’i, aynı SaltKey ile Hashleyip, eşitliği kontrol edilecek diğer Hashli key ile matchlenip sonuç geriye dönülür.
- GenerateSalt: Hashleme amaçlı ihtiyaç duyulan SaltKey’in, üretilmesi için kullanılır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
using System; using System.Collections.Generic; using System.Text; namespace ValidationFactory.Security { public interface IEncryption { string EncryptText(string text, string privateKey = ""); string DecryptText(string text, string privateKey = ""); string HashCreate(string value, string salt); bool ValidateHash(string value, string salt, string hash); string GenerateSalt(); } } |
Security/Encryption: Aşağıda görüldüğü gibi, EncryptValidator ve HashValidator’ın kullanacağı methodlar bu sınıf altında tanımlanmıştır. “EncryptText ve HashCreate” methodları ile istenen içerik gizlenir.
Model/User: Aşağıda, User modelin belli propertyleri, ilerde tanımlanacak Attributelar ile işaretlenmiştir. Bu tanımlamalara göre gönderilen UserData, işlenmeden önce çalıştırılacak validatorler bu attributelara göre çağrılacak ve ilgili alanlar duruma göre, ya kontrol edilecek ya da içerikleri şifrelenecektir.
- [StringData(max = 10)] => UserName’in en fazla 10 karakter olması, Custom bir şekilde sağlanmıştır.
- [HashData] => Password’ün güvenlik amaçlı ,geri dönülmeyecek şekilde şifrelenmesi sağlanmıştır.
- [DateData(minYear =1900)] => BirthDate alanının 1900 yılından daha eski bir tarih olması engellenmiştir.
- [EmailData(type = EmailValidateType.Syntax)] => Email alanının, “Syntax” parametresi ile geçerli olup olmadığına bakılmıştır.
- [EncryptData] => Gene Email alanının, 2. bir kontrol ile şifrelenmesi sağlanmıştır.
- [EmailData(type = EmailValidateType.Gmail)] => Email2 alanının, “Gmail” paramteresi ile Gmail’e uygun bir mail adresi olup olmadığı kontrol edilmiştir.
- [EncryptData] => Gsm alanının güvenlik amaçlı 2 yönlü, yani geriye dönülebilir şekilde şifrelenmesi sağlanmıştır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
using ValidationFactory.Attributes; namespace ValidationFactory.Models { public class User { public int IdUser { get; set; } public string Name { get; set; } public string LastName { get; set; } [StringData(max = 10)] public string UserName { get; set; } [HashData] public string Password { get; set; } [DateData(minYear =1900)] public DateTime BirthDate { get; set; } [EmailData(type = EmailValidateType.Syntax)] [EncryptData] public string Email { get; set; } [EmailData(type = EmailValidateType.Gmail)] public string Email2 { get; set; } [EncryptData] public string Gsm { get; set; } public bool IsDeleted { get; set; } = false; } } |
Sıra geldi, bir sınıfın propertylerini işaretleme amaçlı kullanılan attributelere. Doğru işaretleme ve istenen bussines’a göre doğru parametre atama, ilgili validator’un koşturulması sırasında, tanımlı olduğu property için çağrılmasını ve gerekli kontrollerin yapılmasını sağlar. Aslında tanımlayacağımız attributelar, birer kabuktur. Yani içleri boş, parametre alan sadece işaretleme amaçlı kullanılan basit sınıflardır. Bu tanımlamaları, bir sonraki makalede kaldığımız yerden devam ederek detaylıca inceleyeceğiz. Daha sonrasında, bu attributelara göre doğru validator’ı nasıl seçeceğimize ve birden fazla attribute olması durumunda ne yapacağımıza hep beraber bakacağız. Ayrıca attributelere ait birden fazla parametre durumlarını da ayrıca araştıracağız.
Bir sonraki makalede görüşmek üzere hepinize hoşçakalın.
Son Yorumlar