book list api

This commit is contained in:
Hamid Reza Mohammadi 2026-10-02 13:40:21 +03:30
parent 2651e603c4
commit b8c45ed93c
5 changed files with 219 additions and 0 deletions

View File

@ -59,6 +59,50 @@ namespace RMuseum.Controllers
return Ok(poets); return Ok(poets);
} }
/// <summary>
/// get list of books (GanjoorCat entries whose CatType is Book, sorted alphabetically by name),
/// optionally filtered by (part of) name and/or poet id. Not to be confused with
/// GetBooksAsync()/"books" above, which lists GanjoorCat entries by their (separate,
/// legacy) BookName field for cover-image generation.
/// </summary>
/// <param name="name">optional, only books whose name contains this</param>
/// <param name="poetId">optional, only books belonging to this poet</param>
/// <returns></returns>
[HttpGet]
[Route("book-catalog")]
[AllowAnonymous]
[ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(GanjoorBookViewModel[]))]
[ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))]
public async Task<IActionResult> GetBookCatalog(string name = null, int? poetId = null)
{
// only the common, unfiltered call (the one the home page's book search will use to load
// the full list once and filter client-side) is worth caching - a filtered call is cheap
// on a table this small (well under 200 rows) and caching every name/poetId combination
// would just grow the cache for no benefit.
if (string.IsNullOrEmpty(name) && poetId == null)
{
var cacheKey = $"ganjoor/book-catalog";
if (!_memoryCache.TryGetValue(cacheKey, out GanjoorBookViewModel[] cachedBooks))
{
RServiceResult<GanjoorBookViewModel[]> cachedRes =
await _ganjoorService.GetBookCatalogAsync();
if (!string.IsNullOrEmpty(cachedRes.ExceptionString))
return BadRequest(cachedRes.ExceptionString);
cachedBooks = cachedRes.Result;
if (AggressiveCacheEnabled)
_memoryCache.Set(cacheKey, cachedBooks, TimeSpan.FromHours(1));
}
return Ok(cachedBooks);
}
RServiceResult<GanjoorBookViewModel[]> res =
await _ganjoorService.GetBookCatalogAsync(name, poetId);
if (!string.IsNullOrEmpty(res.ExceptionString))
return BadRequest(res.ExceptionString);
return Ok(res.Result);
}
/// <summary> /// <summary>
/// gets list of poets grouped by centuries (first one is the pinned ones) /// gets list of poets grouped by centuries (first one is the pinned ones)
/// </summary> /// </summary>

View File

@ -0,0 +1,39 @@
namespace RMuseum.Models.Ganjoor.ViewModels
{
/// <summary>
/// A GanjoorCat whose CatType is Book - a minimal view model for listing/searching books
/// (e.g. the home page's book search box), not the full category details
/// </summary>
public class GanjoorBookViewModel
{
/// <summary>
/// id (GanjoorCat.Id)
/// </summary>
public int Id { get; set; }
/// <summary>
/// book name (GanjoorCat.Title)
/// </summary>
public string Name { get; set; }
/// <summary>
/// full url, sample: /hafez/ghazal
/// </summary>
public string FullUrl { get; set; }
/// <summary>
/// poet id (GanjoorCat.PoetId)
/// </summary>
public int PoetId { get; set; }
/// <summary>
/// poet short name (GanjoorPoet.Nickname)
/// </summary>
public string PoetName { get; set; }
public override string ToString()
{
return Name;
}
}
}

View File

