refactoring

This commit is contained in:
Hamid Reza Mohammadi 2026-07-15 08:43:40 +03:30
parent 86837dc6c8
commit 64ab66aacd
14 changed files with 303 additions and 509 deletions

View File

@ -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<IActionResult> 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<string>(await response.Content.ReadAsStringAsync());
LastError = await ReadErrorMessageAsync(response);
return Page();
}
Books = JArray.Parse(await response.Content.ReadAsStringAsync()).ToObject<List<GanjoorCatViewModel>>();
return Page();
}
private async Task<bool> preparePoets()
{
var cacheKey = $"/api/ganjoor/poets";
if (!_memoryCache.TryGetValue(cacheKey, out List<GanjoorPoetViewModel> poets))
{
try
{
var response = await _httpClient.GetAsync($"{APIRoot.Url}/api/ganjoor/poets");
if (!response.IsSuccessStatusCode)
{
LastError = JsonConvert.DeserializeObject<string>(await response.Content.ReadAsStringAsync());
return false;
}
poets = JArray.Parse(await response.Content.ReadAsStringAsync()).ToObject<List<GanjoorPoetViewModel>>();
if (AggressiveCacheEnabled)
{
_memoryCache.Set(cacheKey, poets, TimeSpan.FromHours(1));
}
}
catch
{
LastError = "خطا در دسترسی به وب سرویس گنجور";
return false;
}
}
Poets = poets;
return true;
}
/// <summary>
/// configration file reader (appsettings.json)
/// </summary>
protected readonly IConfiguration Configuration;
/// <summary>
/// books
/// </summary>
public List<GanjoorCatViewModel> Books { get; set; }
public List<GanjoorCatViewModel> Books { get; set; }
/// <summary>
/// Poets
@ -91,41 +58,13 @@ namespace GanjooRazor.Pages
/// </summary>
public string LastError { get; set; }
/// <summary>
/// memory cache
/// </summary>
private readonly IMemoryCache _memoryCache;
// <summary>
/// aggressive cache
/// </summary>
public bool AggressiveCacheEnabled
{
get
{
try
{
return bool.Parse(Configuration["AggressiveCacheEnabled"]);
}
catch
{
return false;
}
}
}
/// <summary>
/// constructor
/// </summary>
/// <param name="configuration"></param>
/// <param name="httpClient"></param>
/// <param name="memoryCache"></param>
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;
}
}
}

View File

