Entity Üzerinde Generic Attribute Tanımlayarak Repository Katmanında Özelleştirme
Selamlar,
Bugün DBFirst yöntemi ile oluşturulmuş Entityleri, C#11 ve .Net 7.0 ile gelen Generic Attribute ile nasıl özelleştirebileceğimiz hep beraber inceleyeceğiz. Bu özelleştirmeye göre, Repository katmanında farklı aksiyonlar alacağız.
Öncelikle “GenericAttribute” adında bir “Console Application” oluşturulur.
DAL
DAL: Solution altında, DAL adında class library oluşturulup, Nugetten aşağıdaki kütüphaneler indirilir.
Bu örnekde, Northwind database’i üzerinden DBFirst yapılmıştır. Aşağıdaki “scaffold” komutu ile NorthwindContext ve Entityler oluşturulmuştur.
1 2 3 4 |
dotnet ef dbcontext scaffold "Server=.;Database=Northwind; Trusted_Connection=True;Encrypt =False" Microsoft.EntityFrameworkCore.SqlServer -o Entities --context-dir "Entities\DbContexts" --no-pluralize -c NorthwindContext |
Öncelikle gelin tüm entitylerin türeyeceği BaseEntity oluşturalım.
BaseEntity: Tüm Entityler bu sınıftan türetilecektir. Bu şekilde tüm entitylere ortak propertyler atanabilecektir. Ayrıca Reporsitory katmanına kısıtlama olarak, “BaseEntitiy“‘den türemeyen bir sınıf implemente edilemiyecektir.
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 System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using System.Text; using System.Threading.Tasks; namespace DAL { public abstract class BaseEntity { // DB’de karşılığı olmayan kolonların, [NotMapped] olarak işaretlenmesi gerekmektedir.Aksi takdirde hata alınır. private DateTime dateTime; [NotMapped] public DateTime UsedTime { get { this.dateTime = DateTime.Now; return dateTime; } set { } } // “WriteLog()” kullanıldığı zaman, ekrana kullanım zamanı log olarak basılmaktadır. public void WriteLog() { Console.WriteLine("".PadRight(40, '*')); Console.WriteLine($"UseTime: {UsedTime.ToLongDateString()}"); Console.WriteLine("".PadRight(40, '*')); } } } |
PartialEntites(1): Entity sınıflar, otomatik oluşturulurken partial keyword’ü ile oluşturulmaktadırlar. Böylece partial başka bir lokasyonda, yine aynı sınıfın kodlarını devam ettirebilmektedir. Yani derleme zamanında, alttaki partial DbUser sınıfı ile Entity DBUser sınıfı birleştirilecektir. Burada partial kullanılmasındaki amaç, DB’de bir değişiklik olduğu zaman, yukarıdaki “scaffold” komutu tekrar çalıştırılır ve “Entities” folder’ı altında var olan kodlar ezilir. İşte bu durumun olmaması için, ayrı bir sınıf içine kodlarımızı geliştiriyoruz.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Text; using System.Threading.Tasks; namespace DAL { public class PartialEntites { public partial class DbUsers : BaseEntity { } } } |
Şimdi sıra geldi Users Entity üzerindeki “PasswordHash“, “Gsm” ve “Email” kolonlarına, Custom Attributelar eklemeye. Daha sonra bu işaretleyicilere göre, Repository katmanında property bazında farklı aksiyonlar alınacaktır.
Öncelikle Attribute, Entity’ye ait farklı bir Partial classda, doğrudan property üzerine atanamaz. Orijinal Entity sınıfında aynı atama yapılmak istenir ise, demin de yukarıda bahsedildiği gibi var olan kodlar ezilebilir. Eskiden, herbir kolon için, farklı bir attribute yapmak gerekiyordu. Artık Generic bir Attribute yapıp, bunu tüm kolonlar için kullanabiliyoruz.
GenericEntityAttribute:
Aşağıda görüldüğü gibi 2 parametre alan ve 2. parametresi null olarak atanamasa bile, Constructor’ında nullable olarak tanımlanan bir Attribute görülmektedir. Bu Attribut’ün kullanıldığı bazı durumlarda 1 parametre, bazı durumlarda da 2 paramtereye ihtiyaç duyulmaktadır. Bu nedenle, 2. parametre nullable olarak atanmak istenmektedir. Aslında “T” paramteresi, Enum Attribute tipine, “T2” Parametresi de Length değerine karşılık gelmektedir.
Not: Generic Attributelarda <T, T2>, parametreleri nullable olarak “<T, T2?>” gibi henüz set edilememektedir. Ama onun da eli kulağındadı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 |
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace DAL { public class GenericEntityAttribute<T,T2> : Attribute { private T key { get; set; } private T2? parameter { get; set; } public GenericEntityAttribute(T key, T2? parameter) { this.key = key; this.parameter = parameter; } public T Key { get { return key; } } public T2? Parameter { get { return parameter; } } } } |
PartialEntites(2): Şimdi nasıl olacak da, Users tablosundaki “PasswordHash, Gsm ve Email” kolonlarına bu GenericAttribute’u atanacaktır.
Öncelikle “scaffold” komutu ile önceden oluşturulmuş bir partial class’a, ilgili attributelar başka bir partial sınıf içinde doğrudan atanamaz. Bunun için bir “MetadataType” ve ona ait bir “Class“‘ın tanımlanması gerekmektedir.
Not: Eğer, Entities folder’ı altında otomatik oluşturulmuş Users sınıfı üzerinde, ilgili attribute doğrudan eklenir ise, bir sonraki “scaffold” komutu çalıştırıldığında, yazılan kod üzerine ezileceği için silinecektir.
- “[GenericEntityAttribute<AttributeType, int>(AttributeType.CryptoData, 5)]“: “CryptoData” attribute tipi aşağıda tanımlanmıştır. Parametre olarak integer bir sayı almaktadır. Bu da şifrelenecek olan abağı, temsil etmektedir. Tanımlandığı property’i çift yönlü şifreleyecektir.
- “[GenericEntityAttribute<AttributeType, string>(AttributeType.HashData, null)]“: HashData attribute tanımlandığı kolonu, tek yönlü geri dönülemez olarak şifreleyecektir. Herhangi bir parametre almamaktadır. Bu nedenle string tanımlanan T2 parametresi null olarak atanmıştır. Maalesef GenericAttributelar’da,
“<AttributeType, string?>“şeklinde nullable parameter tanımlaması yoktur. - “[GenericEntityAttribute<AttributeType, int>(AttributeType.NumberValidateData, 6)]“: Üçüncü ve son Attribute’ümüz, “NumberValidateData” attribute’ü dür. Amaç, girilen sayının tam olarak hane sayısının belirlenmesidir. Anlaşıldığı üzere 2. parametre, girilen sayının hanesini göstermektedir.
- “[MetadataType(typeof(UserMetaData))]“: Son olarak, “UserMetaData” sınıfı, DBUser Entity üzerine MetadataType olarak atanmıştır. Eşleşmenin başarılı olması için, tanımlanan kolon isimlerinin iki sınıf içinde aynı olması gerekmektedir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
using System.ComponentModel.DataAnnotations; namespace DAL.Entities { public class PartialEntites { } [MetadataType(typeof(UserMetaData))] public partial class Users : BaseEntity { } public class UserMetaData { [GenericEntityAttribute<AttributeType, int>(AttributeType.CryptoData, 5)] public string Email { get; set; } [GenericEntityAttribute<AttributeType, string>(AttributeType.HashData, null)] public string PasswordHash { get; set; } [GenericEntityAttribute<AttributeType, int>(AttributeType.NumberValidateData, 6)] public string Gsm { get; set; } } } |
Enum: Proje içinde ihtiyaç duyulabilecek Attribute Tipleri, burada tanımlanmıştır.
1 2 3 4 5 6 7 8 9 |
namespace DAL { public enum AttributeType { CryptoData=1, HashData=2, NumberValidateData=3 } } |
Repository
Şimdi gelin Repository katmanını yazalım. Öncelikle Solution altında, Repository adında yeni bir class library oluşturup, kütüphane olarak Projects/DAL’a ekleyelim.
Repository/IRepository.cs: Örnek amaçlı, “Insert(T)” ve “Insert(IEnumerable<T>)” methodları tanımlanmıştır. Amaç, işaretlenmiş propertylerde araya girip, ilgili operasyonların otomatik bir şekilde gerçekleşmesini sağlamaktır. “isEncrypt” parametresi, performans amaçlı her entity için değil de, User tarafından “GenericEntityAttribute” atanmış Entitylerin propertylerinin gezilmesi için tanımlanmıştır. Çünkü Reflection ile kaydedilecek Entity’nin propertylerini gezip, tanımlı attributeları bulmak bir maliyettir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
using DAL; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Repository { public interface IRepository<T> where T : BaseEntity { void Insert(T entity, bool isEncrypt = false); void Insert(IEnumerable<T> entities, bool isEncrypt = false); } } |
Repository/GeneralRepository: Solution içindeki tüm DB işlemlerinin yapıldığı yer, burasıdır. Amaç DB operasyonları için alınan Entitylerin tek bir yerden yönetilerek, tanımlanan Attributelara göre middlewarede araya girilip, dataya istenen müdahalenin yapılabilmesidir. Böylece yeni bir Entity eklendiğinde, hiçbir kod değişikliğine gidilmeyecek, hatta var olan Entitylere yeni Attributeler ile farklı operasyon eklenmek istendiğinde de, kodun sadece tekbir yerinde değişiklik yapılması yeterli olacaktır.
- GeneralRepository<T> sınıfı IRepository’den türetilmiş, using ile Scoped olarak kullanılabilmesi için bir de IDisposible interfaceinden kalıtım alınmıştır. “T” tipinin, yani tanımlanacak Entity’nin mutlaka “BaseEntity”‘den türetilmesine zorlanılmıştır. Böylece Repository katmanına, yanlış tipte bir Entitiy’nin gelmesi engellenmiştir. Son olarak, “DBContext” ve işlem yapılacak “Entity”, private olarak tanımlanmıştır.
- “Insert(T entity)” ve “Insert(IEnumerable<T>)” methodları IRepository interfaceinden kalıtım ile gelmektedir. “_entites” değişkeni işlem yapılacak “<T> Entity“‘ye göre DBContext’den Set()’lenerek alınır. “isEncrypt” parametresine, default olarak false değeri atanmıştır. Eğer ilgili Entity’de tanımlanmış Custom bir Attribute var ise, bu değişken “true” atanmalıdır. Amaç makalenin öncesinde de bahsedilen performans konusudur. “DetachedAttributeEntityFields()” makalenin devamında anlatılacak olan, atanan GenericEntityAttributelere göre, propertylerin değerlerine MiddleWare’de müdahale edip değiştiren methoddur. Değişen entity, _entities’e eklenip, DbContext “Save ()” edilir. Bu sırada, BaseEntity’den gelen ve tüm entityler için ortak olarak tanımlanan “UsedTime” property’si, güncel zaman ile setlenir. Diğer “Insert(IEnumrable<T>)” methodu için bir implementasyon bu makale için yazılmamıştır.
-
DetachedAttributeEntityFields() methodunda, parametre olarak işlem yapılacak Entity ve ilgili DBContext alınır.
- “metadaTypes” değişkenine, Entity üstünde tanımlı tüm “MetadataType” attributeleri çekilir ve loop içinde gezilir. Bizim Users Entity örneğinde, 1 MetadataType’ımız bulunmaktadır(“UserMetaData“).
-
“properties” System.Reflection ile MetadataType class’ında tanımlı tüm tüm propertyler çekilir ve loop içinde gezilir. Bizim “UserMetaData” sınıfında, toplam 3 Property tanımlanmıştır(“Email, PasswordHash, Gsm“).
-
*“if (Attribute.IsDefined(pi, typeof(DAL.GenericEntityAttribute<AttributeType, int>)))“: Bu kısma lütfen dikkat ediniz. UserMetaData sınıfında tanımlanan “GenericAttributelerden <T, T2>” kullanılan parametre tiplerine göre, yani işaretleyicilere göre yakalanan Attribute’un tipi belirlenmeye çalışılmaktadır. Örneğin “<AttributeType, int>” işaretleyicine uygun, “CryptoData” ya da “NumberValidateData” attributeları uygundur.
- “prm” : Attribute içinde tanımlı bir parametre var ise, bu değer buradan çekilir. Örneğin => “(AttributeType.CryptoData, 5)“‘de 5 parametresi atanmıştır. Burada “5”, örneğin şifreleme için hangi abağın kullanılacağını göstermektedir.
- “if (type == AttributeType.CryptoData)” : Yakalanan Attribute’ın yani işaretleyicinin hangisi olduğu, Parametre olarak atanan “AttributeType” enumuna göre belirlenmektedir.
- “$”Encrypted[{prm}]_“: Eğer CryptoData ise, gerçek bir şifreleme yapılmamış ve örnek amaçlı göstermelik yakalanan property değerinin başına, “Encrypted[5]” değeri eklenmiştir.
- “else if (type == AttributeType.NumberValidateData)“: Eğer yakalanan Attribute “NumberValidateData” ise, property değerinin toplam uzunluğu alınır ve olması gereken uzunluk da “prm” değişkeni ile atanan parametreden alınarak, eksik kalan basamak değerleri, ilgili property value’u ya, “0” eklenerek tamamlanır. Örnek : “Paramtere olarak 6 sayısı girilmiş ise 3398 => 339800’e çevrilir.“
- Eğer yakalanan Attribute <T, T2> => <AttributeType, string> imzalarına uyuyor ve AttributeType’ı = “HasData” ise, yakalanan property değerinin başına, örnek amaçlı “HashData_” keyword’ü eklenecektir.
- Son olarak değerleri Middleware’de tanımlı Attiributelara göre değişen Entityler, kaydedilmek üzere geriye dönülürler.
- “Dispose()” Methodu GeneralRepository: IDisposable interface’inden türetildiği için implemente edilmiştir. Amaç GeneralRepository’i Scoped olarak “using()” içinde kullanıldıktan sonra, GC tarafından memory’den kaldırılmasını sağlamaktır.
Repository/GeneralRepository:
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 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
using DAL; using DAL.Entities.DbContexts; using Microsoft.EntityFrameworkCore; using System.ComponentModel.DataAnnotations; namespace Repository { public class GeneralRepository<T> : IRepository<T>, IDisposable where T : BaseEntity { private static NorthwindContext _northwindDbContext; private DbSet<T> _entities; public void Insert(T entity, bool isEncrypt = false) { using (_northwindDbContext = new()) { _entities = _northwindDbContext.Set<T>(); if (isEncrypt) { entity = DetachedAttributeEntityFields(entity, _northwindDbContext); } entity.UsedTime = DateTime.Now; // CreatedDate _entities.Add(entity); _northwindDbContext.SaveChanges(); } } public void Insert(IEnumerable<T> entities, bool isEncrypt = false) { throw new NotImplementedException(); } public virtual T DetachedAttributeEntityFields(T entity, NorthwindContext dbContext) { MetadataTypeAttribute[] metadataTypes = entity.GetType().GetCustomAttributes(true).OfType<MetadataTypeAttribute>().ToArray(); foreach (MetadataTypeAttribute metadata in metadataTypes) { System.Reflection.PropertyInfo[] properties = metadata.MetadataClassType.GetProperties(); //Metadata atanmış entity'nin tüm propertyleri tek tek alınır. foreach (System.Reflection.PropertyInfo pi in properties) { //Eğer ilgili property ait CryptoData flag'i var ise ilgili deger encrypt edilir. if (Attribute.IsDefined(pi, typeof(DAL.GenericEntityAttribute<AttributeType, int>))) { AttributeType type = ((GenericEntityAttribute<AttributeType, int>)pi.GetCustomAttributes(true)[0]).Key; int prm = ((GenericEntityAttribute<AttributeType, int>)pi.GetCustomAttributes(true)[0]).Parameter; if (type == AttributeType.CryptoData) dbContext.Entry(entity).Property(pi.Name).CurrentValue = $"Encrypted[{prm}]_" + dbContext.Entry(entity).Property(pi.Name).CurrentValue.ToString(); //NumberValidateData else if (type == AttributeType.NumberValidateData) { int len = dbContext.Entry(entity).Property(pi.Name).CurrentValue.ToString().Length; if (len < prm) { string addZero = "".PadRight((prm - len), '0'); dbContext.Entry(entity).Property(pi.Name).CurrentValue = dbContext.Entry(entity).Property(pi.Name).CurrentValue.ToString() + addZero; } } } //HashData else if (Attribute.IsDefined(pi, typeof(DAL.GenericEntityAttribute<AttributeType, string>))) { AttributeType type = ((GenericEntityAttribute<AttributeType, string>)pi.GetCustomAttributes(true)[0]).Key; if (type == AttributeType.HashData) dbContext.Entry(entity).Property(pi.Name).CurrentValue = $"HashData_" + dbContext.Entry(entity).Property(pi.Name).CurrentValue.ToString(); } } } return entity; } private bool _disposed = false; protected virtual void Dispose(bool disposing) { if (_disposed) { return; } if (disposing) { } _disposed = true; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } } } |
GenericAttribute
Şimdi sıra geldi GenericAttribute adında bir Console Application yaratıp, yeni girilen bir User’ı, DB’ye kaydetmeye.
GenericAttribute/Program.cs: Aşağıda görüldüğü gibi yeni bir User kaydı oluşturulmuş ve daha sonra Repository katmanındaki “Insert()” methodu çağrılarak yeni User, SqlDB’ye kaydedilmiştir. Repository katmanında, User Entity’nin tüm propertyleri gezilmiş ve tanımlı attributeların bulunduğu propertyler, Middlewarede ilgili işaretleme tipine göre, değiştirilip geri dönülmüştür.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
using DAL.Entities; using Repository; //Console.WriteLine("Hello, World!"); Users newUser = new() { Name = "Baris", LastName = "Manco", UserName = "bmanco", Email = "baris@barismanco.com", PasswordHash = "12345", Gsm = "3334", IsAdmin = true }; using (GeneralRepository<Users> repository = new()) { repository.Insert(newUser, true); } |
Kaydedilen User kaydı, aşağıda görüldüğü gibidir.
- PasswordHash : “HashData” attribute’ü ile işaretlenmiş ve Middleware’de başına “Hashdata_” keyword’ü getirilmiştir.
- Email: “CryptoData” attribute’ü ile işaretlenmiş ve Middleware’de başına “Encrypted[5]” keyword’ü getirilmiştir.
- Gsm: “NumberValidateData” attribute’ü ile işaretlenmiş ve Middleware’de en az “6” hane olması şeklinde bir parametre ataması yapıldığı için “3334” string’inin sonuna “00” getirilerek, uzunluk 6’ya tamamlanmıştır. Son durum “333400” şeklindedir.
Generic Attibutelar, .Net 7.0 ile hayatımıza girmiş olan yeniliklerden sadece 1 tanesidir. Bu makalede, yeni bir teknolojinin kodlarından ziyade, nerde ve nasıl kullanılabileceğinin öğrenmenin, onun nasıl yazıldığını öğrenmekten çok daha önemli olduğunu göstermek istedim. Repository Pattern, güncel iş hayatında ilk yazımı uzun süren ama sonrasında birçok işi kolaylaştıran bir Design Patterndir. Kod okunaklığı ve yeni geliştirmelerin kolaylıkla yapılabilmesi, reflection ile az da olsa yaşanan performans kaybının göz ardı edilmesini sağlamaktadır. Generic Attribute sayesinde, herbir bussines için tanımlanması gereken Attribute sayısı teke düşürülmekte, bu sayede kod okunaklığı ve yönetimi kolaylaştırılmaktadır.
Geldik bir makalenin daha sonuna. Yeni bir makalede görüşmek üzere hepinize hoşçakalın.
Github Source: https://github.com/borakasmer/GenericAttribute
Güzel yazılar yazıyorsunuz ama bence biraz basitleştirerek yazın.
Entity sınıflar, otomatik oluşturulurken partial keyword’ü ile oluşturulmaktadırlar. Böylece partial başka bir lokasyonda, yine aynı sınıfın kodlarını devam ettirebilmektedir. Yani derleme zamanında, alttaki partial DbUser sınıfı ile Entity DBUser sınıfı birleştirilecektir.
Eminim çoğu insan alttaki partial DbUser sınıfı ile Entity DBUser sınıfı birleştirilecektir.anlamamıştır.
Yazının bir çoğu böyle….Yani kodlar vs güzel de naçizane öneri tekrar okuyun ve hiç bilmeyen acaba analr mı deyin.