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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
namespace RedisExample.Core { public class Config { #region Props public string RedisEndPoint { get; set; } public string RedisPort { get; set; } public string RedisPassword { get; set; } public int RedisExpireTime { get; set; } public string EnvironmentName { get; set; } #endregion public Config() { } } } |
Program.cs: appsettings.json üzerinde, “appsettings.json” karşılığının “Config” class’ı olduğu, aşağıdaki gibi tanımlanır.
1 2 3 4 5 6 |
var configuration = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json") .Build(); builder.Services.Configure<Config>(configuration.GetSection("Config")); |
Redis Cache Service
Core/Caching/IRedisCacheService: Projenin tamamında kullanılacak RedisManager interface’i, aşağıdaki gibi tanımlanır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
using System; using System.Collections.Generic; using System.Text; namespace Dashboard.Core.Caching { public interface IRedisCacheService { T Get<T>(string key); IList<T> GetAll<T>(string key); void Set(string key, object data); void Set(string key, object data, DateTime time); void SetAll<T>(IDictionary<string, T> values); bool IsSet(string key); void Remove(string key); void RemoveByPattern(string pattern); void Clear(); int Count(string key); } } |
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.
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 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 |
using Microsoft.Extensions.Options; using ServiceStack.Redis; using System; using System.Collections.Generic; using System.Linq; using System.Text; using RedisExample.Core; using Newtonsoft.Json; namespace Dashboard.Core.Caching { public class RedisCacheService : IRedisCacheService { #region Fields public readonly IOptions<Config> _config; private readonly RedisEndpoint conf = null; #endregion public RedisCacheService(IOptions<Config> config) { _config = config; conf = new RedisEndpoint { Host = _config.Value.RedisEndPoint, Port = Convert.ToInt32(_config.Value.RedisPort), Password = _config.Value.RedisPassword, Ssl = true, SslProtocols= System.Security.Authentication.SslProtocols.Tls12 }; } public T Get<T>(string key) { try { using (IRedisClient client = new RedisClient(conf)) { return client.Get<T>(key); } } catch { throw new RedisNotAvailableException(); //return default; } } public IList<T> GetAll<T>(string key) { try { using (IRedisClient client = new RedisClient(conf)) { var keys = client.SearchKeys(key); if (keys.Any()) { IEnumerable<T> dataList = client.GetAll<T>(keys).Values; return dataList.ToList(); } return new List<T>(); } } catch { throw new RedisNotAvailableException(); } } public void Set(string key, object data) { Set(key, data, DateTime.Now.AddMinutes(_config.Value.RedisExpireTime)); } public void Set(string key, object data, DateTime time) { try { using (IRedisClient client = new RedisClient(conf)) { var dataSerialize = JsonConvert.SerializeObject(data, Formatting.Indented, new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.Objects }); client.Set(key, Encoding.UTF8.GetBytes(dataSerialize), time); } } catch { throw new RedisNotAvailableException(); } } public void SetAll<T>(IDictionary<string, T> values) { try { using (IRedisClient client = new RedisClient(conf)) { client.SetAll(values); } } catch { throw new RedisNotAvailableException(); } } public int Count(string key) { try { using (IRedisClient client = new RedisClient(conf)) { return client.SearchKeys(key).Where(s => s.Contains(":") && s.Contains("Mobile-RefreshToken")).ToList().Count; } } catch { throw new RedisNotAvailableException(); } } public bool IsSet(string key) { try { using (IRedisClient client = new RedisClient(conf)) { return client.ContainsKey(key); } } catch { throw new RedisNotAvailableException(); } } public void Remove(string key) { try { using (IRedisClient client = new RedisClient(conf)) { client.Remove(key); } } catch { throw new RedisNotAvailableException(); } } public void RemoveByPattern(string pattern) { try { using (IRedisClient client = new RedisClient(conf)) { var keys = client.SearchKeys(pattern); client.RemoveAll(keys); } } catch { throw new RedisNotAvailableException(); } } public void Clear() { throw new NotImplementedException(); } public void Dispose() { throw new NotImplementedException(); } public string CreatePassword(int length) { const string valid = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; StringBuilder res = new StringBuilder(); Random rnd = new Random(); while (0 < length--) { res.Append(valid[rnd.Next(valid.Length)]); } return res.ToString(); } } } |
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.
1 |
builder.Services.AddSingleton<IRedisCacheService, RedisCacheService>(); |
RedisNotAvailableException: Azure üzerinde Redis’e erişilemeyince, fırlatılan custom Redis Exception classıdı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 System; using System.Collections.Generic; using System.Text; namespace Dashboard.Core.Caching { public class RedisNotAvailableException : Exception { public string _errorCode = "431"; public override string Message { get { return "Redis is not available."; } } public string ErrorCode { get { return _errorCode; } } } } |
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.
1 2 3 4 5 6 7 8 9 |
namespace RedisExample.Core.Caching { public class CacheKeys { public const string GetUserPermissionByUserId = "GetUserPermissionByUserId:{0}"; public const string GetAllControllers = "GetAllControllers"; public const string GetAllActions = "GetAllActions"; } } |
SQL RedisDB Context
Proje Root klasöründe aşağıdaki komutlar çağrılarak, ilgili Entity Kütüphaneleri yüklenir.
1 2 3 |
dotnet add package Microsoft.EntityFrameworkCore.SqlServer.Design; dotnet add package Microsoft.EntityFrameworkCore.SqlServer dotnet add package Microsoft.EntityFrameworkCore.Tools |
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.
1 |
dotnet ef dbcontext scaffold "Server=.;Database=RedisDB;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -o Entities --context-dir "Entities\DbContexts" -c RedisDBContext -f |
Program.cs /RedisDBContext: Aşağıdaki tanımlamalar ile RedisDBContext’in, Dependency Injection ile proje içinde kullanılması sağlanmıştır.
1 2 |
var connectionString = builder.Configuration.GetConnectionString("SQLDBConnection"); builder.Services.AddDbContext<RedisDBContext>(x => x.UseSqlServer(connectionString)); |
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.
1 |
if (actionNumber == (userAction.ActionNumberTotal & actionNumber)) |
1 |
if(2 == (63 & 2)) |
Ş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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
namespace RedisExample.Core { public class Enums { public enum HomeControllers { User = 2, Report = 1, UserPermission = 7 } public enum UserActions { GetUserPermissionsById = 1, GetRolePermissionsById = 2, AddRolePermission = 4, } } } |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
using Microsoft.AspNetCore.Mvc.Filters; namespace RedisExample.Core { public class PermissonFilter : IActionFilter { public void OnActionExecuted(ActionExecutedContext context) { } public void OnActionExecuting(ActionExecutingContext context) { } } } |
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.
1 |
builder.Services.AddScoped<PermissonFilter>(); |
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.
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 |
namespace RedisExample.Core { [AttributeUsage(AttributeTargets.All)] public class SecurityActionAttribute : Attribute { int idSecurityController; Int64 actionNumber; Int64[] actionNumbers; public SecurityActionAttribute(int IdSecurityController, Int64 ActionNumber) { this.idSecurityController = IdSecurityController; this.actionNumber = ActionNumber; } public SecurityActionAttribute(int IdSecurityController, Int64[] ActionNumbers) { this.idSecurityController = IdSecurityController; this.actionNumbers = ActionNumbers; } public int IdSecurityController { get { return idSecurityController; } } // property to get description public Int64 ActionNumber { get { return actionNumber; } } } } |
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.
1 2 3 4 5 6 7 8 |
[ServiceFilter(typeof(PermissonFilter))] [SecurityActionAttribute((int)HomeControllers.UserPermission, (Int64)UserPermissionActions.GetRolePermissionsById)] [HttpGet("Index/{userID}")] public IActionResult Index(int userID) { _redisCacheManager.Set("Name", "Bora Kaşmer"); // Test Amçlı Azure Redis'e atılan key.. return View(); } |
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:
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 |
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Filters; using RedisExample.Services; namespace RedisExample.Core { public class PermissonFilter : IActionFilter { IPermissionService _permissonService; public PermissonFilter(IPermissionService permissonService) { _permissonService = permissonService; } public void OnActionExecuted(ActionExecutedContext context) { } public void OnActionExecuting(ActionExecutingContext context) { int userID = int.Parse(context.RouteData.Values["userID"].ToString()); //Action Yetkisine bakılır. if (HasSecurityActionAttribute(context)) { try { var arguments = ((ControllerActionDescriptor)context.ActionDescriptor).MethodInfo.CustomAttributes.FirstOrDefault(fd => fd.AttributeType == typeof(SecurityActionAttribute)).ConstructorArguments; int idSecurityController = (int)arguments[0].Value; long? actionNumber = null; actionNumber = (long)arguments[1].Value; if (!_permissonService.CheckUserPermission(userID, actionNumber, idSecurityController)) { string actionName = _permissonService.GetActionName(actionNumber, idSecurityController); string controllerName = _permissonService.GetControllerName(idSecurityController); //Forbidden 403 Result. Yetkiniz Yoktur.. context.Result = new ObjectResult(context.ModelState) { Value = $"\"{controllerName}\" sayfa için, \"{actionName}\" işlemi için geçerli bir yetkiniz yoktur.", StatusCode = Microsoft.AspNetCore.Http.StatusCodes.Status403Forbidden }; } } catch { } } } public bool HasSecurityActionAttribute(FilterContext context) { return ((ControllerActionDescriptor)context.ActionDescriptor).MethodInfo.CustomAttributes.Any(filterDescriptors => filterDescriptors.AttributeType == typeof(SecurityActionAttribute)); } } } |
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:
1 2 3 4 5 6 7 8 9 |
namespace RedisExample.Services { public interface IPermissionService { bool CheckUserPermission(int userID, long? actionID, int controllerID); string GetActionName(long? actionID,int controllerID); string GetControllerName(int controllerID); } } |
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:
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 92 93 94 95 96 97 98 99 100 101 102 |
using Dashboard.Core.Caching; using Microsoft.EntityFrameworkCore; using RedisExample.Core.Caching; using RedisExample.Entities; using RedisExample.Entities.DbContexts; namespace RedisExample.Services { public class PermissionService : IPermissionService { private readonly IRedisCacheService _redisCacheManager; RedisDBContext _context; public PermissionService(IRedisCacheService redisCacheManager, RedisDBContext context) { _redisCacheManager = redisCacheManager; _context = context; } public bool CheckUserPermission(int userID, long? actionID, int controllerID) { //Check Redis var cachkeyGetUserPermissionByUserId = string.Format(CacheKeys.GetUserPermissionByUserId, userID); var getUserPermissionByUserId = _redisCacheManager.Get<List<DbSecurityUserAction>>(cachkeyGetUserPermissionByUserId); if (getUserPermissionByUserId == null) { var response = _context.DbSecurityUserActions.AsNoTracking().Where(user => user.IdUser == userID).ToList(); if (response != null) { _redisCacheManager.Set(cachkeyGetUserPermissionByUserId, response); var userActionPermision = response.Where(sa => sa.IdSecurityController == controllerID).FirstOrDefault(); if (userActionPermision != null) { long ActionNumberTotal = (long)userActionPermision.ActionNumberTotal; return (actionID == (ActionNumberTotal & actionID)); } else return false; } } else { var userActionPermision = getUserPermissionByUserId.Where(sa => sa.IdSecurityController == controllerID).FirstOrDefault(); if (userActionPermision != null) { long ActionNumberTotal = (long)userActionPermision.ActionNumberTotal; return (actionID == (ActionNumberTotal & actionID)); } else return false; } return false; } public string GetActionName(long? actionID,int controllerID) { //Check Redis var cacheKeyGetAllActions = string.Format(CacheKeys.GetAllActions); var getAllActionsResult = _redisCacheManager.Get<List<DbSecurityAction>>(cacheKeyGetAllActions); if (getAllActionsResult == null) { var response = _context.DbSecurityActions.AsNoTracking().ToList(); if (response != null) { _redisCacheManager.Set(cacheKeyGetAllActions, response); string actionName = response.Where(action => action.ActionNumber == actionID && action.IdSecurityController== controllerID).FirstOrDefault().ActionName; return actionName; } } else { string actionName = getAllActionsResult.Where(action => action.ActionNumber == actionID && action.IdSecurityController == controllerID).FirstOrDefault().ActionName; return actionName; } return string.Empty; } public string GetControllerName(int controllerID) { //Check Redis var cacheKeyGetAllControllers = string.Format(CacheKeys.GetAllControllers); var getAllControllersResult = _redisCacheManager.Get<List<DbSecurityController>>(cacheKeyGetAllControllers); if (getAllControllersResult == null) { var response = _context.DbSecurityControllers.AsNoTracking().ToList(); if (response != null) { _redisCacheManager.Set(cacheKeyGetAllControllers, response); string controllerName = response.Where(controller => controller.IdSecurityController == controllerID).FirstOrDefault().ControllerName; return controllerName; } } else { string controllerName = getAllControllersResult.Where(controller => controller.IdSecurityController == controllerID).FirstOrDefault().ControllerName; return controllerName; } return string.Empty; } } } |
Program.cs/PermissionService:
Yukarıda yazılan PermissionService, her request’de yenisinin yaratıldığı “AddTransient()” methodu ile, aşağıdaki gibi tanımlanır.
1 |
builder.Services.AddTransient<IPermissionService, PermissionService>(); |
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:
- https://docs.microsoft.com/en-us/azure/azure-cache-for-redis/cache-web-app-howto
- https://docs.servicestack.net/
- https://redis.io/documentation
Son Yorumlar