#495 rating comments

This commit is contained in:
Hamid Reza Mohammadi 2026-08-04 15:47:05 +03:30
parent 430370a6b9
commit ca2f708add
6 changed files with 241 additions and 30 deletions

View File

@ -835,6 +835,7 @@ namespace RMuseum.Controllers
/// </summary> /// </summary>
/// <param name="url"></param> /// <param name="url"></param>
/// <param name="catPoems"></param> /// <param name="catPoems"></param>
/// <param name="commentsSortByRanking">true: order comments by rating then date, false: order by date. Only applies to poem pages.</param>
/// <returns></returns> /// <returns></returns>
[HttpGet] [HttpGet]
[Route("page")] [Route("page")]
@ -842,10 +843,10 @@ namespace RMuseum.Controllers
[ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(GanjoorPageCompleteViewModel))] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(GanjoorPageCompleteViewModel))]
[ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))]
[ProducesResponseType((int)HttpStatusCode.NotFound)] [ProducesResponseType((int)HttpStatusCode.NotFound)]
public async Task<IActionResult> GetPageByUrl(string url, bool catPoems = false) public async Task<IActionResult> GetPageByUrl(string url, bool catPoems = false, bool commentsSortByRanking = true)
{ {
RServiceResult<GanjoorPageCompleteViewModel> res = RServiceResult<GanjoorPageCompleteViewModel> res =
await _ganjoorService.GetPageByUrl(url, catPoems); await _ganjoorService.GetPageByUrl(url, catPoems, commentsSortByRanking);
if (!string.IsNullOrEmpty(res.ExceptionString)) if (!string.IsNullOrEmpty(res.ExceptionString))
return BadRequest(res.ExceptionString); return BadRequest(res.ExceptionString);
if (res.Result == null) if (res.Result == null)
@ -1225,18 +1226,39 @@ namespace RMuseum.Controllers
[HttpPost] [HttpPost]
[Route("poem/comment/{id}/rate")] [Route("poem/comment/{id}/rate")]
[Authorize] [Authorize]
[ProducesResponseType((int)HttpStatusCode.OK)] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(GanjoorCommentRatingResultViewModel))]
[ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))]
public async Task<IActionResult> RateComment(int id, short value) public async Task<IActionResult> RateComment(int id, short value)
{ {
Guid userId = new Guid(User.Claims.First(c => c.Type == "UserId").Value); Guid userId = new Guid(User.Claims.First(c => c.Type == "UserId").Value);
RServiceResult<bool> res = RServiceResult<GanjoorCommentRatingResultViewModel> res =
await _ganjoorService.RateCommentAsync(userId, id, value); await _ganjoorService.RateCommentAsync(userId, id, value);
if (!string.IsNullOrEmpty(res.ExceptionString)) if (!string.IsNullOrEmpty(res.ExceptionString))
return BadRequest(res.ExceptionString); return BadRequest(res.ExceptionString);
return Ok(res.Result); return Ok(res.Result);
} }
/// <summary>
/// get the logged on user's own rating values for a poem's comments
/// (meant to be merged client-side into an already fetched, anonymously cacheable comment list)
/// </summary>
/// <param name="id">poem id</param>
/// <returns></returns>
[HttpGet]
[Route("poem/{id}/comments/myratings")]
[Authorize]
[ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(GanjoorCommentUserRatingViewModel[]))]
[ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))]
public async Task<IActionResult> GetUserCommentRatings(int id)
{
var loggedOnUserId = new Guid(User.Claims.FirstOrDefault(c => c.Type == "UserId").Value);
RServiceResult<GanjoorCommentUserRatingViewModel[]> res =
await _ganjoorService.GetUserCommentRatings(id, loggedOnUserId);
if (!string.IsNullOrEmpty(res.ExceptionString))
return BadRequest(res.ExceptionString);
return Ok(res.Result);
}
/// <summary> /// <summary>
/// Get Section Related ones /// Get Section Related ones
/// </summary> /// </summary>

View File

