Azure Üzerinde Redis Cache İle .Net 6.0 Servis Üzerinde Yetki Performansı

Selamlar,

Video Resim Kaynağı: res.cloudinary.com 

Bugün .Net 6.0 üzerinde, ActionFilter, Menu, Yetki gibi çok kullanılan yapılarda, Azure Cache for Redis” hizmetinden faydalanarak, nasıl performansı arttırabileceğimizi tartışacağız.

Öncelikle gelin, Azure üzerinde Redis servisi oluşturalım:

1-)Aşağıda görüldüğü gibi Azure üzerinde “Azure Cache Redis” servisi, arama kutusundan aranarak create edilir.

2-)Aşağıda görüldüğü gibi ilgili Redis Cache servisin Subscription(Hangi üyelik üzerinden başlatılacağı), Resource group(Eklenecek çalışma gurubu), DNS name(Redis cache’e erişilecek url), Location(server konumu) ve en son Cache Type(sunucu özellikleri) belirlenir.

3-)Diğer ayarlar isteğe bağlı olarak atlanıp, aşağıdaki onay ekranına gelinir. Ve Redis servis ayağı kaldırılır.

4-) Aşağıda görüldüğü gibi güvenlik amaçlı SSL açık ve portu 6380’dir. Ayrıca minimum TLS versiyon 1.2 olarak seçilmiştir.

5-)Azure üzerinde Console sekmesine gelinip, Redis Cli’da “Ping” komutuna karşılık => “PONG” cevabı alınıyor ise, herşey yolunda demektir.

Kendi makinanızdan Azure Redis Servise erişim testi için, “Redis Desktop Manager”‘ı kullanabilirsiniz. Aşağıdaki ekranda, Azure bağlanmak için Address ve Password alanları girildikten sonra, SSL/TLS seçeceğinin seçilmesi gerekmektedir.

Şimdi gelin Visual Studio 2022 ile aşağıda görüldüğü gibi, Asp.Net Core Web App bir proje oluşturalım.

En başta, aşağıdaki kütüphaneler Nuget’den indirilir.

appsettings.json: En başta Azure üzerindeki Redis’e ulaşabilmek için, aşağıdaki “Config” sekmesi açılıp ilgili propertyler tanımlanır.

Core/Confg.cs: Aşağıda görüldüğü gibi, appsettings.json’ın class karşılığı aşağıdaki gibi tanımlamıştır. Amaç, ilgili config dosyasındaki propertylere, bu class üzerinden ulaşılmaktır.

Program.cs: appsettings.json üzerinde,  “appsettings.json” karşılığının “Config” class’ı olduğu, aşağıdaki gibi tanımlanır.

Redis Cache Service

Core/Caching/IRedisCacheService: Projenin tamamında kullanılacak RedisManager interface’i, aşağıdaki gibi tanımlanır.

