Repository Pattern ile İşlem Yapılacak Bir Kaydın Kitlenmesi ve Bir Dizi Kural Setinin Çalıştırılması
Selamlar,
Bu makalede, iş hayatında karşımıza bolca çıkabilecek olan bir kayıdın güncellenmesi durumunda, bir başkası tarafından değiştirilememesi konusuna ve bir de DB’ karşılığı olmayan sadece ViewModel’de olan kolonların, belirlenen bir takım Bussines Rulelar ile nasıl doldurulabileceği konularını tartışacağız. Ayrıca bütün bunların yanında, Repository katmanın gerçekten lazım olup olmadığı konusuna da detaylıca bakacağız.
Transaction:
Bu makalede .NET 5.0, Entity Framework ve DB olarak MSSQL kullanılacaktır. Günümüzde, bir kişinin bir kaydı güncelleme amaçlı üzerine aldığı zaman, bunun başkası tarafından değiştirilememesi amacı ile bir çok yöntem geliştirilmiştir.
- En basit olanı UpdateDate kontrolüdür. Örneğin Can bir kaydı üzerine aldığında, UpdateDate tarihi ile birlikte çeker. İlgili kayıt başka biri tarafından bu esnada güncellenir ise, UpdateDate alanı değişir. İşte bu sırada Can da aynı kaydı güncellemek istediğinde, ilgili kayda ait UpdateDate’in değiştiğini görecek ve güncelleme işlemini yapamayacaktır. Bu maalesef iyi bir çözüm değildir. Evet datanın birbirini ezmesi engellenmiştir ama, Can’ın doldurmuş olduğu form, kaydet butonuna basıldıktan sonra hata alındığı için, çöpe gitmiştir.
if(“2020-12-19 02:48:59.820” == “2020-12-19 02:49:17.550”)
- Genel olarak gördüğüm ikinci bir çözüm ve asla yapılmaması gerekeni, üzerinde işlem yapılan tabloların kaydının ortak bir tabloda loglanmasıdır. Yani, üzerinde işlem yapılan herbir tablonun işlem yapılan kaydı, kimin yaptığı ve ne zaman yaptığı bilgilerinin ortak bir tabloda tutulmasıdır. Böylece 2. bir kişi aynı kayda girmek istediğinde, bu tabloya bakılarak izin verilmekte ya da verilmemektedir. Bu şekilde bakılınca, herşey masumane durmaktadır. Hatta kimin hangi kayıt ile uğraştığı bilgisine, ilgili tablo üzeriden kolayca erişilebilmektedir. Bu belki 50-100 kullanıcılı bir uygulama için gayet güzel bir çözümdür. Ama 5K, 10K gibi kullanıcı sayısının çok olduğu yapılarda, tam bir felakettir. Peki neden?
Resim Kaynağı: assets.site-static.com
Nedeni tamamen Lock mekanizmasıdır. Örneğin aşağıda Transaction açılarak bir locklama örneği verilmiştir. Kritik bir tabloda, kayıdın silinmesi ya da güncellenmesi anında, bir başka client’ın değişen kaydın, eski halini görüntülemesini engellemek için kullanılmaktadır. Ama tabi bu durum, tam bir performans düşmanıdır. Çünkü read ve write işlemlerine kilitli olan bir tablo üzerinde, kimse kilit kalkana kadar bizim örneğimizde Transaction.Commit() ya da Transaction.Rollback() olana kadar, bir işlem yapamaz. Bu da response time’ı doğal olarak arttırır.
Beklemek Güzeldir Ama Doğru Durakta ― Anonim
Örnek .Net 5.0 Transaction kodu: İlgili transaction altında çekilen kayıt, o client için locklanır. Bu arada ilgili kolonlar güncellenir iken, bir başka client’ın ilgili kaydı değiştirmesi engellenir. Bu da farklı tablolar ile çalışan clientların, aynı tabloya yazmaları yüzünden, birbirlerini beklemelerine neden olur.
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 |
var response = new ServiceResponse<bool>(null); using (var transaction = _context.Database.BeginTransaction()) { try { List<T> actions = new List<T>(); T action; actions = _repository.Table.Where(sc => sc.OpenStatusUserId == userId).ToList(); foreach (var act in actions) { act.OpenStatus = true; act.OpenStatusUserId = null; act.ModDate = null; } _repository.Update(actions); transaction.Commit(); response.IsSuccessful = true; return response; } catch (Exception ex) { transaction.Rollback(); response.IsSuccessful = false; return response; } } |
SQL DB:
Buradan yola çıkar isek, 2. çözüm olarak sunulan tüm tabloların logların tutulduğu tek bir tablo öreneğine bir bakalım. Aşağıda görüldüğü gibi işlem yapılan tablo, işlem yapılan satır, işlem yapan kişi ve zamanı bir tabloda tutulmaktadır.
Yukarıdaki tabloya yeni bir kayıt girileceği ya da işi biten kaydın silineceği sırada Lock’lanması gerekmektedir. Eğer bu işlem yapılmaz ya da aşağıdaki gibi sorgularda tablonun kitli olup olunmamasına bakılmaz ise, 2 kişi aynı kaydı aynı anda üzerine alıp çalışabilmektedir. Bu da en istenmeyen kirli dataya yol açacaktır.
1 |
SELECT * FROM [dbo].[AB_ABONE] WITH (NOLOCK) |
Eğer tüm tablolar tek bir tabloya kaydedilir ve ilgili tablo Locklanır ise, farklı tablolarda işlem yapan clientlar, birbirlerini beklemek zorunda kalacaklar ve bu da, artan client sayısı ile birlikte büyük bir performans problemine dönüşecektir.
O zaman gelin her tablonun Lock işlemini kendi üzerinde tutalım ve bu Locklama işini tüm tablolara dağıtalım. Böylece X tablosunda çalışan client, Y tablosunda çalışan client’ın işleminin bitmesini beklemesin.
Örnek olarak DB_SECURITY_ACTION tablosuna bakalım: ModDate, OpenStatus ve OpenStatusUserId kolonları, tüm entity tablolara eklenmiştir.
- ModDate: Son değiştirilen tarih.
- OpenStatus: Lockli mı değil mi ?
- OpenStatusUserId: Kaydı üzerine alan kişinin ID’si.
Örnek projede DB First kullanılmıştır. DbSecurityAction sınıfı aşağıdaki gibi scaffold komutu ile DB’den oluşturulmuştur.
Seçilen DB’den ilgili DBContext ve Entitiylerin oluştuğu, Terminalde proje ana dizininde çağrılan örnek scaffold komutu:
1 2 |
dotnet ef dbcontext scaffold "Server=.;Database=ABYS_PROD;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -o Entities --context-dir "Entities\DbContexts" --no-pluralize -c DashboardContext -f --project Dashboard.DB -s Dashboard.DB |
DbSecurityAction.cs: Üzerinde işlem yapılacak tablo.
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 |
using System; using System.Collections.Generic; #nullable disable namespace Dashboard.DB.Entities { public partial class DbSecurityAction { public int IdSecurityAction { get; set; } public long? ActionNumber { get; set; } public string ActionName { get; set; } public int? IdSecurityController { get; set; } public bool? IsDeleted { get; set; } public int? CreUser { get; set; } public DateTime CreDate { get; set; } public int? ModUser { get; set; } public DateTime? ModDate { get; set; } public string Client { get; set; } public string ClientIp { get; set; } public bool Deleted { get; set; } public bool? OpenStatus { get; set; } public int? OpenStatusUserId { get; set; } public virtual DbSecurityController IdSecurityControllerNavigation { get; set; } } } |
Global Hale Getirme:
Şimdiki amacımız, bu locklama işleminin olacağı tabloları işaretlemek ve ona göre bir takım aksiyonlar almak olacaktır.
Bu amaçla IExpired interface’i, aşağıdaki gibi Core katmanında oluşturulmuştur.
IExpired.cs: İlgili interfaceden türemiş sınıflar güncellendiği ya da silindiği zaman, bu interfaceden türeyen sınıfların ilgili kolonları, locklama amaçlı doldurulur. Bu kolonlar aşağıda görüldüğü gibidir:
- ModDate: Güncelleme amaçlı, kayıdın bir client tarafından üzerine alındığı an.
- OpenStatus: Kayıdın birisi tarafından üzerine alındığını belirten işaret.
- OpenStatusUserId: Editleme işlemini yapan kişi.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
using System; using System.Collections.Generic; using System.Text; namespace Dashboard.Core.Extensions { public interface IExpired { public DateTime? ModDate { get; set; } public bool? OpenStatus { get; set; } public int? OpenStatusUserId { get; set; } } } |
Bir başka Partail Class‘da, DB’den otomatik oluşturulan Entitylerin istenen interfaceden türetildiği DB katmanında tanımlanan classdır.
PartialEntites.cs: IExpire interface’inden türetilen classlar, bu şekilde işaretlenirler.
Not: Farklı bir partial class’da, ilgili interface atamasının yapmasının nedeni, scaffold çalıştırıldığı zaman otomatik oluşan classlar ile manuel yazılan kodların ezilmesinin önlenmesidir.
1 2 3 4 5 |
namespace Dashboard.DB.Entities { class PartialEntites{} public partial class DbSecurityAction : BaseEntity, IExpired { } } |
Şimdi DbSecurtiyAction Entitysini, IExpire interface’i ile işaretledik. Gelin bu interface ile işaretli classları, “Get()” işleminde Global olarak Lock’layalım.
1.Yol Inject StatusOperations Service:
İşte tam bu örnek, bazen projelerde servis katmanında nasıl araya girebileceğimizi gösteren bir durumdur.
IStatusOperations: Global olarak kayıdın durumunu değiştiren Interceptor bir servis yazılır. IStatusOperation interface’inin, IExpired interface’inden türemesi, zorunlu kılınır. Tekbir methodu vardır. O da ChangeOpenStatus(). Ve generic tipte bir değer almaktadır. Geriye dönüş tipi olarak da DateTime? yani ModDate dönmektedir.
1 2 3 4 5 6 7 |
namespace Dashboard.Services { public interface IStatusOperation<T> where T : BaseEntity, IExpired { public IServiceResponse<DateTime?> ChangeOpenStatus(int idEntity, int userId, bool state = false); } } |
StatusOperations: Oluşturulan Interceptor servisde, Dependency Injection ile Repository ve DBContext sınıfları almıştır. Amaç, çekilen kaydın ilgili kolonlara set edilerek locklanması, yani başka biri tarafından düzenlenmesinin engellenmesidir.
Seni kendime sakladım. Hepsini ben hesapladım ― Lyrics – Duman
- “using (var transaction = _context.Database.BeginTransaction())”: İşlem yapılmadan önce gönderilen tablo, Transaction açılarak DB bazlı locklanır. Entitiy’de, default Transaction Isolation Level, READ COMMITTED‘dir. Bu işlem SqlProfile edilerek rahatlıkla görülebilir.
- “PART1 actions = _repository.Table.Where(sc => sc.OpenStatusUserId == userId).ToList()“: Client’ın o Entitiy için üzerine aldığı lock’lı olan önceki tüm kayıtlar boşa çıkarılır.
- “//PART2 if (idEntity != 0)“: Eğer seçilen bir kayıt var ise, ilgili Client için kitlenir.
- “action = _repository.GetById(idEntity)“: İlgili kayıt, Repository katmanı kullanılarak Id’sine göre çekilir.
- “action.OpenStatus = state; action.OpenStatusUserId = userId; action.ModDate = DateTime.Now” : İlgili kayıt’ın OpenStatus’ü false olarak setlenir ve başkasının ilgili kaydı üzerine alması engellenir. “ModDate” kayıdın, ilgili kişinin üzerine alandığı tarih ve saat alanıdır. OpenStatusUserId, kayıdı üzerine alan kişidir.
- “if (action.OpenStatus == true)“: Son bir kere başka biri, ilgili kaydı üzerine almış mı diye kontrol edilir.
- “else if (action.OpenStatusUserId != userId)“: Eğer, ilgili kaydı üzerine alan kişi biz değil isek, işleme devam edilmez ve geriye false değeri dönülür.
- “_repository.Update(actions)“: Kayıdın son halinin DB’ye, Repository katmanı kullanılarak kaydedildiği yerdir.
- “transaction.Commit(); response.IsSuccessful = true; return response“: Açılan Transaction kapatılır ve geriye “true” değeri dönülür.
- “catch (Exception ex) { transaction.Rollback()“: Hata durumunda, yapılan DB işlemleri geri alınmaktadır.
- “response.IsSuccessful = false; return response” : Geriye olumsuz cevap dönülmektedir.
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 |
using Dashboard.Core; using Dashboard.Core.ApiResponse; using Dashboard.Core.Extensions; using Dashboard.DB.PartialEntites; using Dashboard.Repository; using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Dashboard.Services { public class StatusOperation<T> : IStatusOperation<T> where T : BaseEntity, IExpired { private readonly IRepository<T> _repository; private readonly VbtContext _context; public StatusOperation(IRepository<T> repository, VbtContext context) { _repository = repository; _context = context; } //Seçilen kaydın locklandığı veya lock'ının kaldırıldığı methoddur.(DB'de çalışır.) public IServiceResponse<DateTime?> ChangeOpenStatus(int idEntity, int userId, bool state = false) { var response = new ServiceResponse<DateTime?>(null); DateTime? modDate = null; using (var transaction = _context.Database.BeginTransaction()) { try { List<T> actions = new List<T>(); T action; //PART1 //Kişi üzerindekileri bırakırken önceki kilitli tüm satırları da bırakılır. Yani bütün kişiye ait lock'ı kayıtlar kaldırılır. actions = _repository.Table.Where(sc => sc.OpenStatusUserId == userId).ToList(); foreach (var act in actions) { act.OpenStatus = true; act.OpenStatusUserId = null; act.ModDate = null; } //PART2 //Seçilen kayıt ilgili kişi tarafından kitlenir. Ya da lock'ı kaldırılır. if (idEntity != 0) { action = _repository.GetById(idEntity); if (action.OpenStatus == true) //Başkası bizden önce almamış ise işleme devam edilir. { action.OpenStatus = state; action.OpenStatusUserId = userId; action.ModDate = DateTime.Now; modDate = action.ModDate; actions.Add(action); } else if (action.OpenStatusUserId != userId) //Başka biri bizden önce almış ise geriye false döner ve hiçbir kaydı güncellemeyiz. { response.IsSuccessful = false; transaction.Commit(); return response; } } _repository.Update(actions); transaction.Commit(); response.IsSuccessful = true; response.Entity = modDate; return response; } catch (Exception ex) { transaction.Rollback(); response.IsSuccessful = false; return response; } } } } } |
Gelin ilgili servisi, .Net 5.0 projede Startup.cs’de aşağıdaki gibi tanıtalım.
1 |
services.AddScoped(typeof(IStatusOperation<>), typeof(StatusOperation<>)); |
Şimdi gelin bu servisi Dependecy Injection ile ,”Get()” ile üzerine alınan servisde kullanalım.
Service/SecurityActionService.cs: Aşağıda görüldüğü gibi, yukarıda tanımlanan”statusOperation” servisi, class’a Dependecy Injection ile ilgili Entity, “DbSecurityAction” şeklinde atanarak tanımlanmıştır.
- ChangeActionOpenStatus() methodunda, ilgili statusOperation sınıfının “ChangeOpenStatus()” methodu, (idSecurityAction, userId, state) parametreleri ile çağrılır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
namespace Dashboard.Services.Management.SecurityAction { public class SecurityActionService : ISecurityActionService { . private readonly IStatusOperation<DB.Entities.DbSecurityAction> _statusOperation; public SecurityActionService(IStatusOperation<DB.Entities.DbSecurityAction> statusOperation,...) { . . _statusOperation = statusOperation; } public IServiceResponse<DateTime?> ChangeActionOpenStatus(int idSecurityAction, int userId, bool state = false) { return _statusOperation.ChangeOpenStatus(idSecurityAction, userId, state); } } } |
WebApi/Controllers/ActionController.cs: Front-End tarafından çağrılan endpoint’dir. “ChangeActionOpenStatus()” editlenecek dosyaları, üzerine alan kişi tarafından kitler.
- “ChangeActionOpenStatus()“: Methodunda parametre olarak OpenStatus sınıfı beklenmektedir. Yani kilitlenecek veya serbest bırakılacak SecurityActionID ve yapılacak işlem status.
- İlgili servis dışarıya, “[Route(“ChangeActionOpenStatus“)]” şeklinde açılmaktadır.
- “_actionService.ChangeActionOpenStatus()”: Yukarıda tanımlanan ChangeActionOpenStatus() methodu parametre olarak, güncellenecek kayıdın ID’si, işlemi yapacak UserID’si ve status’u yani, kayıdın kitleneceği ya da boşa çıkaralacağı bilgisi gönderilir.
Not: “_workContext”, projede session gibi kullanılan ortak kullanılan alanları tutan bir sınıftır. Bu örnekde “CurrentUserId” değeri, _workContext’den alınmaktadır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
. . [Route("ChangeActionOpenStatus")] [HttpPost] public IServiceResponse<DateTime?> ChangeActionOpenStatus([FromBody] OpenStatus data) { var response = new ServiceResponse<DateTime?>(HttpContext); if (data.IdSecurityAction > 0) { var result = _actionService.ChangeActionOpenStatus(data.IdSecurityAction, _workContext.CurrentUserId,data.status); response.IsSuccessful = result.IsSuccessful; response.Entity = result.Entity; } return response; } } public class OpenStatus { public int IdSecurityAction { get; set; } public bool status { get; set; } } |
Aşağıda görüldüğü gibi 3 nolu kayıt UserID 1 tarafından kitlenmiştir.
Kısaca tüm servislere Injecte edilecek, “Generic” yeni bir servis yazılmış, ve böylece kod tekrarında kaçınılarak “Global” bir çözüme gidilmiştir.
2.Yol Repository Katmanı:
Bir çok yerde Repository katmanının gereksiz olduğu belirtilmektedir. Ama bu projesine göre değişiklik göstermektedir. Kısaca bu seneryoda, Repository pattern çok işimize yarıyacaktır. Nasıl mı?
Öncelikle yukarıdaki örnekte, ilgili “IStatusOperation” servisi, her serviste çağrılacak ve kod tekrarı olmasa da, servis heryerden birçok kere call edilecektir. Peki ya bu kodu tekrar tekrar çağırmak yerine, tek bir yerden çağırma şansımız olsa idi. İşte bu katmana Repository katmanı diyoruz. Servis katmanında DB ile iletişime girilmediği, bussines logic’in yazılıp gelen datanın işlendiği ve ilgili datanın en son olarak kaydedilmek ya da DB’den çekilme için gidildiği katman, Repository katmanıdır.
Repository/GeneralRepository/GetById():
Aşağıdaki örnekde tüm servisler, Generic Repository katmanındaki aynı GetById() methodunu çağırdıkları için, kodlar Global hale getirilmiştir. Böylece, her bir servis de ilgili methodun çağrılması ile yapılacak kod tekrarından, kurtulunmuştur.
- “public virtual T GetById(object id)“: Generic tipde tüm Entityler, parametre olarak verilebilir.
- “if (entity is Core.Extensions.IExpired && _context.Database.CurrentTransaction==null)” : Eğer gelen Entity, “IExpired” interface’inden türetilmiş ise, ayrıca ilgili context üzerinde başka bir transaction başlatılmamış ise ilgili method çalıştırılır.
- “using (var transaction = _context.Database.BeginTransaction())“: Locklama işlemi için, transaction başlatılır. Bu şekilde aynı zamanda Lock işlemi yapılmış olunur.
- “var model = (Core.Extensions.IExpired)entity“: İlgili model, IExpire interface’ine cast edilir. Amaç ilgili propertylere erişebilmektir.
- “if (model.OpenStatus == true || (model.OpenStatus == false && model.OpenStatusUserId == _workContext.CurrentUserId))“: Son bir kontrol ile, ilgili kayıt Lock’lı değilse ya da bizim tarafımızdan locklı ise, işleme devam edilir.
- “model.OpenStatus = false; model.OpenStatusUserId = _workContext.CurrentUserId; model.ModDate = DateTime.Now; _context.SaveChanges()” : İlgili kayıt, bizim tarafımızdan o anki zaman ile ilgili propertyler set edilerek, kitlenir.
- “transaction.Commit()” : İşlemin başarılı olması durumunda Transaction tamamlanır.
- “catch (Exception ex) { transaction.Rollback()“: Hata durumunda yapılan tüm işler geri alını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 |
public virtual T GetById(object id) { T entity = Entities.Find(id); //Locklama işlemi Repository katmanında yapılır ise... if (entity is Core.Extensions.IExpired && _context.Database.CurrentTransaction==null) { using (var transaction = _context.Database.BeginTransaction()) { try { var model = (Core.Extensions.IExpired)entity; if (model.OpenStatus == true || (model.OpenStatus == false && model.OpenStatusUserId == _workContext.CurrentUserId)) { model.OpenStatus = false; model.OpenStatusUserId = _workContext.CurrentUserId; model.ModDate = DateTime.Now; _context.SaveChanges(); } transaction.Commit(); } catch (Exception ex) { transaction.Rollback(); } } } return entity; } |
Böylece tüm solution’daki servisler aynı Repository katmanını kullandığı için, “Get()” işleminde gerçek anlamda “Global” olarak istenen yani sadece “IExpired” interface’inden türeyen Entityler lock’lanabilmiştir.
Repository/GeneralRepository/UpdateMatchEntity(): Aşağıda görüldüğü gibi, güncellenen data, “IExpired” interface’inden türemiş ise, güncellenecek data ile DB’deki datanın “ModDate” kolonları karşılaştırılır. Farklı olması durumunda güncelleme işlemine deva edilmez. Bu da, ilgili kaydın başkası tarafından değiştirilip değiştirilmediğinin kontrolünün son adımıdır.
Not: “InsertElastic(updateEntity, “Update”, _elasticConfig.Value.ElasticAuditIndex)”: Konu dışı olarak, IAuditable interface’inden türüyen classlar, ElasticSearch’e loglama amacı ile atılırlar.
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 |
public virtual void UpdateMatchEntity(T updateEntity, T setEntity, bool isEncrypt = false) { //updateEntity: Varolan hali, setEntity: Güncellenmiş hali if (setEntity == null) throw new ArgumentNullException(nameof(setEntity)); if (updateEntity == null) throw new ArgumentNullException(nameof(updateEntity)); if (updateEntity is Dashboard.Core.Extensions.IExpired)//Güvenlik amaçlı eklenmiştir. Başkası tarafından güncellendi mi? { if ( ((Core.Extensions.IExpired)setEntity).ModDate.ToString() != ((Core.Extensions.IExpired)updateEntity).ModDate.ToString()) { throw new Exception("Güncellenecek tablo başkası tarafından değiştirilmiştir."); } } //Audit Eğer güncelenen sınıf IAuditable'dan türemiş ise ElasticSearc'ün "audit_log" indexine kaydedilir. InsertElastic(updateEntity, "Update", _elasticConfig.Value.ElasticAuditIndex); _context.Entry(updateEntity).CurrentValues.SetValues(setEntity);//Tüm kayıtlar, kolon eşitlemesine gitmeden bir entity'den diğerine atanır. foreach (var property in _context.Entry(setEntity).Properties)//Sadece değişen kolonlar SQL'e Update amaçlı gönderilir. { if (property.CurrentValue == null) { _context.Entry(updateEntity).Property(property.Metadata.Name).IsModified = false; } } _context.SaveChanges(); } |
Rule Setleri:
Geldik istenen kural setlerinin, çekilen data üzerinde çalıştırılmasına. Amaç, DB’de karşılığı olmayan kolonların, yani sadece ViewModel’de olan kolonların, önceden belirlenen Bussines Rulelara göre doldurulup client’a gönderilmesini sağlamaktır.
Bu işlemi Global olarak düşünmeli ve kod tekrarından kaçınılmalıdır.
SecurityActionModel: Aşağıda görüldüğü gibi ilgili ViewModel, DB’den bağımsız olarak “IsCancelState” ve “IsApprovelState” adında iki kolona sahiptir. Bu kolonlar bir dizi çalıştırılacak Bussines Rulelara göre doldurulacak, ve ilgili Client’a gönderilecektir. Bu kolonların DB’de bir karşılığı yoktur.
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 |
using System; using System.Collections.Generic; using System.Text; namespace Dashboard.Core.Models.Management { public class SecurityActionModel : BaseModel { public int IdSecurityAction { get; set; } public long? ActionNumber { get; set; } public string ActionName { get; set; } public int? IdSecurityController { get; set; } public bool? IsDeleted { get; set; } public int? CreUser { get; set; } public DateTime CreDate { get; set; } public int? ModUser { get; set; } public DateTime? ModDate { get; set; } public string Client { get; set; } public string ClientIp { get; set; } public bool Deleted { get; set; } public bool? IsApprovelState { get; set; } public bool? IsCancelState { get; set; } public bool? OpenStatus { get; set; } public int? OpenStatusUserId { get; set; } } } |
Gelin önce çalıştırılacak Ruleları tanımlayalım. Rulelar bir Bussines logic olduğuna göre, onları tanımlayacağımız en iyi yer Servis katmanıdır.
Service/Management/SecurityAction/SecurityActionRules:
ISecurityActionRule: Tüm rulelar bu interface’den türetilmektedir. Hepsinin ortak methodu “ExecuteRule“‘dur.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
using Dashboard.Core.Models.Management; using Dashboard.DB.Entities; using System; using System.Collections.Generic; using System.Text; namespace Dashboard.Services.Management.SecurityAction { public interface ISecurityActionRule { List<SecurityActionModel> ExecuteRule(List<SecurityActionModel> actionList); } } |
Service/Management/SecurityAction/SecurityActionRules/SecurityActionApprovelRule: Aşağıda görüldüğü gibi “IsApprovelState” kolonunu dolduran Bussines Ruledur. Yazılan Rule, örnek olması amacı ile tamamen hayal ürünüdür :)
- “public class SecurityActionApprovelRule : ISecurityActionRule”: İlgili Rule sınıfı “ISecurityActionRule” interface’inden türetilmiştir.
- “public SecurityActionApprovelRule(int currentUserId) “: İşlemi gerçekleştiren UserID constructor’da alınır.
- “public List<SecurityActionModel> ExecuteRule(List<SecurityActionModel> actionList)”: Interface’in zorunlu kıldığı ExecuteRule, Approvel Bussines Logiğine göre özelleştirilir.
- “actionList.ForEach(item =>”: Gönderilen tüm kayıtlar gezilir.
- “if (_currentUserId != item.OpenStatusUserId && item.OpenStatus == false) { item.IsApprovelState = false; }”: Eğer ilgili kayıt başkası tarafından kitlenmiş ise, ilgili rule çalıştırılmadan false atanır.
- “else if ((DateTime.Now – item.CreDate).TotalDays > 180) { item.IsApprovelState = false; }”: Esas Bussines Rule bu satırdır. Yaratılma tarihi 180 günden fazla ise ilgili “IsApprovelState” kolonu “false” dönülür.
- “else { item.IsApprovelState = true; }” Eğer 180 günden daha yeni ise “true” dönülü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 |
using Dashboard.DB.Entities; using System; using System.Collections.Generic; using System.Text; using System.Linq; using Dashboard.Core.Models.Management; using Dashboard.Core; namespace Dashboard.Services.Management.SecurityAction { public class SecurityActionApprovelRule : ISecurityActionRule { int _currentUserId; public SecurityActionApprovelRule(int currentUserId) { _currentUserId = currentUserId; } public List<SecurityActionModel> ExecuteRule(List<SecurityActionModel> actionList) { actionList.ForEach(item => { //lock'lı ise ve locklayan kendi değil ise zaten IsApprovelState otomatik false olur. if (_currentUserId != item.OpenStatusUserId && item.OpenStatus == false) { item.IsApprovelState = false; } else if ((DateTime.Now - item.CreDate).TotalDays > 180) { item.IsApprovelState = false; } else { item.IsApprovelState = true; } }); return actionList; } } } |
Service/Management/SecurityAction/SecurityActionRules/SecurityActionCancelRule: Aşağıda görüldüğü gibi “IsCancelState” kolonunu dolduran Bussines Ruledur. Bu yazılan Rule da, örnek olması amacı ile tamamen hayal ürünüdür :)
- “public class SecurityActionCancelRule : ISecurityActionRule”: İlgili Rule sınıfı “ISecurityActionRule” interface’inden türetilmiştir.
- “public SecurityActionCancelRule(int currentUserId) “: İşlemi gerçekleştiren UserID constructor’da alınır.
- “public List<SecurityActionModel> ExecuteRule(List<SecurityActionModel> actionList)”: Interface’in zorunlu kıldığı ExecuteRule, Cancel Bussines Logiğine göre özelleştirilir.
- “actionList.ForEach(item =>”: Gönderilen tüm kayıtlar gezilir.
- “if (_currentUserId != item.OpenStatusUserId && item.OpenStatus == false) { item.IsCancelState = false; }”: Eğer ilgili kayıt başkası tarafından kitlenmiş ise, ilgili rule çalıştırılmadan false atanır.
- “else if (item.ActionNumber % 2 != 0) { item.IsCancelState = false; }”: Esas Bussines Rule bu satırdır. Tamamen örnek amaçlı gönderilen kaydın, ActionNumber kolonu tek ise “IsCancelState” kolonu “false” dönülür.
- “else { item.IsCancelState = true; }” Eğer ilgili kaydın ActionNumber’ı çift ise “true” değeri dönülü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 |
using Dashboard.DB.Entities; using System; using System.Collections.Generic; using System.Text; using System.Linq; using Dashboard.Core.Models.Management; using Dashboard.Core; namespace Dashboard.Services.Management.SecurityAction { public class SecurityActionCancelRule : ISecurityActionRule { int _currentUserId; public SecurityActionCancelRule(int currentUserId) { _currentUserId = currentUserId; } public List<SecurityActionModel> ExecuteRule(List<SecurityActionModel> actionList) { actionList.ForEach(item => { //lock'lı ise zaten IsCancelState otomatik false olur. **** Aynı durum değiştiren kişi için de geçerli mi? if (_currentUserId != item.OpenStatusUserId && item.OpenStatus == false) { item.IsCancelState = false; } else if (item.ActionNumber % 2 != 0) { item.IsCancelState = false; } else { item.IsCancelState = true; } }); return actionList; } } } |
Şu ana kadar SecurityAction için ilgili bussines kuralları yazdık. Şimdi gelin bunların nasıl işletileceğine hep beraber bakalım.
WebApi/Controllers/ActionController.cs: Aşağıda görüldüğü gibi, daha en başta controller katmanında, istenen N tane rule oluşturulur. Burdan şunu anlıyoruz, “ISecurityActionRule“‘dan türemiş olmak kaydı ile, istenen sayıda rule oluşturulabilir. Bu da yeni eklencek rulelar için kodun değiştirilmesine gerek olmayacak anlamına gelmektedir. Tüm Ruleların ortak çalıştırdığı method, ExecuteRule’dur.
-
“List<ISecurityActionRule> rules = new List<ISecurityActionRule>()”: Tüm Ruleların paramtere olarak gönderileceği liste. Tüm Rulelar, “ISecurityActionRule“‘dan türediği için hepsini kapsamaktadır.
- “SecurityActionApprovelRule approvelRule”: Yukarıda tanımlanan ApprovelRule çalıştırılmak üzere, “GetActionListByControllerId” servisine bir List elemanı olarak gönderilir.
-
“SecurityActionCancelRule cancelRule”: Yine yukarıda tanımlanan CancelRule çalıştırılmak üzere ilgili servise, bir List elemanı olarak gönderilir.
- “response.List = _actionService.GetActionListByControllerId(idSecurityController, rules).List”: İlgili servis, Rule Liste’si ve SecurityControllerID parametre gönderilerek çalıştırılır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
[HttpGet("GetActionListByControllerId/{idSecurityController}")] public ServiceResponse<SecurityActionModel> GetActionListByControllerId(int idSecurityController) { var response = new ServiceResponse<SecurityActionModel>(HttpContext); //Rulelar List<ISecurityActionRule> rules = new List<ISecurityActionRule>(); SecurityActionApprovelRule approvelRule = new SecurityActionApprovelRule(_workContext.CurrentUserId); SecurityActionCancelRule cancelRule = new SecurityActionCancelRule(_workContext.CurrentUserId); rules.AddRange(new ISecurityActionRule[] { approvelRule, cancelRule }); //------------ response.List = _actionService.GetActionListByControllerId(idSecurityController, rules).List; response.IsSuccessful = true; return response; } |
Service/Management/SecurityAction/SecurityActionService.cs: Aşağıda görüldüğü gibi, kendisine parametre olarak gelen Rule listesi, tek tek gezilerek herbirinin “ExecuteRule()” methodu, çekilen result üzerinde çalıştırılarak, resultViewModel doldurulmuş ve client’a datanın nihai son hali geri dönülmüştür.
- “var response = new ServiceResponse<SecurityActionModel>(null)”: Geri dönülücek view model. DB karşılığı olmayan “IsApprovelState” ve “IsCancelState” kolonları mevcuttur.
- “var result = _actionRepository.Table.Where(q => q.IdSecurityController == idSecurityController).ToList()”: İstenen data belirlenen koşula göre çekilir.
- “result = (List<DB.Entities.DbSecurityAction>)result.CheckExpire(_vbtConfig.Value.StatusExpireTime)”: İlgili Entity üzerinde, süresi 5 dakkayı geçmiş kilitli kayıtlar, boşa çıkarılır. Bu global amaçlı yazılmış bir extension’dır. Makalenin devamında değinilecektir. 5 dakika süresi configden parametre olarak alınmaktadır.
- “var resultViewModel = _mapper.Map<List<SecurityActionModel>>(result)” : DB’den query ile alınan DBContext tipindeki result List, ilgili Ruleların işlenmesi amacı ile, ViewModel’e çevrilir.
- “rules.ForEach(rule =>”: Tüm rulelar tek tek gezilir.
- “resultViewModel = rule.ExecuteRule(resultViewModel)”: DB çekilen resultViewModel, ilgili rulelar üzerinde çalıştırılarak, DB’de olmayan sadece Client Side’da ihtiyac duyulan propertyler doldurulur.
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 |
public IServiceResponse<SecurityActionModel> GetActionListByControllerId(int idSecurityController, List<ISecurityActionRule> rules) { var response = new ServiceResponse<SecurityActionModel>(null); var result = _actionRepository.Table.Where(q => q.IdSecurityController == idSecurityController).ToList(); if (result != null) { //İlgili Entity üzerinde Süresi 5 dakkayı geçmiş kilitli kayıtlar, boşa atanır. result = (List<DB.Entities.DbSecurityAction>)result.CheckExpire(_vbtConfig.Value.StatusExpireTime); var resultViewModel = _mapper.Map<List<SecurityActionModel>>(result); //Viewmodel'e çevrilir. //Gönderilen Rulelar Çalıştırılır. if (rules != null) { rules.ForEach(rule => { resultViewModel = rule.ExecuteRule(resultViewModel); }); } //------------------------ response.List = resultViewModel; } return response; } |
Buradaki yönteme özellikle dikkat ettiyseniz, genişletilmeye açık ama değiştirmeye kapalı yapılar, ilerde yeni rulelar geldiği zaman kolayca sisteme implemente edilebilmesini sağlamış ve N tane sayıdaki Rule’un, art arda çalıştırılmasına olanak vermiştir.
Bu yöntemin Design Pattern karşılığı “Strategy Design Pattern“‘dir.
Resim Kaynağı: miro.medium.com
Core/Extensions/ExpireExtensions: Son olarak, yukarıda kullanılan, 5 dakka süreden fazala kilitli kalmış kayıtların boşa çıkarıldığı Extensiondır.
Bu işlemin birçok yerden çağrılacağı düşünülür ise, Global hale getirme amacında Extension olarak yazılması mantıklı bir yöntem gibi gözükmektedir.
- “public static IEnumerable<IExpired> CheckExpire(this IEnumerable<IExpired> entityList,int? expireMinute=5)” : “IEnumerable<IExpired>” interface’inden türeyen sınıflar için yazılarak, bir kısıtlamaya gidilmiştir.
- “entityList.Where(re => re.OpenStatusUserId != null && (DateTime.Now – re.ModDate).Value.TotalMinutes >= expireMinute).ToList()”: İlgili entitiy üzerinde, kayıdın kitendiği zaman dilimi “ModDate“, şimdiki zamandan 5 dakika veya daha eski olan kayıtlar alınarak, kilitleri tek tek kaldırılır. Ve üzerinde değişiklik yapılan EntityList geri dönülür. 5 dakika süresi configden parametre olarak alınmaktadır. Burada amaç, sorgulama anında, kilitleri atıl kalmış kayıtların temizlenmesidir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
using Microsoft.AspNetCore.Http; using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Dashboard.Core.Extensions { public static class ExpireExtensions { public static IEnumerable<IExpired> CheckExpire(this IEnumerable<IExpired> entityList,int? expireMinute=5) { //Kitlenip 5 dakkayı geçen kayıtlar'ın kilidi açılır. Rule'a çevir... entityList.Where(re => re.OpenStatusUserId != null && (DateTime.Now - re.ModDate).Value.TotalMinutes >= expireMinute).ToList().ForEach(row => { row.OpenStatus = true; row.OpenStatusUserId = null; row.ModDate = null; }); return entityList; } } } |
Geldik bir makalenin daha sonuna. Bu makalede, kritik bir kaydın güncellenmesi anında, bir başkası tarafından aynı zamanda değiştirilmemesinin yollarını hep beraber araştırdık. Sizin de aklınıza takıldı ise, makalede özellikle atlanan bir durum vardır. O da, bir kayıt bir client tarafından değiştirilmeye başlandığı zaman, bunun diğer clientlara bildirilmesidir. Yani, ekranlarında açık olan güncellenebilen kayıtların Socket ile kitlenmesidir. O konuya da ilerde tekrardan değinebiliriz. Bir ikinci husus da, art arda çalıştırılan Ruleların servis katmanında nasıl daha Global hale getirilebileceği sorusudur. DB ile bir işin olunmaması ve tamamen Bussines kuralların ViewModel üzerinde koşturulması adına, Repository katmanı kesinlikle düşünülmemelidir. O zaman alternatif bir yol olarak Extension düşünülebilir.
Tüm ViewModeller, aşağıda görüldüğü gibi “BaseModel” sınıfından türetilmiştir. İstenirse, “List<BaseModel>” sınıfına static bir Extension yazılıp, parametre olarak işletilecek Rule listesi gönderilebilir. Böylece istenen kod hem daha Global bir hale gelmiş hem de biraz daha pratik olmuş olacaktır. Son olarak her projede olmasa da Repository katmanı, bu projede hayati bir öneme sahiptir. Siz de projelerinizde DB ile olan etkileşim anında, global bir takım işler yapmak isterseniz, Repository katmanını şiddet ile öneririm.
1 |
public class SecurityActionModel : BaseModel |
Yeni bir makalede görüşmek üzere hepinize hoşçakalın. Sağlıcakla kalın.
:) Amatör olarak projeye başlıyorum ve bu lock olayı numara olmuş :) Emeğinize sağlık bir de size email attım bi yapı kurdum sizce olur mu :)