From 64ab66aacdf651a9ae88270ea3118cfa41d487b9 Mon Sep 17 00:00:00 2001 From: Hamid Reza Mohammadi Date: Wed, 15 Jul 2026 08:43:40 +0330 Subject: [PATCH] refactoring --- GanjooRazor/Pages/Books.cshtml.cs | 101 ++------ GanjooRazor/Pages/Contribs.cshtml.cs | 242 +++++------------- GanjooRazor/Pages/FAQ.cshtml.cs | 59 ++--- GanjooRazor/Pages/Hashieha.cshtml.cs | 27 +- GanjooRazor/Pages/Index.cshtml.cs | 99 ++----- GanjooRazor/Pages/Login.cshtml.cs | 3 +- .../Pages/LoginPartialEnabledPageModel.cs | 55 +++- GanjooRazor/Pages/Photos.cshtml.cs | 8 +- GanjooRazor/Pages/Quotes.cshtml.cs | 52 ++-- GanjooRazor/Pages/Search.cshtml.cs | 26 +- GanjooRazor/Pages/Simi.cshtml.cs | 26 +- GanjooRazor/Pages/Tagged.cshtml.cs | 27 +- GanjooRazor/Startup.cs | 2 + GanjooRazor/Utils/PoetCacheService.cs | 85 ++++++ 14 files changed, 303 insertions(+), 509 deletions(-) create mode 100644 GanjooRazor/Utils/PoetCacheService.cs diff --git a/GanjooRazor/Pages/Books.cshtml.cs b/GanjooRazor/Pages/Books.cshtml.cs index 3489e5c8..11f86825 100644 --- a/GanjooRazor/Pages/Books.cshtml.cs +++ b/GanjooRazor/Pages/Books.cshtml.cs @@ -1,85 +1,52 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.RazorPages; -using Microsoft.Extensions.Configuration; +using GanjooRazor.Utils; +using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json.Linq; -using Newtonsoft.Json; using RMuseum.Models.Ganjoor.ViewModels; using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; -using Microsoft.Extensions.Caching.Memory; -using System; +using Microsoft.Extensions.Configuration; namespace GanjooRazor.Pages { [IgnoreAntiforgeryToken(Order = 1001)] public class BooksModel : LoginPartialEnabledPageModel { + private readonly PoetCacheService _poetCache; + public async Task OnGetAsync() { - if (bool.Parse(Configuration["MaintenanceMode"])) + var maintenanceResult = TryGetMaintenanceModeResult(); + if (maintenanceResult != null) { - return StatusCode(503); + return maintenanceResult; } - ViewData["Title"] = "فهرست کتاب‌ها"; - ViewData["TrackingScript"] = Configuration["TrackingScript"] != null && string.IsNullOrEmpty(Request.Cookies["Token"]) ? Configuration["TrackingScript"].Replace("loggedon", "") : Configuration["TrackingScript"]; - LoggedIn = !string.IsNullOrEmpty(Request.Cookies["Token"]); - if (false == await preparePoets()) + ViewData["Title"] = "فهرست کتاب‌ها"; + InitializeCommonPageState(); + + var (poetsOk, poets, poetsError) = await _poetCache.GetPoetsAsync(AggressiveCacheEnabled); + if (!poetsOk) { + LastError = poetsError; return Page(); } + Poets = poets; var response = await _httpClient.GetAsync($"{APIRoot.Url}/api/ganjoor/books"); if (!response.IsSuccessStatusCode) { - LastError = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + LastError = await ReadErrorMessageAsync(response); return Page(); } Books = JArray.Parse(await response.Content.ReadAsStringAsync()).ToObject>(); return Page(); } - private async Task preparePoets() - { - var cacheKey = $"/api/ganjoor/poets"; - if (!_memoryCache.TryGetValue(cacheKey, out List poets)) - { - try - { - var response = await _httpClient.GetAsync($"{APIRoot.Url}/api/ganjoor/poets"); - if (!response.IsSuccessStatusCode) - { - LastError = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); - return false; - } - poets = JArray.Parse(await response.Content.ReadAsStringAsync()).ToObject>(); - if (AggressiveCacheEnabled) - { - _memoryCache.Set(cacheKey, poets, TimeSpan.FromHours(1)); - } - } - catch - { - LastError = "خطا در دسترسی به وب سرویس گنجور"; - return false; - } - - } - - Poets = poets; - return true; - } - - /// - /// configration file reader (appsettings.json) - /// - protected readonly IConfiguration Configuration; - /// /// books /// - public List Books { get; set; } + public List Books { get; set; } /// /// Poets @@ -91,41 +58,13 @@ namespace GanjooRazor.Pages /// public string LastError { get; set; } - /// - /// memory cache - /// - private readonly IMemoryCache _memoryCache; - - // - /// aggressive cache - /// - public bool AggressiveCacheEnabled - { - get - { - try - { - return bool.Parse(Configuration["AggressiveCacheEnabled"]); - } - catch - { - return false; - } - } - } - /// /// constructor /// - /// - /// - /// - public BooksModel(IConfiguration configuration, - HttpClient httpClient, IMemoryCache memoryCache - ) : base(httpClient) + public BooksModel(IConfiguration configuration, HttpClient httpClient, PoetCacheService poetCache) : base(httpClient, configuration) { - Configuration = configuration; - _memoryCache = memoryCache; + _poetCache = poetCache; } } } + diff --git a/GanjooRazor/Pages/Contribs.cshtml.cs b/GanjooRazor/Pages/Contribs.cshtml.cs index be4cfd89..895a2ea4 100644 --- a/GanjooRazor/Pages/Contribs.cshtml.cs +++ b/GanjooRazor/Pages/Contribs.cshtml.cs @@ -1,152 +1,81 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Caching.Memory; -using Newtonsoft.Json.Linq; -using Newtonsoft.Json; -using RMuseum.Models.Ganjoor.ViewModels; -using System.Collections.Generic; -using System.Net.Http; -using System.Threading.Tasks; +using GanjooRazor.Utils; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; -using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using RMuseum.Models.Ganjoor.ViewModels; using RMuseum.Models.Generic.ViewModels; using RSecurityBackend.Models.Generic; +using System.Collections.Generic; using System.Linq; -using static Betalgo.Ranul.OpenAI.ObjectModels.RealtimeModels.RealtimeEventTypes; -using Org.BouncyCastle.Asn1.Ocsp; -using System; +using System.Net.Http; +using System.Threading.Tasks; namespace GanjooRazor.Pages { [IgnoreAntiforgeryToken(Order = 1001)] public class ContribsModel : LoginPartialEnabledPageModel { + private readonly PoetCacheService _poetCache; + public List Poets { get; set; } public int PoetId { get; set; } public GanjoorPoetCompleteViewModel Poet { get; set; } public string LastError { get; set; } - /// - /// aggressive cache - /// - public bool AggressiveCacheEnabled + public async Task OnGetAsync() { - get + var maintenanceResult = TryGetMaintenanceModeResult(); + if (maintenanceResult != null) { - try - { - return bool.Parse(Configuration["AggressiveCacheEnabled"]); - } - catch - { - return false; - } - } - } - - - - private async Task preparePoets() - { - var cacheKey = $"/api/ganjoor/poets"; - if (!_memoryCache.TryGetValue(cacheKey, out List poets)) - { - var response = await _httpClient.GetAsync($"{APIRoot.Url}/api/ganjoor/poets"); - if (!response.IsSuccessStatusCode) - { - LastError = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); - return false; - } - poets = JArray.Parse(await response.Content.ReadAsStringAsync()).ToObject>(); - if (AggressiveCacheEnabled) - { - _memoryCache.Set(cacheKey, poets, TimeSpan.FromHours(1)); - } + return maintenanceResult; } + InitializeCommonPageState(); + PoetId = string.IsNullOrEmpty(Request.Query["a"]) ? 0 : int.Parse(Request.Query["a"]); + + var (poetsOk, poets, poetsError) = await _poetCache.GetPoetsAsync(AggressiveCacheEnabled); + if (!poetsOk) + { + LastError = poetsError; + return Page(); + } Poets = poets; - return true; - } - private async Task preparePoet() - { - var cacheKey = $"/api/ganjoor/poet/{PoetId}"; - if (!_memoryCache.TryGetValue(cacheKey, out GanjoorPoetCompleteViewModel poet)) + if (PoetId != 0) { - var poetResponse = await _httpClient.GetAsync($"{APIRoot.Url}/api/ganjoor/poet/{PoetId}"); - if (!poetResponse.IsSuccessStatusCode) + var (poetOk, poet, poetError) = await _poetCache.GetPoetAsync(PoetId, AggressiveCacheEnabled); + if (!poetOk) { - LastError = JsonConvert.DeserializeObject(await poetResponse.Content.ReadAsStringAsync()); - return false; - } - poet = JObject.Parse(await poetResponse.Content.ReadAsStringAsync()).ToObject(); - if (AggressiveCacheEnabled) - { - _memoryCache.Set(cacheKey, poet, TimeSpan.FromHours(1)); + LastError = poetError; + return Page(); } + Poet = poet; } - Poet = poet; - return true; + return Page(); } public async Task OnGetPoetInformationAsync(int id) { if (id == 0) - return new OkObjectResult(null); - var cacheKey = $"/api/ganjoor/poet/{id}"; - if (!_memoryCache.TryGetValue(cacheKey, out GanjoorPoetCompleteViewModel poet)) { - var poetResponse = await _httpClient.GetAsync($"{APIRoot.Url}/api/ganjoor/poet/{id}"); - if (!poetResponse.IsSuccessStatusCode) - { - return BadRequest(JsonConvert.DeserializeObject(await poetResponse.Content.ReadAsStringAsync())); - } - poet = JObject.Parse(await poetResponse.Content.ReadAsStringAsync()).ToObject(); - if (AggressiveCacheEnabled) - { - _memoryCache.Set(cacheKey, poet, TimeSpan.FromHours(1)); - } + return new OkObjectResult(null); + } + var (success, poet, error) = await _poetCache.GetPoetAsync(id, AggressiveCacheEnabled); + if (!success) + { + return BadRequest(error); } return new OkObjectResult(poet); } - public async Task OnGetAsync() - { - if (bool.Parse(Configuration["MaintenanceMode"])) - { - return StatusCode(503); - } - - LoggedIn = !string.IsNullOrEmpty(Request.Cookies["Token"]); - - PoetId = string.IsNullOrEmpty(Request.Query["a"]) ? 0 : int.Parse(Request.Query["a"]); - - ViewData["TrackingScript"] = Configuration["TrackingScript"] != null && string.IsNullOrEmpty(Request.Cookies["Token"]) ? Configuration["TrackingScript"].Replace("loggedon", "") : Configuration["TrackingScript"]; - - //todo: use html master layout or make it partial - // 1. poets - if (false == (await preparePoets())) - return Page(); - - if (PoetId != 0) - { - if (false == (await preparePoet())) - return Page(); - } - - - - return Page(); - } - - public async Task OnGetGroupedByDateAsync(string dataType) + public async Task OnGetGroupedByDateAsync(string dataType) { var responseDays = await _httpClient.GetAsync($"{APIRoot.Url}/api/contributions/{dataType}/daily?PageNumber=1&PageSize=30"); - if (!responseDays.IsSuccessStatusCode) { - return new BadRequestObjectResult(JsonConvert.DeserializeObject(await responseDays.Content.ReadAsStringAsync())); + return new BadRequestObjectResult(await ReadErrorMessageAsync(responseDays)); } var days = JArray.Parse(await responseDays.Content.ReadAsStringAsync()).ToObject>(); @@ -155,116 +84,73 @@ namespace GanjooRazor.Pages if (dataType != "users") { var responseUsers = await _httpClient.GetAsync($"{APIRoot.Url}/api/contributions/{dataType}/by/user?PageNumber=1&PageSize=30"); - if (!responseUsers.IsSuccessStatusCode) { - return new BadRequestObjectResult(JsonConvert.DeserializeObject(await responseUsers.Content.ReadAsStringAsync())); + return new BadRequestObjectResult(await ReadErrorMessageAsync(responseUsers)); } users = JArray.Parse(await responseUsers.Content.ReadAsStringAsync()).ToObject>(); usersPagination = JsonConvert.DeserializeObject(responseUsers.Headers.GetValues("paging-headers").Single()); } var response = await _httpClient.GetAsync($"{APIRoot.Url}/api/contributions/{dataType}/summary"); - if (!response.IsSuccessStatusCode) { - return new BadRequestObjectResult(JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync())); + return new BadRequestObjectResult(await ReadErrorMessageAsync(response)); } SummedUpViewModel summary = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); - return new PartialViewResult() + return Partial("_GroupedByDateViewPartial", new _GroupedByDateViewPartialModel() { - ViewName = "_GroupedByDateViewPartial", - ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary()) - { - Model = new _GroupedByDateViewPartialModel() - { - DataType = dataType, - Days = days.ToArray(), - DaysPagination = JsonConvert.DeserializeObject(responseDays.Headers.GetValues("paging-headers").Single()), - Users = users == null ? null :users.ToArray(), - UsersPagination = usersPagination, - Summary = summary - } - } - }; + DataType = dataType, + Days = days.ToArray(), + DaysPagination = JsonConvert.DeserializeObject(responseDays.Headers.GetValues("paging-headers").Single()), + Users = users?.ToArray(), + UsersPagination = usersPagination, + Summary = summary + }); } - - public async Task OnGetGroupedByUsersAsync(string dataType, int pageNumber) + public async Task OnGetGroupedByUsersAsync(string dataType, int pageNumber) { var responseUsers = await _httpClient.GetAsync($"{APIRoot.Url}/api/contributions/{dataType}/by/user?PageNumber={pageNumber}&PageSize=30"); - if (!responseUsers.IsSuccessStatusCode) { - return new BadRequestObjectResult(JsonConvert.DeserializeObject(await responseUsers.Content.ReadAsStringAsync())); + return new BadRequestObjectResult(await ReadErrorMessageAsync(responseUsers)); } var users = JArray.Parse(await responseUsers.Content.ReadAsStringAsync()).ToObject>(); var usersPagination = JsonConvert.DeserializeObject(responseUsers.Headers.GetValues("paging-headers").Single()); - return new PartialViewResult() + return Partial("_GroupedByDateViewTablePartial", new _GroupedByDateViewTablePartialModel() { - ViewName = "_GroupedByDateViewTablePartial", - ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary()) - { - Model = new _GroupedByDateViewTablePartialModel() - { - DataType = dataType, - Users = users.ToArray(), - UsersPagination = usersPagination, - } - } - }; + DataType = dataType, + Users = users.ToArray(), + UsersPagination = usersPagination, + }); } - public async Task OnGetGroupedByDayAsync(string dataType, int pageNumber) + public async Task OnGetGroupedByDayAsync(string dataType, int pageNumber) { var responseDays = await _httpClient.GetAsync($"{APIRoot.Url}/api/contributions/{dataType}/daily?PageNumber={pageNumber}&PageSize=30"); - if (!responseDays.IsSuccessStatusCode) { - return new BadRequestObjectResult(JsonConvert.DeserializeObject(await responseDays.Content.ReadAsStringAsync())); + return new BadRequestObjectResult(await ReadErrorMessageAsync(responseDays)); } var days = JArray.Parse(await responseDays.Content.ReadAsStringAsync()).ToObject>(); - return new PartialViewResult() + return Partial("_GroupedByDateViewTablePartial", new _GroupedByDateViewTablePartialModel() { - ViewName = "_GroupedByDateViewTablePartial", - ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary()) - { - Model = new _GroupedByDateViewTablePartialModel() - { - DataType = dataType, - Days = days.ToArray(), - DaysPagination = JsonConvert.DeserializeObject(responseDays.Headers.GetValues("paging-headers").Single()), - } - } - }; + DataType = dataType, + Days = days.ToArray(), + DaysPagination = JsonConvert.DeserializeObject(responseDays.Headers.GetValues("paging-headers").Single()), + }); } - - - /// - /// memory cache - /// - private readonly IMemoryCache _memoryCache; - - /// - /// configration file reader (appsettings.json) - /// - private readonly IConfiguration Configuration; - /// /// constructor /// - /// - /// - /// - public ContribsModel(HttpClient httpClient, IMemoryCache memoryCache, IConfiguration configuration) : base(httpClient) + public ContribsModel(HttpClient httpClient, IConfiguration configuration, PoetCacheService poetCache) : base(httpClient, configuration) { - _memoryCache = memoryCache; - Configuration - = configuration; + _poetCache = poetCache; } } } diff --git a/GanjooRazor/Pages/FAQ.cshtml.cs b/GanjooRazor/Pages/FAQ.cshtml.cs index f9319b58..47df7b09 100644 --- a/GanjooRazor/Pages/FAQ.cshtml.cs +++ b/GanjooRazor/Pages/FAQ.cshtml.cs @@ -1,11 +1,10 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.RazorPages; +using GanjooRazor.Utils; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using RMuseum.Models.FAQ; using RMuseum.Models.Ganjoor.ViewModels; -using RSecurityBackend.Models.Auth.ViewModels; using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; @@ -15,6 +14,8 @@ namespace GanjooRazor.Pages [IgnoreAntiforgeryToken(Order = 1001)] public class FAQModel : LoginPartialEnabledPageModel { + private readonly PoetCacheService _poetCache; + public string LastError { get; set; } public List Poets { get; set; } @@ -23,43 +24,32 @@ namespace GanjooRazor.Pages public FAQItem Question { get; set; } - private async Task preparePoets() - { - var response = await _httpClient.GetAsync($"{APIRoot.Url}/api/ganjoor/poets"); - if (!response.IsSuccessStatusCode) - { - LastError = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); - return false; - } - Poets = JArray.Parse(await response.Content.ReadAsStringAsync()).ToObject>(); - - return true; - } - public async Task OnGetAsync() { - if (bool.Parse(Configuration["MaintenanceMode"])) + var maintenanceResult = TryGetMaintenanceModeResult(); + if (maintenanceResult != null) { - return StatusCode(503); + return maintenanceResult; } ViewData["Title"] = $"گنجور » پرسش‌های متداول"; - LoggedIn = !string.IsNullOrEmpty(Request.Cookies["Token"]); + InitializeCommonPageState(); - - ViewData["TrackingScript"] = Configuration["TrackingScript"] != null && string.IsNullOrEmpty(Request.Cookies["Token"]) ? Configuration["TrackingScript"].Replace("loggedon", "") : Configuration["TrackingScript"]; - - //todo: use html master layout or make it partial - if (false == (await preparePoets())) + var (poetsOk, poets, poetsError) = await _poetCache.GetPoetsAsync(AggressiveCacheEnabled); + if (!poetsOk) + { + LastError = poetsError; return Page(); + } + Poets = poets; - if(!string.IsNullOrEmpty(Request.Query["id"])) + if (!string.IsNullOrEmpty(Request.Query["id"])) { var response = await _httpClient.GetAsync($"{APIRoot.Url}/api/faq/{Request.Query["id"]}"); if (!response.IsSuccessStatusCode) { - LastError = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); - if(string.IsNullOrEmpty(LastError)) + LastError = await ReadErrorMessageAsync(response); + if (string.IsNullOrEmpty(LastError)) { LastError = $"خطا در دریافت اطلاعات پرسش مد نظر - کد خطا = {response.StatusCode}"; } @@ -73,7 +63,7 @@ namespace GanjooRazor.Pages var response = await _httpClient.GetAsync($"{APIRoot.Url}/api/faq/pinned"); if (!response.IsSuccessStatusCode) { - LastError = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + LastError = await ReadErrorMessageAsync(response); return Page(); } PinnedItemsCategories = JArray.Parse(await response.Content.ReadAsStringAsync()).ToObject>(); @@ -82,21 +72,12 @@ namespace GanjooRazor.Pages return Page(); } - /// - /// configration file reader (appsettings.json) - /// - private readonly IConfiguration Configuration; - - /// /// constructor /// - /// - /// - public FAQModel(HttpClient httpClient, IConfiguration configuration) : base(httpClient) + public FAQModel(HttpClient httpClient, IConfiguration configuration, PoetCacheService poetCache) : base(httpClient, configuration) { - Configuration - = configuration; + _poetCache = poetCache; } } } diff --git a/GanjooRazor/Pages/Hashieha.cshtml.cs b/GanjooRazor/Pages/Hashieha.cshtml.cs index b121091f..fdc00477 100644 --- a/GanjooRazor/Pages/Hashieha.cshtml.cs +++ b/GanjooRazor/Pages/Hashieha.cshtml.cs @@ -28,11 +28,6 @@ namespace GanjooRazor.Pages /// private readonly IMemoryCache _memoryCache; - /// - /// configration file reader (appsettings.json) - /// - private readonly IConfiguration Configuration; - /// /// constructor @@ -40,29 +35,9 @@ namespace GanjooRazor.Pages /// /// /// - public HashiehaModel(HttpClient httpClient, IMemoryCache memoryCache, IConfiguration configuration) : base(httpClient) + public HashiehaModel(HttpClient httpClient, IMemoryCache memoryCache, IConfiguration configuration) : base(httpClient, configuration) { _memoryCache = memoryCache; - Configuration - = configuration; - } - - /// - /// aggressive cache - /// - public bool AggressiveCacheEnabled - { - get - { - try - { - return bool.Parse(Configuration["AggressiveCacheEnabled"]); - } - catch - { - return false; - } - } } public List Poets { get; set; } diff --git a/GanjooRazor/Pages/Index.cshtml.cs b/GanjooRazor/Pages/Index.cshtml.cs index 491caf55..1b8a5ea2 100644 --- a/GanjooRazor/Pages/Index.cshtml.cs +++ b/GanjooRazor/Pages/Index.cshtml.cs @@ -3,7 +3,6 @@ using GanjooRazor.Utils; using KontorService.Models.Reporting.ViewModels; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Configuration; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -45,19 +44,10 @@ namespace GanjooRazor.Pages }; /// - /// configration file reader (appsettings.json) + /// poet list/detail cache (shared with Books, Contribs, FAQ, Quotes, and eventually + /// Hashieha/Search/Simi once they're migrated too) /// - private readonly IConfiguration Configuration; - - /// - /// memory cache - /// - private readonly IMemoryCache _memoryCache; - - /// - /// aggressive cache - /// - public bool AggressiveCacheEnabled => GetConfigFlag("AggressiveCacheEnabled"); + private readonly PoetCacheService _poetCache; public bool OfflineMode => GetConfigFlag("OfflineMode"); @@ -68,14 +58,13 @@ namespace GanjooRazor.Pages /// /// /// - /// + /// public IndexModel(IConfiguration configuration, HttpClient httpClient, - IMemoryCache memoryCache - ) : base(httpClient) + PoetCacheService poetCache + ) : base(httpClient, configuration) { - Configuration = configuration; - _memoryCache = memoryCache; + _poetCache = poetCache; } /// @@ -89,20 +78,11 @@ namespace GanjooRazor.Pages public GanjoorSiteBannerViewModel Banner { get; set; } #region Shared helpers - // GetConfigFlag and CaptureErrorIfFailedAsync stay local to this class since they touch - // IndexModel-specific state (Configuration, LastError). NotLoggedInMessage, - // ReadErrorMessageAsync, and WithSecureClientAsync now live on GanjoorPageModelBase - // (inherited via LoginPartialEnabledPageModel) since they're used across every page model, - // not just this one. - - /// - /// Reads a boolean feature flag from configuration, defaulting to - /// if the key is missing or not a valid bool, instead of each flag having its own try/catch. - /// - private bool GetConfigFlag(string key, bool defaultValue = false) - { - return bool.TryParse(Configuration[key], out var value) ? value : defaultValue; - } + // CaptureErrorIfFailedAsync stays local to this class since it touches IndexModel-specific + // state (LastError). GetConfigFlag, AggressiveCacheEnabled, NotLoggedInMessage, + // ReadErrorMessageAsync, and WithSecureClientAsync now live on GanjoorPageModelBase / + // LoginPartialEnabledPageModel (inherited) since they're used across every page model, not + // just this one. /// /// If the response failed, stores the API's error message in and @@ -446,30 +426,12 @@ namespace GanjooRazor.Pages private async Task preparePoets() { - const string cacheKey = "/api/ganjoor/poets"; - if (!_memoryCache.TryGetValue(cacheKey, out List poets)) + var (success, poets, error) = await _poetCache.GetPoetsAsync(AggressiveCacheEnabled); + if (!success) { - try - { - var response = await _httpClient.GetAsync($"{APIRoot.Url}/api/ganjoor/poets"); - if (await CaptureErrorIfFailedAsync(response)) - { - return false; - } - poets = JArray.Parse(await response.Content.ReadAsStringAsync()).ToObject>(); - if (AggressiveCacheEnabled) - { - _memoryCache.Set(cacheKey, poets, TimeSpan.FromHours(1)); - } - } - catch - { - LastError = "خطا در دسترسی به وب سرویس گنجور"; - return false; - } - + LastError = error; + return false; } - Poets = poets; return true; } @@ -477,20 +439,13 @@ namespace GanjooRazor.Pages public async Task OnGetPoetInformationAsync(int id) { if (id == 0) - return new OkObjectResult(null); - var cacheKey = $"/api/ganjoor/poet/{id}"; - if (!_memoryCache.TryGetValue(cacheKey, out GanjoorPoetCompleteViewModel poet)) { - var poetResponse = await _httpClient.GetAsync($"{APIRoot.Url}/api/ganjoor/poet/{id}"); - if (!poetResponse.IsSuccessStatusCode) - { - return BadRequest(await ReadErrorMessageAsync(poetResponse)); - } - poet = JObject.Parse(await poetResponse.Content.ReadAsStringAsync()).ToObject(); - if (AggressiveCacheEnabled) - { - _memoryCache.Set(cacheKey, poet, TimeSpan.FromHours(1)); - } + return new OkObjectResult(null); + } + var (success, poet, error) = await _poetCache.GetPoetAsync(id, AggressiveCacheEnabled); + if (!success) + { + return BadRequest(error); } return new OkObjectResult(poet); } @@ -624,9 +579,10 @@ namespace GanjooRazor.Pages /// public async Task OnGetAsync() { - if (GetConfigFlag("MaintenanceMode")) + var maintenanceResult = TryGetMaintenanceModeResult(); + if (maintenanceResult != null) { - return StatusCode(503); + return maintenanceResult; } var legacyRedirect = TryHandleLegacyUrlRedirect(); @@ -705,7 +661,7 @@ namespace GanjooRazor.Pages private void InitializeRequestState() { LastError = ""; - LoggedIn = !string.IsNullOrEmpty(Request.Cookies["Token"]); + InitializeCommonPageState(); CanEdit = Request.Cookies["CanEdit"] == "True"; KeepHistory = Request.Cookies["KeepHistory"] == "True"; CanTranslate = Request.Cookies["CanTranslate"] == "True"; @@ -715,9 +671,6 @@ namespace GanjooRazor.Pages IsHomePage = Request.Path == "/"; PinterestUrl = Request.Query["pinterest_url"]; ShowAllRecitaions = Request.Query["allaudio"] == "1"; - ViewData["TrackingScript"] = Configuration["TrackingScript"] != null && string.IsNullOrEmpty(Request.Cookies["Token"]) - ? Configuration["TrackingScript"].Replace("loggedon", "") - : Configuration["TrackingScript"]; ActiveTab = Request.Query["tab"]; if (ShowAllRecitaions && string.IsNullOrEmpty(ActiveTab)) diff --git a/GanjooRazor/Pages/Login.cshtml.cs b/GanjooRazor/Pages/Login.cshtml.cs index 84bd69ff..93975c62 100644 --- a/GanjooRazor/Pages/Login.cshtml.cs +++ b/GanjooRazor/Pages/Login.cshtml.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; using System.Net.Http; namespace GanjooRazor.Pages @@ -7,7 +8,7 @@ namespace GanjooRazor.Pages public class LoginModel : LoginPartialEnabledPageModel { - public LoginModel(HttpClient httpClient) : base(httpClient) { } + public LoginModel(HttpClient httpClient, IConfiguration configuration) : base(httpClient, configuration) { } public string UserFriendlyName { get; set; } public string LastError { get; set; } diff --git a/GanjooRazor/Pages/LoginPartialEnabledPageModel.cs b/GanjooRazor/Pages/LoginPartialEnabledPageModel.cs index 9bbccec1..60d7d1e8 100644 --- a/GanjooRazor/Pages/LoginPartialEnabledPageModel.cs +++ b/GanjooRazor/Pages/LoginPartialEnabledPageModel.cs @@ -2,6 +2,7 @@ using GanjooRazor.Utils; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; using Newtonsoft.Json; using RMuseum.Models.Auth.Memory; using RMuseum.Models.Auth.ViewModel; @@ -21,6 +22,57 @@ namespace GanjooRazor.Pages { public class LoginPartialEnabledPageModel : GanjoorPageModelBase { + /// + /// configuration file reader (appsettings.json). Was previously duplicated as a private field + /// on almost every page model that derives from this class (Index, Contribs, Quotes, FAQ, + /// Books, Hashieha, Search, Simi all had their own copy). + /// + protected readonly IConfiguration Configuration; + + /// + /// Reads a boolean feature flag from configuration, defaulting to + /// if the key is missing or not a valid bool. Was previously a per-page try/catch block + /// (and in IndexModel's case, was the ONLY one of these that had the try/catch at all - + /// every other page's `bool.Parse(Configuration["MaintenanceMode"])` below would throw on a + /// missing/malformed config key instead of defaulting to false). + /// + protected bool GetConfigFlag(string key, bool defaultValue = false) + { + return bool.TryParse(Configuration[key], out var value) ? value : defaultValue; + } + + /// + /// aggressive cache flag. Every page model that had this property defined it identically - + /// same try/catch-wrapped bool.Parse now handled once by GetConfigFlag. + /// + protected bool AggressiveCacheEnabled => GetConfigFlag("AggressiveCacheEnabled"); + + /// + /// Returns a 503 result if the site is in maintenance mode, or null otherwise. Was previously + /// `if (bool.Parse(Configuration["MaintenanceMode"])) { return StatusCode(503); }` duplicated + /// verbatim at the top of OnGetAsync in 8 different page models, all without try/catch (so a + /// missing "MaintenanceMode" config key would throw a FormatException/ArgumentNullException + /// instead of just... not being in maintenance mode). + /// + protected IActionResult TryGetMaintenanceModeResult() + { + return GetConfigFlag("MaintenanceMode") ? StatusCode(503) : null; + } + + /// + /// Sets the two pieces of per-request state that were duplicated character-for-character + /// across every page model in this hierarchy: whether the current visitor is logged in, and + /// the tracking script to render (with "loggedon" stripped out for anonymous visitors). + /// Call once near the top of each page's OnGetAsync instead of repeating both lines. + /// + protected void InitializeCommonPageState() + { + LoggedIn = !string.IsNullOrEmpty(Request.Cookies["Token"]); + ViewData["TrackingScript"] = Configuration["TrackingScript"] != null && string.IsNullOrEmpty(Request.Cookies["Token"]) + ? Configuration["TrackingScript"].Replace("loggedon", "") + : Configuration["TrackingScript"]; + } + /// /// is logged on /// @@ -175,8 +227,9 @@ namespace GanjooRazor.Pages }); } - public LoginPartialEnabledPageModel(HttpClient httpClient) : base(httpClient) + public LoginPartialEnabledPageModel(HttpClient httpClient, IConfiguration configuration) : base(httpClient) { + Configuration = configuration; } } } diff --git a/GanjooRazor/Pages/Photos.cshtml.cs b/GanjooRazor/Pages/Photos.cshtml.cs index bb49c60e..ab5157cf 100644 --- a/GanjooRazor/Pages/Photos.cshtml.cs +++ b/GanjooRazor/Pages/Photos.cshtml.cs @@ -304,14 +304,10 @@ namespace GanjooRazor.Pages return new OkResult(); } - /// - /// configration file reader (appsettings.json) - /// - private readonly IConfiguration Configuration; - public PhotosModel(HttpClient httpClient, IConfiguration configuration) : base(httpClient) + public PhotosModel(HttpClient httpClient, IConfiguration configuration) : base(httpClient, configuration) { - Configuration = configuration; + } } } diff --git a/GanjooRazor/Pages/Quotes.cshtml.cs b/GanjooRazor/Pages/Quotes.cshtml.cs index aea204ac..1008c821 100644 --- a/GanjooRazor/Pages/Quotes.cshtml.cs +++ b/GanjooRazor/Pages/Quotes.cshtml.cs @@ -1,7 +1,7 @@ -using Microsoft.AspNetCore.Mvc; +using GanjooRazor.Utils; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Newtonsoft.Json.Linq; -using Newtonsoft.Json; using RMuseum.Models.Ganjoor.ViewModels; using System.Collections.Generic; using System.Net.Http; @@ -12,56 +12,52 @@ namespace GanjooRazor.Pages [IgnoreAntiforgeryToken(Order = 1001)] public class QuotesModel : LoginPartialEnabledPageModel { - private readonly IConfiguration Configuration; - public QuotesModel(HttpClient httpClient, IConfiguration configuration) : base(httpClient) + private readonly PoetCacheService _poetCache; + + public QuotesModel(HttpClient httpClient, IConfiguration configuration, PoetCacheService poetCache) : base(httpClient, configuration) { - Configuration = configuration; + _poetCache = poetCache; } + public string LastError { get; set; } public List Poets { get; set; } public GanjoorPoetViewModel Poet { get; set; } public List ClaimedQuotes { get; set; } public List Quotes { get; set; } - private async Task preparePoets() - { - var response = await _httpClient.GetAsync($"{APIRoot.Url}/api/ganjoor/poets"); - if (!response.IsSuccessStatusCode) - { - LastError = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); - return false; - } - Poets = JArray.Parse(await response.Content.ReadAsStringAsync()).ToObject>(); - - return true; - } public async Task OnGetAsync() { - if (bool.Parse(Configuration["MaintenanceMode"])) + var maintenanceResult = TryGetMaintenanceModeResult(); + if (maintenanceResult != null) { - return StatusCode(503); + return maintenanceResult; } - LoggedIn = !string.IsNullOrEmpty(Request.Cookies["Token"]); - ViewData["TrackingScript"] = Configuration["TrackingScript"] != null && string.IsNullOrEmpty(Request.Cookies["Token"]) ? Configuration["TrackingScript"].Replace("loggedon", "") : Configuration["TrackingScript"]; + InitializeCommonPageState(); - //todo: use html master layout or make it partial - if (false == (await preparePoets())) + var (poetsOk, poets, poetsError) = await _poetCache.GetPoetsAsync(AggressiveCacheEnabled); + if (!poetsOk) + { + LastError = poetsError; return Page(); + } + Poets = poets; if (!string.IsNullOrEmpty(Request.Query["p"])) { + // Fetched by URL slug rather than id, and returns the "complete" poet shape rather + // than the list shape - different enough from PoetCacheService's GetPoetAsync(id) + // that it stays a direct call here rather than being folded into that service. var response = await _httpClient.GetAsync($"{APIRoot.Url}/api/ganjoor/poet?url=/{Request.Query["p"]}"); if (!response.IsSuccessStatusCode) { - LastError = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + LastError = await ReadErrorMessageAsync(response); return Page(); } Poet = JObject.Parse(await response.Content.ReadAsStringAsync()).ToObject().Poet; Poet.ImageUrl = $"{APIRoot.InternetUrl}{Poet.ImageUrl}"; } - var url = $"{APIRoot.Url}/api/ganjoor/quoted?published=true"; if (Poet != null) { @@ -70,19 +66,17 @@ namespace GanjooRazor.Pages var responseQuotes = await _httpClient.GetAsync(url + "&claimed=false"); if (!responseQuotes.IsSuccessStatusCode) { - LastError = JsonConvert.DeserializeObject(await responseQuotes.Content.ReadAsStringAsync()); + LastError = await ReadErrorMessageAsync(responseQuotes); return Page(); } - Quotes = JArray.Parse(await responseQuotes.Content.ReadAsStringAsync()).ToObject>(); var responseClaimedQuotes = await _httpClient.GetAsync(url + "&claimed=true"); if (!responseClaimedQuotes.IsSuccessStatusCode) { - LastError = JsonConvert.DeserializeObject(await responseClaimedQuotes.Content.ReadAsStringAsync()); + LastError = await ReadErrorMessageAsync(responseClaimedQuotes); return Page(); } - ClaimedQuotes = JArray.Parse(await responseClaimedQuotes.Content.ReadAsStringAsync()).ToObject>(); ViewData["Title"] = Poet == null ? "نقل قول‌های شاعران" : $"نقل قول‌ها و شعرهای مرتبط {Poet.Nickname}"; diff --git a/GanjooRazor/Pages/Search.cshtml.cs b/GanjooRazor/Pages/Search.cshtml.cs index 8e469042..6c271b2f 100644 --- a/GanjooRazor/Pages/Search.cshtml.cs +++ b/GanjooRazor/Pages/Search.cshtml.cs @@ -28,10 +28,6 @@ namespace GanjooRazor.Pages /// private readonly IMemoryCache _memoryCache; - /// - /// configration file reader (appsettings.json) - /// - private readonly IConfiguration Configuration; /// @@ -40,29 +36,9 @@ namespace GanjooRazor.Pages /// /// /// - public SearchModel(HttpClient httpClient, IMemoryCache memoryCache, IConfiguration configuration) : base(httpClient) + public SearchModel(HttpClient httpClient, IMemoryCache memoryCache, IConfiguration configuration) : base(httpClient, configuration) { _memoryCache = memoryCache; - Configuration - = configuration; - } - - /// - /// aggressive cache - /// - public bool AggressiveCacheEnabled - { - get - { - try - { - return bool.Parse(Configuration["AggressiveCacheEnabled"]); - } - catch - { - return false; - } - } } public List Poets { get; set; } diff --git a/GanjooRazor/Pages/Simi.cshtml.cs b/GanjooRazor/Pages/Simi.cshtml.cs index 5da6379a..ee43819e 100644 --- a/GanjooRazor/Pages/Simi.cshtml.cs +++ b/GanjooRazor/Pages/Simi.cshtml.cs @@ -28,10 +28,6 @@ namespace GanjooRazor.Pages /// private readonly IMemoryCache _memoryCache; - /// - /// configration file reader (appsettings.json) - /// - private readonly IConfiguration Configuration; /// @@ -40,29 +36,9 @@ namespace GanjooRazor.Pages /// /// /// - public SimiModel(HttpClient httpClient, IMemoryCache memoryCache, IConfiguration configuration) : base(httpClient) + public SimiModel(HttpClient httpClient, IMemoryCache memoryCache, IConfiguration configuration) : base(httpClient, configuration) { _memoryCache = memoryCache; - Configuration - = configuration; - } - - /// - /// aggressive cache - /// - public bool AggressiveCacheEnabled - { - get - { - try - { - return bool.Parse(Configuration["AggressiveCacheEnabled"]); - } - catch - { - return false; - } - } } public List Poets { get; set; } diff --git a/GanjooRazor/Pages/Tagged.cshtml.cs b/GanjooRazor/Pages/Tagged.cshtml.cs index 24679ee7..6c479d59 100644 --- a/GanjooRazor/Pages/Tagged.cshtml.cs +++ b/GanjooRazor/Pages/Tagged.cshtml.cs @@ -25,11 +25,7 @@ namespace GanjooRazor.Pages /// private readonly IMemoryCache _memoryCache; - /// - /// configration file reader (appsettings.json) - /// - private readonly IConfiguration Configuration; - + /// /// constructor @@ -37,30 +33,11 @@ namespace GanjooRazor.Pages /// /// /// - public TaggedModel(HttpClient httpClient, IMemoryCache memoryCache, IConfiguration configuration) : base(httpClient) + public TaggedModel(HttpClient httpClient, IMemoryCache memoryCache, IConfiguration configuration) : base(httpClient, configuration) { _memoryCache = memoryCache; - Configuration - = configuration; } - /// - /// aggressive cache - /// - public bool AggressiveCacheEnabled - { - get - { - try - { - return bool.Parse(Configuration["AggressiveCacheEnabled"]); - } - catch - { - return false; - } - } - } public List Poets { get; set; } public int PoetId { get; set; } diff --git a/GanjooRazor/Startup.cs b/GanjooRazor/Startup.cs index e09ac07d..597c4948 100644 --- a/GanjooRazor/Startup.cs +++ b/GanjooRazor/Startup.cs @@ -24,6 +24,8 @@ namespace GanjooRazor services.AddMemoryCache(); + services.AddScoped(); + services.AddSingleton( HtmlEncoder.Create(allowedRanges: new[] { UnicodeRanges.BasicLatin, UnicodeRanges.Arabic })); diff --git a/GanjooRazor/Utils/PoetCacheService.cs b/GanjooRazor/Utils/PoetCacheService.cs new file mode 100644 index 00000000..fd935808 --- /dev/null +++ b/GanjooRazor/Utils/PoetCacheService.cs @@ -0,0 +1,85 @@ +using Microsoft.Extensions.Caching.Memory; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using RMuseum.Models.Ganjoor.ViewModels; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; + +namespace GanjooRazor.Utils +{ + /// + /// Fetches the poet list and individual poet details from the Ganjoor API, optionally caching + /// the results in-memory. This logic (fetch-or-return-cached, deserialize, cache if enabled) was + /// previously copy-pasted as three near-identical private methods (a "get all poets" fetcher, a + /// "get one poet by id and store on Model.Poet" fetcher, and an "get one poet by id and return as + /// JSON" AJAX handler) in IndexModel, ContribsModel, HashiehaModel, SearchModel, and SimiModel. + /// + /// Registered as a scoped service in Startup.cs; inject via constructor like any other service. + /// + public class PoetCacheService + { + private readonly HttpClient _httpClient; + private readonly IMemoryCache _memoryCache; + + public PoetCacheService(HttpClient httpClient, IMemoryCache memoryCache) + { + _httpClient = httpClient; + _memoryCache = memoryCache; + } + + /// + /// Gets the full poet list. Returns (true, poets, null) on success, or (false, null, error) + /// on failure - callers decide what to do with the error (set LastError and return Page(), + /// return BadRequest(error), etc.) since that varies per caller. + /// + public async Task<(bool success, List poets, string error)> GetPoetsAsync(bool cacheResult) + { + const string cacheKey = "/api/ganjoor/poets"; + if (_memoryCache.TryGetValue(cacheKey, out List poets)) + { + return (true, poets, null); + } + + var response = await _httpClient.GetAsync($"{APIRoot.Url}/api/ganjoor/poets"); + if (!response.IsSuccessStatusCode) + { + return (false, null, JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync())); + } + + poets = JArray.Parse(await response.Content.ReadAsStringAsync()).ToObject>(); + if (cacheResult) + { + _memoryCache.Set(cacheKey, poets, TimeSpan.FromHours(1)); + } + return (true, poets, null); + } + + /// + /// Gets a single poet's full details by id. Returns (true, poet, null) on success, or + /// (false, null, error) on failure. + /// + public async Task<(bool success, GanjoorPoetCompleteViewModel poet, string error)> GetPoetAsync(int poetId, bool cacheResult) + { + var cacheKey = $"/api/ganjoor/poet/{poetId}"; + if (_memoryCache.TryGetValue(cacheKey, out GanjoorPoetCompleteViewModel poet)) + { + return (true, poet, null); + } + + var response = await _httpClient.GetAsync($"{APIRoot.Url}/api/ganjoor/poet/{poetId}"); + if (!response.IsSuccessStatusCode) + { + return (false, null, JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync())); + } + + poet = JObject.Parse(await response.Content.ReadAsStringAsync()).ToObject(); + if (cacheResult) + { + _memoryCache.Set(cacheKey, poet, TimeSpan.FromHours(1)); + } + return (true, poet, null); + } + } +}