Core/Caching/RedisCacheService: Aşağıda görüldüğü gibi, Redis üzerinde yapılabilecek işlemler birer method olarak tanımlanmıştır. Projenin tamamında aynı kütüphane kullanılarak, kod standartı sağlanıp okunabilirlik arttırılmıştır.

  • “public RedisCacheService(IOptions<Config> config)” : Constructor’da Config class’ı, dependency injection ile alınmıştır.
  • “conf = new RedisEndpoint { “: Azure üzerindeki Redis’e erişim için appsettings.json’daki ayarlardan, yeni bir RedisEndpoint oluşturulmuştur.
  • “public T Get<T>(string key)”: Redis’den, belirli bir string key’e göre <T> tipinde tek bir value çekilmektedir.
  • “public IList<T> GetAll<T>(string key)”: Redis’den, belirli bir string key’e göre <T> tipinde bir Liste çekilmektedir.
  • “public void Set(string key, object data)”: Redis’e, string key ve Object value şeklinde bir değer belli bir expire süresi ile atanmaktadır. (_config.Value.RedisExpireTime) tanımlaması ile config dosyasında yazılı expire süresi ile redis’e ilgili key-value değeri atanmıştır.
  • “public void Set(string key, object data, DateTime time)”: Redis’e, string key ve Object value Newtonsoft ile Serialize edilip, belli bir expire süresi ile atanmaktadır.
  • “public int Count(string key)”: Redis’de, ilgili key’e ait toplam kayıt sayısını dönmektedir. Özellikle Pagination’da, toplam kayıt sayısının bilinemsi gerektiğinden, çokça kullanılmaktadır.
  • “public bool IsSet(string key)”: İlgili key’e ait bir kaydın, redisde olup olmadığına bakılır.
  • “public void Remove(string key)”: İlgili key’e ait kayıt, Redis’den çıkartılır.
  • “public void RemoveByPattern(string pattern)”: İlgili string pattern’e ait (Regex gibi) “Lua script” ile kayıtlar bulunup, Redisden silinir.
  • “CreatePassword()”: Belirtilen length’e göre, random password üreten methoddur.

Program.cs/RedisCacheService: Yukarıda yazılan RedisCacheService’i, herkes için en başta bir kere oluşturulduğu yani singleton olarak projeye eklendiği “AddSingleton()” methodu , aşağıdaki gibi tanımlanır.

RedisNotAvailableException: Azure üzerinde Redis’e erişilemeyince, fırlatılan custom Redis Exception classıdır.

Core/CacheKeys: Redis’e Action, Controller ve UserPermission kayıtlarının set edileceği Key, bu sınıftan alınır. Böylece Redis isimlendirmeye bir standart getirilmiş ve kod okunaklığı arttırılmıştır.

SQL RedisDB Context

Proje Root klasöründe aşağıdaki komutlar çağrılarak, ilgili Entity Kütüphaneleri yüklenir.

Nuget Package’in son durumu aşağıdaki gibidir:

DBFirst RedisDB:

Aşağıdaki scaffold komutu çalıştırılarak, DB First yapılmış ve RedisDB’ye ait tüm Entityler, Entites folder’ı oluşturulup altına ilgili DBSet sınıfları yaratılmıştır. Ayrıca DbContext folder’ı altına RedisDBContext oluşturulmuştur.

Program.cs /RedisDBContext: Aşağıdaki tanımlamalar ile RedisDBContext’in, Dependency Injection ile proje içinde kullanılması sağlanmıştır.

RedisDB Diagram:

SQLDB Bitwise Yetkilendirme

Aşağıdaki örnekte Controller ve herbir controller’a ait Actionlar, için 2^n şeklinde (ActionNumber) yetki değeri verilmiştir. Mesela User Controller’ına ait Actionlar, aşağıdaki sorgu ile listelenmiştir.

Aşağıdaki sorguda IdUser değeri, 2 olan Bora Kaşmerin, IDSecurityController’ı 2 yani “User” Controller için toplam yetkisinin “ActionNumberTotal“‘in, 63 olduğu görülmüştür. Kısaca, Bora’nın yetkili olduğu Actionların ActionNumberları toplanmış, ve 63 sonucu “DB_SECURITY_USER_ACTION” tablosuna yazılmıştır. Bitwise öteleme ile Custom yazılacak bir PermissionFilter ile, gelen user’ın ilgili Action’a yetkisinin olup olmadığı bakılacaktır.

Örneğin Index Action’ın, ActionNumber’ı 2 dir. Bora’nın, User Controller için toplam yetkisi 63’dür.

Bitwise ile 2 => 63’ün içinde var mı şeklinde bakılacak, ve buna göre Bora’nın yetkisinin olup olmadığı belirlenecektir. Örnek kod aşağıdaki gibidir.

Şimdi gelin proje tarafında, Controller ve Action için Enumlar oluşturalım.

Core/Enums: Aşağıda görüldüğü gibi ilgili Action’a yetki tanımlama amaçlı, Aspect Oriented programlama da olduğu gibi Attribute yani ActionFilterlar kullanılacak ve paramtere olarak aşağıdaki Enumlar verilecektir.

ActionFilter-1

Core/PermissionFilter(1): Aşağıda henüz template’i yazılmış, ilgili Action’a girmeden önce kullanıcının yetkisinin bakılacağı PermissionFilter yazılmıştır. Sonradan detaylıca ilgili “OnActionExecuting()” methodu yazılacaktır.

Program.cs/RedisCacheService: Yukarıda yazılan PermissionFilter’ın, farklı sessionlarında ve her request’de yenisinin yaratıldığı “AddScoped()” methodu, aşağıdaki gibi tanımlanır.

Core/SecurityActionAttribute: Yetkilendirme yapılacak methodun başında tanımlamak amacı ile, ActionID ve ControllerID parametrelerini alan bir Attribute aşağıdaki gibi tanımlanmıştır.

HomeController/Index(): Aşağıda, HomeController’a ait Index() action’ına girmek için gerekli olan yetkiler, parametrik olarak üstünde Attribute olarak tanımlanmıştır. Bu parametrelere göre PermissionFilter’da, userID’ye göre kullanıcı yetkisine bakılacak ve buna göre izin ya da red işlemi yapılacaktır.

  • “(int)HomeControllers.UserPermission”: Girilmesi gereken Controller’ın ID’sini temsil etmekde ve kod okunaklığı için, Enum şeklinde kullanılmıştır.
  • “(Int64)UserPermissionActions.GetRolePermissionsById)”: Girilmesi gereken Controller’a ait Action’ın ID’sini temsil etmektedir. Burada gene kod okunaklığı için, Enum şeklinde kullanılmıştır.
  • “userID”: Normalde Login olan kullanıcının ID’sinin alınması gerekmededir. Ama makalenin konusu dışına, daha fazla çıkılmaması adına temsili olarak parametre şeklinde alınmıştır.