@ -0,0 +1,46 @@
namespace RMuseum.Models.Ganjoor.ViewModels
{
/// <summary>
/// a single user's rating value for a single comment
/// (used to merge "my rating" state into an already fetched, anonymously cacheable comment list)
/// </summary>
public class GanjoorCommentUserRatingViewModel
{
/// <summary>
/// GanjoorComment Id
/// </summary>
public int CommentId { get; set; }
/// <summary>
/// +1 = Like, -1 = Dislike
/// </summary>
public short Value { get; set; }
}
/// <summary>
/// result of rating/unrating a comment, returned so the client can update counts in place
/// without re-fetching the whole comment list
/// </summary>
public class GanjoorCommentRatingResultViewModel
{
/// <summary>
/// comment id
/// </summary>
public int CommentId { get; set; }
/// <summary>
/// updated total likes
/// </summary>
public int LikeCount { get; set; }
/// <summary>
/// updated total dislikes
/// </summary>
public int DislikeCount { get; set; }
/// <summary>
/// the rating value the requesting user now has for this comment (0 if cleared)
/// </summary>
public short CurrentUserRatingValue { get; set; }
}
}

View File

@ -1292,12 +1292,13 @@
</summary> </summary>
<returns></returns> <returns></returns>
</member> </member>
<member name="M:RMuseum.Controllers.GanjoorController.GetPageByUrl(System.String,System.Boolean)"> <member name="M:RMuseum.Controllers.GanjoorController.GetPageByUrl(System.String,System.Boolean,System.Boolean)">
<summary> <summary>
page by url page by url
</summary> </summary>
<param name="url"></param> <param name="url"></param>
<param name="catPoems"></param> <param name="catPoems"></param>
<param name="commentsSortByRanking">true: order comments by rating then date, false: order by date. Only applies to poem pages.</param>
<returns></returns> <returns></returns>
</member> </member>
<member name="M:RMuseum.Controllers.GanjoorController.ModifyPage(System.Int32,RMuseum.Models.Ganjoor.ViewModels.GanjoorModifyPageViewModel)"> <member name="M:RMuseum.Controllers.GanjoorController.ModifyPage(System.Int32,RMuseum.Models.Ganjoor.ViewModels.GanjoorModifyPageViewModel)">
@ -1447,6 +1448,14 @@
<param name="value">+1: like, -1: dislike, 0: remove previous rating</param> <param name="value">+1: like, -1: dislike, 0: remove previous rating</param>
<returns></returns> <returns></returns>
</member> </member>
<member name="M:RMuseum.Controllers.GanjoorController.GetUserCommentRatings(System.Int32)">
<summary>
get the logged on user's own rating values for a poem's comments
(meant to be merged client-side into an already fetched, anonymously cacheable comment list)
</summary>
<param name="id">poem id</param>
<returns></returns>
</member>
<member name="M:RMuseum.Controllers.GanjoorController.GetRelatedSections(System.Int32,System.Int32,System.Int32,System.Int32)"> <member name="M:RMuseum.Controllers.GanjoorController.GetRelatedSections(System.Int32,System.Int32,System.Int32,System.Int32)">
<summary> <summary>
Get Section Related ones Get Section Related ones
@ -4595,6 +4604,18 @@
<member name="M:RMuseum.Migrations.RChFix.BuildTargetModel(Microsoft.EntityFrameworkCore.ModelBuilder)"> <member name="M:RMuseum.Migrations.RChFix.BuildTargetModel(Microsoft.EntityFrameworkCore.ModelBuilder)">
<inheritdoc /> <inheritdoc />
</member> </member>
<member name="T:RMuseum.Migrations.CommentsRatings">
<inheritdoc />
</member>
<member name="M:RMuseum.Migrations.CommentsRatings.Up(Microsoft.EntityFrameworkCore.Migrations.MigrationBuilder)">
<inheritdoc />
</member>
<member name="M:RMuseum.Migrations.CommentsRatings.Down(Microsoft.EntityFrameworkCore.Migrations.MigrationBuilder)">
<inheritdoc />
</member>
<member name="M:RMuseum.Migrations.CommentsRatings.BuildTargetModel(Microsoft.EntityFrameworkCore.ModelBuilder)">
<inheritdoc />
</member>
<member name="T:RMuseum.Models.Accounting.DonationExpenditure"> <member name="T:RMuseum.Models.Accounting.DonationExpenditure">
<summary> <summary>
donation expenditures donation expenditures
@ -11695,6 +11716,48 @@
-1 = Dislike -1 = Dislike
</summary> </summary>
</member> </member>
<member name="T:RMuseum.Models.Ganjoor.ViewModels.GanjoorCommentUserRatingViewModel">
<summary>
a single user's rating value for a single comment
(used to merge "my rating" state into an already fetched, anonymously cacheable comment list)
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.ViewModels.GanjoorCommentUserRatingViewModel.CommentId">
<summary>
GanjoorComment Id
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.ViewModels.GanjoorCommentUserRatingViewModel.Value">
<summary>
+1 = Like, -1 = Dislike
</summary>
</member>
<member name="T:RMuseum.Models.Ganjoor.ViewModels.GanjoorCommentRatingResultViewModel">
<summary>
result of rating/unrating a comment, returned so the client can update counts in place
without re-fetching the whole comment list
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.ViewModels.GanjoorCommentRatingResultViewModel.CommentId">
<summary>
comment id
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.ViewModels.GanjoorCommentRatingResultViewModel.LikeCount">
<summary>
updated total likes
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.ViewModels.GanjoorCommentRatingResultViewModel.DislikeCount">
<summary>
updated total dislikes
</summary>
</member>
<member name="P:RMuseum.Models.Ganjoor.ViewModels.GanjoorCommentRatingResultViewModel.CurrentUserRatingValue">
<summary>
the rating value the requesting user now has for this comment (0 if cleared)
</summary>
</member>
<member name="T:RMuseum.Models.Ganjoor.ViewModels.GanjoorCoupletNumberViewModel"> <member name="T:RMuseum.Models.Ganjoor.ViewModels.GanjoorCoupletNumberViewModel">
<summary> <summary>
Ganjoor Couplet Number View Model Ganjoor Couplet Number View Model
@ -16738,7 +16801,7 @@
</summary> </summary>
<returns></returns> <returns></returns>
</member> </member>
<member name="M:RMuseum.Services.IGanjoorService.GetPageByUrl(System.String,System.Boolean)"> <member name="M:RMuseum.Services.IGanjoorService.GetPageByUrl(System.String,System.Boolean,System.Boolean)">
<summary> <summary>
get page by url get page by url
</summary> </summary>
@ -16882,6 +16945,16 @@
<param name="value">+1: like, -1: dislike, 0: remove previous rating</param> <param name="value">+1: like, -1: dislike, 0: remove previous rating</param>
<returns></returns> <returns></returns>
</member> </member>
<member name="M:RMuseum.Services.IGanjoorService.GetUserCommentRatings(System.Int32,System.Guid)">
<summary>
get the requesting user's own rating values for all comments of a poem
(mirrors GetUserPoemRecitationsUpVotes - meant to be merged client-side into an
already fetched, anonymously cacheable GetPoemComments/GetPoemById result)
</summary>
<param name="poemId"></param>
<param name="userId"></param>
<returns></returns>
</member>
<member name="M:RMuseum.Services.IGanjoorService.GetRelatedSections(System.Int32,System.Int32,System.Int32,System.Int32)"> <member name="M:RMuseum.Services.IGanjoorService.GetRelatedSections(System.Int32,System.Int32,System.Int32,System.Int32)">
<summary> <summary>
get a section related sections get a section related sections
@ -20347,13 +20420,22 @@
</member> </member>
<member name="M:RMuseum.Services.Implementation.GanjoorService.RateCommentAsync(System.Guid,System.Int32,System.Int16)"> <member name="M:RMuseum.Services.Implementation.GanjoorService.RateCommentAsync(System.Guid,System.Int32,System.Int16)">
<summary> <summary>
like / dislike / clear rating for a comment
</summary> </summary>
<param name="userId"></param> <param name="userId"></param>
<param name="commentId"></param> <param name="commentId"></param>
<param name="value">+1: like, -1: dislike, 0: remove previous rating</param> <param name="value">+1: like, -1: dislike, 0: remove previous rating</param>
<returns></returns> <returns></returns>
</member> </member>
<member name="M:RMuseum.Services.Implementation.GanjoorService.GetUserCommentRatings(System.Int32,System.Guid)">
<summary>
get the requesting user's own rating values for all comments of a poem
(meant to be merged client-side into an already fetched, anonymously cacheable comment list)
</summary>
<param name="poemId"></param>
<param name="userId"></param>
<returns></returns>
</member>
<member name="M:RMuseum.Services.Implementation.GanjoorService.GetDigitalSourceFromTagAsync(System.String)"> <member name="M:RMuseum.Services.Implementation.GanjoorService.GetDigitalSourceFromTagAsync(System.String)">
<summary> <summary>
get digital source from tag get digital source from tag
@ -21345,7 +21427,7 @@
<param name="url"></param> <param name="url"></param>
<returns></returns> <returns></returns>
</member> </member>
<member name="M:RMuseum.Services.Implementation.GanjoorService.GetPageByUrl(System.String,System.Boolean)"> <member name="M:RMuseum.Services.Implementation.GanjoorService.GetPageByUrl(System.String,System.Boolean,System.Boolean)">
<summary> <summary>
get page by url get page by url
</summary> </summary>