@ -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<GanjoorPoetViewModel> Poets { get; set; }
public int PoetId { get; set; }
public GanjoorPoetCompleteViewModel Poet { get; set; }
public string LastError { get; set; }
/// <summary>
/// aggressive cache
/// </summary>
public bool AggressiveCacheEnabled
public async Task<IActionResult> OnGetAsync()
{
get
var maintenanceResult = TryGetMaintenanceModeResult();
if (maintenanceResult != null)
{
try
{
return bool.Parse(Configuration["AggressiveCacheEnabled"]);
}
catch
{
return false;
}
}
}
private async Task<bool> preparePoets()
{
var cacheKey = $"/api/ganjoor/poets";
if (!_memoryCache.TryGetValue(cacheKey, out List<GanjoorPoetViewModel> poets))
{
var response = await _httpClient.GetAsync($"{APIRoot.Url}/api/ganjoor/poets");
if (!response.IsSuccessStatusCode)
{
LastError = JsonConvert.DeserializeObject<string>(await response.Content.ReadAsStringAsync());
return false;
}
poets = JArray.Parse(await response.Content.ReadAsStringAsync()).ToObject<List<GanjoorPoetViewModel>>();
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<bool> 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<string>(await poetResponse.Content.ReadAsStringAsync());
return false;
}
poet = JObject.Parse(await poetResponse.Content.ReadAsStringAsync()).ToObject<GanjoorPoetCompleteViewModel>();
if (AggressiveCacheEnabled)
{
_memoryCache.Set(cacheKey, poet, TimeSpan.FromHours(1));
LastError = poetError;
return Page();
}
Poet = poet;
}
Poet = poet;
return true;
return Page();
}
public async Task<IActionResult> 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<string>(await poetResponse.Content.ReadAsStringAsync()));
}
poet = JObject.Parse(await poetResponse.Content.ReadAsStringAsync()).ToObject<GanjoorPoetCompleteViewModel>();
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<IActionResult> 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<ActionResult> OnGetGroupedByDateAsync(string dataType)
public async Task<IActionResult> 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<string>(await responseDays.Content.ReadAsStringAsync()));
return new BadRequestObjectResult(await ReadErrorMessageAsync(responseDays));
}
var days = JArray.Parse(await responseDays.Content.ReadAsStringAsync()).ToObject<List<GroupedByDateViewModel>>();
@ -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<string>(await responseUsers.Content.ReadAsStringAsync()));
return new BadRequestObjectResult(await ReadErrorMessageAsync(responseUsers));
}
users = JArray.Parse(await responseUsers.Content.ReadAsStringAsync()).ToObject<List<GroupedByUserViewModel>>();
usersPagination = JsonConvert.DeserializeObject<PaginationMetadata>(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<string>(await response.Content.ReadAsStringAsync()));
return new BadRequestObjectResult(await ReadErrorMessageAsync(response));
}
SummedUpViewModel summary = JsonConvert.DeserializeObject<SummedUpViewModel>(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<PaginationMetadata>(responseDays.Headers.GetValues("paging-headers").Single()),
Users = users == null ? null :users.ToArray(),
UsersPagination = usersPagination,
Summary = summary
}
}
};
DataType = dataType,
Days = days.ToArray(),
DaysPagination = JsonConvert.DeserializeObject<PaginationMetadata>(responseDays.Headers.GetValues("paging-headers").Single()),
Users = users?.ToArray(),
UsersPagination = usersPagination,
Summary = summary
});
}
public async Task<ActionResult> OnGetGroupedByUsersAsync(string dataType, int pageNumber)
public async Task<IActionResult> 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<string>(await responseUsers.Content.ReadAsStringAsync()));
return new BadRequestObjectResult(await ReadErrorMessageAsync(responseUsers));
}
var users = JArray.Parse(await responseUsers.Content.ReadAsStringAsync()).ToObject<List<GroupedByUserViewModel>>();
var usersPagination = JsonConvert.DeserializeObject<PaginationMetadata>(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<ActionResult> OnGetGroupedByDayAsync(string dataType, int pageNumber)
public async Task<IActionResult> 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<string>(await responseDays.Content.ReadAsStringAsync()));
return new BadRequestObjectResult(await ReadErrorMessageAsync(responseDays));
}
var days = JArray.Parse(await responseDays.Content.ReadAsStringAsync()).ToObject<List<GroupedByDateViewModel>>();
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<PaginationMetadata>(responseDays.Headers.GetValues("paging-headers").Single()),
}
}
};
DataType = dataType,
Days = days.ToArray(),
DaysPagination = JsonConvert.DeserializeObject<PaginationMetadata>(responseDays.Headers.GetValues("paging-headers").Single()),
});
}
/// <summary>
/// memory cache
/// </summary>
private readonly IMemoryCache _memoryCache;
/// <summary>
/// configration file reader (appsettings.json)
/// </summary>
private readonly IConfiguration Configuration;
/// <summary>
/// constructor
/// </summary>
/// <param name="httpClient"></param>
/// <param name="memoryCache"></param>
/// <param name="configuration"></param>
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;
}
}
}

View File