ActionFilter-2

Core/PermissionFilter.cs: Bir action’a girilmeden önce, eğer ilgili filter üstünde tanımlı ise , öncelikle “OnActionExecuting()” methodu, action’ın sonlanmasından sonra da, “OnActionExecuted()” methodu çağrılır. Biz öncelikle yetki işlemlerini, ilgili methoda girmeden önce “OnActionExecuting()” methodunda kontrol edeceğiz.

  • İlgili methoda girmek isteyen kişinin yetkisinin DB’den kontrol edildiği servis, dependency injection ile sınıfa dahil edilmiştir.

  • “int userID = int.Parse(context.RouteData.Values[“userID”].ToString())”: Methodu çağıran client’ın userID’si, test amaçlı url parametredan çekilir.

  • “if (HasSecurityActionAttribute(context))”: İlgili methodun başında, yetki attribute’ü var mı diye kontrol edilir. Yoksa bu method, herkese açık bir sayfadır.

  • Action’ın başında Attribute olarak tanımlanan, yetki Controller ve Action Enum paramereleri, ilgili değişkenlere atanır.

  • userID’ye göre client’ın, ilgili Controller’a ait Action’a yetkisinin olup olmadığına bakılır. Yetkisi yok ise, geriye anlamlı mesaj döndürmek amacı ile ilgili Controller yani sayfanın ve yasaklı Action’ı’nın adı permissonService kullanılarak alınır ve geriye “… Şu sayfanın … bu Action’ına yetkiniz yoktur” şeklinde Custom 403 Forbidden hatası dönülür.

Core/PermissionFilter.cs:

Services

Service/PermissionService: Kullanıcının yetkilerinin kontrol edildiği, tüm Action ve Controller’ların varsa Redis’den yoksa SqlDB’den çekildiği servisdir.

Services/IPermissionService:

Services/PermissionService: 

Dependecy Injection kullanım amacı ile IPermissionService interface’inden türetilmiştir. Constructor’da redis işlemleri için “_redisCacheManager” ve DB işlemleri için “_contex” sınıfları inject edilmiştir..

1-)CheckUserPermission()