View File

@ -94,7 +94,7 @@ namespace RMuseum.Services
/// <param name="url"></param> /// <param name="url"></param>
/// <param name="catPoems"></param> /// <param name="catPoems"></param>
/// <returns></returns> /// <returns></returns>
Task<RServiceResult<GanjoorPageCompleteViewModel>> GetPageByUrl(string url, bool catPoems = false); Task<RServiceResult<GanjoorPageCompleteViewModel>> GetPageByUrl(string url, bool catPoems = false, bool commentsSortByRanking = true);
/// <summary> /// <summary>
/// get page url by id /// get page url by id
@ -230,7 +230,17 @@ namespace RMuseum.Services
/// <param name="commentId"></param> /// <param name="commentId"></param>
/// <param name="value">+1: like, -1: dislike, 0: remove previous rating</param> /// <param name="value">+1: like, -1: dislike, 0: remove previous rating</param>
/// <returns></returns> /// <returns></returns>
Task<RServiceResult<bool>> RateCommentAsync(Guid userId, int commentId, short value); Task<RServiceResult<GanjoorCommentRatingResultViewModel>> RateCommentAsync(Guid userId, int commentId, short value);
/// <summary>
/// get the requesting user's own rating values for all comments of a poem
/// (mirrors GetUserPoemRecitationsUpVotes - meant to be merged client-side into an
/// already fetched, anonymously cacheable GetPoemComments/GetPoemById result)
/// </summary>
/// <param name="poemId"></param>
/// <param name="userId"></param>
/// <returns></returns>
Task<RServiceResult<GanjoorCommentUserRatingViewModel[]>> GetUserCommentRatings(int poemId, Guid userId);
/// <summary> /// <summary>
/// get a section related sections /// get a section related sections