@ -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<GanjoorPoetViewModel> Poets { get; set; }
@ -23,43 +24,32 @@ namespace GanjooRazor.Pages
public FAQItem Question { get; set; }
private async Task<bool> preparePoets()
{
var response = await _httpClient.GetAsync($"{APIRoot.Url}/api/ganjoor/poets");
if (!response.IsSuccessStatusCode)
{
LastError = JsonConvert.DeserializeObject<string>(await response.Content.ReadAsStringAsync());
return false;
}
Poets = JArray.Parse(await response.Content.ReadAsStringAsync()).ToObject<List<GanjoorPoetViewModel>>();
return true;
}
public async Task<IActionResult> 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<string>(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<string>(await response.Content.ReadAsStringAsync());
LastError = await ReadErrorMessageAsync(response);
return Page();
}
PinnedItemsCategories = JArray.Parse(await response.Content.ReadAsStringAsync()).ToObject<List<FAQCategory>>();
@ -82,21 +72,12 @@ namespace GanjooRazor.Pages
return Page();
}
/// <summary>
/// configration file reader (appsettings.json)
/// </summary>
private readonly IConfiguration Configuration;
/// <summary>
/// constructor
/// </summary>
/// <param name="httpClient"></param>
/// <param name="configuration"></param>
public FAQModel(HttpClient httpClient, IConfiguration configuration) : base(httpClient)
public FAQModel(HttpClient httpClient, IConfiguration configuration, PoetCacheService poetCache) : base(httpClient, configuration)
{
Configuration
= configuration;
_poetCache = poetCache;
}
}
}

View File

@ -28,11 +28,6 @@ namespace GanjooRazor.Pages
/// </summary>
private readonly IMemoryCache _memoryCache;
/// <summary>
/// configration file reader (appsettings.json)
/// </summary>
private readonly IConfiguration Configuration;
/// <summary>
/// constructor
@ -40,29 +35,9 @@ namespace GanjooRazor.Pages
/// <param name="httpClient"></param>
/// <param name="memoryCache"></param>
/// <param name="configuration"></param>
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;
}
/// <summary>
/// aggressive cache
/// </summary>
public bool AggressiveCacheEnabled
{
get
{
try
{
return bool.Parse(Configuration["AggressiveCacheEnabled"]);
}
catch
{
return false;
}
}
}
public List<GanjoorPoetViewModel> Poets { get; set; }

View File

@ -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
};
/// <summary>
/// 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)
/// </summary>
private readonly IConfiguration Configuration;
/// <summary>
/// memory cache
/// </summary>
private readonly IMemoryCache _memoryCache;
/// <summary>
/// aggressive cache
/// </summary>
public bool AggressiveCacheEnabled => GetConfigFlag("AggressiveCacheEnabled");
private readonly PoetCacheService _poetCache;
public bool OfflineMode => GetConfigFlag("OfflineMode");
@ -68,14 +58,13 @@ namespace GanjooRazor.Pages
/// </summary>
/// <param name="configuration"></param>
/// <param name="httpClient"></param>
/// <param name="memoryCache"></param>
/// <param name="poetCache"></param>
public IndexModel(IConfiguration configuration,
HttpClient httpClient,
IMemoryCache memoryCache
) : base(httpClient)
PoetCacheService poetCache
) : base(httpClient, configuration)
{
Configuration = configuration;
_memoryCache = memoryCache;
_poetCache = poetCache;
}
/// <summary>
@ -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.
/// <summary>
/// Reads a boolean feature flag from configuration, defaulting to <paramref name="defaultValue"/>
/// if the key is missing or not a valid bool, instead of each flag having its own try/catch.
/// </summary>
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.
/// <summary>
/// If the response failed, stores the API's error message in <see cref="LastError"/> and
@ -446,30 +426,12 @@ namespace GanjooRazor.Pages
private async Task<bool> preparePoets()
{
const string cacheKey = "/api/ganjoor/poets";
if (!_memoryCache.TryGetValue(cacheKey, out List<GanjoorPoetViewModel> 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<List<GanjoorPoetViewModel>>();
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<IActionResult> 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<GanjoorPoetCompleteViewModel>();
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
/// <returns></returns>
public async Task<IActionResult> 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))

View File

@ -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; }

View File