CheckUserPermission(), kullanıcya ait Controller bazında Action yetkilerinin çekildiği bir methoddur. Yukarıdaki SQL sorgusunda görüldüğü gibi, kullancının(IdUser) ve her Controller için(IdSecurityController) tek bir yetki (ActionNumberTotal) değeri vardır. Çünkü bitwise ile user’ın yetkili olduğu Actionların ID’si toplanarak, ilgili kolona tek bir satır olarak yazılmıştır. Daha sonra herhangi bir action’a, örneğin 4 nolu action’ın => ActionNumberTotal (63) içinde bitwise ile geçip geçmediğine bakılarak, user’ın yetkili olup olmadığına karar verilmiştir.

Performans işlemi için ilgili data ilk önce Azure üzerinde Redis’de aranmış, yok ise “_context” kütüphanesi kullanılarak Linq ile DB’den çekilmiştir. Ve daha sonra boş olan Redis, _redisCacheManager sınıfı ile süreli olarak doldurulmuştur.  Aranan kayıt Redis’de varsa, doğrudan redisden çekilerek alınmıştır. Son olarak, çekilen tüm yetki kayıtları, aranan controllerID ile filitrelenmiş ve bitwise ile user’ın, ilgili Action’a yetkisinin olup olmadığına  bakılmıştır.

  • “CheckUserPermission(int userID, long? actionID, int controllerID)”: CheckUserPermission’a paramtere olarak yetkisi bakılacak userID, girmeye çalıştığı method actionID ve bulunduğu sayfanın id’si controllerID parametre olarak verilmiştir.

string.Format(CacheKeys.GetUserPermissionByUserId, userID)“: Redis’de isimlendirmenin Global olması için, tek bir yerden yönetilmektedir. Böylece isimlendirmeye bir standart getirilmiştir.

  • “_redisCacheManager.Get”: Redis’den ilgili key’e göre, “DbSecurityUserAction” listesi çekilmiştir.

    • “if (getUserPermissionByUserId == null)”: Redis’de ilgili kayıt yok ise, SqlDB’den çekilmiş ve ControllerID’ye göre, toplam kullanıcı yetkisi filitrelenmiştir.

  • long ActionNumberTotal = (long)userActionPermision.ActionNumberTotal;”: İlgili kayıt DB’den başarıl bir şekilde çekilmiş ise, user’ın o controller’a ait tüm yetkisi, ActionNumberTotal field’i ile çekilir.
  • return (actionID == (ActionNumberTotal & actionID))” : Bitwise ile, kullanıcının o Action için sahip olması gereken yetki, o Controller için toplam yetkisi içinde aranır.

  • Eğer Redis dolu ise, redisden çekilen kullanıcı yetkisi, IdSecurityController ile filitrelenip, bitwise ile Action yetkisine bakılır.

2-) GetActionName() 

Sayfaya ait Action’a girme yetkisi olmayan kullanıcıya, daha detaylı bir hata mesajı dönmek için, Action name’in çekildiği methoddur.

  • “GetActionName(long? actionID,int controllerID)” Tüm Action listesini çekip, ilgili sayfaya ait yetki ismini bulmak için actionID ve controllerID parametre olarak alınır.
  • “var cacheKeyGetAllActions = string.Format(CacheKeys.GetAllActions)”: Redis’deki kayda ulaşmak için, belirlenen string formata göre key oluşturulur.
  • “var getAllActionsResult = _redisCacheManager.Get<List<DbSecurityAction>>(cacheKeyGetAllActions);”: İlgili List of “DbSecurityAction” Redis’den, belirlenen key’e göre çekilir.

  • “var response = _context.DbSecurityActions.AsNoTracking().ToList();” İlgili kayıt Redis’de yok ise. DB’den entity ile “AsNoTracking()” methodu ile çekilir. Insert ve Update gibi kayıt üzerinde bir işlem yapılmayacağı ve sadece okuma işlemi yapacağı için performan amaçlı kullanılan bir methoddur.
  • “_redisCacheManager.Set(cacheKeyGetAllActions, response);”: Çekilen kayıt, Redis’e set edilir.
  • “string actionName = response.Where(action => action.ActionNumber == actionID && action.IdSecurityController== controllerID).FirstOrDefault().ActionName;”: Çekilen data, action ve controller’a göre filitrelenip, Action Adı geri dönülür.

  • Eğer redis dolu ise, redisden çekilen kayıt, action ve controller’a göre filitrelenip, Action Adı geri dönülür.