@ -1096,6 +1096,17 @@
</summary> </summary>
<returns></returns> <returns></returns>
</member> </member>
<member name="M:RMuseum.Controllers.GanjoorController.GetBookCatalog(System.String,System.Nullable{System.Int32})">
<summary>
get list of books (GanjoorCat entries whose CatType is Book, sorted alphabetically by name),
optionally filtered by (part of) name and/or poet id. Not to be confused with
GetBooksAsync()/"books" above, which lists GanjoorCat entries by their (separate,
legacy) BookName field for cover-image generation.
</summary>
<param name="name">optional, only books whose name contains this</param>
<param name="poetId">optional, only books belonging to this poet</param>
<returns></returns>
</member>
<member name="M:RMuseum.Controllers.GanjoorController.GetCenturiesAsync"> <member name="M:RMuseum.Controllers.GanjoorController.GetCenturiesAsync">
<summary> <summary>
gets list of poets grouped by centuries (first one is the pinned ones) gets list of poets grouped by centuries (first one is the pinned ones)
@ -12578,6 +12589,37 @@
simulate naming simulate naming
</summary> </summary>
</member> </member>
<member name="T:RMuseum.Models.Ganjoor.ViewModels.GanjoorBookViewModel">
<summary>
A GanjoorCat whose CatType is Book - a minimal view model for listing/searching books
(e.g. the home page's book search box), not the full category details
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.ViewModels.GanjoorBookViewModel.Id">
<summary>
id (GanjoorCat.Id)
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.ViewModels.GanjoorBookViewModel.Name">
<summary>
book name (GanjoorCat.Title)
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.ViewModels.GanjoorBookViewModel.FullUrl">
<summary>
full url, sample: /hafez/ghazal
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.ViewModels.GanjoorBookViewModel.PoetId">
<summary>
poet id (GanjoorCat.PoetId)
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.ViewModels.GanjoorBookViewModel.PoetName">
<summary>
poet short name (GanjoorPoet.Nickname)
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.ViewModels.GanjoorCatCorrectionViewModel.Id"> <member name="P:RMuseum.Models.Ganjoor.ViewModels.GanjoorCatCorrectionViewModel.Id">
<summary> <summary>
correct id correct id
@ -18549,6 +18591,16 @@
<param name="includeBio"></param> <param name="includeBio"></param>
<returns></returns> <returns></returns>
</member> </member>
<member name="M:RMuseum.Services.IGanjoorService.GetBookCatalogAsync(System.String,System.Nullable{System.Int32})">
<summary>
Get list of books (GanjoorCat entries whose CatType is Book), sorted alphabetically by name.
Not to be confused with GetBooksAsync() below, which lists GanjoorCat entries by their
(separate, legacy) BookName field for cover-image generation.
</summary>
<param name="name">optional, only books whose name contains this (case-insensitive)</param>
<param name="poetId">optional, only books belonging to this poet</param>
<returns></returns>
</member>
<member name="M:RMuseum.Services.IGanjoorService.GetPoetById(System.Int32,System.Boolean)"> <member name="M:RMuseum.Services.IGanjoorService.GetPoetById(System.Int32,System.Boolean)">
<summary> <summary>
get poet by id get poet by id
@ -22286,6 +22338,9 @@
<summary> <summary>
IGanjoorService implementation IGanjoorService implementation
</summary> </summary>
<summary>
IGanjoorService implementation
</summary>
</member> </member>
<member name="M:RMuseum.Services.Implementation.GanjoorService.SwitchCoupletBookmark(System.Guid,System.Int32,System.Int32)"> <member name="M:RMuseum.Services.Implementation.GanjoorService.SwitchCoupletBookmark(System.Guid,System.Int32,System.Int32)">
<summary> <summary>
@ -22366,6 +22421,16 @@
<param name="q"></param> <param name="q"></param>
<returns></returns> <returns></returns>
</member> </member>
<member name="M:RMuseum.Services.Implementation.GanjoorService.GetBookCatalogAsync(System.String,System.Nullable{System.Int32})">
<summary>
Get list of books (GanjoorCat entries whose CatType is Book), sorted alphabetically by name.
Not to be confused with GetBooksAsync() elsewhere in this class, which lists GanjoorCat
entries by their (separate, legacy) BookName field for cover-image generation.
</summary>
<param name="name">optional, only books whose name contains this (case-insensitive)</param>
<param name="poetId">optional, only books belonging to this poet</param>
<returns></returns>
</member>
<member name="M:RMuseum.Services.Implementation.GanjoorService.GetCategoryWordCountsAsync(System.Int32,System.String,RSecurityBackend.Models.Generic.PagingParameterModel)"> <member name="M:RMuseum.Services.Implementation.GanjoorService.GetCategoryWordCountsAsync(System.Int32,System.String,RSecurityBackend.Models.Generic.PagingParameterModel)">
<summary> <summary>
category word counts category word counts

View File

@ -22,6 +22,16 @@ namespace RMuseum.Services
/// <returns></returns> /// <returns></returns>
Task<RServiceResult<GanjoorPoetViewModel[]>> GetPoets(bool published, bool includeBio = true); Task<RServiceResult<GanjoorPoetViewModel[]>> GetPoets(bool published, bool includeBio = true);
/// <summary>
/// Get list of books (GanjoorCat entries whose CatType is Book), sorted alphabetically by name.
/// Not to be confused with GetBooksAsync() below, which lists GanjoorCat entries by their
/// (separate, legacy) BookName field for cover-image generation.
/// </summary>
/// <param name="name">optional, only books whose name contains this (case-insensitive)</param>
/// <param name="poetId">optional, only books belonging to this poet</param>
/// <returns></returns>
Task<RServiceResult<GanjoorBookViewModel[]>> GetBookCatalogAsync(string name = null, int? poetId = null);
/// <summary> /// <summary>
/// get poet by id /// get poet by id
/// </summary> /// </summary>

View File

@ -0,0 +1,61 @@
using Microsoft.EntityFrameworkCore;
using RMuseum.Models.Ganjoor;
using RMuseum.Models.Ganjoor.ViewModels;
using RSecurityBackend.Models.Generic;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
namespace RMuseum.Services.Implementation
{
/// <summary>
/// IGanjoorService implementation
/// </summary>
public partial class GanjoorService : IGanjoorService
{
/// <summary>
/// Get list of books (GanjoorCat entries whose CatType is Book), sorted alphabetically by name.
/// Not to be confused with GetBooksAsync() elsewhere in this class, which lists GanjoorCat
/// entries by their (separate, legacy) BookName field for cover-image generation.
/// </summary>
/// <param name="name">optional, only books whose name contains this (case-insensitive)</param>
/// <param name="poetId">optional, only books belonging to this poet</param>
/// <returns></returns>
public async Task<RServiceResult<GanjoorBookViewModel[]>> GetBookCatalogAsync(string name = null, int? poetId = null)
{
var query = _context.GanjoorCategories.AsNoTracking()
.Where(c => c.CatType == GanjoorCatType.Book);
if (!string.IsNullOrWhiteSpace(name))
{
query = query.Where(c => c.Title.Contains(name));
}
if (poetId != null)
{
query = query.Where(c => c.PoetId == poetId.Value);
}
List<GanjoorBookViewModel> books =
await query
.Select(c => new GanjoorBookViewModel()
{
Id = c.Id,
Name = c.Title,
FullUrl = c.FullUrl,
PoetId = c.PoetId,
PoetName = c.Poet.Nickname,
})
.ToListAsync();
// the fa-IR comparer can't be translated to SQL, so sorting happens here in memory -
// the same approach GetPoets() above uses, fine for a list this small (< 200 rows)
StringComparer fa = StringComparer.Create(new CultureInfo("fa-IR"), true);
books.Sort((a, b) => fa.Compare(a.Name, b.Name));
return new RServiceResult<GanjoorBookViewModel[]>(books.ToArray());
}
}
}