@ -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
{
/// <summary>
/// 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).
/// </summary>
protected readonly IConfiguration Configuration;
/// <summary>
/// Reads a boolean feature flag from configuration, defaulting to <paramref name="defaultValue"/>
/// 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).
/// </summary>
protected bool GetConfigFlag(string key, bool defaultValue = false)
{
return bool.TryParse(Configuration[key], out var value) ? value : defaultValue;
}
/// <summary>
/// aggressive cache flag. Every page model that had this property defined it identically -
/// same try/catch-wrapped bool.Parse now handled once by GetConfigFlag.
/// </summary>
protected bool AggressiveCacheEnabled => GetConfigFlag("AggressiveCacheEnabled");
/// <summary>
/// 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).
/// </summary>
protected IActionResult TryGetMaintenanceModeResult()
{
return GetConfigFlag("MaintenanceMode") ? StatusCode(503) : null;
}
/// <summary>
/// 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.
/// </summary>
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"];
}
/// <summary>
/// is logged on
/// </summary>
@ -175,8 +227,9 @@ namespace GanjooRazor.Pages
});
}
public LoginPartialEnabledPageModel(HttpClient httpClient) : base(httpClient)
public LoginPartialEnabledPageModel(HttpClient httpClient, IConfiguration configuration) : base(httpClient)
{
Configuration = configuration;
}
}
}

View File

@ -304,14 +304,10 @@ namespace GanjooRazor.Pages
return new OkResult();
}
/// <summary>
/// configration file reader (appsettings.json)
/// </summary>
private readonly IConfiguration Configuration;
public PhotosModel(HttpClient httpClient, IConfiguration configuration) : base(httpClient)
public PhotosModel(HttpClient httpClient, IConfiguration configuration) : base(httpClient, configuration)
{
Configuration = configuration;
}
}
}

View File

@ -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<GanjoorPoetViewModel> Poets { get; set; }
public GanjoorPoetViewModel Poet { get; set; }
public List<GanjoorQuotedPoemViewModel> ClaimedQuotes { get; set; }
public List<GanjoorQuotedPoemViewModel> Quotes { get; set; }
private async Task<bool> preparePoets()
{
var response = await _httpClient.GetAsync($"{APIRoot.Url}/api/ganjoor/poets");
if (!response.IsSuccessStatusCode)
{
LastError = JsonConvert.DeserializeObject<string>(await response.Content.ReadAsStringAsync());
return false;
}
Poets = JArray.Parse(await response.Content.ReadAsStringAsync()).ToObject<List<GanjoorPoetViewModel>>();
return true;
}
public async Task<IActionResult> 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<string>(await response.Content.ReadAsStringAsync());
LastError = await ReadErrorMessageAsync(response);
return Page();
}
Poet = JObject.Parse(await response.Content.ReadAsStringAsync()).ToObject<GanjoorPoetCompleteViewModel>().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<string>(await responseQuotes.Content.ReadAsStringAsync());
LastError = await ReadErrorMessageAsync(responseQuotes);
return Page();
}
Quotes = JArray.Parse(await responseQuotes.Content.ReadAsStringAsync()).ToObject<List<GanjoorQuotedPoemViewModel>>();
var responseClaimedQuotes = await _httpClient.GetAsync(url + "&claimed=true");
if (!responseClaimedQuotes.IsSuccessStatusCode)
{
LastError = JsonConvert.DeserializeObject<string>(await responseClaimedQuotes.Content.ReadAsStringAsync());
LastError = await ReadErrorMessageAsync(responseClaimedQuotes);
return Page();
}
ClaimedQuotes = JArray.Parse(await responseClaimedQuotes.Content.ReadAsStringAsync()).ToObject<List<GanjoorQuotedPoemViewModel>>();
ViewData["Title"] = Poet == null ? "نقل قول‌های شاعران" : $"نقل قول‌ها و شعرهای مرتبط {Poet.Nickname}";

View File

@ -28,10 +28,6 @@ namespace GanjooRazor.Pages
/// </summary>
private readonly IMemoryCache _memoryCache;
/// <summary>
/// configration file reader (appsettings.json)
/// </summary>
private readonly IConfiguration Configuration;
/// <summary>
@ -40,29 +36,9 @@ namespace GanjooRazor.Pages
/// <param name="httpClient"></param>
/// <param name="memoryCache"></param>
/// <param name="configuration"></param>
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;
}
/// <summary>
/// aggressive cache
/// </summary>
public bool AggressiveCacheEnabled
{
get
{
try
{
return bool.Parse(Configuration["AggressiveCacheEnabled"]);
}
catch
{
return false;
}
}
}
public List<GanjoorPoetViewModel> Poets { get; set; }