3-) GetControllerName():

Sayfaya yani Controller’a ait Action’a girme yetkisi olmayan kullanıcıya, daha detaylı bir hata mesajı dönmek için, Sayfanın yani ControllerName’in çekildiği methoddur.

  • “GetControllerName(int controllerID)”: Seçilen controllerID’nin isminin, geriye dönüldüğü bir methoddur.
  • “var cacheKeyGetAllControllers = string.Format(CacheKeys.GetAllControllers);”: Redis Key belirlenen şablonda oluşturulur.
  • “var getAllControllersResult = _redisCacheManager.Get<List<DbSecurityController>>(cacheKeyGetAllControllers)”: İlgili kaydın, redis’de olup olmadığına bakılır.

  • İlgili kayıt Redis’de yok ise, DB’den çekilerek Redis’e oluşturulan key ile set edilir. Daha sonra parametre olarak verilen controllerID’ye göre filitrelenip, sayfanın ismi geri dönülür.

  • Eğer ilgili kayıt redis’de var ise, parametre olarak gönderilen controllerID’ye göre filitrelenip, sayfanın ismi geri dönülür.

Services/PermissionService:

Program.cs/PermissionService:

Yukarıda yazılan PermissionService, her request’de yenisinin yaratıldığı “AddTransient()” methodu ile, aşağıdaki gibi tanımlanır.

Bu makalede, .Net 6.0 bir projede, yetki sorgulaması amacı ile en çok kullanılan ActionFilter’da, DB işlemlerini paypass edip, Redis kullanılması sağlanmıştır. Siz de kendi projeleriniz’de monitor tooları kullanarak, DB’ye en çok yük bindiren sorugaları belirleyerek çeşitli iyileştirmeler yapabilirsiniz. Redis distributed cache, var olan sunucu haricinde kullanılabilmesi ve böylece kaynak tüketimini dağıtabilmesi, DB’ye göre verileri çok daha hızlı Rem’den okuyup yazabilmesi adına, uygulamada performansı arttırma adına kullanılabilecek kendini kanıtlamış en iyi toollardan biridir. Özellikle Azure üzerinde kolay set edilebilmesi, scale işlemlerinin otomatikleştirilmesi ve farklı kıtalara göre  disaster recovery yapılabilmesi, Azure’u bir adım öne çıkarmıştır.

Sık değişen datalarda cacheden uzak durmalı, DB’deki datayı değiştiren (Insert, Update, Delete) gibi işlemlerde, Redis’in de güncellenmesinin unutulmaması, gerekiyor ise Microservislerin kullanılarak ilgili işlemlerin asenkron ve yükü dağıtarak yapılması sağlanmalıdır. Aksi takdirde, Client’a eksik veya yanlış datanın gösterilmesi, içten bile değildir. Son olarak herkesin Global olarak kullandığı cachelerin, timeout durumlarında “Double-checked Lock” kullanılmalı, ve her clientin expire olan redisi, tekrardan doldurması engellenmelidir.

Geldik bir makalenin daha sonuna. Yeni bir makalede görüşmek üzere hepinize hoşçakalın.

Source Code:  https://github.com/borakasmer/RedisPerformance

Sql RedisDB Script: http://borakasmer.com/projects/RedisDB.sql

Kaynaklar:

 

Herkes Görsün:

Bunlar da hoşunuza gidebilir...

Bir cevap yazın

E-posta hesabınız yayımlanmayacak.