using Core; using Core.DTO; using LiMan.DB; using LiMan.DB.DBModels; using LiMan.DB.DTO; using Microsoft.AspNetCore.Identity.UI.Services; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.VisualBasic; using Newtonsoft.Json; using NLog; using Org.BouncyCastle.Asn1.X500; using StackExchange.Redis; using StackExchange.Redis.Extensions.Core.Abstractions; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Runtime; using System.Threading.Tasks; using static Core.Enum; namespace LiMan.APi.Data { /// /// Classe astrazione accesso dati /// public class ApiDataService : IDisposable { #region Public Fields /// /// Classe Accesso metodi DB /// public static DB.Controllers.DbController dbController; #endregion Public Fields #region Public Constructors /// /// Init classe /// /// /// /// /// public ApiDataService(IConfiguration configuration, ILogger logger, IEmailSender emailSender, IRedisCacheClient redisCacheClient, IConnectionMultiplexer redisConnMult) //public ApiDataService(IConfiguration configuration, ILogger logger, IDistributedCache distributedCache, IEmailSender emailSender, IRedisCacheClient redisCacheClient) { _logger = logger; _configuration = configuration; _emailSender = emailSender; _redisCacheClient = redisCacheClient; //this.distributedCache = distributedCache; // 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 }; // conf messagepipe: setup canali pub/sub EnrollMessPipe = new MessagePipe(redisConn, Const.ENRL_MSG_PIPE); TaskMessPipe = new MessagePipe(redisConn, Const.TASK_MSG_PIPE); // conf DB string connStrDB = _configuration.GetConnectionString("LiMan.DB"); if (string.IsNullOrEmpty(connStrDB)) { _logger.LogError("ConnString empty!"); } else { dbController = new LiMan.DB.Controllers.DbController(configuration); _logger.LogInformation("DbController OK"); } } #endregion Public Constructors #region Public Properties /// /// Wrapper x invio/ricezione messaggi sul canale dedicato agli eventi enroll /// public MessagePipe EnrollMessPipe { get; set; } = null!; /// /// Wrapper x invio/ricezione messaggi sul canale dedicato agli eventi Task /// public MessagePipe TaskMessPipe { get; set; } = null!; #endregion Public Properties #region Public Methods /// Elenco Applicativi (all) public async Task> ApplicativiGetAll() { List dbResult = new List(); Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); dbResult = dbController.GetApplicazioni(); stopWatch.Stop(); TimeSpan ts = stopWatch.Elapsed; Log.Trace($"Effettuata lettura da DB per ApplicativiGetAll: {ts.TotalMilliseconds} ms"); return await Task.FromResult(dbResult); } /// /// Elenco licenze dato cliente /// /// Codice Installaizone /Cliente /// Codice Applicazione /// Indica se nascondere i dati sensibili /// public async Task> ApplicativiSearch(string CodInst, string CodApp, bool HideData) { List dbResult = new List(); Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); dbResult = dbController.GetApplicativiFilt(true, CodApp, CodInst, HideData); stopWatch.Stop(); TimeSpan ts = stopWatch.Elapsed; Log.Trace($"Effettuata lettura da DB per ApplicativiByCliente: {ts.TotalMilliseconds} ms"); return await Task.FromResult(dbResult); } /// /// Aggiornameto/Inserimento record applicativo /// /// record Release /// public async Task ApplicativoUpsert(ApplicativoModel newRec) { await Task.Delay(1); bool fatto = false; Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); // inserisco fatto = dbController.ApplicativoUpsert(newRec); stopWatch.Stop(); TimeSpan ts = stopWatch.Elapsed; Log.Trace($"Effettuato upsert su DB per ApplicativoUpsert | {newRec.CodApp} | {ts.TotalMilliseconds} ms"); return fatto; } /// /// Elenco licenze dato cliente /// /// Licenza MASTER /// Codice Impiego licenza /// Indica se nascondere i dati sensibili /// public async Task AttivazioneSearch(string Chiave, string CodImpiego, bool HideData) { AttivazioneDTO dbResult = new AttivazioneDTO(); Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); dbResult = dbController.GetAttivazione(Chiave, CodImpiego, HideData); stopWatch.Stop(); TimeSpan ts = stopWatch.Elapsed; Log.Trace($"Effettuata lettura da DB per AttivazioniSearch: {ts.TotalMilliseconds} ms"); return await Task.FromResult(dbResult); } /// /// Elenco Attivaizoni da ID Licenza master /// /// Idx Licenza Master /// Indica se nascondere i dati sensibili /// public async Task> AttivazioniByLic(int idxLic, bool hideData) { List dbResult = new List(); string cacheKey = $"{rKeyAttivByLic}:{hideData}:{idxLic}"; trackCache(cacheKey); string rawData = await getRSV(cacheKey); if (!string.IsNullOrEmpty(rawData)) { dbResult = JsonConvert.DeserializeObject>(rawData); } else { Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); dbResult = dbController.GetAttivazioniByLic(idxLic, hideData); rawData = JsonConvert.SerializeObject(dbResult); await setRSV(cacheKey, rawData, redCacheTtlStd); stopWatch.Stop(); TimeSpan ts = stopWatch.Elapsed; Log.Trace($"Effettuata lettura da DB per AttivazioniByLic: {ts.TotalMilliseconds} ms"); } return await Task.FromResult(dbResult); } /// /// Elenco Attivaizoni da valore Licenza master /// /// Licenza Master /// Indica se nascondere i dati sensibili /// public async Task> AttivazioniByMasterKey(string MasterKey, bool HideData) { List dbResult = new List(); Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); LicenzaModel licenza = await LicenzaByMasterKey(MasterKey); if (licenza != null) { dbResult = await AttivazioniByLic(licenza.IdxLic, HideData); } stopWatch.Stop(); TimeSpan ts = stopWatch.Elapsed; Log.Trace($"Effettuata lettura da DB per AttivazioniByMasterKey: {ts.TotalMilliseconds} ms"); return await Task.FromResult(dbResult); } /// /// Elimina un Attivaizone /// /// Licenza Master /// Elenco delle attivazioni da eliminare /// public async Task AttivazioniDelete(string MasterKey, Dictionary ParamDict) { bool answ = false; Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); LicenzaModel licenza = await LicenzaByMasterKey(MasterKey); if (licenza != null) { answ = dbController.AttivazioniDelete(ParamDict, MasterKey); await InvalidateAllCache(); } stopWatch.Stop(); TimeSpan ts = stopWatch.Elapsed; Log.Trace($"Effettuata lettura da DB per AttivazioniDelete: {ts.TotalMilliseconds} ms"); return await Task.FromResult(answ); } /// /// Elimina attivaizoni con veto scaduto /// /// Licenza Master /// public async Task AttivazioniResetAvail(string MasterKey) { bool answ = false; Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); LicenzaModel licenza = await LicenzaByMasterKey(MasterKey); if (licenza != null) { answ = dbController.AttivazioniResetAvail(MasterKey); await InvalidateAllCache(); } stopWatch.Stop(); TimeSpan ts = stopWatch.Elapsed; Log.Trace($"Effettuata lettura da DB per AttivazioniResetAvail: {ts.TotalMilliseconds} ms"); return await Task.FromResult(answ); } /// /// Effettua registrazione (se possibile) delle licenze indicate dall'elenco codici di /// impiego indicati /// /// Codice Licenza Master /// Elenco codici impiego (key) + valori in formato dizionari /// Numero giorni x scadenza veto modifica /// Tipo di licenza da registrare /// /// public async Task AttivazioniTryAdd(string MasterKey, Dictionary ParamDict, int DayVeto, TipoLicenza TipoLic) { bool taskDone = false; Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); taskDone = dbController.AttivazioniTryAdd(MasterKey, ParamDict, DayVeto, TipoLic); await InvalidateAllCache(); stopWatch.Stop(); TimeSpan ts = stopWatch.Elapsed; Log.Trace($"Effettuata scrittura + rilettura da DB per AttivazioniTryAdd: {ts.TotalMilliseconds} ms"); return await Task.FromResult(taskDone); } /// /// Effettua update (se possibile) delle licenze indicate dall'elenco codici di impiego indicati /// /// Codice Licenza Master /// Elenco codici impiego (key) + valori in formato dizionari /// /// public async Task AttivazioniTryRefresh(string MasterKey, Dictionary ParamDict) { bool taskDone = false; Stopwatch sw = new Stopwatch(); sw.Start(); taskDone = dbController.AttivazioniTryRefresh(MasterKey, ParamDict); await InvalidateAllCache(); sw.Stop(); TimeSpan ts = sw.Elapsed; Log.Trace($"Effettuata scrittura + rilettura da DB per AttivazioniTryRefresh: {ts.TotalMilliseconds} ms"); return await Task.FromResult(taskDone); } /// /// Dispose classe /// public void Dispose() { // Clear database controller dbController.Dispose(); } /// /// Crea record richiesta enroll (univoco rispetto richieste correnti) /// /// Dati da associare alla richeista /// public async Task EnrollReqCreate(Dictionary MachineInfo) { Stopwatch sw = new Stopwatch(); sw.Start(); // svuoto elenco richieste scaduteù var cleanDone = await EnrollReqPurgeInvalid(); // prendo elenco attive x evitare duplicazioni codici TOTP... var resList = await EnrollReqGetActive(); int totpCode = rnd.Next(1, 100000000); // verifico che non sia preesistente... if (resList.Count > 0) { // cerco se fosse già presente... while (resList.Where(x => x.Passcode == totpCode).Any()) { // genero nuovo... await Task.Delay(rnd.Next(50)); totpCode = rnd.Next(1, 100000000); } } // serializzo i dati della richiesta.. string reqPayload = JsonConvert.SerializeObject(MachineInfo); // preparo il record da registrare... EnrollRequestModel newReq = new EnrollRequestModel() { DtReq = DateTime.Now, Passcode = totpCode, ReqPayload = reqPayload }; var dbRec = await EnrollReqUpsert(newReq); sw.Stop(); TimeSpan ts = sw.Elapsed; Log.Trace($"Effettuata EnrollReqCreate: {ts.TotalMilliseconds} ms"); // invio string in messagepipe x forzare refresh... EnrollMessPipe.sendMessage("NewEnrollReq"); // restituisce record return dbRec; } /// /// Elimino una richiesta enroll (anche se già approvata...) /// /// ID record public async Task EnrollReqDelete(int idReq) { bool fatto = false; // inserimento! Stopwatch sw = new Stopwatch(); sw.Start(); fatto = dbController.EnrollReqDelete(idReq); // svuota eventuale cache redis... await FlushRedisCachePattern("Enroll"); await FlushRedisCachePattern("InstVerSta"); sw.Stop(); TimeSpan ts = sw.Elapsed; Log.Trace($"Effettuata EnrollReqDelete: {ts.TotalMilliseconds} ms"); // invio string in messagepipe x forzare refresh... EnrollMessPipe.sendMessage("NewEnrollReq"); return fatto; } /// /// Elenco richeiste enroll attive al momento /// /// public async Task> EnrollReqGetActive() { string source = "DB"; List dbResult = new List(); try { string currKey = $"{Const.rKeyConfig}:Enroll:ActiveList"; Stopwatch sw = new Stopwatch(); sw.Start(); string? rawData = await redisDb.StringGetAsync(currKey); if (!string.IsNullOrEmpty(rawData)) { source = "REDIS"; var tempResult = JsonConvert.DeserializeObject>(rawData); if (tempResult == null) { dbResult = new List(); } else { dbResult = tempResult; } } else { dbResult = dbController.EnrollReqGetActive(); rawData = JsonConvert.SerializeObject(dbResult, JSSettings); await redisDb.StringSetAsync(currKey, rawData, LongCache); } if (dbResult == null) { dbResult = new List(); } sw.Stop(); TimeSpan ts = sw.Elapsed; Log.Debug($"EnrollReqGetActive | {source} in: {ts.TotalMilliseconds} ms"); } catch (Exception exc) { Log.Error($"Error during EnrollReqGetActive:{Environment.NewLine}{exc}"); } return dbResult; } /// /// Recupera una richiesta dato suo ID x verificare approvazione e dati associati... /// /// ID record public async Task EnrollReqGetById(int idReq) { string source = "DB"; EnrollRequestModel dbResult = new EnrollRequestModel() { IdReq = idReq }; try { string currKey = $"{Const.rKeyConfig}:Enroll:ById:{idReq}"; Stopwatch sw = new Stopwatch(); sw.Start(); string? rawData = await redisDb.StringGetAsync(currKey); if (!string.IsNullOrEmpty(rawData)) { source = "REDIS"; var tempResult = JsonConvert.DeserializeObject(rawData); if (tempResult == null) { dbResult = new EnrollRequestModel() { IdReq = idReq }; } else { dbResult = tempResult; } } else { dbResult = dbController.EnrollReqGetById(idReq); rawData = JsonConvert.SerializeObject(dbResult, JSSettings); await redisDb.StringSetAsync(currKey, rawData, FastCache); } if (dbResult == null) { dbResult = new EnrollRequestModel() { IdReq = idReq }; } sw.Stop(); TimeSpan ts = sw.Elapsed; Log.Debug($"EnrollReqGetById | {source} in: {ts.TotalMilliseconds} ms"); } catch (Exception exc) { Log.Error($"Error during EnrollReqGetById:{Environment.NewLine}{exc}"); } return dbResult; } /// /// Elimino eventuali richieste non approvate e scadute /// public async Task EnrollReqPurgeInvalid() { var reqPurged = dbController.EnrollReqPurgeInvalid(); // svuota eventuale cache redis... await FlushRedisCachePattern("Enroll"); await FlushRedisCachePattern("InstVerSta"); // invio string in messagepipe x forzare refresh... EnrollMessPipe.sendMessage("NewEnrollReq"); return reqPurged; } /// /// Upsert record richiesta enroll /// /// /// public async Task EnrollReqUpsert(EnrollRequestModel newRec) { int recId = 0; // inserimento! Stopwatch sw = new Stopwatch(); sw.Start(); recId = dbController.EnrollReqUpsert(newRec); await FlushRedisCachePattern("Enroll"); await FlushRedisCachePattern("InstVerSta"); var dbResult = await EnrollReqGetById(recId); // svuota eventuale cache redis... sw.Stop(); TimeSpan ts = sw.Elapsed; Log.Trace($"Effettuata EnrollReqUpsert: {ts.TotalMilliseconds} ms"); // invio string in messagepipe x forzare refresh... EnrollMessPipe.sendMessage("NewEnrollReq"); // restituisce risultato return dbResult; } /// /// Esegue aggiunta file dato ticket e list uploadResult /// /// Identificativo del ticket /// Directory di salvataggio dei file /// lista risultati della funzione di upload /// public async Task FileAdd(int idxTicket, string baseDir, List fileUploaded) { bool fatto = false; // inserimento! Stopwatch sw = new Stopwatch(); sw.Start(); fatto = dbController.FileAdd(idxTicket, baseDir, fileUploaded); sw.Stop(); TimeSpan ts = sw.Elapsed; Log.Trace($"Effettuata inserimento con FileAdd: {ts.TotalMilliseconds} ms"); // restituisce elenco return await Task.FromResult(fatto); } /// /// Elenco file registrati dato ticket id /// /// Identificativo del ticket /// public async Task> FileGetFilt(int idxTicket) { List dbResult = new List(); Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); dbResult = dbController.FileGetFilt(idxTicket); stopWatch.Stop(); TimeSpan ts = stopWatch.Elapsed; Log.Trace($"Effettuata lettura da DB per FileGetFilt: {ts.TotalMilliseconds} ms"); return await Task.FromResult(dbResult); } /// /// Refresh globale cache Redis /// /// public async Task FlushRedisCache() { Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); await Task.Delay(1); RedisValue pattern = new RedisValue($"{Const.rKeyConfig}:*"); bool answ = await ExecFlushRedisPattern(pattern); stopWatch.Stop(); Log.Debug($"FlushRedisCache in {stopWatch.Elapsed.TotalMilliseconds} ms"); return answ; } /// /// Refresh cache Redis dato redPattern /// /// Pattern da eliminare /// public async Task FlushRedisCachePattern(string pattern) { Stopwatch sw = new Stopwatch(); sw.Start(); await Task.Delay(1); RedisValue redPattern = new RedisValue($"{Const.rKeyConfig}:{pattern}*"); bool answ = await ExecFlushRedisPattern(redPattern); sw.Stop(); Log.Debug($"FlushRedisCachePattern in {sw.Elapsed.TotalMilliseconds} ms"); return answ; } /// /// Effettua pulizia record registrazione InstalledRelease eliminando quelli non presenti in elenco /// /// CodImpiego /// Chiave App /// Elenco app gestite (=da tenere) /// Num rec eliminati public int InstallRelClean(string CodImp, string AppKey, List ListCodApp) { return dbController.InstallRelClean(CodImp, AppKey, ListCodApp); } /// /// Effettua salvataggio della situazione delle installazioni attive (se non c'è veto per registrazione appena effettuata...) /// /// Forza salvataggio comunque /// public async Task InstallRelHistSnapshot(bool doForce) { bool fatto = false; DateTime adesso = DateTime.Now; if (adesso > VetoInstRelHistSnap || doForce) { // veto x 4h (+/- rand 60 min) VetoInstRelHistSnap = adesso.AddHours(4).AddMinutes(rnd.Next(-60, 60)); // calcolo periodo come x interfaccia... DateTime DtFine = DateTime.Today.AddHours(DateTime.Now.Hour); DateTime DtInizio = DateTime.Today.AddMonths(-1); // recupero info DTO var rawData = await InstallStatusGetInfo(DtInizio, DtFine, ""); fatto = dbController.InstallRelHistSnapshot(rawData.InstallStatus); } return fatto; } /// /// Registro su DB il record della licenza attuale relativo alla richiesta di verifica licenza ricevuta /// /// record da inserire/aggiornare public bool InstallRelUpsert(InstalledReleasesModel upRec) { bool fatto = dbController.InstallRelUpsert(upRec); return fatto; } /// /// Recupera info statistiche installazione dato periodo riferimento, da cache o da db /// /// /// /// Filtro Cliente (CodInstall) public async Task InstallStatusGetInfo(DateTime dtStart, DateTime dtEnd, string CodInst) { string source = "DB"; InstallStatusDTO dbResult = new InstallStatusDTO(); try { string filtCli = string.IsNullOrEmpty(CodInst) ? ":ALL-INSTALL" : $":{CodInst}"; string currKey = $"{Const.rKeyConfig}:InstVerSta{filtCli}:{dtStart:yyyyMMdd-HHmmss}:{dtEnd:yyyyMMdd-HHmmss}"; Stopwatch sw = new Stopwatch(); sw.Start(); string? rawData = await redisDb.StringGetAsync(currKey); if (!string.IsNullOrEmpty(rawData)) { source = "REDIS"; var tempResult = JsonConvert.DeserializeObject(rawData); if (tempResult == null) { dbResult = new InstallStatusDTO(); } else { dbResult = tempResult; } } else { dbResult = dbController.InstallStatusGetInfo(dtStart, dtEnd, CodInst); rawData = JsonConvert.SerializeObject(dbResult, JSSettings); await redisDb.StringSetAsync(currKey, rawData, UltraLongCache); } if (dbResult == null) { dbResult = new InstallStatusDTO(); } sw.Stop(); TimeSpan ts = sw.Elapsed; Log.Debug($"InstallStatusGetInfo | {source} in: {ts.TotalMilliseconds} ms"); } catch (Exception exc) { Log.Error($"Error during InstallStatusGetInfo:{Environment.NewLine}{exc}"); } return dbResult; } /// /// invalida tutta la cache in caso di update /// /// public async Task InvalidateAllCache() { foreach (var item in cachedDataList) { await _redisCacheClient.GetDbFromConfiguration().RemoveAsync(item); } cachedDataList = new List(); } /// /// Elenco licenze dato ID /// /// ID licenza (DB) /// public async Task LicenzaById(int licId) { LicenzaModel dbResult = new LicenzaModel(); string cacheKey = $"{rKeyLicById}:{licId}"; trackCache(cacheKey); string rawData = await getRSV(cacheKey); if (!string.IsNullOrEmpty(rawData)) { dbResult = JsonConvert.DeserializeObject(rawData); } else { Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); dbResult = dbController.LicenzaById(licId); if (dbResult != null) { rawData = JsonConvert.SerializeObject(dbResult); await setRSV(cacheKey, rawData, hourTTL); } stopWatch.Stop(); TimeSpan ts = stopWatch.Elapsed; Log.Trace($"Effettuata lettura da DB per LicenzaById: {ts.TotalMilliseconds} ms"); } return dbResult; } /// /// Record licenza data masterKey /// /// Chiave Licenza x ricerca /// public async Task LicenzaByMasterKey(string chiave) { LicenzaModel dbResult = new LicenzaModel(); string cacheKey = $"{rKeyLicByMKey}:{chiave}"; trackCache(cacheKey); string rawData = await getRSV(cacheKey); if (!string.IsNullOrEmpty(rawData)) { dbResult = JsonConvert.DeserializeObject(rawData); } else { Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); dbResult = dbController.LicenzaByKey(chiave); if (dbResult != null) { rawData = JsonConvert.SerializeObject(dbResult); await setRSV(cacheKey, rawData, hourTTL); } stopWatch.Stop(); TimeSpan ts = stopWatch.Elapsed; Log.Trace($"Effettuata lettura da DB per LicenzaByMasterKey: {ts.TotalMilliseconds} ms"); } return await Task.FromResult(dbResult); } /// /// Effettua refresh del payload della licenza dato info + enigma generato dal client /// /// /// public async Task LicenzaRefreshPayload(LicenseCoord appInfo) { // chiamo metodo x ricalcolare payload dato enigma bool done = await dbController.LicenseUpdatePayload(appInfo.CodInst, appInfo.CodApp, appInfo.MasterKey, appInfo.Enigma); await InvalidateAllCache(); // ora recupero i dati var licList = await LicenzeSearch(appInfo.CodInst, appInfo.CodApp, appInfo.MasterKey, false); return licList.FirstOrDefault(); } /// /// Elenco licenze dato cliente /// /// /// public async Task> LicenzeByCliente(string cliente) { List dbResult = new List(); Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); dbResult = dbController.GetLicenzeFilt(true, "", cliente); stopWatch.Stop(); TimeSpan ts = stopWatch.Elapsed; Log.Trace($"Effettuata lettura da DB per LicenzeByCliente: {ts.TotalMilliseconds} ms"); return await Task.FromResult(dbResult); } /// /// Elenco licenze x data scadenza /// /// Data minima di scadenza /// Data massima di scadenza /// public async Task> LicenzeExpiring(DateTime minDate, DateTime maxDate) { List dbResult = new List(); Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); dbResult = dbController.GetApplicativiExpiring(minDate, maxDate).ToList(); stopWatch.Stop(); TimeSpan ts = stopWatch.Elapsed; Log.Trace($"Effettuata lettura da DB per LicenzeExpiring: {ts.TotalMilliseconds} ms"); return await Task.FromResult(dbResult); } /// /// Elenco licenze dato cliente /// /// Codice Installaizone /Cliente /// Codice Applicazione /// Chiave Licenza da validare /// Indica se nascondere i dati sensibili /// public async Task> LicenzeSearch(string CodInst, string CodApp, string Chiave, bool HideData) { List dbResult = new List(); Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); dbResult = dbController.GetApplicativiFilt(true, CodApp, CodInst, HideData).Where(x => x.Chiave == Chiave).ToList(); stopWatch.Stop(); TimeSpan ts = stopWatch.Elapsed; Log.Trace($"Effettuata lettura da DB per ApplicativiByCliente: {ts.TotalMilliseconds} ms"); return await Task.FromResult(dbResult); } /// /// Effettua registrazione chiamata verificando se vada messa sul DB o in redis... /// /// /// /// public async Task recordCall(string codInst, string codApp, string targetUrl) { bool fatto = false; // in primis recupero statistiche (e nel mentre eventualmente salvo su DB) SampleStats currStats = await getCurrStats(); // preparo chiave x dato da loggare string currKey = $"{rKeySampleVars}:{codInst}:{codApp}:{targetUrl}"; // verifico presenza contatore corrente altrimenti aggiungo e salvo... if (!currStats.VList.Contains(currKey)) { currStats.VList.Add(currKey); // salvo! await setCurrStats(currStats); } // incremento valore contatore corrente await redCountIncr(currKey); return fatto; } /// /// Effettua registrazione da codice chiave... /// /// /// public async Task recordCall(string chiave, string targetUrl) { // valutare se cache key --> lic... var currLic = await LicenzaByMasterKey(chiave); var fatto = await recordCall(currLic.CodInst, currLic.CodApp, targetUrl); return fatto; } /// /// Elenco Release dato Applicativo /// /// Codice Applicazione /// public async Task> ReleaseGetByApp(string CodApp) { await Task.Delay(1); List dbResult = new List(); Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); dbResult = dbController.ReleaseDtoGetByApp(CodApp); stopWatch.Stop(); TimeSpan ts = stopWatch.Elapsed; Log.Trace($"Effettuata lettura da DB per ReleaseDtoGetByApp | {CodApp} | {ts.TotalMilliseconds} ms"); return dbResult; } /// /// Elenco Release dato Applicativo + versione minima /// /// Codice Applicazione /// Versione minima richiesta /// public async Task> ReleaseGetByAppVers(string CodApp, string VersMin) { await Task.Delay(1); List dbResult = new List(); Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); dbResult = dbController.ReleaseDtoGetByAppVers(CodApp, VersMin); stopWatch.Stop(); TimeSpan ts = stopWatch.Elapsed; Log.Trace($"Effettuata lettura da DB per ReleaseDtoGetByAppVers | {CodApp} | vers >= {VersMin} | {ts.TotalMilliseconds} ms"); return dbResult; } /// /// Elenco Release dato Applicativo + versione minima /// /// Codice Applicazione /// Versione minima richiesta /// Versione massima consentita /// public async Task> ReleaseGetByAppVersLimit(string CodApp, string VersMin, string VersMax) { await Task.Delay(1); List dbResult = new List(); Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); dbResult = dbController.ReleaseDtoGetByAppVersLimit(CodApp, VersMin, VersMax); stopWatch.Stop(); TimeSpan ts = stopWatch.Elapsed; Log.Trace($"Effettuata lettura da DB per ReleaseDtoGetByAppVersLimit | {CodApp} | vers >= {VersMin} | {ts.TotalMilliseconds} ms"); return dbResult; } /// /// Elenco di TUTTE le release CRITICAL come dizionario CodApp - release /// /// public async Task> ReleaseGetCritical() { Dictionary dbResult = new Dictionary(); Stopwatch sw = new Stopwatch(); sw.Start(); string cacheKey = $"{rKeyAttivByLic}:AppReleases:CRITICAL"; string source = "REDIS"; trackCache(cacheKey); string rawData = await getRSV(cacheKey); if (!string.IsNullOrEmpty(rawData)) { dbResult = JsonConvert.DeserializeObject>(rawData); } else { source = "DB"; dbResult = dbController.ReleaseGetCritical(); rawData = JsonConvert.SerializeObject(dbResult); await setRSV(cacheKey, rawData, redCacheTtlFast); } sw.Stop(); Log.Trace($"Effettuata lettura da DB per ReleaseGetCritical | {source} | # found: {dbResult.Count} | {sw.ElapsedMilliseconds} ms"); return dbResult; } /// /// Aggiornameto/Inserimento record release /// /// record Release /// public async Task ReleaseUpsert(ReleaseModel newRec) { await Task.Delay(1); bool fatto = false; Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); // inserisco fatto = await dbController.ReleaseUpsert(newRec); await InvalidateAllCache(); await FlushRedisCache(); stopWatch.Stop(); TimeSpan ts = stopWatch.Elapsed; Log.Trace($"Effettuato upsert su DB per ReleaseUpsert | {newRec.CodApp} | {newRec.VersNum} | {ts.TotalMilliseconds} ms"); return fatto; } /// /// Registro su DB le statistiche delle chiavi in elenco, resettando i vari contatori quando res /// /// Elenco key nel formato {rKeySampleVars}:{codInst}:{codApp}:{targetUrl} public async Task saveStatsToDb(List keyList) { bool fatto = false; // ciclo x eseguire 1:1 foreach (var item in keyList) { // recupero counter... var currCount = await redCount(item); // scompongo key... senza url di base string[] valStr = item.Replace($"{rKeySampleVars}:", "").Split(":"); if (valStr.Length > 2) { LogCallModel newRec = new LogCallModel() { CodInst = valStr[0], CodApp = valStr[1], TargetUrl = item.Replace($"{rKeySampleVars}:", "").Replace($"{valStr[0]}:{valStr[1]}:", ""), DataRif = DateTime.Now, NumCall = currCount }; fatto = await dbController.LogCallUpsert(newRec); if (fatto) { await redCountClear(item); } } } return fatto; } /// /// Invio email richiesta /// /// /// /// /// public async Task SendEmail(string destEmail, string oggetto, string corpo) { bool answ = false; try { await _emailSender.SendEmailAsync(destEmail, oggetto, corpo); answ = true; } catch { } return answ; } /// /// Statistiche del LOG chiamate all'API dato filtro /// /// Data minima /// DataMax /// Valore cercato, se "" è tutti /// public async Task> StatsLogCallGetFilt(DateTime DateFrom, DateTime DateTo, string SearchVal = "") { List dbResult = new List(); string cacheKey = $"{rKeyStatslogCall}:{DateFrom:yyyyMMdd}:{DateTo:yyyyMMdd}"; if (!string.IsNullOrEmpty(SearchVal)) { cacheKey += $":{SearchVal}"; } trackCache(cacheKey); string rawData = await getRSV(cacheKey); if (!string.IsNullOrEmpty(rawData)) { dbResult = JsonConvert.DeserializeObject>(rawData); } else { Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); var rawResult = dbController.StatsLogCallGetFilt(DateFrom, DateTo, SearchVal); dbResult = rawResult .OrderByDescending(x => x.YearRef) .ThenByDescending(x => x.TotCall) .ToList(); if (dbResult != null) { rawData = JsonConvert.SerializeObject(dbResult); await setRSV(cacheKey, rawData, redCacheTtlStd); } stopWatch.Stop(); TimeSpan ts = stopWatch.Elapsed; Log.Trace($"Effettuata lettura da DB per StatsLogCallGetFilt: {ts.TotalMilliseconds} ms"); } return await Task.FromResult(dbResult); } /// /// Salva come running i task indicati (togliendo da richiesti) /// /// /// /// /// public int TaskSetRunning(string CodImp, Dictionary DataPayload) { int done = 0; RedisKey reqKey = (RedisKey)$"{rKeyTaskReq}:{CodImp}"; RedisKey runKey = (RedisKey)$"{rKeyTaskRun}:{CodImp}"; // imposto scadenza a 30gg... double scadenza = 30 * 24 * 60; // upsert per ogni singolo record + rimozxione da richiesti foreach (var res in DataPayload) { bool answ = redisHashKeySet(runKey, res.Key, res.Value, scadenza); redisHashKeyDelete(reqKey, res.Key); done += answ ? 1 : 0; } // invio string in messagepipe x forzare refresh... TaskMessPipe.sendMessage(CodImp); return done; } /// /// Salva il risultato dell'esecuzione dei task effettuata /// /// /// /// /// public int TaskSetDone(string CodImp, Dictionary DataPayload) { int done = 0; RedisKey runKey = (RedisKey)$"{rKeyTaskRun}:{CodImp}"; RedisKey doneKey = (RedisKey)$"{rKeyTaskDone}:{CodImp}"; // imposto scadenza a 30gg... double scadenza = 30 * 24 * 60; // upsert per ogni singolo record + rimozxione da running foreach (var res in DataPayload) { bool answ = redisHashKeySet(doneKey, res.Key, res.Value, scadenza); redisHashKeyDelete(runKey, res.Key); done += answ ? 1 : 0; } // invio string in messagepipe x forzare refresh... TaskMessPipe.sendMessage(CodImp); return done; } /// /// Restituisce i task associati ad un dato EgwACC /// /// /// public Dictionary TaskListGet(string CodImp) { RedisKey currKey = (RedisKey)$"{rKeyTaskReq}:{CodImp}"; Dictionary answ = redisHashDictGet(currKey); return answ; } /// /// Resetta i task di un dato EgwACC /// /// /// public Dictionary TaskListReset(string CodImp) { Dictionary answ = new Dictionary(); bool ok01 = redisHashDictDelete((RedisKey)$"{rKeyTaskReq}:{CodImp}"); bool ok02 = redisHashDictDelete((RedisKey)$"{rKeyTaskRun}:{CodImp}"); bool ok03 = redisHashDictDelete((RedisKey)$"{rKeyTaskDone}:{CodImp}"); // leggo remaining x verificare sia ok.. var dictReq = new Dictionary(redisHashDictGet((RedisKey)$"{rKeyTaskReq}:{CodImp}")); var dictRun = new Dictionary(redisHashDictGet((RedisKey)$"{rKeyTaskRun}:{CodImp}")); var dictDone = new Dictionary(redisHashDictGet((RedisKey)$"{rKeyTaskDone}:{CodImp}")); answ = new Dictionary( dictReq); foreach (var item in dictRun) { answ.TryAdd(item.Key, item.Value); } foreach (var item in dictDone) { answ.TryAdd(item.Key, item.Value); } // invio string in messagepipe x forzare refresh... TaskMessPipe.sendMessage(CodImp); return answ; } /// /// Esegue aggiunta Ticket richiesto + restitusice aperti x cliente /// /// /// /// public async Task TicketAdd(SupportRequest currRequest) { bool fatto = false; // inserimento! Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); fatto = dbController.TicketAddNew(currRequest); stopWatch.Stop(); TimeSpan ts = stopWatch.Elapsed; Log.Trace($"Effettuata inserimento con TicketAdd: {ts.TotalMilliseconds} ms"); // restituisce elenco return await Task.FromResult(fatto); } /// /// Elenco ticket dato cliente + App + MasterKey /// /// /// /// /// /// public async Task> TicketByCliente(string CodInst, string CodApp, string MasterKey, int numRec = 1000) { List dbResult = new List(); Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); dbResult = dbController.TicketGetFilt(false, TipologiaTicket.ND, CodApp, CodInst, MasterKey, numRec); stopWatch.Stop(); TimeSpan ts = stopWatch.Elapsed; Log.Trace($"Effettuata lettura da DB per TicketByCliente: {ts.TotalMilliseconds} ms"); return await Task.FromResult(dbResult); } /// /// Aggiornamentos tato ticket /// /// /// /// public async Task TicketUpdateState(int IdxTicket, StatoRichiesta NewStatus) { bool fatto = false; // inserimento! Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); fatto = dbController.TicketUpdateState(IdxTicket, NewStatus); stopWatch.Stop(); TimeSpan ts = stopWatch.Elapsed; Log.Trace($"Effettuata update con TicketUpdateState: {ts.TotalMilliseconds} ms"); // restituisce elenco return await Task.FromResult(fatto); } #endregion Public Methods #region Protected Fields /// /// TTL da 1 h x cache Redis /// protected const int hourTTL = 60 * 60; /// /// TTL da 1 min x cache Redis /// protected const int redCacheTtlFast = 60 * 1; /// /// TTL da 5 min x cache Redis /// protected const int redCacheTtlStd = 60 * 5; /// /// Chiave redis x attivazioni da IdxLic /// protected const string rKeyAttivByLic = "LiMan.UI:Licenze:AttByIdxLic"; /// /// Chiave base dati Redis gestiti API /// protected const string rKeyBaseApi = "LiMan:API"; /// /// Chiave base dati Redis gestiti UI/API /// protected const string rKeyBaseComm = "LiMan:ALL"; /// /// Chiave degli elenchi dei Task req/pending /// protected const string rKeyTaskReq = $"{rKeyBaseComm}:TaskReq"; /// /// Chiave degli elenchi dei Task eseguiti /// protected const string rKeyTaskDone = $"{rKeyBaseComm}:TaskDone"; /// /// Chiave degli elenchi dei Task in esecuzione /// protected const string rKeyTaskRun = $"{rKeyBaseComm}:TaskRun"; /// /// Chiave base dati Redis gestiti UI /// protected const string rKeyBaseUi = "LiMan:UI"; /// /// Chiave redis x licenze da ID /// protected const string rKeyLicById = "LiMan.UI:Licenze:ById"; /// /// Chiave redis x licenze da MasterKey /// protected const string rKeyLicByMKey = "LiMan.UI:Licenze:ListByKey"; /// /// Chiave redis x statistiche in acquisizione /// protected const string rKeySampleStats = "LiMan.UI:SampleStats:Curr"; /// /// Chiave redis x statistiche in acquisizione /// protected const string rKeySampleVars = "LiMan.UI:SampleStats:Vars"; /// /// Chiave redis x statistiche chiamate /// protected const string rKeyStatslogCall = "LiMan.UI:StatsLogCall"; protected static JsonSerializerSettings? JSSettings; /// /// Durata cache lunga IN SECONDI /// protected int cacheTtlLong = 60 * 5; /// /// Durata cache breve IN SECONDI /// protected int cacheTtlShort = 60 * 1; /// /// Oggetto per connessione a REDIS /// protected IConnectionMultiplexer redisConn = null!; /// /// Oggetto DB redis da impiegare x chiamate R/W /// protected IDatabase redisDb = null!; protected Random rnd = new Random(); #endregion Protected Fields #region Protected Properties /// /// Durata cache breve (1 min circa + perturbazione percentuale +/-10%) /// protected TimeSpan FastCache { get => TimeSpan.FromSeconds(cacheTtlShort * rnd.Next(900, 1100) / 1000); } /// /// Durata cache lunga (+ perturbazione percentuale +/-10%) /// protected TimeSpan LongCache { get => TimeSpan.FromSeconds(cacheTtlLong * rnd.Next(900, 1100) / 1000); } /// /// Durata cache MOLTO breve (10 sec circa + perturbazione percentuale +/-10%) /// protected TimeSpan UltraFastCache { get => TimeSpan.FromSeconds(cacheTtlShort / 6 * rnd.Next(900, 1100) / 1000); } /// /// Durata cache MOLTO lunga (+ perturbazione percentuale +/-10%) /// protected TimeSpan UltraLongCache { get => TimeSpan.FromSeconds(cacheTtlLong * 10 * rnd.Next(900, 1100) / 1000); } #endregion Protected Properties #region Protected Methods /// /// Esegue flush memoria redis dato redPattern /// /// /// protected 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); } answ = true; } } return answ; } /// /// Recupera statistiche correnti /// /// protected async Task getCurrStats() { DateTime adesso = DateTime.Now; SampleStats answ = new SampleStats() { Name = "ApiStats" }; // in primis check data/ora prima/ultima scrittura del set... (2 date, lista chiavi gestite) string rawData = await getRSV(rKeySampleStats); if (rawData != null) { answ = JsonConvert.DeserializeObject(rawData); // aggiorno ultimo controllo e salvo... answ.DtLast = adesso; // salvo! await setCurrStats(answ); } // controllo se scadute... if (adesso.Subtract(answ.DtFirst).TotalMinutes > 60) { // se scaduto --> registrazione set sul DB (async), resettando i vari contatori... bool salvato = await saveStatsToDb(answ.VList); // inizio NUOVO set vuoto con record corrente answ = new SampleStats() { Name = "ApiStats" }; // salvo! await setCurrStats(answ); } // restituisco record! return answ; } /// /// Recupero chiave da redis /// /// /// protected async Task getRSV(string rKey) { string answ = await _redisCacheClient.GetDbFromConfiguration().GetAsync(rKey); return answ; } /// /// Recupera contatore x la chiave redis indicata... /// /// protected async Task redCount(string rKey) { int currCount = 0; string rawVal = await getRSV(rKey); if (!string.IsNullOrEmpty(rawVal)) { int.TryParse(rawVal, out currCount); } return currCount; } /// /// Resetta contatore x la chiave redis indicata... /// /// protected async Task redCountClear(string rKey) { bool answ = false; int currCount = 0; answ = await setRSV(rKey, currCount, 2 * hourTTL); return answ; } /// /// Incrementa contatore x la chiave redis indicata... /// /// protected async Task redCountIncr(string rKey) { bool answ = false; int currCount = 0; string rawVal = await getRSV(rKey); if (!string.IsNullOrEmpty(rawVal)) { int.TryParse(rawVal, out currCount); } currCount++; answ = await setRSV(rKey, currCount, 2 * hourTTL); return answ; } /// /// Salva statistiche correnti /// /// protected async Task setCurrStats(SampleStats newVal) { bool answ = false; string rawData = JsonConvert.SerializeObject(newVal); answ = await setRSV(rKeySampleStats, rawData, 24 * hourTTL); return answ; } /// /// Salvataggio chiave in redis /// /// /// /// /// protected async Task setRSV(string rKey, string rVal, int ttlSec) { bool fatto = false; await _redisCacheClient.GetDbFromConfiguration().AddAsync(rKey, rVal, DateTimeOffset.Now.AddSeconds(ttlSec)); fatto = true; return fatto; } /// /// Salvataggio chiave in redis /// /// /// /// /// protected async Task setRSV(string rKey, int rValInt, int ttlSec) { bool fatto = false; await _redisCacheClient.GetDbFromConfiguration().AddAsync(rKey, rValInt, DateTimeOffset.Now.AddSeconds(ttlSec)); fatto = true; return fatto; } /// /// Registra in cache chiave se non fosse già in elenco /// /// protected void trackCache(string newKey) { if (!cachedDataList.Contains(newKey)) { cachedDataList.Add(newKey); } } #endregion Protected Methods #region Private Fields private static IConfiguration _configuration; private static ILogger _logger; private static NLog.Logger Log = LogManager.GetCurrentClassLogger(); private readonly IEmailSender _emailSender; //private readonly IDistributedCache distributedCache; private readonly IRedisCacheClient _redisCacheClient; /// /// Elenco obj in cache /// private List cachedDataList = new List(); /// /// Durata assoluta massima della cache IN SECONDI /// private int chAbsExp = 60 * 5; /// /// Durata della cache IN SECONDI in modalità inattiva (non acceduta) prima di venire /// rimossa NON estende oltre il tempo massimo di validità della cache (chAbsExp) /// private int chSliExp = 60 * 2; /// /// data-ora veto registrazione di una nuova registrazione di snapshot installazioni attive al giorno corrente /// private DateTime VetoInstRelHistSnap = DateTime.Today; #endregion Private Fields #region Private Methods /// /// Parametri per generare opzioni cache /// /// Fattore di moltiplica cache (se 1 --> 2 e 5 min) /// private DistributedCacheEntryOptions cacheOpt(int multFact) { var numSecAbsExp = multFact <= 0 ? chAbsExp : chAbsExp * multFact; var numSecSliExp = multFact <= 0 ? chSliExp : chSliExp * multFact; return new DistributedCacheEntryOptions().SetAbsoluteExpiration(DateTime.Now.AddSeconds(numSecAbsExp)).SetSlidingExpiration(TimeSpan.FromSeconds(numSecSliExp)); } /// /// Eliminazione di un HashSet Redis /// /// Chiave del dizionario private bool redisHashDictDelete(RedisKey dictKey) { bool fatto = false; try { // rimuovo scrivendo un set vuoto con expiry a 1 sec HashEntry[] data2ins = new HashEntry[1]; data2ins[0] = new HashEntry("removed", $"{DateTime.Now}"); // salvo! redisDb.HashSet(dictKey, data2ins); redisDb.KeyExpire(dictKey, DateTime.Now.AddMilliseconds(1)); fatto = true; } catch (Exception exc) { Log.Error($"Eccezione in redisHashDictDelete | dictKey: {dictKey}{Environment.NewLine}{exc}"); } return fatto; } /// /// Recupero HashSet redis come Dictionary /// /// Chiave del dizionario /// Dizionario valori salvato private Dictionary redisHashDictGet(RedisKey dictKey) { Dictionary answ = new Dictionary(); try { answ = redisDb .HashGetAll(dictKey) .ToDictionary(x => $"{x.Name}", x => $"{x.Value}"); } catch (Exception exc) { Log.Info($"Errore redisHashDictGet | dictKey: {dictKey}{Environment.NewLine}{exc}"); } return answ; } /// /// Salvataggio Dictionary come HashSet Redis, con expiry NON gestito (0 = mai) /// /// Chiave del dizionario /// Valore Dizionario da salvare private bool redisHashDictSet(RedisKey dictKey, Dictionary dict) { // ove non indicato expiry è 0 = MAI return redisHashDictSet(dictKey, dict, 0); } /// /// Salvataggio Dictionary come HashSet Redis /// /// Chiave del dizionario /// Valore Dizionario da salvare /// Expiry in minuti del valore, se 0 = mai private bool redisHashDictSet(RedisKey dictKey, Dictionary dict, double expireMin) { bool fatto = false; try { HashEntry[] data2ins = new HashEntry[dict.Count]; int i = 0; foreach (KeyValuePair kvp in dict) { data2ins[i] = new HashEntry(kvp.Key, kvp.Value); i++; } // salvo! redisDb.HashSet(dictKey, data2ins); // se richiesto imposto Expiry if (expireMin > 0) { redisDb.KeyExpire(dictKey, DateTime.Now.AddMinutes(expireMin)); } fatto = true; } catch (Exception exc) { Log.Error($"Eccezione in redisHashDictSet | dictKey: {dictKey}{Environment.NewLine}{exc}"); } return fatto; } /// /// Eliminazione di un singolo valore da un HashSet Redis /// /// Chiave del dizionario /// Chiave del valore da eliminare (singolo record) private bool redisHashKeyDelete(RedisKey dictKey, string recKey) { bool fatto = false; try { redisDb.HashDelete(dictKey, (RedisValue)recKey); fatto = true; } catch (Exception exc) { Log.Error($"Eccezione in redisHashKeyDelete | dictKey: {dictKey}{Environment.NewLine}{exc}"); } return fatto; } /// /// Salvataggio di un singolo valore in HashSet Redis, con expiry NON gestito (0 = mai) /// /// Chiave del dizionario /// Chiave valore da salvare /// Valore da salvare private bool redisHashKeySet(RedisKey dictKey, string recKey, string recVal) { // ove non indicato expiry è 0 = MAI return redisHashKeySet(dictKey, recKey, recVal, 0); } /// /// Salvataggio di un singolo valore in HashSet Redis /// /// Chiave del dizionario /// Chiave valore da salvare /// Valore da salvare /// Expiry in minuti del valore, se 0 = mai private bool redisHashKeySet(RedisKey dictKey, string recKey, string recVal, double expireMin) { bool fatto = false; try { HashEntry[] data2ins = new HashEntry[1] { new HashEntry(recKey, recVal) }; // salvo! redisDb.HashSet(dictKey, data2ins); // se richiesto imposto Expiry if (expireMin > 0) { redisDb.KeyExpire(dictKey, DateTime.Now.AddMinutes(expireMin)); } fatto = true; } catch (Exception exc) { Log.Error($"Eccezione in redisHashKeySet | dictKey: {dictKey}{Environment.NewLine}{exc}"); } return fatto; } #endregion Private Methods } }