View File

@ -28,10 +28,6 @@ namespace GanjooRazor.Pages
/// </summary>
private readonly IMemoryCache _memoryCache;
/// <summary>
/// configration file reader (appsettings.json)
/// </summary>
private readonly IConfiguration Configuration;
/// <summary>
@ -40,29 +36,9 @@ namespace GanjooRazor.Pages
/// <param name="httpClient"></param>
/// <param name="memoryCache"></param>
/// <param name="configuration"></param>
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;
}
/// <summary>
/// aggressive cache
/// </summary>
public bool AggressiveCacheEnabled
{
get
{
try
{
return bool.Parse(Configuration["AggressiveCacheEnabled"]);
}
catch
{
return false;
}
}
}
public List<GanjoorPoetViewModel> Poets { get; set; }

View File

@ -25,11 +25,7 @@ namespace GanjooRazor.Pages
/// </summary>
private readonly IMemoryCache _memoryCache;
/// <summary>
/// configration file reader (appsettings.json)
/// </summary>
private readonly IConfiguration Configuration;
/// <summary>
/// constructor
@ -37,30 +33,11 @@ namespace GanjooRazor.Pages
/// <param name="httpClient"></param>
/// <param name="memoryCache"></param>
/// <param name="configuration"></param>
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;
}
/// <summary>
/// aggressive cache
/// </summary>
public bool AggressiveCacheEnabled
{
get
{
try
{
return bool.Parse(Configuration["AggressiveCacheEnabled"]);
}
catch
{
return false;
}
}
}
public List<GanjoorPoetViewModel> Poets { get; set; }
public int PoetId { get; set; }

View File

@ -24,6 +24,8 @@ namespace GanjooRazor
services.AddMemoryCache();
services.AddScoped<GanjooRazor.Utils.PoetCacheService>();
services.AddSingleton(
HtmlEncoder.Create(allowedRanges: new[] { UnicodeRanges.BasicLatin,
UnicodeRanges.Arabic }));

View File

@ -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
{
/// <summary>
/// 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.
/// </summary>
public class PoetCacheService
{
private readonly HttpClient _httpClient;
private readonly IMemoryCache _memoryCache;
public PoetCacheService(HttpClient httpClient, IMemoryCache memoryCache)
{
_httpClient = httpClient;
_memoryCache = memoryCache;
}
/// <summary>
/// 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.
/// </summary>
public async Task<(bool success, List<GanjoorPoetViewModel> poets, string error)> GetPoetsAsync(bool cacheResult)
{
const string cacheKey = "/api/ganjoor/poets";
if (_memoryCache.TryGetValue(cacheKey, out List<GanjoorPoetViewModel> poets))
{
return (true, poets, null);
}
var response = await _httpClient.GetAsync($"{APIRoot.Url}/api/ganjoor/poets");
if (!response.IsSuccessStatusCode)
{
return (false, null, JsonConvert.DeserializeObject<string>(await response.Content.ReadAsStringAsync()));
}
poets = JArray.Parse(await response.Content.ReadAsStringAsync()).ToObject<List<GanjoorPoetViewModel>>();
if (cacheResult)
{
_memoryCache.Set(cacheKey, poets, TimeSpan.FromHours(1));
}
return (true, poets, null);
}
/// <summary>
/// Gets a single poet's full details by id. Returns (true, poet, null) on success, or
/// (false, null, error) on failure.
/// </summary>
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<string>(await response.Content.ReadAsStringAsync()));
}
poet = JObject.Parse(await response.Content.ReadAsStringAsync()).ToObject<GanjoorPoetCompleteViewModel>();
if (cacheResult)
{
_memoryCache.Set(cacheKey, poet, TimeSpan.FromHours(1));
}
return (true, poet, null);
}
}
}