View File

@ -1,5 +1,6 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using RMuseum.Models.Ganjoor; using RMuseum.Models.Ganjoor;
using RMuseum.Models.Ganjoor.ViewModels;
using RMuseum.Utils; using RMuseum.Utils;
using RSecurityBackend.Models.Generic; using RSecurityBackend.Models.Generic;
using System; using System;
@ -14,38 +15,38 @@ namespace RMuseum.Services.Implementation
public partial class GanjoorService : IGanjoorService public partial class GanjoorService : IGanjoorService
{ {
/// <summary> /// <summary>
/// /// like / dislike / clear rating for a comment
/// </summary> /// </summary>
/// <param name="userId"></param> /// <param name="userId"></param>
/// <param name="commentId"></param> /// <param name="commentId"></param>
/// <param name="value">+1: like, -1: dislike, 0: remove previous rating</param> /// <param name="value">+1: like, -1: dislike, 0: remove previous rating</param>
/// <returns></returns> /// <returns></returns>
public async Task<RServiceResult<bool>> RateCommentAsync(Guid userId, int commentId, short value) public async Task<RServiceResult<GanjoorCommentRatingResultViewModel>> RateCommentAsync(Guid userId, int commentId, short value)
{ {
if(value != 1 && value != -1 && value != 0) if (value != 1 && value != -1 && value != 0)
{ {
return new RServiceResult<bool>(false, "value != 1 && value != -1 && value != 0"); return new RServiceResult<GanjoorCommentRatingResultViewModel>(null, "value != 1 && value != -1 && value != 0");
} }
try try
{ {
var comment = await _context.GanjoorComments.Where(c => c.Id == commentId).SingleOrDefaultAsync(); var comment = await _context.GanjoorComments.Where(c => c.Id == commentId).SingleOrDefaultAsync();
if (comment == null) if (comment == null)
{ {
return new RServiceResult<bool>(false, "comment == null"); return new RServiceResult<GanjoorCommentRatingResultViewModel>(null, "comment == null");
} }
var oldRating = await _context.GanjoorCommentReactions.Where(c => c.GanjoorCommentId == commentId && c.UserId == userId).SingleOrDefaultAsync(); var oldRating = await _context.GanjoorCommentReactions.Where(c => c.GanjoorCommentId == commentId && c.UserId == userId).SingleOrDefaultAsync();
if(value == 0 && oldRating == null) if (value == 0 && oldRating == null)
{ {
return new RServiceResult<bool>(true); return await _CurrentCommentRatingResultAsync(comment, 0);
} }
if (oldRating != null) if (oldRating != null)
{ {
if (oldRating.Value == value) if (oldRating.Value == value)
{ {
return new RServiceResult<bool>(true); return await _CurrentCommentRatingResultAsync(comment, oldRating.Value);
} }
if(value == 0) if (value == 0)
{ {
_context.Remove(oldRating); _context.Remove(oldRating);
} }
@ -57,9 +58,9 @@ namespace RMuseum.Services.Implementation
} }
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
return await ReCalculateCommentSortKeyAsync(comment); return await ReCalculateCommentSortKeyAsync(comment, value);
} }
else else
{ {
var rating = new GanjoorCommentReaction() var rating = new GanjoorCommentReaction()
{ {
@ -71,17 +72,57 @@ namespace RMuseum.Services.Implementation
}; };
_context.Add(rating); _context.Add(rating);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
return await ReCalculateCommentSortKeyAsync(comment); return await ReCalculateCommentSortKeyAsync(comment, value);
} }
} }
catch (Exception exp) catch (Exception exp)
{ {
return new RServiceResult<bool>(false, exp.ToString()); return new RServiceResult<GanjoorCommentRatingResultViewModel>(null, exp.ToString());
} }
} }
private async Task<RServiceResult<bool>> ReCalculateCommentSortKeyAsync(GanjoorComment comment) /// <summary>
/// get the requesting user's own rating values for all comments of a poem
/// (meant to be merged client-side into an already fetched, anonymously cacheable comment list)
/// </summary>
/// <param name="poemId"></param>
/// <param name="userId"></param>
/// <returns></returns>
public async Task<RServiceResult<GanjoorCommentUserRatingViewModel[]>> GetUserCommentRatings(int poemId, Guid userId)
{
try
{
var ratings =
await _context.GanjoorCommentReactions.AsNoTracking()
.Where(r => r.PoemId == poemId && r.UserId == userId)
.Select(r => new GanjoorCommentUserRatingViewModel()
{
CommentId = r.GanjoorCommentId,
Value = r.Value,
})
.ToArrayAsync();
return new RServiceResult<GanjoorCommentUserRatingViewModel[]>(ratings);
}
catch (Exception exp)
{
return new RServiceResult<GanjoorCommentUserRatingViewModel[]>(null, exp.ToString());
}
}
private Task<RServiceResult<GanjoorCommentRatingResultViewModel>> _CurrentCommentRatingResultAsync(GanjoorComment comment, short currentUserRatingValue)
{
return Task.FromResult(new RServiceResult<GanjoorCommentRatingResultViewModel>(
new GanjoorCommentRatingResultViewModel()
{
CommentId = comment.Id,
LikeCount = comment.LikeCount,
DislikeCount = comment.DislikeCount,
CurrentUserRatingValue = currentUserRatingValue,
}));
}
private async Task<RServiceResult<GanjoorCommentRatingResultViewModel>> ReCalculateCommentSortKeyAsync(GanjoorComment comment, short currentUserRatingValue)
{ {
try try
{ {
@ -92,11 +133,19 @@ namespace RMuseum.Services.Implementation
comment.SortKey = GanjoorCommentRankingScoreCalculator.ComputeRankingScore(likes, dislikes); comment.SortKey = GanjoorCommentRankingScoreCalculator.ComputeRankingScore(likes, dislikes);
_context.Update(comment); _context.Update(comment);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
return new RServiceResult<bool>(true); await CacheCleanForComment(comment.Id);
return new RServiceResult<GanjoorCommentRatingResultViewModel>(
new GanjoorCommentRatingResultViewModel()
{
CommentId = comment.Id,
LikeCount = comment.LikeCount,
DislikeCount = comment.DislikeCount,
CurrentUserRatingValue = currentUserRatingValue,
});
} }
catch (Exception exp) catch (Exception exp)
{ {
return new RServiceResult<bool>(false, exp.ToString()); return new RServiceResult<GanjoorCommentRatingResultViewModel>(null, exp.ToString());
} }
} }
} }

