From 050c57c93af62d49dd31aa8baa6a33d61376f17b Mon Sep 17 00:00:00 2001 From: Ziad Ashraf Date: Thu, 11 Sep 2025 00:59:06 +0300 Subject: [PATCH 1/7] Added CreateThread endpoint --- .../AskFm.API/Controllers/ThreadController.cs | 92 ++++++++++++ AskFm/AskFm.API/Program.cs | 1 + AskFm/AskFm.BLL/DTO/CreateThreadDto.cs | 16 +++ AskFm/AskFm.BLL/DTO/ThreadResponseDto.cs | 25 ++++ AskFm/AskFm.BLL/Services/IThreadService.cs | 9 ++ AskFm/AskFm.BLL/Services/ThreadService.cs | 136 ++++++++++++++++++ 6 files changed, 279 insertions(+) create mode 100644 AskFm/AskFm.API/Controllers/ThreadController.cs create mode 100644 AskFm/AskFm.BLL/DTO/CreateThreadDto.cs create mode 100644 AskFm/AskFm.BLL/DTO/ThreadResponseDto.cs create mode 100644 AskFm/AskFm.BLL/Services/IThreadService.cs create mode 100644 AskFm/AskFm.BLL/Services/ThreadService.cs diff --git a/AskFm/AskFm.API/Controllers/ThreadController.cs b/AskFm/AskFm.API/Controllers/ThreadController.cs new file mode 100644 index 0000000..9202b09 --- /dev/null +++ b/AskFm/AskFm.API/Controllers/ThreadController.cs @@ -0,0 +1,92 @@ +using AskFm.BLL.DTO; +using AskFm.BLL.Services; +using AskFm.BLL.Services.UserIdentityService; +using AutoMapper; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; + +namespace AskFm.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +[Authorize(AuthenticationSchemes = "Bearer")] +public class ThreadController : ControllerBase +{ + private readonly ILogger _logger; + private readonly IUserService _userService; + private readonly IThreadService _threadService; + + public ThreadController( + ILogger logger, + IUserService userService, + IThreadService threadService) + { + _logger = logger; + _userService = userService; + _threadService = threadService; + } + + + // POST api/threads/ - Ask a question + [HttpPost] + [Route("thread")] + public async Task AskQuestion(CreateThreadDto createThreadDto) + { + // Asker Id -> Current User + var user = await _userService.GetCurrentUserAsync(); + if (!user.success) + { + return BadRequest(user.Errors); + } + var userId = user.Data.Id; + + var result = await _threadService.AddThread(userId, createThreadDto); + + if (!result.success) + { + return BadRequest(result.Errors); + } + return Ok(result.Data); + + } + + + [HttpGet] + [Route("thread/{id}")] + public async Task GetAllThreads([FromRoute] int id) + { + var threads = await _threadService.GetAllThreads(id); + if (!threads.success) + { + return BadRequest(threads.Errors); + } + return Ok(threads.Data); + } + + // GET api/threads/{id} - Getting the Thread with id = {id} + + [HttpGet] + [Route("thread/{id}")] + void GetThreadWithId([FromRoute] string id) + { + + } + + // PUT api/threads/{id}/answer - Add an Answer on the thread with id = {id} + [HttpPut] + [Route("threads/{id}/answer")] + void AnswerQuestion([FromRoute] string id) + { + + } + + // POST api/threads/{id}/likes - Add a Like to the thread with id = {id} + // GET api/threads/{id}/likes - Get all the Likes to the thread with id = {id} + // DELETE api/threads/{id}/likes - Unlike to the thread with id = {id} + + + // POST api/threads/{id}/comments - Add a comment to the thread with id = {id} + // GET api/threads/{id}/comments - Get all the comments to the thread with id = {id} + // DELETE api/threads/{id}/comments - Remove the comment to the thread with id = {id} + +} \ No newline at end of file diff --git a/AskFm/AskFm.API/Program.cs b/AskFm/AskFm.API/Program.cs index 9bf0d5a..bbcc284 100644 --- a/AskFm/AskFm.API/Program.cs +++ b/AskFm/AskFm.API/Program.cs @@ -56,6 +56,7 @@ public static void Main(string[] args) builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/AskFm/AskFm.BLL/DTO/CreateThreadDto.cs b/AskFm/AskFm.BLL/DTO/CreateThreadDto.cs new file mode 100644 index 0000000..4b078ab --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/CreateThreadDto.cs @@ -0,0 +1,16 @@ +using AskFm.DAL.Enums; +using AskFm.DAL.Models; + +namespace AskFm.BLL.DTO; + +public class CreateThreadDto +{ + public int AskedId { get; set; } + + public string QuestionContent { get; set; } + + public ThreadStatus Status { get; set; } + + public bool isAnonymous { get; set; } + +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/ThreadResponseDto.cs b/AskFm/AskFm.BLL/DTO/ThreadResponseDto.cs new file mode 100644 index 0000000..92c3b63 --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/ThreadResponseDto.cs @@ -0,0 +1,25 @@ +using AskFm.DAL.Enums; + +namespace AskFm.BLL.DTO; + +public class ThreadResponseDto +{ + public int Id { get; set; } + + public string QuestionContent { get; set; } + + public ThreadStatus Status { get; set; } + + public bool IsAnonymous { get; set; } + + public DateTime CreatedAt { get; set; } + + // Simplified user info instead of full objects + public int AskerId { get; set; } + + public string AskerName { get; set; } + + public int AskedId { get; set; } + + public string AskedName { get; set; } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/IThreadService.cs b/AskFm/AskFm.BLL/Services/IThreadService.cs new file mode 100644 index 0000000..c0a8d9f --- /dev/null +++ b/AskFm/AskFm.BLL/Services/IThreadService.cs @@ -0,0 +1,9 @@ +using AskFm.BLL.DTO; +using Thread = AskFm.DAL.Models.Thread; + +namespace AskFm.BLL.Services; +public interface IThreadService +{ + Task> AddThread(int userId, CreateThreadDto createThreadDto); + Task>> GetAllThreads(int userId); +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/ThreadService.cs b/AskFm/AskFm.BLL/Services/ThreadService.cs new file mode 100644 index 0000000..a8bec73 --- /dev/null +++ b/AskFm/AskFm.BLL/Services/ThreadService.cs @@ -0,0 +1,136 @@ +using AskFm.BLL.DTO; +using AskFm.DAL.Interfaces; +using AutoMapper; +using Microsoft.Extensions.Logging; +using Thread = AskFm.DAL.Models.Thread; + +namespace AskFm.BLL.Services; + +public class ThreadService : IThreadService +{ + + private IUnitOfWork _unitOfWork; + private readonly ILogger _logger; + private readonly IMapper _mapper; + public ThreadService(IUnitOfWork unitOfWork, ILogger logger) + { + _unitOfWork = unitOfWork; + _logger = logger; + } + public async Task> AddThread(int askerId, CreateThreadDto createThreadDto) + { + var transaction = await _unitOfWork.BeginTransactionAsync(); + + try + { + // check if the Asked user exite or not + var askedId = createThreadDto.AskedId; + + var askedUser = await _unitOfWork.Users.GetByIdAsync(askedId); + if (askedUser == null) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List(){"Could not find asked user"}); + } + + // getting the Asker user object + var askerUser = await _unitOfWork.Users.GetByIdAsync(askerId); + if (askerUser == null) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List(){"Could not find asker user"}); + } + + + + // check if the Asked User's id == Asker User's Id + if (askedId == askerId) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List(){"User can not ask him self"}); + } + + + // check if the QuestionContent is empty or not + if (string.IsNullOrEmpty(createThreadDto.QuestionContent)) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List(){"Question Can't be null or empty"}); + } + + // creating a new thread + Thread thread = new Thread + { + AskedId = askedId, + + AskerId = askerId, + + QuestionContent = createThreadDto.QuestionContent, + + AnswerContent = "", + Status = createThreadDto.Status, + isAnonymous = createThreadDto.isAnonymous, + CreatedAt = DateTime.Now, + }; + + await _unitOfWork.Threads.AddAsync(thread); + askerUser.AskedThreads?.Add(thread); + askedUser.ReceivedThreads?.Add(thread); + await _unitOfWork.Users.UpdateAsync(askedUser); + await _unitOfWork.Users.UpdateAsync(askerUser); + await _unitOfWork.SaveAsync(); + + transaction.Commit(); + + var responseDto = new ThreadResponseDto + { + Id = thread.Id, + QuestionContent = thread.QuestionContent, + Status = thread.Status, + IsAnonymous = thread.isAnonymous, + CreatedAt = thread.CreatedAt, + AskerId = thread.AskerId.Value, + AskerName = thread.Asker?.Name ?? "Unknown", + AskedId = thread.AskedId, + AskedName = thread.Asked?.Name ?? "Unknown" + }; + + return await ServiceResult.Success(responseDto); + } + catch (Exception e) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List(){e.Message}); + } + } + + public async Task>> GetAllThreads(int userId) + { + var user = await _unitOfWork.Users.FindAsync( + predicate: u => u.Id == userId, + includes: new[] { "ReceivedThreads" } + ); + + + if (user == null) + { + return await ServiceResult>.Failure(new List(){"Could not find user"}); + } + + var res = user.ReceivedThreads.Select(t => new ThreadResponseDto + { + Id = t.Id, + QuestionContent = t.QuestionContent, + IsAnonymous = t.isAnonymous, + CreatedAt = t.CreatedAt, + AskedId = t.AskedId, + Status = t.Status, + AskedName = t.Asked?.Name ?? "Unknown", + AskerId = t.AskerId.Value, + AskerName = t.Asker?.Name ?? "Unknown" + + }).ToList(); + + return await ServiceResult>.Success(res); + } +} \ No newline at end of file From d01a36a7eec3ef073e77b3a918da1b3ad9144050 Mon Sep 17 00:00:00 2001 From: Ziad Ashraf Date: Sat, 27 Sep 2025 08:22:56 +0300 Subject: [PATCH 2/7] Feat: Added the answer thread endpoint, created the ThreadLikeController --- .../AskFm.API/Controllers/ThreadController.cs | 6 +- .../Controllers/ThreadLikeController.cs | 34 +++++ AskFm/AskFm.BLL/DTO/ThreadAnswerDto.cs | 8 ++ AskFm/AskFm.BLL/DTO/ThreadResponseDto.cs | 2 + AskFm/AskFm.BLL/Services/IThreadService.cs | 1 + AskFm/AskFm.BLL/Services/ThreadService.cs | 136 ++++++++++++------ 6 files changed, 143 insertions(+), 44 deletions(-) create mode 100644 AskFm/AskFm.API/Controllers/ThreadLikeController.cs create mode 100644 AskFm/AskFm.BLL/DTO/ThreadAnswerDto.cs diff --git a/AskFm/AskFm.API/Controllers/ThreadController.cs b/AskFm/AskFm.API/Controllers/ThreadController.cs index 9202b09..24ab814 100644 --- a/AskFm/AskFm.API/Controllers/ThreadController.cs +++ b/AskFm/AskFm.API/Controllers/ThreadController.cs @@ -29,7 +29,7 @@ public ThreadController( // POST api/threads/ - Ask a question [HttpPost] - [Route("thread")] + [Route("threads")] public async Task AskQuestion(CreateThreadDto createThreadDto) { // Asker Id -> Current User @@ -51,6 +51,7 @@ public async Task AskQuestion(CreateThreadDto createThreadDto) } + // get all threads for user by user id [HttpGet] [Route("thread/{id}")] public async Task GetAllThreads([FromRoute] int id) @@ -66,11 +67,12 @@ public async Task GetAllThreads([FromRoute] int id) // GET api/threads/{id} - Getting the Thread with id = {id} [HttpGet] - [Route("thread/{id}")] + [Route("threads/{id}")] void GetThreadWithId([FromRoute] string id) { } + // PUT api/threads/{id}/answer - Add an Answer on the thread with id = {id} [HttpPut] diff --git a/AskFm/AskFm.API/Controllers/ThreadLikeController.cs b/AskFm/AskFm.API/Controllers/ThreadLikeController.cs new file mode 100644 index 0000000..eb59f73 --- /dev/null +++ b/AskFm/AskFm.API/Controllers/ThreadLikeController.cs @@ -0,0 +1,34 @@ +using AskFm.BLL.Services; +using AskFm.BLL.Services.UserIdentityService; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace AskFm.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +[Authorize(AuthenticationSchemes = "Bearer")] +public class ThreadLikeController +{ + private readonly ILogger _logger; + private readonly IUserService _userService; + private readonly IThreadService _threadService; + + public ThreadLikeController( + ILogger logger, + IUserService userService, + IThreadService threadService) + { + _logger = logger; + _userService = userService; + _threadService = threadService; + } + + + // POST api/threads/{id}/likes - Add a Like to the thread with id = {id} + // GET api/threads/{id}/likes - Get all the Likes to the thread with id = {id} + // DELETE api/threads/{id}/likes - Unlike to the thread with id = {id} + + + +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/ThreadAnswerDto.cs b/AskFm/AskFm.BLL/DTO/ThreadAnswerDto.cs new file mode 100644 index 0000000..16c3cfc --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/ThreadAnswerDto.cs @@ -0,0 +1,8 @@ +namespace AskFm.BLL.DTO; + +public class ThreadAnswerDto +{ + public int threadId; + public string answer; + +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/ThreadResponseDto.cs b/AskFm/AskFm.BLL/DTO/ThreadResponseDto.cs index 92c3b63..1a6775c 100644 --- a/AskFm/AskFm.BLL/DTO/ThreadResponseDto.cs +++ b/AskFm/AskFm.BLL/DTO/ThreadResponseDto.cs @@ -22,4 +22,6 @@ public class ThreadResponseDto public int AskedId { get; set; } public string AskedName { get; set; } + + public string answer; } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/IThreadService.cs b/AskFm/AskFm.BLL/Services/IThreadService.cs index c0a8d9f..7f550fc 100644 --- a/AskFm/AskFm.BLL/Services/IThreadService.cs +++ b/AskFm/AskFm.BLL/Services/IThreadService.cs @@ -6,4 +6,5 @@ public interface IThreadService { Task> AddThread(int userId, CreateThreadDto createThreadDto); Task>> GetAllThreads(int userId); + Task> AnswerThread(ThreadAnswerDto threadAnswerDto); } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/ThreadService.cs b/AskFm/AskFm.BLL/Services/ThreadService.cs index a8bec73..080b0eb 100644 --- a/AskFm/AskFm.BLL/Services/ThreadService.cs +++ b/AskFm/AskFm.BLL/Services/ThreadService.cs @@ -8,15 +8,16 @@ namespace AskFm.BLL.Services; public class ThreadService : IThreadService { - private IUnitOfWork _unitOfWork; private readonly ILogger _logger; private readonly IMapper _mapper; - public ThreadService(IUnitOfWork unitOfWork, ILogger logger) + + public ThreadService(IUnitOfWork unitOfWork, ILogger logger) { _unitOfWork = unitOfWork; _logger = logger; } + public async Task> AddThread(int askerId, CreateThreadDto createThreadDto) { var transaction = await _unitOfWork.BeginTransactionAsync(); @@ -25,63 +26,66 @@ public async Task> AddThread(int askerId, Creat { // check if the Asked user exite or not var askedId = createThreadDto.AskedId; - + var askedUser = await _unitOfWork.Users.GetByIdAsync(askedId); if (askedUser == null) { await transaction.RollbackAsync(); - return await ServiceResult.Failure(new List(){"Could not find asked user"}); + return await ServiceResult.Failure( + new List() { "Could not find asked user" }); } - + // getting the Asker user object var askerUser = await _unitOfWork.Users.GetByIdAsync(askerId); if (askerUser == null) { await transaction.RollbackAsync(); - return await ServiceResult.Failure(new List(){"Could not find asker user"}); + return await ServiceResult.Failure( + new List() { "Could not find asker user" }); } - - + // check if the Asked User's id == Asker User's Id if (askedId == askerId) { await transaction.RollbackAsync(); - return await ServiceResult.Failure(new List(){"User can not ask him self"}); + return await ServiceResult.Failure( + new List() { "User can not ask him self" }); } - - + + // check if the QuestionContent is empty or not if (string.IsNullOrEmpty(createThreadDto.QuestionContent)) { await transaction.RollbackAsync(); - return await ServiceResult.Failure(new List(){"Question Can't be null or empty"}); + return await ServiceResult.Failure(new List() + { "Question Can't be null or empty" }); } - + // creating a new thread Thread thread = new Thread { AskedId = askedId, - + AskerId = askerId, QuestionContent = createThreadDto.QuestionContent, - + AnswerContent = "", Status = createThreadDto.Status, isAnonymous = createThreadDto.isAnonymous, CreatedAt = DateTime.Now, }; - + await _unitOfWork.Threads.AddAsync(thread); askerUser.AskedThreads?.Add(thread); askedUser.ReceivedThreads?.Add(thread); await _unitOfWork.Users.UpdateAsync(askedUser); await _unitOfWork.Users.UpdateAsync(askerUser); await _unitOfWork.SaveAsync(); - + transaction.Commit(); - + var responseDto = new ThreadResponseDto { Id = thread.Id, @@ -94,43 +98,91 @@ public async Task> AddThread(int askerId, Creat AskedId = thread.AskedId, AskedName = thread.Asked?.Name ?? "Unknown" }; - + return await ServiceResult.Success(responseDto); } catch (Exception e) { await transaction.RollbackAsync(); - return await ServiceResult.Failure(new List(){e.Message}); + return await ServiceResult.Failure(new List() { e.Message }); } } public async Task>> GetAllThreads(int userId) { - var user = await _unitOfWork.Users.FindAsync( - predicate: u => u.Id == userId, - includes: new[] { "ReceivedThreads" } - ); - - - if (user == null) + try + { + var user = await _unitOfWork.Users.FindAsync( + predicate: u => u.Id == userId, + includes: new[] { "ReceivedThreads" } + ); + + + if (user == null) + { + return await ServiceResult>.Failure( + new List() { "Could not find user" }); + } + + var res = user.ReceivedThreads.Select(t => new ThreadResponseDto + { + Id = t.Id, + QuestionContent = t.QuestionContent, + IsAnonymous = t.isAnonymous, + CreatedAt = t.CreatedAt, + AskedId = t.AskedId, + Status = t.Status, + AskedName = t.Asked?.Name ?? "Unknown", + AskerId = t.AskerId.Value, + AskerName = t.Asker?.Name ?? "Unknown" + }).ToList(); + + return await ServiceResult>.Success(res); + } + catch (Exception e) { - return await ServiceResult>.Failure(new List(){"Could not find user"}); + return await ServiceResult>.Failure(new List() { e.Message }); } + } + + public async Task> AnswerThread(ThreadAnswerDto threadAnswerDto) + { + try + { + // get thread + var threadId = threadAnswerDto.threadId; + var thread = await _unitOfWork.Threads.GetByIdAsync(threadId); + // chekc if thread + if (thread == null) + { + return await ServiceResult.Failure(new List() { "Could not find thread" }); + } + // put the answer on it + thread.AnswerContent = threadAnswerDto.answer; + // save changes + await _unitOfWork.Threads.UpdateAsync(thread); + await _unitOfWork.SaveAsync(); - var res = user.ReceivedThreads.Select(t => new ThreadResponseDto + ThreadResponseDto threadRes = new ThreadResponseDto() + { + Id = thread.Id, + QuestionContent = thread.QuestionContent, + IsAnonymous = thread.isAnonymous, + CreatedAt = thread.CreatedAt, + AskedId = thread.AskedId, + AskedName = thread?.Asked.Name ?? "Unknown", + AskerId = thread.AskerId.Value, + AskerName = thread.isAnonymous? "Unknown" : thread?.Asker.Name, + answer = thread.AnswerContent, + Status = thread.Status, + }; + return await ServiceResult.Success(threadRes); + } + catch (Exception e) { - Id = t.Id, - QuestionContent = t.QuestionContent, - IsAnonymous = t.isAnonymous, - CreatedAt = t.CreatedAt, - AskedId = t.AskedId, - Status = t.Status, - AskedName = t.Asked?.Name ?? "Unknown", - AskerId = t.AskerId.Value, - AskerName = t.Asker?.Name ?? "Unknown" - - }).ToList(); - - return await ServiceResult>.Success(res); + return await ServiceResult.Failure(new List() { e.Message }); + } } + + } \ No newline at end of file From e71d3b388d762899b67d757f56e83cdbdfa4c3b5 Mon Sep 17 00:00:00 2001 From: Ziad Ashraf Date: Mon, 6 Oct 2025 00:12:33 +0300 Subject: [PATCH 3/7] Finished the ThreadLikeService and endpoints --- .../AskFm.API/Controllers/ThreadController.cs | 3 +- .../Controllers/ThreadLikeController.cs | 62 +++++- AskFm/AskFm.API/Program.cs | 1 + AskFm/AskFm.BLL/DTO/ThreadAnswerDto.cs | 4 +- AskFm/AskFm.BLL/DTO/ThreadLikeResponseDto.cs | 12 ++ .../AskFm.BLL/Services/IThreadLikeService.cs | 11 + AskFm/AskFm.BLL/Services/ThreadLikeService.cs | 202 ++++++++++++++++++ AskFm/AskFm.BLL/Services/ThreadService.cs | 6 +- 8 files changed, 288 insertions(+), 13 deletions(-) create mode 100644 AskFm/AskFm.BLL/DTO/ThreadLikeResponseDto.cs create mode 100644 AskFm/AskFm.BLL/Services/IThreadLikeService.cs create mode 100644 AskFm/AskFm.BLL/Services/ThreadLikeService.cs diff --git a/AskFm/AskFm.API/Controllers/ThreadController.cs b/AskFm/AskFm.API/Controllers/ThreadController.cs index 24ab814..3a9a539 100644 --- a/AskFm/AskFm.API/Controllers/ThreadController.cs +++ b/AskFm/AskFm.API/Controllers/ThreadController.cs @@ -29,7 +29,7 @@ public ThreadController( // POST api/threads/ - Ask a question [HttpPost] - [Route("threads")] + [Route("thread")] public async Task AskQuestion(CreateThreadDto createThreadDto) { // Asker Id -> Current User @@ -83,6 +83,7 @@ void AnswerQuestion([FromRoute] string id) } // POST api/threads/{id}/likes - Add a Like to the thread with id = {id} + // GET api/threads/{id}/likes - Get all the Likes to the thread with id = {id} // DELETE api/threads/{id}/likes - Unlike to the thread with id = {id} diff --git a/AskFm/AskFm.API/Controllers/ThreadLikeController.cs b/AskFm/AskFm.API/Controllers/ThreadLikeController.cs index eb59f73..0ba9367 100644 --- a/AskFm/AskFm.API/Controllers/ThreadLikeController.cs +++ b/AskFm/AskFm.API/Controllers/ThreadLikeController.cs @@ -1,34 +1,82 @@ +using AskFm.BLL.DTO; using AskFm.BLL.Services; using AskFm.BLL.Services.UserIdentityService; -using Microsoft.AspNetCore.Authorization; +using AutoMapper; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; namespace AskFm.API.Controllers; [ApiController] [Route("api/[controller]")] [Authorize(AuthenticationSchemes = "Bearer")] -public class ThreadLikeController +public class ThreadLikeController : ControllerBase { private readonly ILogger _logger; private readonly IUserService _userService; - private readonly IThreadService _threadService; + private readonly IThreadLikeService _threadLikeService; public ThreadLikeController( ILogger logger, IUserService userService, - IThreadService threadService) + IThreadLikeService threadService) { _logger = logger; _userService = userService; - _threadService = threadService; + _threadLikeService = threadService; } // POST api/threads/{id}/likes - Add a Like to the thread with id = {id} - // GET api/threads/{id}/likes - Get all the Likes to the thread with id = {id} - // DELETE api/threads/{id}/likes - Unlike to the thread with id = {id} + [HttpPost] + [Route("threads/{id}/likes")] + public async Task LikeThread([FromRoute] int id) + { + var user = await _userService.GetCurrentUserAsync(); + if (!user.success) + { + return BadRequest(user.Errors); + } + var res = await _threadLikeService.AddLike(id, user.Data.Id); + if (!res.success) + { + return BadRequest(res.Errors); + } + return Ok(res.Data); + } + // GET api/threads/{id}/likes - Get all the Likes to the thread with id = {id} + [HttpGet] + [Route("threads/{id}/likes")] + public async Task GetLikes([FromRoute] int id) + { + var likes = await _threadLikeService.GetLikes(id); + if (!likes.success) + { + return BadRequest(likes.Errors); + } + return Ok(likes.Data); + } + + // DELETE api/threads/{id}/likes - Unlike to the thread with id = {id} + [HttpDelete] + [Route("threads/{id}/likes")] + public async Task UnlikeThread([FromRoute] int id) + { + var user = await _userService.GetCurrentUserAsync(); + if (!user.success) + { + return BadRequest(user.Errors); + } + + var res = await _threadLikeService.RemoveLike(id, user.Data.Id); + if (!res.success) + { + return BadRequest(res.Errors); + } + + return Ok(new { message = "Thread unliked successfully" }); + } } \ No newline at end of file diff --git a/AskFm/AskFm.API/Program.cs b/AskFm/AskFm.API/Program.cs index bbcc284..0bad463 100644 --- a/AskFm/AskFm.API/Program.cs +++ b/AskFm/AskFm.API/Program.cs @@ -57,6 +57,7 @@ public static void Main(string[] args) builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/AskFm/AskFm.BLL/DTO/ThreadAnswerDto.cs b/AskFm/AskFm.BLL/DTO/ThreadAnswerDto.cs index 16c3cfc..cf68455 100644 --- a/AskFm/AskFm.BLL/DTO/ThreadAnswerDto.cs +++ b/AskFm/AskFm.BLL/DTO/ThreadAnswerDto.cs @@ -2,7 +2,7 @@ namespace AskFm.BLL.DTO; public class ThreadAnswerDto { - public int threadId; - public string answer; + public int ThreadId {get;set;} + public string Answer {get;set;} } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/ThreadLikeResponseDto.cs b/AskFm/AskFm.BLL/DTO/ThreadLikeResponseDto.cs new file mode 100644 index 0000000..1cc52d5 --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/ThreadLikeResponseDto.cs @@ -0,0 +1,12 @@ +namespace AskFm.BLL.DTO; + +public class ThreadLikeResponseDto +{ + public int userId {get;set;} + public int threadId {get;set;} + public DateTime createdAt {get;set;} + public string UserName {get;set;} + string ProfilePicture {get;set;} + + +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/IThreadLikeService.cs b/AskFm/AskFm.BLL/Services/IThreadLikeService.cs new file mode 100644 index 0000000..156e8d8 --- /dev/null +++ b/AskFm/AskFm.BLL/Services/IThreadLikeService.cs @@ -0,0 +1,11 @@ +using AskFm.BLL.DTO; +using Microsoft.AspNetCore.Mvc; + +namespace AskFm.BLL.Services; + +public interface IThreadLikeService +{ + public Task> AddLike(int id, int userId); + public Task>> GetLikes(int id); + public Task> RemoveLike(int threadId, int userId); +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/ThreadLikeService.cs b/AskFm/AskFm.BLL/Services/ThreadLikeService.cs new file mode 100644 index 0000000..622c5aa --- /dev/null +++ b/AskFm/AskFm.BLL/Services/ThreadLikeService.cs @@ -0,0 +1,202 @@ +using AskFm.BLL.DTO; +using AskFm.DAL.Interfaces; +using AskFm.DAL.Models; +using AutoMapper; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace AskFm.BLL.Services; + +public class ThreadLikeService : IThreadLikeService +{ + private IUnitOfWork _unitOfWork; + private readonly ILogger _logger; + private readonly IMapper _mapper; + + public ThreadLikeService(IUnitOfWork unitOfWork, ILogger logger) + { + _unitOfWork = unitOfWork; + _logger = logger; + } + + // add a like on the Thread that has id = id + public async Task> AddLike(int id, int userId) + { + try + { + var transaction = await _unitOfWork.BeginTransactionAsync(); + // get the thread, Including the ThreadLike Collection + var thread = await _unitOfWork.Threads.FindAsync( + predicate: thread => thread.Id == id, + includes: new[] { "ThreadLikes" } + ); + + // check if thread exists + if (thread == null) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() { "Thread not found" }); + } + + // check if user has already liked this thread + var existingLike = thread.ThreadLikes?.FirstOrDefault(like => like.UserId == userId); + if (existingLike != null) + { + // if the The use already liked this thread + if (!existingLike.IsDeleted) + { + var errors = new List() + { + "User has already liked this thread" + }; + await transaction.RollbackAsync(); + return await ServiceResult.Failure(errors); + } + + // otherwise , the user liked the comment , then unliked it , and then wants to like it again + existingLike.IsDeleted = false; + thread.ThreadLikes.Add(existingLike); + _unitOfWork.Threads.Update(thread); + await _unitOfWork.SaveAsync(); + await transaction.CommitAsync(); + + _logger.LogInformation("Like added successfully for thread id: {threadId}", thread.Id); + + + // updating the createdAt column to Now , ignoring the first time the user liked the comment + existingLike.CreatedAt = DateTime.Now; + return await ServiceResult.Success(new ThreadLikeResponseDto() + { + threadId = existingLike.ThreadId, + userId = existingLike.UserId, + createdAt = existingLike.CreatedAt + }); + } + + + try + { + // create a new ThreadLike + var threadLike = new ThreadLike + { + ThreadId = id, + UserId = userId, + CreatedAt = DateTime.Now + }; + + // add the ThreadLike to the Thread + thread.ThreadLikes?.Add(threadLike); + + // update the thread + await _unitOfWork.Threads.UpdateAsync(thread); + await _unitOfWork.SaveAsync(); + + await transaction.CommitAsync(); + + // create and return the response DTO + var response = new ThreadLikeResponseDto + { + threadId = threadLike.ThreadId, + userId = threadLike.UserId, + createdAt = threadLike.CreatedAt + }; + + return await ServiceResult.Success(response); + } + catch (Exception e) + { + await transaction.RollbackAsync(); + throw; + } + } + catch (Exception e) + { + return await ServiceResult.Failure(new List() { e.Message }); + } + } + + public async Task>> GetLikes(int id) + { + try + { + // get the thread, Including the ThreadLikes collection + var thread = await _unitOfWork.Threads.FindAsync( + predicate: thread => thread.Id == id, + includes: new[] { "ThreadLikes.User" } + ); + + // check if thread exists + if (thread == null) + { + return await ServiceResult>.Failure(new List() + { "Thread not found" }); + } + + // return the list of likes + var likes = thread.ThreadLikes.Select(like => new ThreadLikeResponseDto + { + threadId = like.ThreadId, + userId = like.UserId, + UserName = like.User?.Name ?? "Unknown", + createdAt = like.CreatedAt + }).ToList(); + + return await ServiceResult>.Success(likes); + } + catch (Exception e) + { + return await ServiceResult>.Failure(new List() { e.Message }); + } + } + + // Remove a like from the thread with id = threadId + public async Task> RemoveLike(int threadId, int userId) + { + try + { + // get the thread with its likes + var thread = await _unitOfWork.Threads.FindAsync( + predicate: thread => thread.Id == threadId, + includes: new[] { "ThreadLikes" } + ); + + // check if thread exists + if (thread == null) + { + return await ServiceResult.Failure(new List() { "Thread not found" }); + } + + // find the like to remove + var likeToRemove = thread.ThreadLikes?.FirstOrDefault(like => like.UserId == userId); + if (likeToRemove == null) + { + return await ServiceResult.Failure(new List() { "Like not found for this user" }); + } + + var transaction = await _unitOfWork.BeginTransactionAsync(); + + try + { + // remove the like from the thread + thread.ThreadLikes?.Remove(likeToRemove); + + // update the thread + await _unitOfWork.Threads.UpdateAsync(thread); + await _unitOfWork.SaveAsync(); + + transaction.Commit(); + + return await ServiceResult.Success(true); + } + catch (Exception e) + { + await transaction.RollbackAsync(); + throw; + } + } + catch (Exception e) + { + return await ServiceResult.Failure(new List() { e.Message }); + } + } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/ThreadService.cs b/AskFm/AskFm.BLL/Services/ThreadService.cs index 080b0eb..c9b46a1 100644 --- a/AskFm/AskFm.BLL/Services/ThreadService.cs +++ b/AskFm/AskFm.BLL/Services/ThreadService.cs @@ -84,7 +84,7 @@ public async Task> AddThread(int askerId, Creat await _unitOfWork.Users.UpdateAsync(askerUser); await _unitOfWork.SaveAsync(); - transaction.Commit(); + await transaction.CommitAsync(); var responseDto = new ThreadResponseDto { @@ -150,7 +150,7 @@ public async Task> AnswerThread(ThreadAnswerDto try { // get thread - var threadId = threadAnswerDto.threadId; + var threadId = threadAnswerDto.ThreadId; var thread = await _unitOfWork.Threads.GetByIdAsync(threadId); // chekc if thread if (thread == null) @@ -158,7 +158,7 @@ public async Task> AnswerThread(ThreadAnswerDto return await ServiceResult.Failure(new List() { "Could not find thread" }); } // put the answer on it - thread.AnswerContent = threadAnswerDto.answer; + thread.AnswerContent = threadAnswerDto.Answer; // save changes await _unitOfWork.Threads.UpdateAsync(thread); await _unitOfWork.SaveAsync(); From cb8a0eac29483238f591f91cae0b5192af9b705d Mon Sep 17 00:00:00 2001 From: Ziad Ashraf Date: Mon, 6 Oct 2025 15:31:58 +0300 Subject: [PATCH 4/7] Refactor: resolved most of the notes --- AskFm/AskFm.API/AskFm.API.csproj | 2 +- AskFm/AskFm.API/Controllers/ThreadController.cs | 1 - AskFm/AskFm.API/Program.cs | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/AskFm/AskFm.API/AskFm.API.csproj b/AskFm/AskFm.API/AskFm.API.csproj index 60af690..932256c 100644 --- a/AskFm/AskFm.API/AskFm.API.csproj +++ b/AskFm/AskFm.API/AskFm.API.csproj @@ -7,7 +7,7 @@ - + diff --git a/AskFm/AskFm.API/Controllers/ThreadController.cs b/AskFm/AskFm.API/Controllers/ThreadController.cs index 3a9a539..be5e545 100644 --- a/AskFm/AskFm.API/Controllers/ThreadController.cs +++ b/AskFm/AskFm.API/Controllers/ThreadController.cs @@ -83,7 +83,6 @@ void AnswerQuestion([FromRoute] string id) } // POST api/threads/{id}/likes - Add a Like to the thread with id = {id} - // GET api/threads/{id}/likes - Get all the Likes to the thread with id = {id} // DELETE api/threads/{id}/likes - Unlike to the thread with id = {id} diff --git a/AskFm/AskFm.API/Program.cs b/AskFm/AskFm.API/Program.cs index 0bad463..6d7c379 100644 --- a/AskFm/AskFm.API/Program.cs +++ b/AskFm/AskFm.API/Program.cs @@ -50,6 +50,7 @@ public static void Main(string[] args) .UseSqlServer(ConnectionString)); // ------------------------------------------------- // Register the repositories and services + builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); From 155fd0cd7bea1e09c6e9e19fee943c2142ffe5e9 Mon Sep 17 00:00:00 2001 From: Ziad Ashraf Date: Wed, 8 Oct 2025 17:27:54 +0300 Subject: [PATCH 5/7] Finished the Threads , implemented the Saved endpoints and modified the rest --- .../AskFm.API/Controllers/ThreadController.cs | 155 ++++++- AskFm/AskFm.BLL/AskFm.BLL.csproj | 3 +- AskFm/AskFm.BLL/DTO/AnswerThreadDto.cs | 6 + AskFm/AskFm.BLL/DTO/PagedResponseDto.cs | 10 + AskFm/AskFm.BLL/DTO/ThreadAnswerDto.cs | 8 - AskFm/AskFm.BLL/DTO/ThreadLikeResponseDto.cs | 2 +- AskFm/AskFm.BLL/DTO/ThreadResponseDto.cs | 21 +- .../AskFm.BLL/Services/CommentLikeService.cs | 3 +- AskFm/AskFm.BLL/Services/IThreadService.cs | 16 +- AskFm/AskFm.BLL/Services/ThreadLikeService.cs | 5 +- AskFm/AskFm.BLL/Services/ThreadService.cs | 421 ++++++++++++++++-- AskFm/AskFm.DAL/Enums/ThreadStatus.cs | 4 +- AskFm/AskFm.DAL/Interfaces/IRepository.cs | 37 +- AskFm/AskFm.DAL/Repositories/Repository.cs | 112 +++-- 14 files changed, 665 insertions(+), 138 deletions(-) create mode 100644 AskFm/AskFm.BLL/DTO/AnswerThreadDto.cs create mode 100644 AskFm/AskFm.BLL/DTO/PagedResponseDto.cs delete mode 100644 AskFm/AskFm.BLL/DTO/ThreadAnswerDto.cs diff --git a/AskFm/AskFm.API/Controllers/ThreadController.cs b/AskFm/AskFm.API/Controllers/ThreadController.cs index be5e545..c995dee 100644 --- a/AskFm/AskFm.API/Controllers/ThreadController.cs +++ b/AskFm/AskFm.API/Controllers/ThreadController.cs @@ -25,12 +25,12 @@ public ThreadController( _userService = userService; _threadService = threadService; } - - + + // POST api/threads/ - Ask a question [HttpPost] [Route("thread")] - public async Task AskQuestion(CreateThreadDto createThreadDto) + public async Task AskQuestion(CreateThreadDto createThreadDto) { // Asker Id -> Current User var user = await _userService.GetCurrentUserAsync(); @@ -39,7 +39,7 @@ public async Task AskQuestion(CreateThreadDto createThreadDto) return BadRequest(user.Errors); } var userId = user.Data.Id; - + var result = await _threadService.AddThread(userId, createThreadDto); if (!result.success) @@ -61,34 +61,153 @@ public async Task GetAllThreads([FromRoute] int id) { return BadRequest(threads.Errors); } - return Ok(threads.Data); + return Ok(threads.Data); } - - // GET api/threads/{id} - Getting the Thread with id = {id} + // GET api/threads/{id} - Getting the Thread with id = {id} [HttpGet] [Route("threads/{id}")] - void GetThreadWithId([FromRoute] string id) + public async Task GetThreadWithId([FromRoute] int id) { - + var thread = await _threadService.GetThreadById(id); + if (!thread.success) + { + return BadRequest(thread.Errors); + } + return Ok(thread.Data); } - + // PUT api/threads/{id}/answer - Add an Answer on the thread with id = {id} [HttpPut] [Route("threads/{id}/answer")] - void AnswerQuestion([FromRoute] string id) + public async Task AnswerQuestion([FromRoute] int id, [FromBody] AnswerThreadDto answerDto) + { + var user = await _userService.GetCurrentUserAsync(); + if (!user.success) + { + return BadRequest(user.Errors); + } + + var result = await _threadService.AnswerThread(id, user.Data.Id, answerDto); + if (!result.success) + { + return BadRequest(result.Errors); + } + + return Ok(result.Data); + } + + // GET api/threads - Get all threads (with pagination) + [HttpGet] + [Route("threads")] + public async Task GetThreads([FromQuery] int page = 1, [FromQuery] int pageSize = 10) + { + var threads = await _threadService.GetThreads(page, pageSize); + if (!threads.success) + { + return BadRequest(threads.Errors); + } + return Ok(threads.Data); + } + + // DELETE api/threads/{id} - Delete a thread + [HttpDelete] + [Route("threads/{id}")] + public async Task DeleteThread([FromRoute] int id) + { + var user = await _userService.GetCurrentUserAsync(); + if (!user.success) + { + return BadRequest(user.Errors); + } + + var result = await _threadService.DeleteThread(id, user.Data.Id); + if (!result.success) + { + return BadRequest(result.Errors); + } + + return Ok(new { message = "Thread deleted successfully" }); + } + + // GET api/threads/feed - Get personalized feed for current user + [HttpGet] + [Route("threads/feed")] + public async Task GetFeed([FromQuery] int page = 1, [FromQuery] int pageSize = 10) + { + var user = await _userService.GetCurrentUserAsync(); + if (!user.success) + { + return BadRequest(user.Errors); + } + + var feed = await _threadService.GetFeed(user.Data.Id, page, pageSize); + if (!feed.success) + { + return BadRequest(feed.Errors); + } + + return Ok(feed.Data); + } + + // POST api/threads/{id}/save - Save a thread + [HttpPost] + [Route("threads/{id}/save")] + public async Task SaveThread([FromRoute] int id) { - + var user = await _userService.GetCurrentUserAsync(); + if (!user.success) + { + return BadRequest(user.Errors); + } + + var result = await _threadService.SaveThread(id, user.Data.Id); + if (!result.success) + { + return BadRequest(result.Errors); + } + + return Ok(new { message = "Thread saved successfully" }); } - // POST api/threads/{id}/likes - Add a Like to the thread with id = {id} - // GET api/threads/{id}/likes - Get all the Likes to the thread with id = {id} - // DELETE api/threads/{id}/likes - Unlike to the thread with id = {id} + // DELETE api/threads/{id}/save - Unsave a thread + [HttpDelete] + [Route("threads/{id}/save")] + public async Task UnsaveThread([FromRoute] int id) + { + var user = await _userService.GetCurrentUserAsync(); + if (!user.success) + { + return BadRequest(user.Errors); + } + + var result = await _threadService.UnsaveThread(id, user.Data.Id); + if (!result.success) + { + return BadRequest(result.Errors); + } + + return Ok(new { message = "Thread unsaved successfully" }); + } + // GET api/threads/saved - Get all saved threads for current user + [HttpGet] + [Route("threads/saved")] + public async Task GetSavedThreads([FromQuery] int page = 1, [FromQuery] int pageSize = 10) + { + var user = await _userService.GetCurrentUserAsync(); + if (!user.success) + { + return BadRequest(user.Errors); + } - // POST api/threads/{id}/comments - Add a comment to the thread with id = {id} - // GET api/threads/{id}/comments - Get all the comments to the thread with id = {id} - // DELETE api/threads/{id}/comments - Remove the comment to the thread with id = {id} + var threads = await _threadService.GetSavedThreads(user.Data.Id, page, pageSize); + if (!threads.success) + { + return BadRequest(threads.Errors); + } + return Ok(threads.Data); + } } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/AskFm.BLL.csproj b/AskFm/AskFm.BLL/AskFm.BLL.csproj index bc89689..5bfac97 100644 --- a/AskFm/AskFm.BLL/AskFm.BLL.csproj +++ b/AskFm/AskFm.BLL/AskFm.BLL.csproj @@ -9,9 +9,8 @@ + - - diff --git a/AskFm/AskFm.BLL/DTO/AnswerThreadDto.cs b/AskFm/AskFm.BLL/DTO/AnswerThreadDto.cs new file mode 100644 index 0000000..b6a118e --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/AnswerThreadDto.cs @@ -0,0 +1,6 @@ +namespace AskFm.BLL.DTO; + +public class AnswerThreadDto +{ + public string AnswerContent { get; set; } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/PagedResponseDto.cs b/AskFm/AskFm.BLL/DTO/PagedResponseDto.cs new file mode 100644 index 0000000..3b693f6 --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/PagedResponseDto.cs @@ -0,0 +1,10 @@ +namespace AskFm.BLL.DTO; + +public class PagedResponseDto +{ + public List Items { get; set; } + public int PageNumber { get; set; } + public int PageSize { get; set; } + public int TotalCount { get; set; } + public int TotalPages { get; set; } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/ThreadAnswerDto.cs b/AskFm/AskFm.BLL/DTO/ThreadAnswerDto.cs deleted file mode 100644 index cf68455..0000000 --- a/AskFm/AskFm.BLL/DTO/ThreadAnswerDto.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace AskFm.BLL.DTO; - -public class ThreadAnswerDto -{ - public int ThreadId {get;set;} - public string Answer {get;set;} - -} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/ThreadLikeResponseDto.cs b/AskFm/AskFm.BLL/DTO/ThreadLikeResponseDto.cs index 1cc52d5..f5f82fe 100644 --- a/AskFm/AskFm.BLL/DTO/ThreadLikeResponseDto.cs +++ b/AskFm/AskFm.BLL/DTO/ThreadLikeResponseDto.cs @@ -6,7 +6,7 @@ public class ThreadLikeResponseDto public int threadId {get;set;} public DateTime createdAt {get;set;} public string UserName {get;set;} - string ProfilePicture {get;set;} + public string ProfilePicture {get;set;} } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/ThreadResponseDto.cs b/AskFm/AskFm.BLL/DTO/ThreadResponseDto.cs index 1a6775c..c713574 100644 --- a/AskFm/AskFm.BLL/DTO/ThreadResponseDto.cs +++ b/AskFm/AskFm.BLL/DTO/ThreadResponseDto.cs @@ -5,23 +5,16 @@ namespace AskFm.BLL.DTO; public class ThreadResponseDto { public int Id { get; set; } - public string QuestionContent { get; set; } - + public string? AnswerContent { get; set; } public ThreadStatus Status { get; set; } - public bool IsAnonymous { get; set; } - public DateTime CreatedAt { get; set; } - - // Simplified user info instead of full objects - public int AskerId { get; set; } - - public string AskerName { get; set; } - + public int? AskerId { get; set; } + public string? AskerName { get; set; } public int AskedId { get; set; } - - public string AskedName { get; set; } - - public string answer; + public string? AskedName { get; set; } + public int LikesCount { get; set; } + public int CommentsCount { get; set; } + public DateTime? SavedAt { get; set; } } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/CommentLikeService.cs b/AskFm/AskFm.BLL/Services/CommentLikeService.cs index 2033711..a11dd37 100644 --- a/AskFm/AskFm.BLL/Services/CommentLikeService.cs +++ b/AskFm/AskFm.BLL/Services/CommentLikeService.cs @@ -4,7 +4,6 @@ using AskFm.DAL.Models; using AutoMapper; using Microsoft.Extensions.Logging; -using Microsoft.VisualBasic; namespace AskFm.BLL.Services; @@ -111,6 +110,7 @@ public async Task> AddLikeAsync(int commentId, int // otherwise , the user liked the comment , then unliked it , and then wants to like it again existingLike.IsDeleted = false; comment.LikeCount++; + existingLike.CreatedAt = DateTime.Now; _unitOfWork.Comments.Update(comment); await _unitOfWork.SaveAsync(); await transaction.CommitAsync(); @@ -119,7 +119,6 @@ public async Task> AddLikeAsync(int commentId, int // updating the createdAt column to Now , ignoring the first time the user liked the comment - existingLike.CreatedAt = DateTime.Now; return await ServiceResult.Success(new CommentLikeDto { CommentId = existingLike.CommentId, diff --git a/AskFm/AskFm.BLL/Services/IThreadService.cs b/AskFm/AskFm.BLL/Services/IThreadService.cs index 7f550fc..09fa151 100644 --- a/AskFm/AskFm.BLL/Services/IThreadService.cs +++ b/AskFm/AskFm.BLL/Services/IThreadService.cs @@ -1,10 +1,18 @@ using AskFm.BLL.DTO; -using Thread = AskFm.DAL.Models.Thread; +using Microsoft.AspNetCore.Mvc; namespace AskFm.BLL.Services; + public interface IThreadService { - Task> AddThread(int userId, CreateThreadDto createThreadDto); - Task>> GetAllThreads(int userId); - Task> AnswerThread(ThreadAnswerDto threadAnswerDto); + public Task> AddThread(int askerId, CreateThreadDto createThreadDto); + public Task> GetThreadById(int id); + public Task>> GetAllThreads(int askedId); + public Task> AnswerThread(int threadId, int userId, AnswerThreadDto answerDto); + public Task>> GetThreads(int page, int pageSize); + public Task> DeleteThread(int threadId, int userId); + public Task>> GetFeed(int userId, int page, int pageSize); + public Task> SaveThread(int threadId, int userId); + public Task> UnsaveThread(int threadId, int userId); + public Task>> GetSavedThreads(int userId, int page, int pageSize); } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/ThreadLikeService.cs b/AskFm/AskFm.BLL/Services/ThreadLikeService.cs index 622c5aa..29fcbfe 100644 --- a/AskFm/AskFm.BLL/Services/ThreadLikeService.cs +++ b/AskFm/AskFm.BLL/Services/ThreadLikeService.cs @@ -13,10 +13,11 @@ public class ThreadLikeService : IThreadLikeService private readonly ILogger _logger; private readonly IMapper _mapper; - public ThreadLikeService(IUnitOfWork unitOfWork, ILogger logger) + public ThreadLikeService(IUnitOfWork unitOfWork, ILogger logger, IMapper mapper) { _unitOfWork = unitOfWork; _logger = logger; + _mapper = mapper; } // add a like on the Thread that has id = id @@ -55,6 +56,7 @@ public async Task> AddLike(int id, int user // otherwise , the user liked the comment , then unliked it , and then wants to like it again existingLike.IsDeleted = false; + existingLike.CreatedAt = DateTime.Now; thread.ThreadLikes.Add(existingLike); _unitOfWork.Threads.Update(thread); await _unitOfWork.SaveAsync(); @@ -64,7 +66,6 @@ public async Task> AddLike(int id, int user // updating the createdAt column to Now , ignoring the first time the user liked the comment - existingLike.CreatedAt = DateTime.Now; return await ServiceResult.Success(new ThreadLikeResponseDto() { threadId = existingLike.ThreadId, diff --git a/AskFm/AskFm.BLL/Services/ThreadService.cs b/AskFm/AskFm.BLL/Services/ThreadService.cs index c9b46a1..6674763 100644 --- a/AskFm/AskFm.BLL/Services/ThreadService.cs +++ b/AskFm/AskFm.BLL/Services/ThreadService.cs @@ -1,5 +1,7 @@ using AskFm.BLL.DTO; +using AskFm.DAL.Enums; using AskFm.DAL.Interfaces; +using AskFm.DAL.Models; using AutoMapper; using Microsoft.Extensions.Logging; using Thread = AskFm.DAL.Models.Thread; @@ -9,15 +11,16 @@ namespace AskFm.BLL.Services; public class ThreadService : IThreadService { private IUnitOfWork _unitOfWork; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IMapper _mapper; - public ThreadService(IUnitOfWork unitOfWork, ILogger logger) + public ThreadService(IUnitOfWork unitOfWork, ILogger logger, IMapper mapper) { _unitOfWork = unitOfWork; _logger = logger; + _mapper = mapper; } - + public async Task> AddThread(int askerId, CreateThreadDto createThreadDto) { var transaction = await _unitOfWork.BeginTransactionAsync(); @@ -103,86 +106,426 @@ public async Task> AddThread(int askerId, Creat } catch (Exception e) { - await transaction.RollbackAsync(); + _logger.LogError(e, "Error adding thread"); return await ServiceResult.Failure(new List() { e.Message }); } } - public async Task>> GetAllThreads(int userId) + public async Task> GetThreadById(int id) { try { - var user = await _unitOfWork.Users.FindAsync( - predicate: u => u.Id == userId, - includes: new[] { "ReceivedThreads" } + var thread = await _unitOfWork.Threads.FindAsync( + predicate: t => t.Id == id, + includes: new[] { "Asker", "Asked", "Comments", "ThreadLikes" } ); + if (thread == null) + { + return await ServiceResult.Failure(new List() { "Thread not found" }); + } + + var threadDto = new ThreadResponseDto + { + Id = thread.Id, + QuestionContent = thread.QuestionContent, + AnswerContent = thread.AnswerContent, + Status = thread.Status, + IsAnonymous = thread.isAnonymous, + CreatedAt = thread.CreatedAt, + AskerId = thread.AskerId ?? 0, + AskerName = thread.isAnonymous ? "Anonymous" : thread.Asker?.Name, + AskedId = thread.AskedId, + AskedName = thread.Asked?.Name, + LikesCount = thread.ThreadLikes?.Count ?? 0, + CommentsCount = thread.Comments?.Count ?? 0 + }; + + return await ServiceResult.Success(threadDto); + } + catch (Exception e) + { + _logger.LogError(e, "Error retrieving thread"); + return await ServiceResult.Failure(new List() { e.Message }); + } + } + + public async Task>> GetAllThreads(int askedId) + { + try + { + // Get all threads for user + var threads = await _unitOfWork.Threads.FindAllAsync( + predicate: t => t.AskedId == askedId, + includes: new[] { "Asker", "Asked", "Comments", "ThreadLikes" } + ); - if (user == null) + if (threads == null || !threads.Any()) { - return await ServiceResult>.Failure( - new List() { "Could not find user" }); + return await ServiceResult>.Success(new List()); } - var res = user.ReceivedThreads.Select(t => new ThreadResponseDto - { - Id = t.Id, - QuestionContent = t.QuestionContent, - IsAnonymous = t.isAnonymous, - CreatedAt = t.CreatedAt, - AskedId = t.AskedId, - Status = t.Status, - AskedName = t.Asked?.Name ?? "Unknown", - AskerId = t.AskerId.Value, - AskerName = t.Asker?.Name ?? "Unknown" + var threadDtos = threads.Select(thread => new ThreadResponseDto + { + Id = thread.Id, + QuestionContent = thread.QuestionContent, + AnswerContent = thread.AnswerContent, + Status = thread.Status, + IsAnonymous = thread.isAnonymous, + CreatedAt = thread.CreatedAt, + AskerId = thread.AskerId ?? 0, + AskerName = thread.isAnonymous ? "Anonymous" : thread.Asker?.Name, + AskedId = thread.AskedId, + AskedName = thread.Asked?.Name, + LikesCount = thread.ThreadLikes?.Count ?? 0, + CommentsCount = thread.Comments?.Count ?? 0 }).ToList(); - return await ServiceResult>.Success(res); + return await ServiceResult>.Success(threadDtos); } catch (Exception e) { + _logger.LogError(e, "Error retrieving threads"); return await ServiceResult>.Failure(new List() { e.Message }); } } - public async Task> AnswerThread(ThreadAnswerDto threadAnswerDto) + public async Task> AnswerThread(int threadId, int userId, + AnswerThreadDto answerDto) { + var transaction = await _unitOfWork.BeginTransactionAsync(); + try { - // get thread - var threadId = threadAnswerDto.ThreadId; - var thread = await _unitOfWork.Threads.GetByIdAsync(threadId); - // chekc if thread + var thread = await _unitOfWork.Threads.FindAsync( + predicate: t => t.Id == threadId, + includes: new[] { "Asker", "Asked" } + ); + if (thread == null) { - return await ServiceResult.Failure(new List() { "Could not find thread" }); + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() { "Thread not found" }); } - // put the answer on it - thread.AnswerContent = threadAnswerDto.Answer; - // save changes + + if (thread.AskedId != userId) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() + { "User not authorized to answer this thread" }); + } + + thread.AnswerContent = answerDto.AnswerContent; + thread.Status = ThreadStatus.Answered; + await _unitOfWork.Threads.UpdateAsync(thread); await _unitOfWork.SaveAsync(); - ThreadResponseDto threadRes = new ThreadResponseDto() + await transaction.CommitAsync(); + + var threadDto = new ThreadResponseDto { Id = thread.Id, QuestionContent = thread.QuestionContent, + AnswerContent = thread.AnswerContent, + Status = thread.Status, IsAnonymous = thread.isAnonymous, CreatedAt = thread.CreatedAt, + AskerId = thread.AskerId ?? 0, + AskerName = thread.isAnonymous ? "Anonymous" : thread.Asker?.Name, AskedId = thread.AskedId, - AskedName = thread?.Asked.Name ?? "Unknown", - AskerId = thread.AskerId.Value, - AskerName = thread.isAnonymous? "Unknown" : thread?.Asker.Name, - answer = thread.AnswerContent, - Status = thread.Status, + AskedName = thread.Asked?.Name }; - return await ServiceResult.Success(threadRes); + + return await ServiceResult.Success(threadDto); } catch (Exception e) { + await transaction.RollbackAsync(); + _logger.LogError(e, "Error answering thread"); return await ServiceResult.Failure(new List() { e.Message }); } } + public async Task>> GetThreads(int page, int pageSize) + { + try + { + int skipCount = (page - 1) * pageSize; + + var totalCount = await _unitOfWork.Threads.CountAsync(); + + var threads = await _unitOfWork.Threads.GetPagedAsync( + skipCount, + pageSize, + t => t.CreatedAt, + false, + t => true, + new[] { "Asker", "Asked", "Comments", "ThreadLikes" } + ); + + var threadDtos = threads.Select(thread => new ThreadResponseDto + { + Id = thread.Id, + QuestionContent = thread.QuestionContent, + AnswerContent = thread.AnswerContent, + Status = thread.Status, + IsAnonymous = thread.isAnonymous, + CreatedAt = thread.CreatedAt, + AskerId = thread.AskerId ?? 0, + AskerName = thread.isAnonymous ? "Anonymous" : thread.Asker?.Name, + AskedId = thread.AskedId, + AskedName = thread.Asked?.Name, + LikesCount = thread.ThreadLikes?.Count ?? 0, + CommentsCount = thread.Comments?.Count ?? 0 + }).ToList(); + + var response = new PagedResponseDto + { + Items = threadDtos, + PageNumber = page, + PageSize = pageSize, + TotalCount = totalCount, + TotalPages = (int)Math.Ceiling((double)totalCount / pageSize) + }; + + return await ServiceResult>.Success(response); + } + catch (Exception e) + { + _logger.LogError(e, "Error retrieving threads"); + return await ServiceResult>.Failure(new List() { e.Message }); + } + } + + public async Task> DeleteThread(int threadId, int userId) + { + var transaction = await _unitOfWork.BeginTransactionAsync(); + + try + { + // Get the thread + var thread = await _unitOfWork.Threads.FindAsync(t => t.Id == threadId); + if (thread == null) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() { "Thread not found" }); + } + + // Check if user is authorized to delete this thread (either asker or asked) + if (thread.AskerId != userId && thread.AskedId != userId) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() + { "User not authorized to delete this thread" }); + } + + // Delete the thread + await _unitOfWork.Threads.RemoveAsync(thread); + await _unitOfWork.SaveAsync(); + + await transaction.CommitAsync(); + + return await ServiceResult.Success(true); + } + catch (Exception e) + { + await transaction.RollbackAsync(); + _logger.LogError(e, "Error deleting thread"); + return await ServiceResult.Failure(new List() { e.Message }); + } + } + + public async Task>> GetFeed(int userId, int page, int pageSize) + { + try + { + int skipCount = (page - 1) * pageSize; + + var followedUsers = await _unitOfWork.Follows.FindAllAsync(f => f.FollowerId == userId); + var followedUserIds = followedUsers.Select(f => f.FollowedId).ToList(); + + followedUserIds.Add(userId); + + var totalCount = await _unitOfWork.Threads.CountAsync(t => + followedUserIds.Contains(t.AskedId) && t.Status == ThreadStatus.Answered); + + var threads = await _unitOfWork.Threads.GetPagedAsync( + skipCount, + pageSize, + t => t.CreatedAt, + false, + t => followedUserIds.Contains(t.AskedId) && t.Status == ThreadStatus.Answered, + new[] { "Asker", "Asked", "Comments", "ThreadLikes" } + ); + + var threadDtos = threads.Select(thread => new ThreadResponseDto + { + Id = thread.Id, + QuestionContent = thread.QuestionContent, + AnswerContent = thread.AnswerContent, + Status = thread.Status, + IsAnonymous = thread.isAnonymous, + CreatedAt = thread.CreatedAt, + AskerId = thread.AskerId ?? 0, + AskerName = thread.isAnonymous ? "Anonymous" : thread.Asker?.Name, + AskedId = thread.AskedId, + AskedName = thread.Asked?.Name, + LikesCount = thread.ThreadLikes?.Count ?? 0, + CommentsCount = thread.Comments?.Count ?? 0 + }).ToList(); + + var response = new PagedResponseDto + { + Items = threadDtos, + PageNumber = page, + PageSize = pageSize, + TotalCount = totalCount, + TotalPages = (int)Math.Ceiling((double)totalCount / pageSize) + }; + + return await ServiceResult>.Success(response); + } + catch (Exception e) + { + _logger.LogError(e, "Error retrieving feed"); + return await ServiceResult>.Failure(new List() { e.Message }); + } + } + + public async Task> SaveThread(int threadId, int userId) + { + var transaction = await _unitOfWork.BeginTransactionAsync(); + + try + { + // Check if thread exists + var thread = await _unitOfWork.Threads.FindAsync(t => t.Id == threadId); + if (thread == null) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() { "Thread not found" }); + } + + // Check if thread is already saved + var existingSave = await _unitOfWork.SavedThreads.FindAsync( + st => st.SavedThreadId == threadId && st.UserId == userId + ); + + if (existingSave != null) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() { "Thread already saved" }); + } + + // Create saved thread + var savedThread = new SavedThreads + { + SavedThreadId = threadId, + UserId = userId, + CreatedAt = DateTime.Now + }; + + // Add saved thread + await _unitOfWork.SavedThreads.AddAsync(savedThread); + await _unitOfWork.SaveAsync(); + + await transaction.CommitAsync(); + + return await ServiceResult.Success(true); + } + catch (Exception e) + { + await transaction.RollbackAsync(); + _logger.LogError(e, "Error saving thread"); + return await ServiceResult.Failure(new List() { e.Message }); + } + } + + public async Task> UnsaveThread(int threadId, int userId) + { + var transaction = await _unitOfWork.BeginTransactionAsync(); + + try + { + // Find saved thread + var savedThread = await _unitOfWork.SavedThreads.FindAsync( + st => st.SavedThreadId == threadId && st.UserId == userId + ); + + if (savedThread == null) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() { "Thread not saved" }); + } + + // Remove saved thread + await _unitOfWork.SavedThreads.RemoveAsync(savedThread); + await _unitOfWork.SaveAsync(); + + await transaction.CommitAsync(); + + return await ServiceResult.Success(true); + } + catch (Exception e) + { + await transaction.RollbackAsync(); + _logger.LogError(e, "Error unsaving thread"); + return await ServiceResult.Failure(new List() { e.Message }); + } + } + + public async Task>> GetSavedThreads(int userId, int page, + int pageSize) + { + try + { + int skipCount = (page - 1) * pageSize; + + var totalCount = await _unitOfWork.SavedThreads.CountAsync(st => st.UserId == userId); + + var savedThreads = await _unitOfWork.SavedThreads.GetPagedAsync( + skipCount, + pageSize, + st => st.CreatedAt, + false, + st => st.UserId == userId, + new[] { "Thread", "Thread.Asker", "Thread.Asked", "Thread.Comments", "Thread.ThreadLikes" } + ); + + var threadDtos = savedThreads.Select(st => new ThreadResponseDto + { + Id = st.Thread.Id, + QuestionContent = st.Thread.QuestionContent, + AnswerContent = st.Thread.AnswerContent, + Status = st.Thread.Status, + IsAnonymous = st.Thread.isAnonymous, + CreatedAt = st.Thread.CreatedAt, + AskerId = st.Thread.AskerId ?? 0, + AskerName = st.Thread.isAnonymous ? "Anonymous" : st.Thread.Asker?.Name, + AskedId = st.Thread.AskedId, + AskedName = st.Thread.Asked?.Name, + LikesCount = st.Thread.ThreadLikes?.Count ?? 0, + CommentsCount = st.Thread.Comments?.Count ?? 0, + SavedAt = st.CreatedAt + }).ToList(); + + var response = new PagedResponseDto + { + Items = threadDtos, + PageNumber = page, + PageSize = pageSize, + TotalCount = totalCount, + TotalPages = (int)Math.Ceiling((double)totalCount / pageSize) + }; + + return await ServiceResult>.Success(response); + } + catch (Exception e) + { + _logger.LogError(e, "Error retrieving saved threads"); + return await ServiceResult>.Failure(new List() { e.Message }); + } + } } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Enums/ThreadStatus.cs b/AskFm/AskFm.DAL/Enums/ThreadStatus.cs index 8bd4d98..7075457 100644 --- a/AskFm/AskFm.DAL/Enums/ThreadStatus.cs +++ b/AskFm/AskFm.DAL/Enums/ThreadStatus.cs @@ -4,5 +4,7 @@ public enum ThreadStatus PRIVATE, PUBLIC, Closed, - PRIVATEQUESTION + PRIVATEQUESTION, + Answered, + Pending } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Interfaces/IRepository.cs b/AskFm/AskFm.DAL/Interfaces/IRepository.cs index 27b6962..469cd08 100644 --- a/AskFm/AskFm.DAL/Interfaces/IRepository.cs +++ b/AskFm/AskFm.DAL/Interfaces/IRepository.cs @@ -1,4 +1,5 @@ using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore; namespace AskFm.DAL.Interfaces; @@ -6,31 +7,39 @@ public interface IRepository where T : class { IQueryable GetAll(); Task> GetAllAsync(); - - + + T? GetById(int id); Task GetByIdAsync(int id); - - + + IQueryable FindAll(Expression> predicate, string[] includes = null); Task> FindAllAsync(Expression> predicate = null, string[] includes = null); - - + + T? Find(Expression> predicate, string[] includes = null); Task FindAsync(Expression> predicate, string[] includes = null); - - + + void Add(T entity); Task AddAsync(T entity); - - + + void Update(T entity); Task UpdateAsync(T entity); - - + + void Remove(T entity); Task RemoveAsync(T entity); - - + + Task CountAsync(Expression>? predicate = null); + + Task> GetPagedAsync( + int skip, + int take, + Expression> orderBy, + bool ascending = true, + Expression>? predicate = null, + string[]? includes = null); } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Repositories/Repository.cs b/AskFm/AskFm.DAL/Repositories/Repository.cs index e928038..7b4e8fd 100644 --- a/AskFm/AskFm.DAL/Repositories/Repository.cs +++ b/AskFm/AskFm.DAL/Repositories/Repository.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore; namespace AskFm.DAL.Repositories; -public class Repository : IRepository where T : class +public class Repository : IRepository where T : class { protected readonly AppDbContext _context; private readonly DbSet _dbSet; @@ -13,21 +13,21 @@ public Repository(AppDbContext context) _context = context; _dbSet = context.Set(); } - - - + + + public IQueryable GetAll() => _dbSet.AsQueryable(); public async Task> GetAllAsync() => await _dbSet.ToListAsync(); - - - - public T? GetById(int id) => _dbSet.Find(id); + + + + public T? GetById(int id) => _dbSet.Find(id); public async Task GetByIdAsync(int id) => await _dbSet.FindAsync(id); - - - - + + + + public IQueryable FindAll(Expression> predicate, string[] includes = null) { IQueryable query = _dbSet; @@ -50,14 +50,14 @@ public async Task> FindAllAsync(Expression> predica return await query.Where(predicate).ToListAsync(); } - - - - + + + + public T? Find(Expression> predicate, string[] includes = null) { IQueryable query = _dbSet; - + if (includes != null) { foreach (var include in includes) @@ -65,14 +65,14 @@ public async Task> FindAllAsync(Expression> predica query = query.Include(include); } } - - return query.FirstOrDefault(predicate); + + return query.FirstOrDefault(predicate); } - + public async Task FindAsync(Expression> predicate, string[] includes = null) { IQueryable query = _dbSet; - + if (includes != null) { foreach (var include in includes) @@ -80,29 +80,29 @@ public async Task> FindAllAsync(Expression> predica query = query.Include(include); } } - + return await query.FirstOrDefaultAsync(predicate); } - - - - + + + + public void Add(T entity) => _dbSet.Add(entity); public async Task AddAsync(T entity) => await _dbSet.AddAsync(entity); - - - - + + + + public void Update(T entity) => _dbSet.Update(entity); - + public Task UpdateAsync(T entity) { _dbSet.Update(entity); return Task.CompletedTask; } - - + + public void Remove(T entity) => _dbSet.Remove(entity); public Task RemoveAsync(T entity) @@ -111,4 +111,50 @@ public Task RemoveAsync(T entity) return Task.CompletedTask; } + public async Task CountAsync(Expression>? predicate = null) + { + IQueryable query = _dbSet; + + if (predicate != null) + { + query = query.Where(predicate); + } + + return await query.CountAsync(); + } + + public async Task> GetPagedAsync( + int skip, + int take, + Expression> orderBy, + bool ascending = true, + Expression>? predicate = null, + string[]? includes = null) + { + IQueryable query = _dbSet; + + if (predicate != null) + { + query = query.Where(predicate); + } + + if (includes != null) + { + foreach (var include in includes) + { + query = query.Include(include); + } + } + + if (ascending) + { + query = query.OrderBy(orderBy); + } + else + { + query = query.OrderByDescending(orderBy); + } + + return await query.Skip(skip).Take(take).ToListAsync(); + } } \ No newline at end of file From bb19796162ee66715ce485c015914105adbd1342 Mon Sep 17 00:00:00 2001 From: Ziad Ashraf Date: Sat, 11 Oct 2025 22:34:33 +0300 Subject: [PATCH 6/7] Finish the Comment Controller and service section --- .../Controllers/CommentController.cs | 97 +++++--- .../AskFm.API/Controllers/ThreadController.cs | 4 +- .../Controllers/ThreadLikeController.cs | 4 +- .../Controllers/WeatherForecastController.cs | 2 +- AskFm/AskFm.BLL/DTO/CommentResponseDto.cs | 13 ++ AskFm/AskFm.BLL/DTO/CreateCommentDto.cs | 6 + AskFm/AskFm.BLL/DTO/RefreshTokenDto.cs | 2 +- AskFm/AskFm.BLL/DTO/ThreadLikeResponseDto.cs | 6 +- .../AskFm.BLL/Services/CommentLikeService.cs | 2 +- AskFm/AskFm.BLL/Services/CommentService.cs | 217 +++++++++++++++++- AskFm/AskFm.BLL/Services/ICommentService.cs | 13 +- AskFm/AskFm.BLL/Services/IThreadService.cs | 1 - AskFm/AskFm.BLL/Services/ThreadLikeService.cs | 22 +- AskFm/AskFm.BLL/Services/ThreadService.cs | 4 +- .../UserIdentityService/UserService.cs | 2 +- AskFm/AskFm.DAL/Repositories/Repository.cs | 20 +- 16 files changed, 338 insertions(+), 77 deletions(-) create mode 100644 AskFm/AskFm.BLL/DTO/CommentResponseDto.cs create mode 100644 AskFm/AskFm.BLL/DTO/CreateCommentDto.cs diff --git a/AskFm/AskFm.API/Controllers/CommentController.cs b/AskFm/AskFm.API/Controllers/CommentController.cs index 21887c9..3af4fa5 100644 --- a/AskFm/AskFm.API/Controllers/CommentController.cs +++ b/AskFm/AskFm.API/Controllers/CommentController.cs @@ -13,13 +13,11 @@ namespace AskFm.API.Controllers; [Authorize(AuthenticationSchemes = "Bearer")] public class CommentController : ControllerBase { - private readonly ICommentLikeService _commentLikeService; private readonly ICommentService _commentService; private readonly ILogger _logger; private readonly IUserService _userService; - - + public CommentController( ICommentLikeService commentLikeService, ICommentService commentService, @@ -31,11 +29,7 @@ public CommentController( _commentService = commentService; _userService = userService; } - - - - - + // GET api/comment/{id}/likes -> get all the likes for a Comment with id = id [HttpGet("{id}/likes")] public async Task GetAllLikes(int id) @@ -55,7 +49,6 @@ public async Task GetAllLikes(int id) return NotFound(new { message = ex.Message, - }); } catch (Exception ex) @@ -65,9 +58,6 @@ public async Task GetAllLikes(int id) } } - - - // POST api/comment/{id}/likes -> add a like for a Comment with id = id [HttpPost("{id}/likes")] public async Task AddLike(int id) @@ -75,21 +65,21 @@ public async Task AddLike(int id) try { var user = await _userService.GetCurrentUserAsync(); - + if (!user.success) { return BadRequest(user.Errors); } - + var createdLike = await _commentLikeService.AddLikeAsync(id, user.Data.Id); - + if (!createdLike.success) { return BadRequest(createdLike.Errors); } return CreatedAtAction( - nameof(GetAllLikes), - new { id = id }, + nameof(GetAllLikes), + new { id = id }, createdLike.Data); } catch (ArgumentException ex) @@ -108,37 +98,31 @@ public async Task AddLike(int id) return StatusCode(500, new { message = "An error occurred while adding like" }); } } - - - - + [HttpDelete("{id}/likes")] public async Task DeleteLike(int id) { int userId = 0; try { - var user = await _userService.GetCurrentUserAsync(); - if (user==null || !user.success) + if (user == null || !user.success) return BadRequest(user.Errors); - - + userId = user.Data.Id; var comment = await _commentService.GetCommentAsync(id); - + if (comment == null || !user.success) return BadRequest(user.Errors); - - + var result = await _commentLikeService.DeleteLikeAsync(id, userId); if (!result.success) { return BadRequest(result.Errors); } - + return NoContent(); } catch (ArgumentException ex) @@ -157,5 +141,58 @@ public async Task DeleteLike(int id) return StatusCode(500, new { message = "An error occurred while deleting like" }); } } - + + // POST api/threads/{id}/comments - Add a comment to the thread with id = {id} + [HttpPost] + [Route("threads/{id}/comments")] + public async Task AddComment([FromRoute] int id, [FromBody] CreateCommentDto createCommentDto) + { + var user = await _userService.GetCurrentUserAsync(); + if (!user.success) + { + return BadRequest(user.Errors); + } + + var result = await _commentService.AddComment(id, user.Data.Id, createCommentDto); + if (!result.success) + { + return BadRequest(result.Errors); + } + + return Ok(result.Data); + } + + // GET api/threads/{id}/comments - Get all comments for the thread with id = {id} + [HttpGet] + [Route("threads/{id}/comments")] + public async Task GetComments([FromRoute] int id, [FromQuery] int page = 1, [FromQuery] int pageSize = 10) + { + var result = await _commentService.GetCommentsByThreadId(id, page, pageSize); + if (!result.success) + { + return BadRequest(result.Errors); + } + + return Ok(result.Data); + } + + // DELETE api/threads/{threadId}/comments/{commentId} - Delete a comment with id = {commentId} + [HttpDelete] + [Route("threads/{threadId}/comments/{commentId}")] + public async Task DeleteComment([FromRoute] int threadId, [FromRoute] int commentId) + { + var user = await _userService.GetCurrentUserAsync(); + if (!user.success) + { + return BadRequest(user.Errors); + } + + var result = await _commentService.DeleteComment(threadId, commentId, user.Data.Id); + if (!result.success) + { + return BadRequest(result.Errors); + } + + return Ok(new { message = "Comment deleted successfully" }); + } } \ No newline at end of file diff --git a/AskFm/AskFm.API/Controllers/ThreadController.cs b/AskFm/AskFm.API/Controllers/ThreadController.cs index c995dee..d82796f 100644 --- a/AskFm/AskFm.API/Controllers/ThreadController.cs +++ b/AskFm/AskFm.API/Controllers/ThreadController.cs @@ -12,12 +12,12 @@ namespace AskFm.API.Controllers; [Authorize(AuthenticationSchemes = "Bearer")] public class ThreadController : ControllerBase { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IUserService _userService; private readonly IThreadService _threadService; public ThreadController( - ILogger logger, + ILogger logger, IUserService userService, IThreadService threadService) { diff --git a/AskFm/AskFm.API/Controllers/ThreadLikeController.cs b/AskFm/AskFm.API/Controllers/ThreadLikeController.cs index 0ba9367..e861388 100644 --- a/AskFm/AskFm.API/Controllers/ThreadLikeController.cs +++ b/AskFm/AskFm.API/Controllers/ThreadLikeController.cs @@ -12,12 +12,12 @@ namespace AskFm.API.Controllers; [Authorize(AuthenticationSchemes = "Bearer")] public class ThreadLikeController : ControllerBase { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IUserService _userService; private readonly IThreadLikeService _threadLikeService; public ThreadLikeController( - ILogger logger, + ILogger logger, IUserService userService, IThreadLikeService threadService) { diff --git a/AskFm/AskFm.API/Controllers/WeatherForecastController.cs b/AskFm/AskFm.API/Controllers/WeatherForecastController.cs index a2f0751..bf50c5f 100644 --- a/AskFm/AskFm.API/Controllers/WeatherForecastController.cs +++ b/AskFm/AskFm.API/Controllers/WeatherForecastController.cs @@ -23,7 +23,7 @@ public IEnumerable Get() { return Enumerable.Range(1, 5).Select(index => new WeatherForecast { - Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + Date = DateOnly.FromDateTime(DateTime.UtcNow.AddDays(index)), TemperatureC = Random.Shared.Next(-20, 55), Summary = Summaries[Random.Shared.Next(Summaries.Length)] }) diff --git a/AskFm/AskFm.BLL/DTO/CommentResponseDto.cs b/AskFm/AskFm.BLL/DTO/CommentResponseDto.cs new file mode 100644 index 0000000..ea64316 --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/CommentResponseDto.cs @@ -0,0 +1,13 @@ +namespace AskFm.BLL.DTO; + +public class CommentResponseDto +{ + public int Id { get; set; } + public string Content { get; set; } + public int? UserId { get; set; } + public string UserName { get; set; } + public int ThreadId { get; set; } + public DateTime CreatedAt { get; set; } + public int LikesCount { get; set; } + public bool IsLiked { get; set; } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/CreateCommentDto.cs b/AskFm/AskFm.BLL/DTO/CreateCommentDto.cs new file mode 100644 index 0000000..51d8ae3 --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/CreateCommentDto.cs @@ -0,0 +1,6 @@ +namespace AskFm.BLL.DTO; + +public class CreateCommentDto +{ + public string Content { get; set; } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/RefreshTokenDto.cs b/AskFm/AskFm.BLL/DTO/RefreshTokenDto.cs index cf524b2..68a00a1 100644 --- a/AskFm/AskFm.BLL/DTO/RefreshTokenDto.cs +++ b/AskFm/AskFm.BLL/DTO/RefreshTokenDto.cs @@ -5,7 +5,7 @@ public class RefreshTokenDto { public string Token { get; set; } public DateTime ExpireOn { get; set; } - public bool IsExpired => DateTime.Now >= ExpireOn; + public bool IsExpired => DateTime.UtcNow >= ExpireOn; public int ExpireAfter { get; set; } public DateTime CreatedOn { get; set; } } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/ThreadLikeResponseDto.cs b/AskFm/AskFm.BLL/DTO/ThreadLikeResponseDto.cs index f5f82fe..dc0be27 100644 --- a/AskFm/AskFm.BLL/DTO/ThreadLikeResponseDto.cs +++ b/AskFm/AskFm.BLL/DTO/ThreadLikeResponseDto.cs @@ -2,9 +2,9 @@ namespace AskFm.BLL.DTO; public class ThreadLikeResponseDto { - public int userId {get;set;} - public int threadId {get;set;} - public DateTime createdAt {get;set;} + public int UserId {get;set;} + public int ThreadId {get;set;} + public DateTime CreatedAt {get;set;} public string UserName {get;set;} public string ProfilePicture {get;set;} diff --git a/AskFm/AskFm.BLL/Services/CommentLikeService.cs b/AskFm/AskFm.BLL/Services/CommentLikeService.cs index a11dd37..27d0a09 100644 --- a/AskFm/AskFm.BLL/Services/CommentLikeService.cs +++ b/AskFm/AskFm.BLL/Services/CommentLikeService.cs @@ -110,7 +110,7 @@ public async Task> AddLikeAsync(int commentId, int // otherwise , the user liked the comment , then unliked it , and then wants to like it again existingLike.IsDeleted = false; comment.LikeCount++; - existingLike.CreatedAt = DateTime.Now; + existingLike.CreatedAt = DateTime.UtcNow; _unitOfWork.Comments.Update(comment); await _unitOfWork.SaveAsync(); await transaction.CommitAsync(); diff --git a/AskFm/AskFm.BLL/Services/CommentService.cs b/AskFm/AskFm.BLL/Services/CommentService.cs index 81f7c98..8f55704 100644 --- a/AskFm/AskFm.BLL/Services/CommentService.cs +++ b/AskFm/AskFm.BLL/Services/CommentService.cs @@ -1,28 +1,229 @@ +using AskFm.BLL.DTO; using AskFm.DAL.Interfaces; using AskFm.DAL.Models; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; namespace AskFm.BLL.Services; public class CommentService : ICommentService { - private IUnitOfWork _unitOfWork; - public CommentService(IUnitOfWork unitOfWork) + private readonly IUnitOfWork _unitOfWork; + private readonly ILogger _logger; + + public CommentService(IUnitOfWork unitOfWork, ILogger logger) { _unitOfWork = unitOfWork; + _logger = logger; } - - public async Task> GetCommentAsync(int commentId) + + public async Task> GetCommentAsync(int id) { try { - var comment = await _unitOfWork.Comments.GetByIdAsync(commentId); - return await ServiceResult.Success(comment); + var comment = await _unitOfWork.Comments.FindAsync( + c => c.Id == id, + new[] { "User", "CommentLikes" } + ); + + if (comment == null) + { + return await ServiceResult.Failure(new List() { "Comment not found" }); + } + + var commentDto = new CommentResponseDto + { + Id = comment.Id, + Content = comment.Content, + UserId = comment.UserId, + UserName = comment.User?.Name ?? "Unknown", + ThreadId = comment.ThreadId, + CreatedAt = comment.CreatedAt, + LikesCount = comment.CommentLikes?.Count ?? 0, + }; + + return await ServiceResult.Success(commentDto); } catch (Exception e) { - return await ServiceResult.Failure(new List(){e.Message}); + _logger.LogError(e, "Error retrieving comment with ID {Id}", id); + return await ServiceResult.Failure(new List() { e.Message }); } } - + public async Task> AddComment(int threadId, int userId, CreateCommentDto commentDto) + { + var transaction = await _unitOfWork.BeginTransactionAsync(); + + try + { + // Check if thread exists + var thread = await _unitOfWork.Threads.FindAsync(t => t.Id == threadId); + if (thread == null) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() { "Thread not found" }); + } + + // Check if user exists + var user = await _unitOfWork.Users.FindAsync(u => u.Id == userId); + if (user == null) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() { "User not found" }); + } + + // Validate comment content + if (string.IsNullOrWhiteSpace(commentDto.Content)) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() { "Comment content cannot be empty" }); + } + + // Create comment + var comment = new Comment + { + Content = commentDto.Content, + UserId = userId, + ThreadId = threadId, + CreatedAt = DateTime.UtcNow, + CommentLikes = new List() + }; + + // Add comment to database + await _unitOfWork.Comments.AddAsync(comment); + await _unitOfWork.SaveAsync(); + + await transaction.CommitAsync(); + + // Return response dto + var commentResponseDto = new CommentResponseDto + { + Id = comment.Id, + Content = comment.Content, + UserId = comment.UserId, + UserName = user.Name, + ThreadId = comment.ThreadId, + CreatedAt = comment.CreatedAt, + LikesCount = 0, + }; + + return await ServiceResult.Success(commentResponseDto); + } + catch (Exception e) + { + await transaction.RollbackAsync(); + _logger.LogError(e, "Error adding comment to thread {ThreadId}", threadId); + return await ServiceResult.Failure(new List() { e.Message }); + } + } + + public async Task>> GetCommentsByThreadId(int threadId, int page, int pageSize) + { + try + { + // Check if thread exists + var thread = await _unitOfWork.Threads.FindAsync(t => t.Id == threadId); + if (thread == null) + { + return await ServiceResult>.Failure(new List() { "Thread not found" }); + } + + // Get total count of comments for this thread + var totalCount = await _unitOfWork.Comments.CountAsync(c => c.ThreadId == threadId); + + // Get paginated comments + var comments = await _unitOfWork.Comments.GetPagedAsync( + skip: (page - 1) * pageSize, + take: pageSize, + orderBy: c => c.CreatedAt, + ascending: false, // Newest first + predicate: c => c.ThreadId == threadId, + includes: new[] { "User", "CommentLikes" } + ); + + var commentDtos = comments.Select(c => new CommentResponseDto + { + Id = c.Id, + Content = c.Content, + UserId = c.UserId, + UserName = c.User?.Name ?? "Unknown", + ThreadId = c.ThreadId, + CreatedAt = c.CreatedAt, + LikesCount = c.CommentLikes?.Count ?? 0, + }).ToList(); + + // Create paged response + var response = new PagedResponseDto + { + Items = commentDtos, + PageNumber = page, + PageSize = pageSize, + TotalCount = totalCount, + TotalPages = (int)Math.Ceiling((double)totalCount / pageSize) + }; + + return await ServiceResult>.Success(response); + } + catch (Exception e) + { + _logger.LogError(e, "Error retrieving comments for thread {ThreadId}", threadId); + return await ServiceResult>.Failure(new List() { e.Message }); + } + } + + public async Task> DeleteComment(int threadId, int commentId, int userId) + { + var transaction = await _unitOfWork.BeginTransactionAsync(); + + try + { + // Check if comment exists + var comment = await _unitOfWork.Comments.FindAsync( + c => c.Id == commentId && c.ThreadId == threadId, + new[] { "CommentLikes" } + ); + + if (comment == null) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() { "Comment not found" }); + } + + // Check if user is authorized to delete (either comment owner or thread owner) + var thread = await _unitOfWork.Threads.FindAsync(t => t.Id == threadId); + + if (comment.UserId != userId && thread.AskedId != userId && thread.AskerId != userId) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure(new List() { "User not authorized to delete this comment" }); + } + + // Remove comment likes first + if (comment.CommentLikes != null && comment.CommentLikes.Any()) + { + foreach (var like in comment.CommentLikes.ToList()) + { + await _unitOfWork.CommentLikes.RemoveAsync(like); + } + } + + // Remove comment + await _unitOfWork.Comments.RemoveAsync(comment); + await _unitOfWork.SaveAsync(); + + await transaction.CommitAsync(); + + return await ServiceResult.Success(true); + } + catch (Exception e) + { + await transaction.RollbackAsync(); + _logger.LogError(e, "Error deleting comment {CommentId} from thread {ThreadId}", commentId, threadId); + return await ServiceResult.Failure(new List() { e.Message }); + } + } } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/ICommentService.cs b/AskFm/AskFm.BLL/Services/ICommentService.cs index dda8314..8c58275 100644 --- a/AskFm/AskFm.BLL/Services/ICommentService.cs +++ b/AskFm/AskFm.BLL/Services/ICommentService.cs @@ -1,8 +1,19 @@ +using AskFm.BLL.DTO; using AskFm.DAL.Models; namespace AskFm.BLL.Services; public interface ICommentService { - Task> GetCommentAsync(int commentId); + // Get a specific comment by ID + Task> GetCommentAsync(int id); + + // Add a comment to a thread + Task> AddComment(int threadId, int userId, CreateCommentDto commentDto); + + // Get all comments for a thread with pagination + Task>> GetCommentsByThreadId(int threadId, int page, int pageSize); + + // Delete a specific comment + Task> DeleteComment(int threadId, int commentId, int userId); } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/IThreadService.cs b/AskFm/AskFm.BLL/Services/IThreadService.cs index 09fa151..d329412 100644 --- a/AskFm/AskFm.BLL/Services/IThreadService.cs +++ b/AskFm/AskFm.BLL/Services/IThreadService.cs @@ -1,5 +1,4 @@ using AskFm.BLL.DTO; -using Microsoft.AspNetCore.Mvc; namespace AskFm.BLL.Services; diff --git a/AskFm/AskFm.BLL/Services/ThreadLikeService.cs b/AskFm/AskFm.BLL/Services/ThreadLikeService.cs index 29fcbfe..27facb4 100644 --- a/AskFm/AskFm.BLL/Services/ThreadLikeService.cs +++ b/AskFm/AskFm.BLL/Services/ThreadLikeService.cs @@ -56,7 +56,7 @@ public async Task> AddLike(int id, int user // otherwise , the user liked the comment , then unliked it , and then wants to like it again existingLike.IsDeleted = false; - existingLike.CreatedAt = DateTime.Now; + existingLike.CreatedAt = DateTime.UtcNow; thread.ThreadLikes.Add(existingLike); _unitOfWork.Threads.Update(thread); await _unitOfWork.SaveAsync(); @@ -68,9 +68,9 @@ public async Task> AddLike(int id, int user // updating the createdAt column to Now , ignoring the first time the user liked the comment return await ServiceResult.Success(new ThreadLikeResponseDto() { - threadId = existingLike.ThreadId, - userId = existingLike.UserId, - createdAt = existingLike.CreatedAt + ThreadId = existingLike.ThreadId, + UserId = existingLike.UserId, + CreatedAt = existingLike.CreatedAt }); } @@ -82,7 +82,7 @@ public async Task> AddLike(int id, int user { ThreadId = id, UserId = userId, - CreatedAt = DateTime.Now + CreatedAt = DateTime.UtcNow }; // add the ThreadLike to the Thread @@ -97,9 +97,9 @@ public async Task> AddLike(int id, int user // create and return the response DTO var response = new ThreadLikeResponseDto { - threadId = threadLike.ThreadId, - userId = threadLike.UserId, - createdAt = threadLike.CreatedAt + ThreadId = threadLike.ThreadId, + UserId = threadLike.UserId, + CreatedAt = threadLike.CreatedAt }; return await ServiceResult.Success(response); @@ -136,10 +136,10 @@ public async Task>> GetLikes(int id) // return the list of likes var likes = thread.ThreadLikes.Select(like => new ThreadLikeResponseDto { - threadId = like.ThreadId, - userId = like.UserId, + ThreadId = like.ThreadId, + UserId = like.UserId, UserName = like.User?.Name ?? "Unknown", - createdAt = like.CreatedAt + CreatedAt = like.CreatedAt }).ToList(); return await ServiceResult>.Success(likes); diff --git a/AskFm/AskFm.BLL/Services/ThreadService.cs b/AskFm/AskFm.BLL/Services/ThreadService.cs index 6674763..4552a5a 100644 --- a/AskFm/AskFm.BLL/Services/ThreadService.cs +++ b/AskFm/AskFm.BLL/Services/ThreadService.cs @@ -77,7 +77,7 @@ public async Task> AddThread(int askerId, Creat AnswerContent = "", Status = createThreadDto.Status, isAnonymous = createThreadDto.isAnonymous, - CreatedAt = DateTime.Now, + CreatedAt = DateTime.UtcNow, }; await _unitOfWork.Threads.AddAsync(thread); @@ -424,7 +424,7 @@ public async Task> SaveThread(int threadId, int userId) { SavedThreadId = threadId, UserId = userId, - CreatedAt = DateTime.Now + CreatedAt = DateTime.UtcNow }; // Add saved thread diff --git a/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs b/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs index 7dcc1a9..fdbf633 100644 --- a/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs +++ b/AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs @@ -170,7 +170,7 @@ public async Task> UpdateLastSeenAsync(int userId) await using var transaction = await _unitOfWork.BeginTransactionAsync(); try { - appUser.LastSeen = DateTime.Now; + appUser.LastSeen = DateTime.UtcNow; await _unitOfWork.Users.UpdateAsync(appUser); await _unitOfWork.SaveAsync(); await transaction.CommitAsync(); diff --git a/AskFm/AskFm.DAL/Repositories/Repository.cs b/AskFm/AskFm.DAL/Repositories/Repository.cs index 7b4e8fd..1a3a7c1 100644 --- a/AskFm/AskFm.DAL/Repositories/Repository.cs +++ b/AskFm/AskFm.DAL/Repositories/Repository.cs @@ -1,6 +1,7 @@ using System.Linq.Expressions; using AskFm.DAL.Interfaces; using Microsoft.EntityFrameworkCore; + namespace AskFm.DAL.Repositories; public class Repository : IRepository where T : class @@ -15,19 +16,14 @@ public Repository(AppDbContext context) } - public IQueryable GetAll() => _dbSet.AsQueryable(); public async Task> GetAllAsync() => await _dbSet.ToListAsync(); - - public T? GetById(int id) => _dbSet.Find(id); public async Task GetByIdAsync(int id) => await _dbSet.FindAsync(id); - - public IQueryable FindAll(Expression> predicate, string[] includes = null) { IQueryable query = _dbSet; @@ -36,6 +32,7 @@ public IQueryable FindAll(Expression> predicate, string[] inclu foreach (var include in includes) query = query.Include(include); } + return query.Where(predicate); } @@ -51,9 +48,6 @@ public async Task> FindAllAsync(Expression> predica } - - - public T? Find(Expression> predicate, string[] includes = null) { IQueryable query = _dbSet; @@ -85,15 +79,11 @@ public async Task> FindAllAsync(Expression> predica } - - public void Add(T entity) => _dbSet.Add(entity); public async Task AddAsync(T entity) => await _dbSet.AddAsync(entity); - - public void Update(T entity) => _dbSet.Update(entity); public Task UpdateAsync(T entity) @@ -104,7 +94,7 @@ public Task UpdateAsync(T entity) public void Remove(T entity) => _dbSet.Remove(entity); - + public Task RemoveAsync(T entity) { _dbSet.Remove(entity); @@ -131,6 +121,10 @@ public async Task> GetPagedAsync( Expression>? predicate = null, string[]? includes = null) { + if (skip < 0) + throw new ArgumentOutOfRangeException(nameof(skip), "Skip must be non-negative"); + if (take <= 0) + throw new ArgumentOutOfRangeException(nameof(take), "Take must be positive"); IQueryable query = _dbSet; if (predicate != null) From 767337672222a7944a0790afcca77e34e0c8bbde Mon Sep 17 00:00:00 2001 From: Ziad Ashraf Date: Sat, 29 Nov 2025 20:28:46 +0200 Subject: [PATCH 7/7] refactor(totalCount & totalPages): - removed the totalCount and totalPages from pagedResponseDto as thier uses is so overhead on the database. - instead, replaced them with HasMore, meaning are there any remaining pages left here to make the client or the frontend use as a flage. --- AskFm/AskFm.BLL/DTO/CommentResponseDto.cs | 1 - AskFm/AskFm.BLL/DTO/CreateThreadDto.cs | 6 +- AskFm/AskFm.BLL/DTO/PagedResponseDto.cs | 3 +- AskFm/AskFm.BLL/Services/CommentService.cs | 18 ++--- AskFm/AskFm.BLL/Services/ThreadService.cs | 77 ++++++++++++++++------ 5 files changed, 74 insertions(+), 31 deletions(-) diff --git a/AskFm/AskFm.BLL/DTO/CommentResponseDto.cs b/AskFm/AskFm.BLL/DTO/CommentResponseDto.cs index ea64316..f19df0a 100644 --- a/AskFm/AskFm.BLL/DTO/CommentResponseDto.cs +++ b/AskFm/AskFm.BLL/DTO/CommentResponseDto.cs @@ -9,5 +9,4 @@ public class CommentResponseDto public int ThreadId { get; set; } public DateTime CreatedAt { get; set; } public int LikesCount { get; set; } - public bool IsLiked { get; set; } } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/CreateThreadDto.cs b/AskFm/AskFm.BLL/DTO/CreateThreadDto.cs index 4b078ab..998c944 100644 --- a/AskFm/AskFm.BLL/DTO/CreateThreadDto.cs +++ b/AskFm/AskFm.BLL/DTO/CreateThreadDto.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using AskFm.DAL.Enums; using AskFm.DAL.Models; @@ -5,12 +6,15 @@ namespace AskFm.BLL.DTO; public class CreateThreadDto { + [Required(ErrorMessage = "Asked user ID is required")] public int AskedId { get; set; } + [Required(ErrorMessage = "Question content is required")] + [StringLength(1000, MinimumLength = 2, ErrorMessage = "Question must be between 2 and 1000 characters")] public string QuestionContent { get; set; } public ThreadStatus Status { get; set; } - public bool isAnonymous { get; set; } + public bool IsAnonymous { get; set; } } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/PagedResponseDto.cs b/AskFm/AskFm.BLL/DTO/PagedResponseDto.cs index 3b693f6..e6f0636 100644 --- a/AskFm/AskFm.BLL/DTO/PagedResponseDto.cs +++ b/AskFm/AskFm.BLL/DTO/PagedResponseDto.cs @@ -5,6 +5,5 @@ public class PagedResponseDto public List Items { get; set; } public int PageNumber { get; set; } public int PageSize { get; set; } - public int TotalCount { get; set; } - public int TotalPages { get; set; } + public bool HasMore { get; set; } } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/CommentService.cs b/AskFm/AskFm.BLL/Services/CommentService.cs index 8f55704..6400a88 100644 --- a/AskFm/AskFm.BLL/Services/CommentService.cs +++ b/AskFm/AskFm.BLL/Services/CommentService.cs @@ -132,20 +132,23 @@ public async Task>> GetCommen return await ServiceResult>.Failure(new List() { "Thread not found" }); } - // Get total count of comments for this thread - var totalCount = await _unitOfWork.Comments.CountAsync(c => c.ThreadId == threadId); - + + int skip = (page - 1) * pageSize; // Get paginated comments var comments = await _unitOfWork.Comments.GetPagedAsync( - skip: (page - 1) * pageSize, - take: pageSize, + skip: skip, + take: pageSize + 1, orderBy: c => c.CreatedAt, ascending: false, // Newest first predicate: c => c.ThreadId == threadId, includes: new[] { "User", "CommentLikes" } ); + + bool hasMore = comments.Count > pageSize; + + var trimmed = comments.Take(pageSize).ToList(); - var commentDtos = comments.Select(c => new CommentResponseDto + var commentDtos = trimmed.Select(c => new CommentResponseDto { Id = c.Id, Content = c.Content, @@ -162,8 +165,7 @@ public async Task>> GetCommen Items = commentDtos, PageNumber = page, PageSize = pageSize, - TotalCount = totalCount, - TotalPages = (int)Math.Ceiling((double)totalCount / pageSize) + HasMore = hasMore }; return await ServiceResult>.Success(response); diff --git a/AskFm/AskFm.BLL/Services/ThreadService.cs b/AskFm/AskFm.BLL/Services/ThreadService.cs index 4552a5a..77cb2a7 100644 --- a/AskFm/AskFm.BLL/Services/ThreadService.cs +++ b/AskFm/AskFm.BLL/Services/ThreadService.cs @@ -76,7 +76,7 @@ public async Task> AddThread(int askerId, Creat AnswerContent = "", Status = createThreadDto.Status, - isAnonymous = createThreadDto.isAnonymous, + isAnonymous = createThreadDto.IsAnonymous, CreatedAt = DateTime.UtcNow, }; @@ -97,9 +97,9 @@ public async Task> AddThread(int askerId, Creat IsAnonymous = thread.isAnonymous, CreatedAt = thread.CreatedAt, AskerId = thread.AskerId.Value, - AskerName = thread.Asker?.Name ?? "Unknown", + AskerName = thread.isAnonymous ? "Anonymous" : thread.Asker?.Name, AskedId = thread.AskedId, - AskedName = thread.Asked?.Name ?? "Unknown" + AskedName = askedUser.Name }; return await ServiceResult.Success(responseDto); @@ -116,7 +116,7 @@ public async Task> GetThreadById(int id) try { var thread = await _unitOfWork.Threads.FindAsync( - predicate: t => t.Id == id, + predicate: t => t.Id == id && !t.IsDeleted, includes: new[] { "Asker", "Asked", "Comments", "ThreadLikes" } ); @@ -197,6 +197,21 @@ public async Task> AnswerThread(int threadId, i try { + if (string.IsNullOrWhiteSpace(answerDto.AnswerContent)) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure( + new List { "Answer cannot be empty or contain only whitespace" }); + } + + var trimmedAnswer = answerDto.AnswerContent.Trim(); + if (trimmedAnswer.Length > 5000) + { + await transaction.RollbackAsync(); + return await ServiceResult.Failure( + new List { "Answer cannot exceed 5000 characters" }); + } + var thread = await _unitOfWork.Threads.FindAsync( predicate: t => t.Id == threadId, includes: new[] { "Asker", "Asked" } @@ -234,6 +249,8 @@ public async Task> AnswerThread(int threadId, i AskerId = thread.AskerId ?? 0, AskerName = thread.isAnonymous ? "Anonymous" : thread.Asker?.Name, AskedId = thread.AskedId, + LikesCount = thread.ThreadLikes?.Count ?? 0, + CommentsCount = thread.Comments?.Count ?? 0, AskedName = thread.Asked?.Name }; @@ -253,18 +270,22 @@ public async Task>> GetThreads { int skipCount = (page - 1) * pageSize; - var totalCount = await _unitOfWork.Threads.CountAsync(); + // var totalCount = await _unitOfWork.Threads.CountAsync(); var threads = await _unitOfWork.Threads.GetPagedAsync( skipCount, - pageSize, + pageSize + 1, t => t.CreatedAt, false, t => true, new[] { "Asker", "Asked", "Comments", "ThreadLikes" } ); + + bool hasMore = threads.Count > pageSize; - var threadDtos = threads.Select(thread => new ThreadResponseDto + var trimmed = threads.Take(pageSize).ToList(); + + var threadDtos = trimmed.Select(thread => new ThreadResponseDto { Id = thread.Id, QuestionContent = thread.QuestionContent, @@ -285,8 +306,8 @@ public async Task>> GetThreads Items = threadDtos, PageNumber = page, PageSize = pageSize, - TotalCount = totalCount, - TotalPages = (int)Math.Ceiling((double)totalCount / pageSize) + HasMore = hasMore + }; return await ServiceResult>.Success(response); @@ -353,14 +374,24 @@ public async Task>> GetFeed(in var threads = await _unitOfWork.Threads.GetPagedAsync( skipCount, - pageSize, + pageSize + 1, t => t.CreatedAt, false, t => followedUserIds.Contains(t.AskedId) && t.Status == ThreadStatus.Answered, new[] { "Asker", "Asked", "Comments", "ThreadLikes" } ); - - var threadDtos = threads.Select(thread => new ThreadResponseDto + + // asking if still there is some remaining pages withou using COUNT() + bool hasMore = threads.Count > pageSize; + + /* after getting the threads with size of pageSize + 1 + to find if there are remaining threads (nextPage) without usint TotalCound and totalPages + now we need to return back only the PageSize of threads + that what i'm doing here in the trimmed list, + */ + var trimmed = threads.Take(pageSize).ToList(); + + var threadDtos = trimmed.Select(thread => new ThreadResponseDto { Id = thread.Id, QuestionContent = thread.QuestionContent, @@ -381,8 +412,7 @@ public async Task>> GetFeed(in Items = threadDtos, PageNumber = page, PageSize = pageSize, - TotalCount = totalCount, - TotalPages = (int)Math.Ceiling((double)totalCount / pageSize) + HasMore = hasMore }; return await ServiceResult>.Success(response); @@ -483,18 +513,28 @@ public async Task>> GetSavedTh { int skipCount = (page - 1) * pageSize; - var totalCount = await _unitOfWork.SavedThreads.CountAsync(st => st.UserId == userId); + // var totalCount = await _unitOfWork.SavedThreads.CountAsync(st => st.UserId == userId); var savedThreads = await _unitOfWork.SavedThreads.GetPagedAsync( skipCount, - pageSize, + pageSize + 1, st => st.CreatedAt, false, st => st.UserId == userId, new[] { "Thread", "Thread.Asker", "Thread.Asked", "Thread.Comments", "Thread.ThreadLikes" } ); - var threadDtos = savedThreads.Select(st => new ThreadResponseDto + + bool hasMore = savedThreads.Count > pageSize; + + /* after getting the threads with size of pageSize + 1 + to find if there are remaining threads (nextPage) without usint TotalCound and totalPages + now we need to return back only the PageSize of threads + that what i'm doing here in the trimmed list, + */ + var trimmed = savedThreads.Take(pageSize).ToList(); + + var threadDtos = trimmed.Select(st => new ThreadResponseDto { Id = st.Thread.Id, QuestionContent = st.Thread.QuestionContent, @@ -516,8 +556,7 @@ public async Task>> GetSavedTh Items = threadDtos, PageNumber = page, PageSize = pageSize, - TotalCount = totalCount, - TotalPages = (int)Math.Ceiling((double)totalCount / pageSize) + HasMore = hasMore }; return await ServiceResult>.Success(response);