using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Newtonsoft.Json; using NLog; using StackExchange.Redis; using System.Diagnostics; using System.Security.Claims; using WebDoorCreator.Core; using WebDoorCreator.Data.User; namespace WebDoorCreator.Data.Services { public class UserManDataService : IDisposable { #region Public Constructors /// /// Init classe /// /// /// /// public UserManDataService(IConfiguration configuration, IConnectionMultiplexer redisConnMult, UserManager userManager) { Log.Info("UserManDataService starting..."); _configuration = configuration; _userManager = userManager; // Conf cache redisConn = redisConnMult; redisDb = this.redisConn.GetDatabase(); // json serializer... FIX errore loop circolare https://www.ryadel.com/en/jsonserializationexception-self-referencing-loop-detected-error-fix-entity-framework-asp-net-core/ JSSettings = new JsonSerializerSettings() { ReferenceLoopHandling = ReferenceLoopHandling.Ignore }; Log.Info("UserManDataService started!"); } #endregion Public Constructors #region Public Methods public void Dispose() { _userManager?.Dispose(); } /// /// Effettua reset dati utente... /// /// Indica reset completo (anche dati elenco di base) /// public async Task ResetUserDataCache(bool resetAll) { bool fatto = false; RedisValue pattern = new RedisValue($"{Constants.rKeyUsersAll}"); if (resetAll) { fatto = await ExecFlushRedisPattern(pattern); } // ora resetto il resto pattern = new RedisValue($"{Constants.rKeyUsersData}:*"); fatto = fatto && await ExecFlushRedisPattern(pattern); pattern = new RedisValue($"{Constants.rKeyUsersAll}:*"); fatto = fatto && await ExecFlushRedisPattern(pattern); return fatto; } /// /// Dati utente (TUTTI) da REDIS o DB /// /// public async Task> UserDataGetAll() { string source = "DB"; List dataResult = new List(); // cerco da cache string currKey = $"{Constants.rKeyUsersAll}"; Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); string? rawData = await redisDb.StringGetAsync(currKey); if (!string.IsNullOrEmpty(rawData)) { source = "REDIS"; var tempResult = JsonConvert.DeserializeObject>(rawData); if (tempResult == null) { dataResult = new List(); } else { dataResult = tempResult; } } else { dataResult = await _userManager.Users.ToListAsync(); rawData = JsonConvert.SerializeObject(dataResult, JSSettings); await redisDb.StringSetAsync(currKey, rawData, LongCache); } if (dataResult == null) { dataResult = new List(); } stopWatch.Stop(); TimeSpan ts = stopWatch.Elapsed; Log.Debug($"UserDataGetAll | {source} in: {ts.TotalMilliseconds} ms"); return dataResult; } /// /// Dati utente (singolo da UID) /// /// /// public async Task UserDataGetById(string UserId) { string source = "FULL"; UserData dataResult = new UserData(); Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); // uso metodi helper protected di base... var userRaw = await UserIdentityById(UserId); var userIdent = new IdentityUser { Id = userRaw.Id, UserName = userRaw.UserName, Email = userRaw.Email, PhoneNumber = userRaw.PhoneNumber, PasswordHash = "*****", EmailConfirmed = userRaw.EmailConfirmed }; // cerco ruoli & claims var UserRoles = await UserRolesById(userIdent); var UserClaims = await UserClaimsById(userIdent); // compongo output dataResult = new UserData() { Identity = userIdent, Roles = UserRoles, Claims = UserClaims }; stopWatch.Stop(); TimeSpan ts = stopWatch.Elapsed; Log.Debug($"UserDataGetById | {source} in: {ts.TotalMilliseconds} ms"); return dataResult; } /// /// Dati utente (singolo x UserName) /// /// /// public async Task UserDataGetByName(string UserName) { string source = "FULL"; UserData dataResult = new UserData(); Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); // uso metodi helper protected di base... var userRaw = await UserIdentityByName(UserName); var userIdent = new IdentityUser { Id = userRaw.Id, UserName = userRaw.UserName, Email = userRaw.Email, PhoneNumber = userRaw.PhoneNumber, PasswordHash = "*****", EmailConfirmed = userRaw.EmailConfirmed }; // cerco ruoli & claims var UserRoles = await UserRolesById(userIdent); var UserClaims = await UserClaimsById(userIdent); // compongo output dataResult = new UserData() { Identity = userIdent, Roles = UserRoles, Claims = UserClaims }; stopWatch.Stop(); TimeSpan ts = stopWatch.Elapsed; Log.Debug($"UserDataGetByName | {source} in: {ts.TotalMilliseconds} ms"); return dataResult; } /// /// Dati utente (filtrati da searchVal) /// /// /// public async Task> UserDataGetFilt(string searchVal) { string source = "FULL"; List dataResult = new List(); Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); // uso metodi helper protected di base... List RawList = await UserIdentityBySearch(searchVal); // conversione var user = RawList.Select(x => new IdentityUser { Id = x.Id, UserName = x.UserName, Email = x.Email, PhoneNumber = x.PhoneNumber, PasswordHash = "*****", EmailConfirmed = x.EmailConfirmed, LockoutEnabled = x.LockoutEnabled, LockoutEnd = x.LockoutEnd, AccessFailedCount = x.AccessFailedCount, ConcurrencyStamp = x.ConcurrencyStamp, NormalizedEmail = x.NormalizedEmail, NormalizedUserName = x.NormalizedUserName, PhoneNumberConfirmed = x.PhoneNumberConfirmed, SecurityStamp = x.SecurityStamp, TwoFactorEnabled = x.TwoFactorEnabled }).ToList(); foreach (var item in user) { // cerco ruoli & claims var UserRoles = await UserRolesById(item); var UserClaims = await UserClaimsById(item); var newItem = new UserData() { Identity = item, Roles = UserRoles, Claims = UserClaims }; dataResult.Add(newItem); } stopWatch.Stop(); TimeSpan ts = stopWatch.Elapsed; Log.Debug($"UserDataGetFilt | {source} in: {ts.TotalMilliseconds} ms"); return dataResult; } #endregion Public Methods #region Protected Fields /// /// TTL da 1 min x cache Redis /// protected const int shortTTL = 60 * 5; protected int idxSim = 0; protected Random rnd = new Random(); #endregion Protected Fields #region Protected Methods /// /// Dati utente CLAIMS da REDIS o DB /// /// protected async Task> UserClaimsById(IdentityUser UserIdent) { string source = "DB"; List dataResult = new List(); // cerco da cache string currKey = $"{Constants.rKeyUsersData}:Claims:{UserIdent.Id}"; Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); string? rawData = await redisDb.StringGetAsync(currKey); if (!string.IsNullOrEmpty(rawData)) { source = "REDIS"; var tempResult = JsonConvert.DeserializeObject>(rawData, new ClaimConverter()); if (tempResult != null) { dataResult = tempResult; } } else { var rawResult = await _userManager.GetClaimsAsync(UserIdent); dataResult = rawResult.ToList(); rawData = JsonConvert.SerializeObject(dataResult, JSSettings); await redisDb.StringSetAsync(currKey, rawData, LongCache); } if (dataResult == null) { dataResult = new List(); } stopWatch.Stop(); TimeSpan ts = stopWatch.Elapsed; Log.Debug($"UserClaimsById | {source} in: {ts.TotalMilliseconds} ms"); return dataResult; } /// /// Dati utente IDENTITY da REDIS o DB da UID /// /// protected async Task UserIdentityById(string UserId) { string source = "DB"; IdentityUser dataResult = new IdentityUser(); // cerco da cache string currKey = $"{Constants.rKeyUsersData}:Identity:{UserId}"; Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); string? rawData = await redisDb.StringGetAsync(currKey); if (!string.IsNullOrEmpty(rawData)) { source = "REDIS"; var tempResult = JsonConvert.DeserializeObject(rawData); if (tempResult != null) { dataResult = tempResult; } } else { dataResult = await _userManager.FindByIdAsync(UserId); rawData = JsonConvert.SerializeObject(dataResult, JSSettings); await redisDb.StringSetAsync(currKey, rawData, LongCache); } if (dataResult == null) { dataResult = new IdentityUser(); } stopWatch.Stop(); TimeSpan ts = stopWatch.Elapsed; Log.Debug($"UserIdentityById | {source} in: {ts.TotalMilliseconds} ms"); return dataResult; } /// /// Dati utente IDENTITY da REDIS o DB per NAME /// /// protected async Task UserIdentityByName(string uName) { string source = "DB"; IdentityUser dataResult = new IdentityUser(); // cerco da cache string currKey = $"{Constants.rKeyUsersData}:UN:{uName}"; Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); string? rawData = await redisDb.StringGetAsync(currKey); if (!string.IsNullOrEmpty(rawData)) { source = "REDIS"; var tempResult = JsonConvert.DeserializeObject(rawData); if (tempResult != null) { dataResult = tempResult; } } else { dataResult = await _userManager.FindByNameAsync(uName); rawData = JsonConvert.SerializeObject(dataResult, JSSettings); await redisDb.StringSetAsync(currKey, rawData, LongCache); } if (dataResult == null) { dataResult = new IdentityUser(); } stopWatch.Stop(); TimeSpan ts = stopWatch.Elapsed; Log.Debug($"UserIdentityByName | {source} in: {ts.TotalMilliseconds} ms"); return dataResult; } /// /// Dati utente IDENTITY in modalità SEARCH da REDIS o DB /// /// protected async Task> UserIdentityBySearch(string searchVal) { string source = "DB"; List dataResult = new List(); // cerco da cache string currKey = $"{Constants.rKeyUsersDataSearch}:{searchVal}"; Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); string? rawData = await redisDb.StringGetAsync(currKey); if (!string.IsNullOrEmpty(rawData)) { source = "REDIS"; var tempResult = JsonConvert.DeserializeObject>(rawData); if (tempResult != null) { dataResult = tempResult; } } else { var allData = await UserDataGetAll(); if (!string.IsNullOrEmpty(searchVal)) { dataResult = allData .Where(x => x.NormalizedEmail.Contains(searchVal.ToUpper()) || x.NormalizedUserName.Contains(searchVal.ToUpper())).ToList(); } else { dataResult = allData; } rawData = JsonConvert.SerializeObject(dataResult, JSSettings); await redisDb.StringSetAsync(currKey, rawData, LongCache); } if (dataResult == null) { dataResult = new List(); } stopWatch.Stop(); TimeSpan ts = stopWatch.Elapsed; Log.Debug($"UserIdentityBySearch | {source} in: {ts.TotalMilliseconds} ms"); return dataResult; } /// /// Dati utente ROLES da REDIS o DB /// /// protected async Task> UserRolesById(IdentityUser UserIdent) { string source = "DB"; List dataResult = new List(); // cerco da cache string currKey = $"{Constants.rKeyUsersData}:Roles:{UserIdent.Id}"; Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); string? rawData = await redisDb.StringGetAsync(currKey); if (!string.IsNullOrEmpty(rawData)) { source = "REDIS"; var tempResult = JsonConvert.DeserializeObject>(rawData); if (tempResult != null) { dataResult = tempResult; } } else { var rawResult = await _userManager.GetRolesAsync(UserIdent); dataResult = rawResult.ToList(); rawData = JsonConvert.SerializeObject(dataResult, JSSettings); await redisDb.StringSetAsync(currKey, rawData, LongCache); } if (dataResult == null) { dataResult = new List(); } stopWatch.Stop(); TimeSpan ts = stopWatch.Elapsed; Log.Debug($"UserRolesById | {source} in: {ts.TotalMilliseconds} ms"); return dataResult; } #endregion Protected Methods #region Private Fields private static IConfiguration _configuration = null!; private static JsonSerializerSettings? JSSettings; private static Logger Log = LogManager.GetCurrentClassLogger(); private readonly UserManager _userManager; /// /// Elenco obj in cache /// private List cachedDataList = new List(); /// /// Durata cache lunga IN SECONDI /// private int cacheTtlLong = 60 * 5; /// /// Durata cache breve IN SECONDI /// private int cacheTtlShort = 60 * 1; /// /// Oggetto per connessione a REDIS /// private IConnectionMultiplexer redisConn = null!; /// /// Oggetto DB redis da impiegare x chiamate R/W /// private IDatabase redisDb = null!; #endregion Private Fields #region Private Properties /// /// Durata cache di 24h /// private TimeSpan DayLongCache { get => TimeSpan.FromHours(24); } /// /// Durata cache lunga (+ perturbazione percentuale +/-10%) /// private TimeSpan FastCache { get => TimeSpan.FromSeconds(cacheTtlShort * rnd.Next(900, 1100) / 1000); } /// /// Durata cache lunga (+ perturbazione percentuale +/-10%) /// private TimeSpan LongCache { get => TimeSpan.FromSeconds(cacheTtlLong * rnd.Next(900, 1100) / 1000); } /// /// Durata cache di 24h /// private TimeSpan MonthLongCache { get => TimeSpan.FromDays(30); } /// /// Durata cache lunga (+ perturbazione percentuale +/-10%) /// private TimeSpan UltraLongCache { get => TimeSpan.FromSeconds(cacheTtlLong * 10 * rnd.Next(900, 1100) / 1000); } /// /// Durata cache di 24h /// private TimeSpan WeekLongCache { get => TimeSpan.FromHours(24 * 7); } #endregion Private Properties #region Private Methods /// /// Esegue flush memoria redis dato pattern /// /// /// private async Task ExecFlushRedisPattern(RedisValue pattern) { bool answ = false; var listEndpoints = redisConn.GetEndPoints(); foreach (var endPoint in listEndpoints) { //var server = redisConnAdmin.GetServer(listEndpoints[0]); var server = redisConn.GetServer(endPoint); if (server != null) { var keyList = server.Keys(redisDb.Database, pattern); foreach (var item in keyList) { await redisDb.KeyDeleteAsync(item); } // brutalmente rimuovo intero contenuto DB... DANGER //await server.FlushDatabaseAsync(); answ = true; } } return answ; } #endregion Private Methods } }