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/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
new file mode 100644
index 0000000..d82796f
--- /dev/null
+++ b/AskFm/AskFm.API/Controllers/ThreadController.cs
@@ -0,0 +1,213 @@
+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);
+
+ }
+
+
+ // get all threads for user by user id
+ [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("threads/{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")]
+ 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" });
+ }
+
+ // 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);
+ }
+
+ 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.API/Controllers/ThreadLikeController.cs b/AskFm/AskFm.API/Controllers/ThreadLikeController.cs
new file mode 100644
index 0000000..e861388
--- /dev/null
+++ b/AskFm/AskFm.API/Controllers/ThreadLikeController.cs
@@ -0,0 +1,82 @@
+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 ThreadLikeController : ControllerBase
+{
+ private readonly ILogger _logger;
+ private readonly IUserService _userService;
+ private readonly IThreadLikeService _threadLikeService;
+
+ public ThreadLikeController(
+ ILogger logger,
+ IUserService userService,
+ IThreadLikeService threadService)
+ {
+ _logger = logger;
+ _userService = userService;
+ _threadLikeService = threadService;
+ }
+
+
+ // POST api/threads/{id}/likes - Add a Like 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/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.API/Program.cs b/AskFm/AskFm.API/Program.cs
index 9bf0d5a..6d7c379 100644
--- a/AskFm/AskFm.API/Program.cs
+++ b/AskFm/AskFm.API/Program.cs
@@ -50,12 +50,15 @@ 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();
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/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/CommentResponseDto.cs b/AskFm/AskFm.BLL/DTO/CommentResponseDto.cs
new file mode 100644
index 0000000..f19df0a
--- /dev/null
+++ b/AskFm/AskFm.BLL/DTO/CommentResponseDto.cs
@@ -0,0 +1,12 @@
+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; }
+}
\ 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/CreateThreadDto.cs b/AskFm/AskFm.BLL/DTO/CreateThreadDto.cs
new file mode 100644
index 0000000..998c944
--- /dev/null
+++ b/AskFm/AskFm.BLL/DTO/CreateThreadDto.cs
@@ -0,0 +1,20 @@
+using System.ComponentModel.DataAnnotations;
+using AskFm.DAL.Enums;
+using AskFm.DAL.Models;
+
+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; }
+
+}
\ 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..e6f0636
--- /dev/null
+++ b/AskFm/AskFm.BLL/DTO/PagedResponseDto.cs
@@ -0,0 +1,9 @@
+namespace AskFm.BLL.DTO;
+
+public class PagedResponseDto
+{
+ public List Items { get; set; }
+ public int PageNumber { get; set; }
+ public int PageSize { get; set; }
+ public bool HasMore { 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
new file mode 100644
index 0000000..dc0be27
--- /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;}
+ 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
new file mode 100644
index 0000000..c713574
--- /dev/null
+++ b/AskFm/AskFm.BLL/DTO/ThreadResponseDto.cs
@@ -0,0 +1,20 @@
+using AskFm.DAL.Enums;
+
+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; }
+ public int? AskerId { get; set; }
+ public string? AskerName { get; set; }
+ public int AskedId { get; set; }
+ 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..27d0a09 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.UtcNow;
_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/CommentService.cs b/AskFm/AskFm.BLL/Services/CommentService.cs
index 81f7c98..6400a88 100644
--- a/AskFm/AskFm.BLL/Services/CommentService.cs
+++ b/AskFm/AskFm.BLL/Services/CommentService.cs
@@ -1,28 +1,231 @@
+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" });
+ }
+
+
+ int skip = (page - 1) * pageSize;
+ // Get paginated comments
+ var comments = await _unitOfWork.Comments.GetPagedAsync(
+ 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 = trimmed.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,
+ HasMore = hasMore
+ };
+
+ 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/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/IThreadService.cs b/AskFm/AskFm.BLL/Services/IThreadService.cs
new file mode 100644
index 0000000..d329412
--- /dev/null
+++ b/AskFm/AskFm.BLL/Services/IThreadService.cs
@@ -0,0 +1,17 @@
+using AskFm.BLL.DTO;
+
+namespace AskFm.BLL.Services;
+
+public interface IThreadService
+{
+ 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
new file mode 100644
index 0000000..27facb4
--- /dev/null
+++ b/AskFm/AskFm.BLL/Services/ThreadLikeService.cs
@@ -0,0 +1,203 @@
+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, IMapper mapper)
+ {
+ _unitOfWork = unitOfWork;
+ _logger = logger;
+ _mapper = mapper;
+ }
+
+ // 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;
+ existingLike.CreatedAt = DateTime.UtcNow;
+ 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
+ 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.UtcNow
+ };
+
+ // 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
new file mode 100644
index 0000000..77cb2a7
--- /dev/null
+++ b/AskFm/AskFm.BLL/Services/ThreadService.cs
@@ -0,0 +1,570 @@
+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;
+
+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, IMapper mapper)
+ {
+ _unitOfWork = unitOfWork;
+ _logger = logger;
+ _mapper = mapper;
+ }
+
+ 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.UtcNow,
+ };
+
+ 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();
+
+ await transaction.CommitAsync();
+
+ 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.isAnonymous ? "Anonymous" : thread.Asker?.Name,
+ AskedId = thread.AskedId,
+ AskedName = askedUser.Name
+ };
+
+ return await ServiceResult.Success(responseDto);
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Error adding thread");
+ return await ServiceResult.Failure(new List() { e.Message });
+ }
+ }
+
+ public async Task> GetThreadById(int id)
+ {
+ try
+ {
+ var thread = await _unitOfWork.Threads.FindAsync(
+ predicate: t => t.Id == id && !t.IsDeleted,
+ 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 (threads == null || !threads.Any())
+ {
+ return await ServiceResult>.Success(new List());
+ }
+
+ 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(threadDtos);
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Error retrieving threads");
+ return await ServiceResult>.Failure(new List() { e.Message });
+ }
+ }
+
+ public async Task> AnswerThread(int threadId, int userId,
+ AnswerThreadDto answerDto)
+ {
+ var transaction = await _unitOfWork.BeginTransactionAsync();
+
+ 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" }
+ );
+
+ if (thread == null)
+ {
+ await transaction.RollbackAsync();
+ return await ServiceResult.Failure(new List() { "Thread not found" });
+ }
+
+ 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();
+
+ 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,
+ LikesCount = thread.ThreadLikes?.Count ?? 0,
+ CommentsCount = thread.Comments?.Count ?? 0,
+ AskedName = thread.Asked?.Name
+ };
+
+ 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 + 1,
+ t => t.CreatedAt,
+ false,
+ t => true,
+ new[] { "Asker", "Asked", "Comments", "ThreadLikes" }
+ );
+
+ bool hasMore = threads.Count > pageSize;
+
+ var trimmed = threads.Take(pageSize).ToList();
+
+ var threadDtos = trimmed.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,
+ HasMore = hasMore
+
+ };
+
+ 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 + 1,
+ t => t.CreatedAt,
+ false,
+ t => followedUserIds.Contains(t.AskedId) && t.Status == ThreadStatus.Answered,
+ new[] { "Asker", "Asked", "Comments", "ThreadLikes" }
+ );
+
+ // 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,
+ 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,
+ HasMore = hasMore
+ };
+
+ 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.UtcNow
+ };
+
+ // 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 + 1,
+ st => st.CreatedAt,
+ false,
+ st => st.UserId == userId,
+ new[] { "Thread", "Thread.Asker", "Thread.Asked", "Thread.Comments", "Thread.ThreadLikes" }
+ );
+
+
+ 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,
+ 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,
+ HasMore = hasMore
+ };
+
+ 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.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/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..1a3a7c1 100644
--- a/AskFm/AskFm.DAL/Repositories/Repository.cs
+++ b/AskFm/AskFm.DAL/Repositories/Repository.cs
@@ -1,9 +1,10 @@
using System.Linq.Expressions;
using AskFm.DAL.Interfaces;
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 +14,16 @@ 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;
@@ -36,6 +32,7 @@ public IQueryable FindAll(Expression> predicate, string[] inclu
foreach (var include in includes)
query = query.Include(include);
}
+
return query.Where(predicate);
}
@@ -50,14 +47,11 @@ 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 +59,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,35 +74,81 @@ 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)
{
_dbSet.Remove(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)
+ {
+ 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)
+ {
+ 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