View File

@ -583,7 +583,7 @@ namespace RMuseum.Services.Implementation
/// <param name="url"></param> /// <param name="url"></param>
/// <param name="catPoems"></param> /// <param name="catPoems"></param>
/// <returns></returns> /// <returns></returns>
public async Task<RServiceResult<GanjoorPageCompleteViewModel>> GetPageByUrl(string url, bool catPoems = false) public async Task<RServiceResult<GanjoorPageCompleteViewModel>> GetPageByUrl(string url, bool catPoems = false, bool commentsSortByRanking = true)
{ {
if (url.IndexOf('?') != -1) if (url.IndexOf('?') != -1)
{ {
@ -599,7 +599,9 @@ namespace RMuseum.Services.Implementation
url = url.Replace("//", "/"); //duplicated slashes would be merged url = url.Replace("//", "/"); //duplicated slashes would be merged
var cachKey = $"GanjoorService::GetPageByUrl::{url}"; var cachKey = $"GanjoorService::GetPageByUrl::{url}";
if (!_memoryCache.TryGetValue(cachKey, out GanjoorPageCompleteViewModel page)) // only the default (rating) sort order is cached, so a caller asking for date-sorted
// comments never reads or overwrites the shared cache entry used by everyone else
if (!commentsSortByRanking || !_memoryCache.TryGetValue(cachKey, out GanjoorPageCompleteViewModel page))
{ {
var dbPage = await _context.GanjoorPages.Where(p => p.FullUrl == url).AsNoTracking().SingleOrDefaultAsync(); var dbPage = await _context.GanjoorPages.Where(p => p.FullUrl == url).AsNoTracking().SingleOrDefaultAsync();
if (dbPage == null) if (dbPage == null)
@ -646,7 +648,7 @@ namespace RMuseum.Services.Implementation
{ {
case GanjoorPageType.PoemPage: case GanjoorPageType.PoemPage:
{ {
var poemRes = await GetPoemById((int)dbPage.PoemId); var poemRes = await GetPoemById((int)dbPage.PoemId, sortByRanking: commentsSortByRanking);
if (!string.IsNullOrEmpty(poemRes.ExceptionString)) if (!string.IsNullOrEmpty(poemRes.ExceptionString))
{ {
return new RServiceResult<GanjoorPageCompleteViewModel>(null, poemRes.ExceptionString); return new RServiceResult<GanjoorPageCompleteViewModel>(null, poemRes.ExceptionString);
@ -880,7 +882,7 @@ namespace RMuseum.Services.Implementation
query = sortByRanking query = sortByRanking
? query.OrderByDescending(c => c.SortKey) ? query.OrderByDescending(c => c.SortKey)
.ThenByDescending(c => c.CommentDate) .ThenBy(c => c.CommentDate)
: query.OrderBy(c => c.CommentDate); : query.OrderBy(c => c.CommentDate);
var source = var source =