From 888cfb307397db0f56176cfffb350bed63c4137e Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Mon, 18 Aug 2025 02:25:08 +0300 Subject: [PATCH 01/20] Add SignalR Package --- AskFm/AskFm.API/AskFm.API.csproj | 51 +++++++++---------- .../Controllers/NotificationController.cs | 0 2 files changed, 24 insertions(+), 27 deletions(-) create mode 100644 AskFm/AskFm.API/Controllers/NotificationController.cs diff --git a/AskFm/AskFm.API/AskFm.API.csproj b/AskFm/AskFm.API/AskFm.API.csproj index f10f941..6957d66 100644 --- a/AskFm/AskFm.API/AskFm.API.csproj +++ b/AskFm/AskFm.API/AskFm.API.csproj @@ -1,28 +1,25 @@ - - - net9.0 - enable - enable - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - - - - + + net9.0 + enable + enable + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + \ No newline at end of file diff --git a/AskFm/AskFm.API/Controllers/NotificationController.cs b/AskFm/AskFm.API/Controllers/NotificationController.cs new file mode 100644 index 0000000..e69de29 From ad90d92f6b9597c5af387638130055fe2ec0bdc1 Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Mon, 18 Aug 2025 02:57:33 +0300 Subject: [PATCH 02/20] Add Notification Hub --- AskFm/AskFm.API/Program.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/AskFm/AskFm.API/Program.cs b/AskFm/AskFm.API/Program.cs index ce07055..46be5c6 100644 --- a/AskFm/AskFm.API/Program.cs +++ b/AskFm/AskFm.API/Program.cs @@ -6,6 +6,7 @@ using AskFm.DAL.Repositories; using DotNetEnv; using Microsoft.EntityFrameworkCore.Proxies; +using AskFm.BLL.Hub; namespace AskFm.API; @@ -41,6 +42,7 @@ public static void Main(string[] args) builder.Services.AddScoped(); + builder.Services.AddSignalR(); var app = builder.Build(); // Configure the HTTP request pipeline. @@ -53,6 +55,8 @@ public static void Main(string[] args) app.MapControllers(); + app.MapHub("/notificationHub"); + app.Run(); } } \ No newline at end of file From 47f9e2bcc01f2ed435a8a1f1f7b9bd3b8aab9f3f Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Mon, 18 Aug 2025 17:26:16 +0300 Subject: [PATCH 03/20] Implement NotificationDto --- AskFm/AskFm.BLL/AskFm.BLL.csproj | 37 +++++----- AskFm/AskFm.BLL/DTO/NotificationDto.cs | 13 ++++ AskFm/AskFm.BLL/Hub/NotificationHub.cs | 12 ++++ .../AskFm.BLL/Services/NotificationService.cs | 0 AskFm/AskFm.DAL/AskFm.DAL.csproj | 68 +++++++++---------- 5 files changed, 75 insertions(+), 55 deletions(-) create mode 100644 AskFm/AskFm.BLL/DTO/NotificationDto.cs create mode 100644 AskFm/AskFm.BLL/Hub/NotificationHub.cs create mode 100644 AskFm/AskFm.BLL/Services/NotificationService.cs diff --git a/AskFm/AskFm.BLL/AskFm.BLL.csproj b/AskFm/AskFm.BLL/AskFm.BLL.csproj index 974046b..2fcc00b 100644 --- a/AskFm/AskFm.BLL/AskFm.BLL.csproj +++ b/AskFm/AskFm.BLL/AskFm.BLL.csproj @@ -1,19 +1,18 @@ - - - - net9.0 - enable - enable - - - - - - - - - - - - - + + + net9.0 + enable + enable + + + + + + + + + + + + + \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/NotificationDto.cs b/AskFm/AskFm.BLL/DTO/NotificationDto.cs new file mode 100644 index 0000000..2256cff --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/NotificationDto.cs @@ -0,0 +1,13 @@ +public class NotificationDto +{ + public int Id { get; set; } + public string Type { get; set; } + + public string Message { get; set; } + + public bool IsRead { get; set; } + public DateTime CreatedAt { get; set; } + + public int ResourceId { get; set; } + public int UserId { get; set; } +} diff --git a/AskFm/AskFm.BLL/Hub/NotificationHub.cs b/AskFm/AskFm.BLL/Hub/NotificationHub.cs new file mode 100644 index 0000000..4b98ecb --- /dev/null +++ b/AskFm/AskFm.BLL/Hub/NotificationHub.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace AskFm.BLL.Hub +{ + public class NotificationHub : Microsoft.AspNetCore.SignalR.Hub + { + + } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/NotificationService.cs b/AskFm/AskFm.BLL/Services/NotificationService.cs new file mode 100644 index 0000000..e69de29 diff --git a/AskFm/AskFm.DAL/AskFm.DAL.csproj b/AskFm/AskFm.DAL/AskFm.DAL.csproj index c77d955..e33b6a5 100644 --- a/AskFm/AskFm.DAL/AskFm.DAL.csproj +++ b/AskFm/AskFm.DAL/AskFm.DAL.csproj @@ -1,36 +1,32 @@ - - - - net9.0 - enable - enable - AskFm.DAL - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - ..\..\..\..\.nuget\packages\microsoft.entityframeworkcore.relational\9.0.7\lib\net8.0\Microsoft.EntityFrameworkCore.Relational.dll - - - - - - - - + + + net9.0 + enable + enable + AskFm.DAL + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + ..\..\..\..\.nuget\packages\microsoft.entityframeworkcore.relational\9.0.7\lib\net8.0\Microsoft.EntityFrameworkCore.Relational.dll + + + + + + \ No newline at end of file From fe6259ae03f63faa4282250124884c46c07908f2 Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Mon, 18 Aug 2025 21:12:30 +0300 Subject: [PATCH 04/20] Implement Notification Repository --- .../Interfaces/INotificationRepository.cs | 9 ++- AskFm/AskFm.DAL/Models/Notification.cs | 3 +- .../Repositories/NotificationRepository.cs | 65 ++++++++++++++++++- 3 files changed, 74 insertions(+), 3 deletions(-) diff --git a/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs b/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs index d2883fd..e0d577c 100644 --- a/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs +++ b/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs @@ -1,6 +1,13 @@ +using AskFm.DAL.Enums; +using AskFm.DAL.Models; + namespace AskFm.DAL.Interfaces; public interface INotificationRepository { - + Task> GetAllNotifications(int userId, int pageNumber, int pageSize); + Task GetNotificationById(int notificationId); + Task UpdateNotification(Notification notification); + Task AddNotification(Notification notification); + Task> GetNotificationsByType(int userId, NotificationStatus status, int pageNumber, int pageSize); } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Models/Notification.cs b/AskFm/AskFm.DAL/Models/Notification.cs index 0ea01ef..09469fb 100644 --- a/AskFm/AskFm.DAL/Models/Notification.cs +++ b/AskFm/AskFm.DAL/Models/Notification.cs @@ -1,10 +1,11 @@ using System.Runtime.InteropServices.JavaScript; +using AskFm.DAL.Enums; namespace AskFm.DAL.Models; public class Notification : ITrackable { - public GCNotificationStatus Type; + public NotificationStatus Type; public int Id { get; set; } public int UserId { get; set; } public virtual ApplicationUser? User { get; set; } diff --git a/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs b/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs index 7fa79f1..37a96f7 100644 --- a/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs +++ b/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs @@ -1,6 +1,69 @@ +using AskFm.DAL.Enums; +using AskFm.DAL.Interfaces; +using AskFm.DAL.Models; +using Microsoft.EntityFrameworkCore; + namespace AskFm.DAL.Repositories; -public class NotificationRepository +public class NotificationRepository : INotificationRepository { + private readonly AppDbContext _context; + public NotificationRepository(AppDbContext context) + { + _context = context; + } + public async Task> GetAllNotifications(int userId, int pageNumber, int pageSize) + { + var totalCount = await _context.Notifications.CountAsync(n => n.UserId == userId); + var totalPages = (int)Math.Ceiling((double)totalCount / pageSize); + var notifications = await _context.Notifications + .Where(n => n.UserId == userId) + .OrderByDescending(n => n.CreatedAt) + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); + return notifications; + } + + public async Task GetNotificationById(int notificationId) + { + var notification = await _context.Notifications.FindAsync(notificationId); + if (notification == null) + throw new InvalidOperationException($"Notification with ID {notificationId} not found."); + return notification; + } + + public async Task UpdateNotification(Notification notification) + { + if (notification == null) + throw new ArgumentNullException(nameof(notification)); + + _context.Notifications.Update(notification); + await _context.SaveChangesAsync(); + } + + public async Task AddNotification(Notification notification) + { + if (notification == null) + throw new ArgumentNullException(nameof(notification)); + + await _context.Notifications.AddAsync(notification); + await _context.SaveChangesAsync(); + } + public Task> GetNotificationsByType(int userId, NotificationStatus status, int pageNumber, int pageSize) + { + var query = _context.Notifications.AsQueryable(); + + if (userId > 0) + { + query = query.Where(n => n.UserId == userId); + } + + query = query.Where(n => n.Type == status); + + return Task.FromResult(query.OrderByDescending(n => n.CreatedAt) + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize)); + } } \ No newline at end of file From 6c8330e41b01d40a55b0133b1b1a3f9344f76cf7 Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Tue, 19 Aug 2025 02:32:50 +0300 Subject: [PATCH 05/20] fix: update notification model and repository for API compliance --- AskFm/AskFm.BLL/AskFm.BLL.csproj | 1 - AskFm/AskFm.BLL/DTO/ActorDto.cs | 8 +++ AskFm/AskFm.BLL/DTO/NotificationDto.cs | 4 ++ AskFm/AskFm.BLL/DTO/PaginationDto.cs | 10 +++ .../Interfaces/INotificationRepository.cs | 6 +- AskFm/AskFm.DAL/Models/Notification.cs | 4 +- .../Repositories/NotificationRepository.cs | 64 +++++++++++++------ 7 files changed, 75 insertions(+), 22 deletions(-) create mode 100644 AskFm/AskFm.BLL/DTO/ActorDto.cs create mode 100644 AskFm/AskFm.BLL/DTO/PaginationDto.cs diff --git a/AskFm/AskFm.BLL/AskFm.BLL.csproj b/AskFm/AskFm.BLL/AskFm.BLL.csproj index 2fcc00b..9980d2e 100644 --- a/AskFm/AskFm.BLL/AskFm.BLL.csproj +++ b/AskFm/AskFm.BLL/AskFm.BLL.csproj @@ -9,7 +9,6 @@ - diff --git a/AskFm/AskFm.BLL/DTO/ActorDto.cs b/AskFm/AskFm.BLL/DTO/ActorDto.cs new file mode 100644 index 0000000..029e938 --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/ActorDto.cs @@ -0,0 +1,8 @@ +namespace AskFm.BLL.DTO; + +public class ActorDto +{ + public int Id { get; set; } + public string Username { get; set; } + public string AvatarPath { get; set; } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/NotificationDto.cs b/AskFm/AskFm.BLL/DTO/NotificationDto.cs index 2256cff..8b3c1fb 100644 --- a/AskFm/AskFm.BLL/DTO/NotificationDto.cs +++ b/AskFm/AskFm.BLL/DTO/NotificationDto.cs @@ -1,3 +1,5 @@ +using AskFm.BLL.DTO; + public class NotificationDto { public int Id { get; set; } @@ -10,4 +12,6 @@ public class NotificationDto public int ResourceId { get; set; } public int UserId { get; set; } + public ActorDto? Actor { get; set; } + } diff --git a/AskFm/AskFm.BLL/DTO/PaginationDto.cs b/AskFm/AskFm.BLL/DTO/PaginationDto.cs new file mode 100644 index 0000000..98c9a6b --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/PaginationDto.cs @@ -0,0 +1,10 @@ +namespace AskFm.BLL.DTO; + +public class PaginationDto +{ + public int CurrentPage { get; set; } + public int TotalPages { get; set; } + public int TotalCount { get; set; } + public bool HasNext { get; set; } + public bool HasPrevious { get; set; } +} \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs b/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs index e0d577c..5ab5546 100644 --- a/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs +++ b/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs @@ -5,9 +5,11 @@ namespace AskFm.DAL.Interfaces; public interface INotificationRepository { - Task> GetAllNotifications(int userId, int pageNumber, int pageSize); + Task<(IEnumerable notifications, int totalCount)> GetAllNotifications(int userId, int pageNumber, int pageSize); Task GetNotificationById(int notificationId); Task UpdateNotification(Notification notification); Task AddNotification(Notification notification); - Task> GetNotificationsByType(int userId, NotificationStatus status, int pageNumber, int pageSize); + Task<(IEnumerable notifications, int totalCount)> GetNotificationsByType(int userId, NotificationStatus status, int pageNumber, int pageSize); + Task MarkNotificationAsRead(int notificationId); + Task MarkAllNotificationsAsRead(int userId); } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Models/Notification.cs b/AskFm/AskFm.DAL/Models/Notification.cs index 09469fb..0af0956 100644 --- a/AskFm/AskFm.DAL/Models/Notification.cs +++ b/AskFm/AskFm.DAL/Models/Notification.cs @@ -5,10 +5,12 @@ namespace AskFm.DAL.Models; public class Notification : ITrackable { - public NotificationStatus Type; + public NotificationStatus Type { get; set; } public int Id { get; set; } public int UserId { get; set; } public virtual ApplicationUser? User { get; set; } + public int? ActorUserId { get; set; } + public virtual ApplicationUser? ActorUser { get; set; } public bool isRead { get; set; } public int ResourceId { get; set; } diff --git a/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs b/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs index 37a96f7..27c86b3 100644 --- a/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs +++ b/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs @@ -8,29 +8,38 @@ namespace AskFm.DAL.Repositories; public class NotificationRepository : INotificationRepository { private readonly AppDbContext _context; + public NotificationRepository(AppDbContext context) { _context = context; } - public async Task> GetAllNotifications(int userId, int pageNumber, int pageSize) + public async Task<(IEnumerable notifications, int totalCount)> GetAllNotifications(int userId, int pageNumber, int pageSize) { - var totalCount = await _context.Notifications.CountAsync(n => n.UserId == userId); - var totalPages = (int)Math.Ceiling((double)totalCount / pageSize); - var notifications = await _context.Notifications - .Where(n => n.UserId == userId) + var query = _context.Notifications + .Include(n => n.ActorUser) + .Where(n => n.UserId == userId); + + var totalCount = await query.CountAsync(); + + var notifications = await query .OrderByDescending(n => n.CreatedAt) .Skip((pageNumber - 1) * pageSize) .Take(pageSize) .ToListAsync(); - return notifications; + + return (notifications, totalCount); } public async Task GetNotificationById(int notificationId) { - var notification = await _context.Notifications.FindAsync(notificationId); + var notification = await _context.Notifications + .Include(n => n.ActorUser) + .FirstOrDefaultAsync(n => n.Id == notificationId); + if (notification == null) throw new InvalidOperationException($"Notification with ID {notificationId} not found."); + return notification; } @@ -51,19 +60,38 @@ public async Task AddNotification(Notification notification) await _context.Notifications.AddAsync(notification); await _context.SaveChangesAsync(); } - public Task> GetNotificationsByType(int userId, NotificationStatus status, int pageNumber, int pageSize) + + public async Task<(IEnumerable notifications, int totalCount)> GetNotificationsByType(int userId, NotificationStatus status, int pageNumber, int pageSize) { - var query = _context.Notifications.AsQueryable(); - - if (userId > 0) - { - query = query.Where(n => n.UserId == userId); - } + var query = _context.Notifications + .Include(n => n.ActorUser) + .Where(n => n.UserId == userId && n.Type == status); - query = query.Where(n => n.Type == status); + var totalCount = await query.CountAsync(); + + var notifications = await query + .OrderByDescending(n => n.CreatedAt) + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); + + return (notifications, totalCount); + } - return Task.FromResult(query.OrderByDescending(n => n.CreatedAt) - .Skip((pageNumber - 1) * pageSize) - .Take(pageSize)); + public async Task MarkNotificationAsRead(int notificationId) + { + var notification = await GetNotificationById(notificationId); + notification.isRead = true; + notification.UpdatedAt = DateTime.UtcNow; + await UpdateNotification(notification); + } + + public async Task MarkAllNotificationsAsRead(int userId) + { + await _context.Notifications + .Where(n => n.UserId == userId && !n.isRead) + .ExecuteUpdateAsync(n => n + .SetProperty(x => x.isRead, true) + .SetProperty(x => x.UpdatedAt, DateTime.UtcNow)); } } \ No newline at end of file From 0f1a29c2ec9e932068077509072a567ec96fca53 Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Tue, 19 Aug 2025 03:37:31 +0300 Subject: [PATCH 06/20] Implement Notification Services --- AskFm/AskFm.BLL/AskFm.BLL.csproj | 5 +- .../DTO/NotificationCategoryResponse.cs | 8 ++ .../Services/INotificationService.cs | 13 +++ .../AskFm.BLL/Services/NotificationService.cs | 79 +++++++++++++++++++ 4 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 AskFm/AskFm.BLL/DTO/NotificationCategoryResponse.cs create mode 100644 AskFm/AskFm.BLL/Services/INotificationService.cs diff --git a/AskFm/AskFm.BLL/AskFm.BLL.csproj b/AskFm/AskFm.BLL/AskFm.BLL.csproj index 9980d2e..671c83a 100644 --- a/AskFm/AskFm.BLL/AskFm.BLL.csproj +++ b/AskFm/AskFm.BLL/AskFm.BLL.csproj @@ -8,10 +8,9 @@ - - - + + \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/NotificationCategoryResponse.cs b/AskFm/AskFm.BLL/DTO/NotificationCategoryResponse.cs new file mode 100644 index 0000000..8bce4b7 --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/NotificationCategoryResponse.cs @@ -0,0 +1,8 @@ +namespace AskFm.BLL.DTO; + +public class NotificationCategoryResponse +{ + public string Category { get; set; } + public List Notifications { get; set; } + public PaginationDto Pagination { get; set; } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/INotificationService.cs b/AskFm/AskFm.BLL/Services/INotificationService.cs new file mode 100644 index 0000000..5e31e8c --- /dev/null +++ b/AskFm/AskFm.BLL/Services/INotificationService.cs @@ -0,0 +1,13 @@ +using AskFm.BLL.DTO; +using AskFm.DAL.Enums; + +namespace AskFm.BLL.Services; + +public interface INotificationService +{ + Task> GetUserNotifications(int userId, int pageNumber = 1, int pageSize = 10); + Task GetNotificationsByCategory(int userId, string category, int pageNumber = 1, int pageSize = 10); + Task MarkNotificationAsRead(int notificationId); + Task MarkAllNotificationsAsRead(int userId); + Task CreateNotification(int userId, int? actorUserId, NotificationStatus type, int resourceId, string message); +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/NotificationService.cs b/AskFm/AskFm.BLL/Services/NotificationService.cs index e69de29..84511ba 100644 --- a/AskFm/AskFm.BLL/Services/NotificationService.cs +++ b/AskFm/AskFm.BLL/Services/NotificationService.cs @@ -0,0 +1,79 @@ +using AskFm.BLL.DTO; +using AskFm.BLL.Services; +using AskFm.DAL.Enums; +using AskFm.DAL.Interfaces; +using AskFm.DAL.Models; +using AutoMapper; + +public class NotificationService : INotificationService +{ + private readonly INotificationRepository _notificationRepository; + private readonly IMapper _mapper; + + public NotificationService(INotificationRepository notificationRepository, IMapper mapper) + { + _notificationRepository = notificationRepository; + _mapper = mapper; + } + + public async Task> GetUserNotifications(int userId, int pageNumber = 1, int pageSize = 10) + { + var (notifications, totalCount) = await _notificationRepository.GetAllNotifications(userId, pageNumber, pageSize); + return _mapper.Map>(notifications); + } + + public async Task GetNotificationsByCategory(int userId, string category, int pageNumber = 1, int pageSize = 10) + { + if (!Enum.TryParse(category, true, out var notificationType)) + throw new ArgumentException($"Invalid notification category: {category}"); + + var (notifications, totalCount) = await _notificationRepository.GetNotificationsByType(userId, notificationType, pageNumber, pageSize); + + var notificationDtos = _mapper.Map>(notifications); + + var totalPages = (int)Math.Ceiling((double)totalCount / pageSize); + + return new NotificationCategoryResponse + { + Category = category.ToUpper(), + Notifications = notificationDtos, + Pagination = new PaginationDto + { + CurrentPage = pageNumber, + TotalPages = totalPages, + TotalCount = totalCount, + HasNext = pageNumber < totalPages, + HasPrevious = pageNumber > 1 + } + }; + } + + public async Task MarkNotificationAsRead(int notificationId) + { + await _notificationRepository.MarkNotificationAsRead(notificationId); + return "noti has been read"; + } + + public async Task MarkAllNotificationsAsRead(int userId) + { + await _notificationRepository.MarkAllNotificationsAsRead(userId); + return "All notifications marked as read"; + } + + public async Task CreateNotification(int userId, int? actorUserId, NotificationStatus type, int resourceId, string message) + { + var notification = new Notification + { + UserId = userId, + ActorUserId = actorUserId, + Type = type, + ResourceId = resourceId, + jsonContent = message, + isRead = false, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + await _notificationRepository.AddNotification(notification); + } +} \ No newline at end of file From e65c410b5d038a01670e32187d99ea7bfd88fe93 Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Wed, 20 Aug 2025 02:33:02 +0300 Subject: [PATCH 07/20] fix: NotificationDto by adding PaginationDto and implement manual mapping --- AskFm/AskFm.BLL/DTO/NotificationDto.cs | 2 + ...esponse.cs => NotificationTypeResponse.cs} | 4 +- .../Services/INotificationService.cs | 2 +- .../AskFm.BLL/Services/NotificationService.cs | 60 +++++++++++++++---- 4 files changed, 55 insertions(+), 13 deletions(-) rename AskFm/AskFm.BLL/DTO/{NotificationCategoryResponse.cs => NotificationTypeResponse.cs} (62%) diff --git a/AskFm/AskFm.BLL/DTO/NotificationDto.cs b/AskFm/AskFm.BLL/DTO/NotificationDto.cs index 8b3c1fb..91c1fc3 100644 --- a/AskFm/AskFm.BLL/DTO/NotificationDto.cs +++ b/AskFm/AskFm.BLL/DTO/NotificationDto.cs @@ -13,5 +13,7 @@ public class NotificationDto public int ResourceId { get; set; } public int UserId { get; set; } public ActorDto? Actor { get; set; } + public PaginationDto Pagination { get; set; } + } diff --git a/AskFm/AskFm.BLL/DTO/NotificationCategoryResponse.cs b/AskFm/AskFm.BLL/DTO/NotificationTypeResponse.cs similarity index 62% rename from AskFm/AskFm.BLL/DTO/NotificationCategoryResponse.cs rename to AskFm/AskFm.BLL/DTO/NotificationTypeResponse.cs index 8bce4b7..b7ffd64 100644 --- a/AskFm/AskFm.BLL/DTO/NotificationCategoryResponse.cs +++ b/AskFm/AskFm.BLL/DTO/NotificationTypeResponse.cs @@ -1,8 +1,8 @@ namespace AskFm.BLL.DTO; -public class NotificationCategoryResponse +public class NotificationTypeResponse { - public string Category { get; set; } + public string Type { get; set; } public List Notifications { get; set; } public PaginationDto Pagination { get; set; } } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/INotificationService.cs b/AskFm/AskFm.BLL/Services/INotificationService.cs index 5e31e8c..d844f0a 100644 --- a/AskFm/AskFm.BLL/Services/INotificationService.cs +++ b/AskFm/AskFm.BLL/Services/INotificationService.cs @@ -6,7 +6,7 @@ namespace AskFm.BLL.Services; public interface INotificationService { Task> GetUserNotifications(int userId, int pageNumber = 1, int pageSize = 10); - Task GetNotificationsByCategory(int userId, string category, int pageNumber = 1, int pageSize = 10); + Task GetNotificationsByType(int userId, string category, int pageNumber = 1, int pageSize = 10); Task MarkNotificationAsRead(int notificationId); Task MarkAllNotificationsAsRead(int userId); Task CreateNotification(int userId, int? actorUserId, NotificationStatus type, int resourceId, string message); diff --git a/AskFm/AskFm.BLL/Services/NotificationService.cs b/AskFm/AskFm.BLL/Services/NotificationService.cs index 84511ba..94e3eda 100644 --- a/AskFm/AskFm.BLL/Services/NotificationService.cs +++ b/AskFm/AskFm.BLL/Services/NotificationService.cs @@ -3,39 +3,79 @@ using AskFm.DAL.Enums; using AskFm.DAL.Interfaces; using AskFm.DAL.Models; -using AutoMapper; public class NotificationService : INotificationService { private readonly INotificationRepository _notificationRepository; - private readonly IMapper _mapper; - public NotificationService(INotificationRepository notificationRepository, IMapper mapper) + public NotificationService(INotificationRepository notificationRepository) { _notificationRepository = notificationRepository; - _mapper = mapper; } public async Task> GetUserNotifications(int userId, int pageNumber = 1, int pageSize = 10) { var (notifications, totalCount) = await _notificationRepository.GetAllNotifications(userId, pageNumber, pageSize); - return _mapper.Map>(notifications); + + var totalPages = (int)Math.Ceiling((double)totalCount / pageSize); + + var notificationDtos = notifications.Select(notification => new NotificationDto + { + Id = notification.Id, + UserId = notification.UserId, + Type = notification.Type.ToString(), + ResourceId = notification.ResourceId, + Message = notification.jsonContent, + IsRead = notification.isRead, + CreatedAt = notification.CreatedAt, + Actor = new ActorDto + { + Id = notification.ActorUser?.Id ?? 0, + Username = notification.ActorUser?.UserName ?? "Unknown", + AvatarPath = notification.ActorUser?.AvatarPath ?? string.Empty + }, + Pagination = new PaginationDto + { + CurrentPage = pageNumber, + TotalPages = totalPages, + TotalCount = totalCount, + HasNext = pageNumber < totalPages, + HasPrevious = pageNumber > 1 + } + }).ToList(); + + return notificationDtos; } - public async Task GetNotificationsByCategory(int userId, string category, int pageNumber = 1, int pageSize = 10) + public async Task GetNotificationsByType(int userId, string category, int pageNumber = 1, int pageSize = 10) { if (!Enum.TryParse(category, true, out var notificationType)) throw new ArgumentException($"Invalid notification category: {category}"); var (notifications, totalCount) = await _notificationRepository.GetNotificationsByType(userId, notificationType, pageNumber, pageSize); - var notificationDtos = _mapper.Map>(notifications); + var notificationDtos = notifications.Select(notification => new NotificationDto + { + Id = notification.Id, + UserId = notification.UserId, + Type = notification.Type.ToString(), + ResourceId = notification.ResourceId, + Message = notification.jsonContent, + IsRead = notification.isRead, + CreatedAt = notification.CreatedAt, + Actor = new ActorDto + { + Id = notification.ActorUser?.Id ?? 0, + Username = notification.ActorUser?.UserName ?? "Unknown", + AvatarPath = notification.ActorUser?.AvatarPath ?? string.Empty + } + }).ToList(); var totalPages = (int)Math.Ceiling((double)totalCount / pageSize); - return new NotificationCategoryResponse + return new NotificationTypeResponse { - Category = category.ToUpper(), + Type = category.ToUpper(), Notifications = notificationDtos, Pagination = new PaginationDto { @@ -51,7 +91,7 @@ public async Task GetNotificationsByCategory(int u public async Task MarkNotificationAsRead(int notificationId) { await _notificationRepository.MarkNotificationAsRead(notificationId); - return "noti has been read"; + return "notification has been read"; } public async Task MarkAllNotificationsAsRead(int userId) From 74324033e330bb31116e2df145f66a7a6f5d4f7a Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Wed, 20 Aug 2025 21:46:56 +0300 Subject: [PATCH 08/20] Implement Notification Controller --- .../Controllers/NotificationController.cs | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/AskFm/AskFm.API/Controllers/NotificationController.cs b/AskFm/AskFm.API/Controllers/NotificationController.cs index e69de29..ce55c73 100644 --- a/AskFm/AskFm.API/Controllers/NotificationController.cs +++ b/AskFm/AskFm.API/Controllers/NotificationController.cs @@ -0,0 +1,103 @@ +using AskFm.BLL.Services; +using AskFm.DAL.Enums; +using Microsoft.AspNetCore.Mvc; + +namespace AskFm.API.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class NotificationController : ControllerBase + { + private readonly INotificationService _notificationService; + + public NotificationController(INotificationService notificationService) + { + _notificationService = notificationService; + } + + [HttpGet("{userId}")] + public async Task GetUserNotifications(int userId, [FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 10) + { + try + { + var notifications = await _notificationService.GetUserNotifications(userId, pageNumber, pageSize); + return Ok(notifications); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + + [HttpGet("{userId}/type/{category}")] + public async Task GetNotificationsByType(int userId, string category, [FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 10) + { + try + { + var response = await _notificationService.GetNotificationsByType(userId, category, pageNumber, pageSize); + return Ok(response); + } + catch (ArgumentException ex) + { + return BadRequest(ex.Message); + } + catch (Exception ex) + { + return StatusCode(500, ex.Message); + } + } + + [HttpPut("{notificationId}/read")] + public async Task MarkNotificationAsRead(int notificationId) + { + try + { + var result = await _notificationService.MarkNotificationAsRead(notificationId); + return Ok(new { message = result }); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + + [HttpPut("{userId}/read-all")] + public async Task MarkAllNotificationsAsRead(int userId) + { + try + { + var result = await _notificationService.MarkAllNotificationsAsRead(userId); + return Ok(new { message = result }); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + + [HttpPost] + public async Task CreateNotification( + int userId, + int? actorUserId, + NotificationStatus type, + int resourceId, + string message) + { + try + { + await _notificationService.CreateNotification( + userId, + actorUserId, + type, + resourceId, + message); + + return CreatedAtAction(nameof(GetUserNotifications), new { userId = userId }, new { message = "Notification created successfully" }); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + } +} \ No newline at end of file From 9125016e756b99531e0d9c8f66d53014e5b4ef3b Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Wed, 20 Aug 2025 22:18:11 +0300 Subject: [PATCH 09/20] Implement notification hub --- AskFm/AskFm.BLL/Hub/NotificationHub.cs | 20 +++++++++++++----- .../AskFm.BLL/Services/NotificationService.cs | 21 ++++++++++++++++++- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/AskFm/AskFm.BLL/Hub/NotificationHub.cs b/AskFm/AskFm.BLL/Hub/NotificationHub.cs index 4b98ecb..e881fcd 100644 --- a/AskFm/AskFm.BLL/Hub/NotificationHub.cs +++ b/AskFm/AskFm.BLL/Hub/NotificationHub.cs @@ -1,12 +1,22 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR; namespace AskFm.BLL.Hub { public class NotificationHub : Microsoft.AspNetCore.SignalR.Hub { - + public async Task JoinUserGroup(string userId) + { + await Groups.AddToGroupAsync(Context.ConnectionId, $"user_{userId}"); + } + + public async Task LeaveUserGroup(string userId) + { + await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"user_{userId}"); + } + + public override async Task OnDisconnectedAsync(Exception exception) + { + await base.OnDisconnectedAsync(exception); + } } } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/NotificationService.cs b/AskFm/AskFm.BLL/Services/NotificationService.cs index 94e3eda..3a2fb52 100644 --- a/AskFm/AskFm.BLL/Services/NotificationService.cs +++ b/AskFm/AskFm.BLL/Services/NotificationService.cs @@ -1,16 +1,21 @@ using AskFm.BLL.DTO; +using AskFm.BLL.Hub; using AskFm.BLL.Services; using AskFm.DAL.Enums; using AskFm.DAL.Interfaces; using AskFm.DAL.Models; +using Microsoft.AspNetCore.SignalR; public class NotificationService : INotificationService { private readonly INotificationRepository _notificationRepository; + private readonly IHubContext _hubContext; - public NotificationService(INotificationRepository notificationRepository) + + public NotificationService(INotificationRepository notificationRepository, IHubContext hubContext) { _notificationRepository = notificationRepository; + _hubContext = hubContext; } public async Task> GetUserNotifications(int userId, int pageNumber = 1, int pageSize = 10) @@ -115,5 +120,19 @@ public async Task CreateNotification(int userId, int? actorUserId, NotificationS }; await _notificationRepository.AddNotification(notification); + + var notificationDto = new NotificationDto + { + Id = notification.Id, + UserId = notification.UserId, + Type = notification.Type.ToString(), + ResourceId = notification.ResourceId, + Message = notification.jsonContent, + IsRead = notification.isRead, + CreatedAt = notification.CreatedAt + }; + + await _hubContext.Clients.Group($"user_{userId}") + .SendAsync("ReceiveNotification", notificationDto); } } \ No newline at end of file From 065fdacb320c1a44230a93007873ce18f09c6142 Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Wed, 20 Aug 2025 22:26:20 +0300 Subject: [PATCH 10/20] Add Dependency injection --- AskFm/AskFm.API/Program.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/AskFm/AskFm.API/Program.cs b/AskFm/AskFm.API/Program.cs index 46be5c6..c07d4a8 100644 --- a/AskFm/AskFm.API/Program.cs +++ b/AskFm/AskFm.API/Program.cs @@ -7,6 +7,8 @@ using DotNetEnv; using Microsoft.EntityFrameworkCore.Proxies; using AskFm.BLL.Hub; +using AskFm.BLL.Services; + namespace AskFm.API; @@ -40,6 +42,8 @@ public static void Main(string[] args) builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddSignalR(); From b2dca6d2f7a7687282475bd213876e9618d2f64f Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Sun, 24 Aug 2025 20:22:55 +0300 Subject: [PATCH 11/20] refactor: Remove redundant actor fields and resolve actors via ResourceId, Update Service methods to use new approach --- .../Controllers/NotificationController.cs | 14 +-- .../Services/INotificationService.cs | 2 +- .../AskFm.BLL/Services/NotificationService.cs | 95 +++++++++++-------- .../Interfaces/INotificationRepository.cs | 1 + AskFm/AskFm.DAL/Models/Notification.cs | 2 - .../Repositories/NotificationRepository.cs | 67 ++++++++++--- 6 files changed, 112 insertions(+), 69 deletions(-) diff --git a/AskFm/AskFm.API/Controllers/NotificationController.cs b/AskFm/AskFm.API/Controllers/NotificationController.cs index ce55c73..5630476 100644 --- a/AskFm/AskFm.API/Controllers/NotificationController.cs +++ b/AskFm/AskFm.API/Controllers/NotificationController.cs @@ -76,21 +76,11 @@ public async Task MarkAllNotificationsAsRead(int userId) } [HttpPost] - public async Task CreateNotification( - int userId, - int? actorUserId, - NotificationStatus type, - int resourceId, - string message) + public async Task CreateNotification(int userId, NotificationStatus type, int resourceId, string message) { try { - await _notificationService.CreateNotification( - userId, - actorUserId, - type, - resourceId, - message); + await _notificationService.CreateNotification(userId, type, resourceId, message); return CreatedAtAction(nameof(GetUserNotifications), new { userId = userId }, new { message = "Notification created successfully" }); } diff --git a/AskFm/AskFm.BLL/Services/INotificationService.cs b/AskFm/AskFm.BLL/Services/INotificationService.cs index d844f0a..53955a4 100644 --- a/AskFm/AskFm.BLL/Services/INotificationService.cs +++ b/AskFm/AskFm.BLL/Services/INotificationService.cs @@ -9,5 +9,5 @@ public interface INotificationService Task GetNotificationsByType(int userId, string category, int pageNumber = 1, int pageSize = 10); Task MarkNotificationAsRead(int notificationId); Task MarkAllNotificationsAsRead(int userId); - Task CreateNotification(int userId, int? actorUserId, NotificationStatus type, int resourceId, string message); + Task CreateNotification(int userId, NotificationStatus type, int resourceId, string message); } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/NotificationService.cs b/AskFm/AskFm.BLL/Services/NotificationService.cs index 3a2fb52..174b81c 100644 --- a/AskFm/AskFm.BLL/Services/NotificationService.cs +++ b/AskFm/AskFm.BLL/Services/NotificationService.cs @@ -21,61 +21,73 @@ public NotificationService(INotificationRepository notificationRepository, IHubC public async Task> GetUserNotifications(int userId, int pageNumber = 1, int pageSize = 10) { var (notifications, totalCount) = await _notificationRepository.GetAllNotifications(userId, pageNumber, pageSize); - + var totalPages = (int)Math.Ceiling((double)totalCount / pageSize); - var notificationDtos = notifications.Select(notification => new NotificationDto + var notificationDtos = new List(); + + foreach (var notification in notifications) { - Id = notification.Id, - UserId = notification.UserId, - Type = notification.Type.ToString(), - ResourceId = notification.ResourceId, - Message = notification.jsonContent, - IsRead = notification.isRead, - CreatedAt = notification.CreatedAt, - Actor = new ActorDto - { - Id = notification.ActorUser?.Id ?? 0, - Username = notification.ActorUser?.UserName ?? "Unknown", - AvatarPath = notification.ActorUser?.AvatarPath ?? string.Empty - }, - Pagination = new PaginationDto + var actorUser = await _notificationRepository.GetActorUserByResourceId(notification.ResourceId, notification.Type); + notificationDtos.Add(new NotificationDto { - CurrentPage = pageNumber, - TotalPages = totalPages, - TotalCount = totalCount, - HasNext = pageNumber < totalPages, - HasPrevious = pageNumber > 1 - } - }).ToList(); - + Id = notification.Id, + UserId = notification.UserId, + Type = notification.Type.ToString(), + ResourceId = notification.ResourceId, + Message = notification.jsonContent, + IsRead = notification.isRead, + CreatedAt = notification.CreatedAt, + Actor = actorUser == null ? null : new ActorDto + { + Id = actorUser.Id, + Username = actorUser?.UserName ?? "Unknown", + AvatarPath = actorUser?.AvatarPath ?? String.Empty + }, + Pagination = new PaginationDto + { + CurrentPage = pageNumber, + TotalPages = totalPages, + TotalCount = totalCount, + HasNext = pageNumber < totalPages, + HasPrevious = pageNumber > 1 + } + }); + } return notificationDtos; } public async Task GetNotificationsByType(int userId, string category, int pageNumber = 1, int pageSize = 10) { + // first we have to convert category to upper case and match it with enum if (!Enum.TryParse(category, true, out var notificationType)) throw new ArgumentException($"Invalid notification category: {category}"); var (notifications, totalCount) = await _notificationRepository.GetNotificationsByType(userId, notificationType, pageNumber, pageSize); - var notificationDtos = notifications.Select(notification => new NotificationDto + var notificationDtos = new List(); + + foreach (var notification in notifications) { - Id = notification.Id, - UserId = notification.UserId, - Type = notification.Type.ToString(), - ResourceId = notification.ResourceId, - Message = notification.jsonContent, - IsRead = notification.isRead, - CreatedAt = notification.CreatedAt, - Actor = new ActorDto - { - Id = notification.ActorUser?.Id ?? 0, - Username = notification.ActorUser?.UserName ?? "Unknown", - AvatarPath = notification.ActorUser?.AvatarPath ?? string.Empty - } - }).ToList(); - + var actorUser = await _notificationRepository.GetActorUserByResourceId(notification.ResourceId, notification.Type); + notificationDtos.Add(new NotificationDto + { + Id = notification.Id, + UserId = notification.UserId, + Type = notification.Type.ToString(), + ResourceId = notification.ResourceId, + Message = notification.jsonContent, + IsRead = notification.isRead, + CreatedAt = notification.CreatedAt, + Actor = actorUser == null ? null : new ActorDto + { + Id = actorUser.Id, + Username = actorUser?.UserName ?? "Unknown", + AvatarPath = actorUser?.AvatarPath ?? String.Empty + } + }); + } + var totalPages = (int)Math.Ceiling((double)totalCount / pageSize); return new NotificationTypeResponse @@ -105,12 +117,11 @@ public async Task MarkAllNotificationsAsRead(int userId) return "All notifications marked as read"; } - public async Task CreateNotification(int userId, int? actorUserId, NotificationStatus type, int resourceId, string message) + public async Task CreateNotification(int userId, NotificationStatus type, int resourceId, string message) { var notification = new Notification { UserId = userId, - ActorUserId = actorUserId, Type = type, ResourceId = resourceId, jsonContent = message, diff --git a/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs b/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs index 5ab5546..b59ba3b 100644 --- a/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs +++ b/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs @@ -12,4 +12,5 @@ public interface INotificationRepository Task<(IEnumerable notifications, int totalCount)> GetNotificationsByType(int userId, NotificationStatus status, int pageNumber, int pageSize); Task MarkNotificationAsRead(int notificationId); Task MarkAllNotificationsAsRead(int userId); + Task GetActorUserByResourceId(int resourceId, NotificationStatus type); } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Models/Notification.cs b/AskFm/AskFm.DAL/Models/Notification.cs index 0af0956..799ba14 100644 --- a/AskFm/AskFm.DAL/Models/Notification.cs +++ b/AskFm/AskFm.DAL/Models/Notification.cs @@ -9,8 +9,6 @@ public class Notification : ITrackable public int Id { get; set; } public int UserId { get; set; } public virtual ApplicationUser? User { get; set; } - public int? ActorUserId { get; set; } - public virtual ApplicationUser? ActorUser { get; set; } public bool isRead { get; set; } public int ResourceId { get; set; } diff --git a/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs b/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs index 27c86b3..29e9cfa 100644 --- a/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs +++ b/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs @@ -16,9 +16,7 @@ public NotificationRepository(AppDbContext context) public async Task<(IEnumerable notifications, int totalCount)> GetAllNotifications(int userId, int pageNumber, int pageSize) { - var query = _context.Notifications - .Include(n => n.ActorUser) - .Where(n => n.UserId == userId); + var query = _context.Notifications.Where(n => n.UserId == userId); var totalCount = await query.CountAsync(); @@ -33,9 +31,7 @@ public NotificationRepository(AppDbContext context) public async Task GetNotificationById(int notificationId) { - var notification = await _context.Notifications - .Include(n => n.ActorUser) - .FirstOrDefaultAsync(n => n.Id == notificationId); + var notification = await _context.Notifications.FirstOrDefaultAsync(n => n.Id == notificationId); if (notification == null) throw new InvalidOperationException($"Notification with ID {notificationId} not found."); @@ -63,9 +59,7 @@ public async Task AddNotification(Notification notification) public async Task<(IEnumerable notifications, int totalCount)> GetNotificationsByType(int userId, NotificationStatus status, int pageNumber, int pageSize) { - var query = _context.Notifications - .Include(n => n.ActorUser) - .Where(n => n.UserId == userId && n.Type == status); + var query = _context.Notifications.Where(n => n.UserId == userId && n.Type == status); var totalCount = await query.CountAsync(); @@ -82,7 +76,7 @@ public async Task MarkNotificationAsRead(int notificationId) { var notification = await GetNotificationById(notificationId); notification.isRead = true; - notification.UpdatedAt = DateTime.UtcNow; + //notification.UpdatedAt = DateTime.UtcNow; await UpdateNotification(notification); } @@ -91,7 +85,56 @@ public async Task MarkAllNotificationsAsRead(int userId) await _context.Notifications .Where(n => n.UserId == userId && !n.isRead) .ExecuteUpdateAsync(n => n - .SetProperty(x => x.isRead, true) - .SetProperty(x => x.UpdatedAt, DateTime.UtcNow)); + .SetProperty(x => x.isRead, true)); + // .SetProperty(x => x.UpdatedAt, DateTime.UtcNow)) + } + + public async Task GetActorUserByResourceId(int resourceId, NotificationStatus type) + { + // In follow case the follow model does not have a follow id so we use the followedId to get the actor user + if (type == NotificationStatus.FOLLOW) + { + return await _context.Follows + .Where(f => f.FollowedId == resourceId) + .Select(f => f.Follower) + .FirstOrDefaultAsync(); + } + else if (type == NotificationStatus.QUESTION) + { + return await _context.Threads + .Where(t => t.Id == resourceId) + .Select(t => t.Asker) + .FirstOrDefaultAsync(); + } + else if (type == NotificationStatus.ANSWER) + { + return await _context.Threads + .Where(t => t.Id == resourceId) + .Select(t => t.Asked) + .FirstOrDefaultAsync(); + } + else if (type == NotificationStatus.COMMENT_LIKE) + { + return await _context.CommentLikes + .Where(cl => cl.CommentId == resourceId) + .Select(cl => cl.User) + .FirstOrDefaultAsync(); + } + else if (type == NotificationStatus.QUESTION_LIKE) + { + return await _context.ThreadLikes + .Where(tl => tl.ThreadId == resourceId) + .Select(tl => tl.User) + .FirstOrDefaultAsync(); + } + else if (type == NotificationStatus.REPLAY) + { + return await _context.Comments + .Where(c => c.Id == resourceId) + .Select(c => c.User) + .FirstOrDefaultAsync(); + } + return null; + } } \ No newline at end of file From fda6755c60b9a600c6cb8146565e5e2099bac691 Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Sun, 24 Aug 2025 21:00:02 +0300 Subject: [PATCH 12/20] Remove NotificationResponseType that's because the frontend already sent the type of notification --- .../AskFm.BLL/DTO/NotificationTypeResponse.cs | 8 ---- .../Services/INotificationService.cs | 2 +- .../AskFm.BLL/Services/NotificationService.cs | 37 ++++++++----------- 3 files changed, 17 insertions(+), 30 deletions(-) delete mode 100644 AskFm/AskFm.BLL/DTO/NotificationTypeResponse.cs diff --git a/AskFm/AskFm.BLL/DTO/NotificationTypeResponse.cs b/AskFm/AskFm.BLL/DTO/NotificationTypeResponse.cs deleted file mode 100644 index b7ffd64..0000000 --- a/AskFm/AskFm.BLL/DTO/NotificationTypeResponse.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace AskFm.BLL.DTO; - -public class NotificationTypeResponse -{ - public string Type { get; set; } - public List Notifications { get; set; } - public PaginationDto Pagination { get; set; } -} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/INotificationService.cs b/AskFm/AskFm.BLL/Services/INotificationService.cs index 53955a4..4eb9d63 100644 --- a/AskFm/AskFm.BLL/Services/INotificationService.cs +++ b/AskFm/AskFm.BLL/Services/INotificationService.cs @@ -6,7 +6,7 @@ namespace AskFm.BLL.Services; public interface INotificationService { Task> GetUserNotifications(int userId, int pageNumber = 1, int pageSize = 10); - Task GetNotificationsByType(int userId, string category, int pageNumber = 1, int pageSize = 10); + Task> GetNotificationsByType(int userId, string category, int pageNumber = 1, int pageSize = 10); Task MarkNotificationAsRead(int notificationId); Task MarkAllNotificationsAsRead(int userId); Task CreateNotification(int userId, NotificationStatus type, int resourceId, string message); diff --git a/AskFm/AskFm.BLL/Services/NotificationService.cs b/AskFm/AskFm.BLL/Services/NotificationService.cs index 174b81c..ad11a3e 100644 --- a/AskFm/AskFm.BLL/Services/NotificationService.cs +++ b/AskFm/AskFm.BLL/Services/NotificationService.cs @@ -57,21 +57,23 @@ public async Task> GetUserNotifications(int userId, int pa return notificationDtos; } - public async Task GetNotificationsByType(int userId, string category, int pageNumber = 1, int pageSize = 10) + public async Task> GetNotificationsByType(int userId, string category, int pageNumber = 1, int pageSize = 10) { - // first we have to convert category to upper case and match it with enum - if (!Enum.TryParse(category, true, out var notificationType)) + // Convert category to uppercase and match with enum + if (!Enum.TryParse(category.ToUpper(), out var notificationType)) throw new ArgumentException($"Invalid notification category: {category}"); var (notifications, totalCount) = await _notificationRepository.GetNotificationsByType(userId, notificationType, pageNumber, pageSize); - + + var totalPages = (int)Math.Ceiling((double)totalCount / pageSize); + var notificationDtos = new List(); foreach (var notification in notifications) { var actorUser = await _notificationRepository.GetActorUserByResourceId(notification.ResourceId, notification.Type); notificationDtos.Add(new NotificationDto - { + { Id = notification.Id, UserId = notification.UserId, Type = notification.Type.ToString(), @@ -84,27 +86,20 @@ public async Task GetNotificationsByType(int userId, s Id = actorUser.Id, Username = actorUser?.UserName ?? "Unknown", AvatarPath = actorUser?.AvatarPath ?? String.Empty + }, + Pagination = new PaginationDto + { + CurrentPage = pageNumber, + TotalPages = totalPages, + TotalCount = totalCount, + HasNext = pageNumber < totalPages, + HasPrevious = pageNumber > 1 } }); } - var totalPages = (int)Math.Ceiling((double)totalCount / pageSize); - - return new NotificationTypeResponse - { - Type = category.ToUpper(), - Notifications = notificationDtos, - Pagination = new PaginationDto - { - CurrentPage = pageNumber, - TotalPages = totalPages, - TotalCount = totalCount, - HasNext = pageNumber < totalPages, - HasPrevious = pageNumber > 1 - } - }; + return notificationDtos; } - public async Task MarkNotificationAsRead(int notificationId) { await _notificationRepository.MarkNotificationAsRead(notificationId); From bf3cefd68c0923b2e03cab925477e643d75edf87 Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Sun, 24 Aug 2025 22:08:51 +0300 Subject: [PATCH 13/20] Refactor notifications to use UnitOfWork and Repository pattern --- .../AskFm.BLL/Services/NotificationService.cs | 26 ++++- .../Interfaces/INotificationRepository.cs | 5 - AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs | 3 +- .../Repositories/NotificationRepository.cs | 97 ++++--------------- AskFm/AskFm.DAL/UnitOfWork.cs | 8 +- 5 files changed, 46 insertions(+), 93 deletions(-) diff --git a/AskFm/AskFm.BLL/Services/NotificationService.cs b/AskFm/AskFm.BLL/Services/NotificationService.cs index ad11a3e..81ff78f 100644 --- a/AskFm/AskFm.BLL/Services/NotificationService.cs +++ b/AskFm/AskFm.BLL/Services/NotificationService.cs @@ -9,12 +9,14 @@ public class NotificationService : INotificationService { private readonly INotificationRepository _notificationRepository; + private readonly IUnitOfWork _unitOfWork; private readonly IHubContext _hubContext; - public NotificationService(INotificationRepository notificationRepository, IHubContext hubContext) + public NotificationService(INotificationRepository notificationRepository, IUnitOfWork unitOfWork, IHubContext hubContext) { _notificationRepository = notificationRepository; + _unitOfWork = unitOfWork; _hubContext = hubContext; } @@ -102,13 +104,28 @@ public async Task> GetNotificationsByType(int userId, stri } public async Task MarkNotificationAsRead(int notificationId) { - await _notificationRepository.MarkNotificationAsRead(notificationId); + var notification = await _unitOfWork.Notifications.GetByIdAsync(notificationId); + if (notification == null) + throw new InvalidOperationException($"Notification with ID {notificationId} not found."); + + notification.isRead = true; + _unitOfWork.Notifications.Update(notification); + await _unitOfWork.SaveAsync(); + return "notification has been read"; } public async Task MarkAllNotificationsAsRead(int userId) { - await _notificationRepository.MarkAllNotificationsAsRead(userId); + var unreadNotifications = await _unitOfWork.Notifications.FindAllAsync(n => n.UserId == userId && !n.isRead); + + foreach (var notification in unreadNotifications) + { + notification.isRead = true; + _unitOfWork.Notifications.Update(notification); + } + + await _unitOfWork.SaveAsync(); return "All notifications marked as read"; } @@ -125,7 +142,8 @@ public async Task CreateNotification(int userId, NotificationStatus type, int re UpdatedAt = DateTime.UtcNow }; - await _notificationRepository.AddNotification(notification); + await _unitOfWork.Notifications.AddAsync(notification); + await _unitOfWork.SaveAsync(); var notificationDto = new NotificationDto { diff --git a/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs b/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs index b59ba3b..816c2f1 100644 --- a/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs +++ b/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs @@ -6,11 +6,6 @@ namespace AskFm.DAL.Interfaces; public interface INotificationRepository { Task<(IEnumerable notifications, int totalCount)> GetAllNotifications(int userId, int pageNumber, int pageSize); - Task GetNotificationById(int notificationId); - Task UpdateNotification(Notification notification); - Task AddNotification(Notification notification); Task<(IEnumerable notifications, int totalCount)> GetNotificationsByType(int userId, NotificationStatus status, int pageNumber, int pageSize); - Task MarkNotificationAsRead(int notificationId); - Task MarkAllNotificationsAsRead(int userId); Task GetActorUserByResourceId(int resourceId, NotificationStatus type); } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs b/AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs index 108f184..e50167d 100644 --- a/AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs +++ b/AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs @@ -1,12 +1,11 @@ using AskFm.DAL.Models; -using Thread = System.Threading.Thread; namespace AskFm.DAL.Interfaces; public interface IUnitOfWork : IDisposable { IRepository Users { get; } - IRepository Threads { get; } + IRepository Threads { get; } IRepository SavedThreads { get; } IRepository ThreadLikes { get; } IRepository Comments { get; } diff --git a/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs b/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs index 29e9cfa..d49564d 100644 --- a/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs +++ b/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs @@ -7,16 +7,16 @@ namespace AskFm.DAL.Repositories; public class NotificationRepository : INotificationRepository { - private readonly AppDbContext _context; - - public NotificationRepository(AppDbContext context) + private readonly IUnitOfWork _unitOfWork; + + public NotificationRepository(IUnitOfWork unitOfWork) { - _context = context; + _unitOfWork = unitOfWork; } public async Task<(IEnumerable notifications, int totalCount)> GetAllNotifications(int userId, int pageNumber, int pageSize) { - var query = _context.Notifications.Where(n => n.UserId == userId); + var query = _unitOfWork.Notifications.FindAll(n => n.UserId == userId); var totalCount = await query.CountAsync(); @@ -28,39 +28,11 @@ public NotificationRepository(AppDbContext context) return (notifications, totalCount); } - - public async Task GetNotificationById(int notificationId) - { - var notification = await _context.Notifications.FirstOrDefaultAsync(n => n.Id == notificationId); - - if (notification == null) - throw new InvalidOperationException($"Notification with ID {notificationId} not found."); - - return notification; - } - - public async Task UpdateNotification(Notification notification) - { - if (notification == null) - throw new ArgumentNullException(nameof(notification)); - - _context.Notifications.Update(notification); - await _context.SaveChangesAsync(); - } - - public async Task AddNotification(Notification notification) - { - if (notification == null) - throw new ArgumentNullException(nameof(notification)); - - await _context.Notifications.AddAsync(notification); - await _context.SaveChangesAsync(); - } public async Task<(IEnumerable notifications, int totalCount)> GetNotificationsByType(int userId, NotificationStatus status, int pageNumber, int pageSize) { - var query = _context.Notifications.Where(n => n.UserId == userId && n.Type == status); - + var query = _unitOfWork.Notifications.FindAll(n => n.UserId == userId && n.Type == status); + var totalCount = await query.CountAsync(); var notifications = await query @@ -72,69 +44,38 @@ public async Task AddNotification(Notification notification) return (notifications, totalCount); } - public async Task MarkNotificationAsRead(int notificationId) - { - var notification = await GetNotificationById(notificationId); - notification.isRead = true; - //notification.UpdatedAt = DateTime.UtcNow; - await UpdateNotification(notification); - } - - public async Task MarkAllNotificationsAsRead(int userId) - { - await _context.Notifications - .Where(n => n.UserId == userId && !n.isRead) - .ExecuteUpdateAsync(n => n - .SetProperty(x => x.isRead, true)); - // .SetProperty(x => x.UpdatedAt, DateTime.UtcNow)) - } - public async Task GetActorUserByResourceId(int resourceId, NotificationStatus type) { - // In follow case the follow model does not have a follow id so we use the followedId to get the actor user if (type == NotificationStatus.FOLLOW) { - return await _context.Follows - .Where(f => f.FollowedId == resourceId) - .Select(f => f.Follower) - .FirstOrDefaultAsync(); + var follow = await _unitOfWork.Follows.FindAsync(f => f.FollowedId == resourceId, new[] { "Follower" }); + return follow?.Follower; } else if (type == NotificationStatus.QUESTION) { - return await _context.Threads - .Where(t => t.Id == resourceId) - .Select(t => t.Asker) - .FirstOrDefaultAsync(); + var thread = await _unitOfWork.Threads.FindAsync(t => t.Id == resourceId, new[] { "Asker" }); + return thread?.Asker; } else if (type == NotificationStatus.ANSWER) { - return await _context.Threads - .Where(t => t.Id == resourceId) - .Select(t => t.Asked) - .FirstOrDefaultAsync(); + var thread = await _unitOfWork.Threads.FindAsync(t => t.Id == resourceId, new[] { "Asked" }); + return thread?.Asked; } else if (type == NotificationStatus.COMMENT_LIKE) { - return await _context.CommentLikes - .Where(cl => cl.CommentId == resourceId) - .Select(cl => cl.User) - .FirstOrDefaultAsync(); + var commentLike = await _unitOfWork.CommentLikes.FindAsync(cl => cl.CommentId == resourceId, new[] { "User" }); + return commentLike?.User; } else if (type == NotificationStatus.QUESTION_LIKE) { - return await _context.ThreadLikes - .Where(tl => tl.ThreadId == resourceId) - .Select(tl => tl.User) - .FirstOrDefaultAsync(); + var threadLike = await _unitOfWork.ThreadLikes.FindAsync(tl => tl.ThreadId == resourceId, new[] { "User" }); + return threadLike?.User; } else if (type == NotificationStatus.REPLAY) { - return await _context.Comments - .Where(c => c.Id == resourceId) - .Select(c => c.User) - .FirstOrDefaultAsync(); + var comment = await _unitOfWork.Comments.FindAsync(c => c.Id == resourceId, new[] { "User" }); + return comment?.User; } return null; - } } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/UnitOfWork.cs b/AskFm/AskFm.DAL/UnitOfWork.cs index 573f3ae..396696c 100644 --- a/AskFm/AskFm.DAL/UnitOfWork.cs +++ b/AskFm/AskFm.DAL/UnitOfWork.cs @@ -1,7 +1,6 @@ using AskFm.DAL.Interfaces; using AskFm.DAL.Models; using AskFm.DAL.Repositories; -using Thread = System.Threading.Thread; namespace AskFm.DAL; @@ -10,7 +9,7 @@ public class UnitOfWork : IUnitOfWork private readonly AppDbContext _context; private IRepository _users; - private IRepository _threads; + private IRepository _threads; private IRepository _savedThreads; private IRepository _threadLikes; private IRepository _comments; @@ -36,12 +35,12 @@ public IRepository Users } } - public IRepository Threads { + public IRepository Threads { get { if (_threads == null) { - _threads = new Repository(_context); + _threads = new Repository(_context); } return _threads; } @@ -119,6 +118,7 @@ public IRepository Notifications } } + IRepository IUnitOfWork.Threads => throw new NotImplementedException(); public void Dispose() { From d7bd9cfc14f318a663d561c0b9ba1039b6e9b362 Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Sun, 24 Aug 2025 23:50:44 +0300 Subject: [PATCH 14/20] Update Naming in Notification model and apply migrations for it --- .../AskFm.BLL/Services/NotificationService.cs | 22 +- ...204202_NotificationModelNaming.Designer.cs | 763 ++++++++++++++++++ .../20250824204202_NotificationModelNaming.cs | 49 ++ .../Migrations/AppDbContextModelSnapshot.cs | 20 +- AskFm/AskFm.DAL/Models/Notification.cs | 4 +- .../NotificationConfigration.cs | 4 +- 6 files changed, 840 insertions(+), 22 deletions(-) create mode 100644 AskFm/AskFm.DAL/Migrations/20250824204202_NotificationModelNaming.Designer.cs create mode 100644 AskFm/AskFm.DAL/Migrations/20250824204202_NotificationModelNaming.cs diff --git a/AskFm/AskFm.BLL/Services/NotificationService.cs b/AskFm/AskFm.BLL/Services/NotificationService.cs index 81ff78f..d94ea4f 100644 --- a/AskFm/AskFm.BLL/Services/NotificationService.cs +++ b/AskFm/AskFm.BLL/Services/NotificationService.cs @@ -37,8 +37,8 @@ public async Task> GetUserNotifications(int userId, int pa UserId = notification.UserId, Type = notification.Type.ToString(), ResourceId = notification.ResourceId, - Message = notification.jsonContent, - IsRead = notification.isRead, + Message = notification.Message, + IsRead = notification.IsRead, CreatedAt = notification.CreatedAt, Actor = actorUser == null ? null : new ActorDto { @@ -80,8 +80,8 @@ public async Task> GetNotificationsByType(int userId, stri UserId = notification.UserId, Type = notification.Type.ToString(), ResourceId = notification.ResourceId, - Message = notification.jsonContent, - IsRead = notification.isRead, + Message = notification.Message, + IsRead = notification.IsRead, CreatedAt = notification.CreatedAt, Actor = actorUser == null ? null : new ActorDto { @@ -108,7 +108,7 @@ public async Task MarkNotificationAsRead(int notificationId) if (notification == null) throw new InvalidOperationException($"Notification with ID {notificationId} not found."); - notification.isRead = true; + notification.IsRead = true; _unitOfWork.Notifications.Update(notification); await _unitOfWork.SaveAsync(); @@ -117,11 +117,11 @@ public async Task MarkNotificationAsRead(int notificationId) public async Task MarkAllNotificationsAsRead(int userId) { - var unreadNotifications = await _unitOfWork.Notifications.FindAllAsync(n => n.UserId == userId && !n.isRead); + var unreadNotifications = await _unitOfWork.Notifications.FindAllAsync(n => n.UserId == userId && !n.IsRead); foreach (var notification in unreadNotifications) { - notification.isRead = true; + notification.IsRead = true; _unitOfWork.Notifications.Update(notification); } @@ -136,8 +136,8 @@ public async Task CreateNotification(int userId, NotificationStatus type, int re UserId = userId, Type = type, ResourceId = resourceId, - jsonContent = message, - isRead = false, + Message = message, + IsRead = false, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }; @@ -151,8 +151,8 @@ public async Task CreateNotification(int userId, NotificationStatus type, int re UserId = notification.UserId, Type = notification.Type.ToString(), ResourceId = notification.ResourceId, - Message = notification.jsonContent, - IsRead = notification.isRead, + Message = notification.Message, + IsRead = notification.IsRead, CreatedAt = notification.CreatedAt }; diff --git a/AskFm/AskFm.DAL/Migrations/20250824204202_NotificationModelNaming.Designer.cs b/AskFm/AskFm.DAL/Migrations/20250824204202_NotificationModelNaming.Designer.cs new file mode 100644 index 0000000..ddd28f5 --- /dev/null +++ b/AskFm/AskFm.DAL/Migrations/20250824204202_NotificationModelNaming.Designer.cs @@ -0,0 +1,763 @@ +// +using System; +using AskFm.DAL; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AskFm.DAL.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20250824204202_NotificationModelNaming")] + partial class NotificationModelNaming + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Proxies:ChangeTracking", false) + .HasAnnotation("Proxies:CheckEquality", false) + .HasAnnotation("Proxies:LazyLoading", true) + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("AvatarPath") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Bio") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FollowersCount") + .HasColumnType("int"); + + b.Property("FollowingCount") + .HasColumnType("int"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("LastSeen") + .HasColumnType("datetime2"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("LikeCount") + .HasColumnType("int"); + + b.Property("ParentCommentId") + .HasColumnType("int"); + + b.Property("ThreadId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ParentCommentId"); + + b.HasIndex("ThreadId"); + + b.HasIndex("UserId"); + + b.ToTable("Comments"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.CommentLike", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CommentId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.HasKey("UserId", "CommentId"); + + b.HasIndex("CommentId"); + + b.ToTable("CommentLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Follow", b => + { + b.Property("FollowerId") + .HasColumnType("int"); + + b.Property("FollowedId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.HasKey("FollowerId", "FollowedId"); + + b.HasIndex("FollowedId"); + + b.ToTable("Follows"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("IsRead") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("NVARCHAR"); + + b.Property("ResourceId") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.SavedThreads", b => + { + b.Property("SavedThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.HasKey("SavedThreadId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("SavedThreads"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AnswerContent") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("AskedId") + .HasColumnType("int"); + + b.Property("AskerId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("QuestionContent") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.Property("isAnonymous") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("AskedId"); + + b.HasIndex("AskerId"); + + b.ToTable("Thread", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ThreadLike", b => + { + b.Property("ThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("DATETIME"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.HasKey("ThreadId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ThreadLikes"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.HasOne("AskFm.DAL.Models.Comment", "ParentComment") + .WithMany("Replies") + .HasForeignKey("ParentCommentId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("Comments") + .HasForeignKey("ThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("Comments") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("ParentComment"); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.CommentLike", b => + { + b.HasOne("AskFm.DAL.Models.Comment", "Comment") + .WithMany("CommentLikes") + .HasForeignKey("CommentId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("CommentLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Comment"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Follow", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Followed") + .WithMany("Followers") + .HasForeignKey("FollowedId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Follower") + .WithMany("Following") + .HasForeignKey("FollowerId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Followed"); + + b.Navigation("Follower"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Notification", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("Notifications") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.SavedThreads", b => + { + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("SavedThreads") + .HasForeignKey("SavedThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("SavedThreads") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Asked") + .WithMany("ReceivedThreads") + .HasForeignKey("AskedId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Asker") + .WithMany("AskedThreads") + .HasForeignKey("AskerId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Asked"); + + b.Navigation("Asker"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ThreadLike", b => + { + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("ThreadLikes") + .HasForeignKey("ThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("ThreadLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => + { + b.Navigation("AskedThreads"); + + b.Navigation("CommentLikes"); + + b.Navigation("Comments"); + + b.Navigation("Followers"); + + b.Navigation("Following"); + + b.Navigation("Notifications"); + + b.Navigation("ReceivedThreads"); + + b.Navigation("SavedThreads"); + + b.Navigation("ThreadLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.Navigation("CommentLikes"); + + b.Navigation("Replies"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.Navigation("Comments"); + + b.Navigation("SavedThreads"); + + b.Navigation("ThreadLikes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AskFm/AskFm.DAL/Migrations/20250824204202_NotificationModelNaming.cs b/AskFm/AskFm.DAL/Migrations/20250824204202_NotificationModelNaming.cs new file mode 100644 index 0000000..38fee4d --- /dev/null +++ b/AskFm/AskFm.DAL/Migrations/20250824204202_NotificationModelNaming.cs @@ -0,0 +1,49 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AskFm.DAL.Migrations +{ + /// + public partial class NotificationModelNaming : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "isRead", + table: "Notifications", + newName: "IsRead"); + + migrationBuilder.RenameColumn( + name: "jsonContent", + table: "Notifications", + newName: "Message"); + + migrationBuilder.AddColumn( + name: "Type", + table: "Notifications", + type: "int", + nullable: false, + defaultValue: 0); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Type", + table: "Notifications"); + + migrationBuilder.RenameColumn( + name: "IsRead", + table: "Notifications", + newName: "isRead"); + + migrationBuilder.RenameColumn( + name: "Message", + table: "Notifications", + newName: "jsonContent"); + } + } +} diff --git a/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs b/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs index 0d3ad2e..bdfe5a3 100644 --- a/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs +++ b/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs @@ -18,6 +18,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Proxies:ChangeTracking", false) + .HasAnnotation("Proxies:CheckEquality", false) + .HasAnnotation("Proxies:LazyLoading", true) .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -263,22 +266,25 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("BIT") .HasDefaultValue(false); + b.Property("IsRead") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("NVARCHAR"); + b.Property("ResourceId") .HasColumnType("int"); + b.Property("Type") + .HasColumnType("int"); + b.Property("UpdatedAt") .HasColumnType("DATETIME"); b.Property("UserId") .HasColumnType("int"); - b.Property("isRead") - .HasColumnType("bit"); - - b.Property("jsonContent") - .IsRequired() - .HasColumnType("NVARCHAR"); - b.HasKey("Id"); b.HasIndex("UserId"); diff --git a/AskFm/AskFm.DAL/Models/Notification.cs b/AskFm/AskFm.DAL/Models/Notification.cs index 799ba14..2807d59 100644 --- a/AskFm/AskFm.DAL/Models/Notification.cs +++ b/AskFm/AskFm.DAL/Models/Notification.cs @@ -9,10 +9,10 @@ public class Notification : ITrackable public int Id { get; set; } public int UserId { get; set; } public virtual ApplicationUser? User { get; set; } - public bool isRead { get; set; } + public bool IsRead { get; set; } public int ResourceId { get; set; } - public string jsonContent { get; set; } + public string Message { get; set; } public bool IsDeleted { get; set; } public DateTime DeletedAt { get; set; } diff --git a/AskFm/AskFm.DAL/ModelsConfigrations/NotificationConfigration.cs b/AskFm/AskFm.DAL/ModelsConfigrations/NotificationConfigration.cs index afeda9b..0423d4f 100644 --- a/AskFm/AskFm.DAL/ModelsConfigrations/NotificationConfigration.cs +++ b/AskFm/AskFm.DAL/ModelsConfigrations/NotificationConfigration.cs @@ -10,10 +10,10 @@ public void Configure(EntityTypeBuilder builder) { builder.HasKey(n => n.Id); - builder.Property(n => n.jsonContent) + builder.Property(n => n.Message) .HasColumnType("NVARCHAR"); - builder.Property(n => n.isRead) + builder.Property(n => n.IsRead) .IsRequired(); From 3e53d296f4f734f3a77307c680a539de51c8c58c Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Mon, 25 Aug 2025 00:07:48 +0300 Subject: [PATCH 15/20] Enable Swagger --- AskFm/AskFm.API/AskFm.API.csproj | 1 + AskFm/AskFm.API/Program.cs | 9 ++++++++- AskFm/AskFm.API/Properties/launchSettings.json | 6 ++++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/AskFm/AskFm.API/AskFm.API.csproj b/AskFm/AskFm.API/AskFm.API.csproj index 6957d66..8fb3332 100644 --- a/AskFm/AskFm.API/AskFm.API.csproj +++ b/AskFm/AskFm.API/AskFm.API.csproj @@ -17,6 +17,7 @@ + diff --git a/AskFm/AskFm.API/Program.cs b/AskFm/AskFm.API/Program.cs index c07d4a8..8b14da4 100644 --- a/AskFm/AskFm.API/Program.cs +++ b/AskFm/AskFm.API/Program.cs @@ -50,7 +50,14 @@ public static void Main(string[] args) var app = builder.Build(); // Configure the HTTP request pipeline. - if (app.Environment.IsDevelopment()) app.MapOpenApi(); + if (app.Environment.IsDevelopment()) + { + app.MapOpenApi(); + app.UseSwaggerUI(options => + { + options.SwaggerEndpoint("/openapi/v1.json", "api"); + }); + } app.UseHttpsRedirection(); diff --git a/AskFm/AskFm.API/Properties/launchSettings.json b/AskFm/AskFm.API/Properties/launchSettings.json index dc599be..75f6fa6 100644 --- a/AskFm/AskFm.API/Properties/launchSettings.json +++ b/AskFm/AskFm.API/Properties/launchSettings.json @@ -4,7 +4,8 @@ "http": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": false, + "launchBrowser": true, + "launchUrl": "swagger", "applicationUrl": "http://localhost:5180", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" @@ -13,7 +14,8 @@ "https": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": false, + "launchBrowser": true, + "launchUrl": "swagger", "applicationUrl": "https://localhost:7115;http://localhost:5180", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" From 75057f0d11358028782db0589c50a3a040244f88 Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Tue, 26 Aug 2025 02:25:22 +0300 Subject: [PATCH 16/20] Add Default Values to CreatedAt, UpdatedAt and DeletedAt --- AskFm/AskFm.DAL/AppDbContext.cs | 9 +- ...0825225059_AddCreatedAtDefault.Designer.cs | 779 +++++++++++++++++ .../20250825225059_AddCreatedAtDefault.cs | 163 ++++ ...ddUpdatedAtAndDeletedAtDefault.Designer.cs | 811 ++++++++++++++++++ ...5230515_AddUpdatedAtAndDeletedAtDefault.cs | 307 +++++++ .../Migrations/AppDbContextModelSnapshot.cs | 96 ++- 6 files changed, 2138 insertions(+), 27 deletions(-) create mode 100644 AskFm/AskFm.DAL/Migrations/20250825225059_AddCreatedAtDefault.Designer.cs create mode 100644 AskFm/AskFm.DAL/Migrations/20250825225059_AddCreatedAtDefault.cs create mode 100644 AskFm/AskFm.DAL/Migrations/20250825230515_AddUpdatedAtAndDeletedAtDefault.Designer.cs create mode 100644 AskFm/AskFm.DAL/Migrations/20250825230515_AddUpdatedAtAndDeletedAtDefault.cs diff --git a/AskFm/AskFm.DAL/AppDbContext.cs b/AskFm/AskFm.DAL/AppDbContext.cs index 4f35d3d..7c9196c 100644 --- a/AskFm/AskFm.DAL/AppDbContext.cs +++ b/AskFm/AskFm.DAL/AppDbContext.cs @@ -32,13 +32,16 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity(entityType.ClrType).Property("DeletedAt") - .HasColumnType("DATETIME"); + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); modelBuilder.Entity(entityType.ClrType).Property("UpdatedAt") - .HasColumnType("DATETIME"); + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); modelBuilder.Entity(entityType.ClrType).Property("CreatedAt") - .HasColumnType("DATETIME"); + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); modelBuilder.Entity(entityType.ClrType).Property("IsDeleted") .HasColumnType("BIT") diff --git a/AskFm/AskFm.DAL/Migrations/20250825225059_AddCreatedAtDefault.Designer.cs b/AskFm/AskFm.DAL/Migrations/20250825225059_AddCreatedAtDefault.Designer.cs new file mode 100644 index 0000000..66961a8 --- /dev/null +++ b/AskFm/AskFm.DAL/Migrations/20250825225059_AddCreatedAtDefault.Designer.cs @@ -0,0 +1,779 @@ +// +using System; +using AskFm.DAL; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AskFm.DAL.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20250825225059_AddCreatedAtDefault")] + partial class AddCreatedAtDefault + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Proxies:ChangeTracking", false) + .HasAnnotation("Proxies:CheckEquality", false) + .HasAnnotation("Proxies:LazyLoading", true) + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("AvatarPath") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Bio") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FollowersCount") + .HasColumnType("int"); + + b.Property("FollowingCount") + .HasColumnType("int"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("LastSeen") + .HasColumnType("datetime2"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("LikeCount") + .HasColumnType("int"); + + b.Property("ParentCommentId") + .HasColumnType("int"); + + b.Property("ThreadId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ParentCommentId"); + + b.HasIndex("ThreadId"); + + b.HasIndex("UserId"); + + b.ToTable("Comments"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.CommentLike", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CommentId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.HasKey("UserId", "CommentId"); + + b.HasIndex("CommentId"); + + b.ToTable("CommentLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Follow", b => + { + b.Property("FollowerId") + .HasColumnType("int"); + + b.Property("FollowedId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.HasKey("FollowerId", "FollowedId"); + + b.HasIndex("FollowedId"); + + b.ToTable("Follows"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("IsRead") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("NVARCHAR"); + + b.Property("ResourceId") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.SavedThreads", b => + { + b.Property("SavedThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.HasKey("SavedThreadId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("SavedThreads"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AnswerContent") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("AskedId") + .HasColumnType("int"); + + b.Property("AskerId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("QuestionContent") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.Property("isAnonymous") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("AskedId"); + + b.HasIndex("AskerId"); + + b.ToTable("Thread", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ThreadLike", b => + { + b.Property("ThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .HasColumnType("DATETIME"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .HasColumnType("DATETIME"); + + b.HasKey("ThreadId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ThreadLikes"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.HasOne("AskFm.DAL.Models.Comment", "ParentComment") + .WithMany("Replies") + .HasForeignKey("ParentCommentId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("Comments") + .HasForeignKey("ThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("Comments") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("ParentComment"); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.CommentLike", b => + { + b.HasOne("AskFm.DAL.Models.Comment", "Comment") + .WithMany("CommentLikes") + .HasForeignKey("CommentId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("CommentLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Comment"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Follow", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Followed") + .WithMany("Followers") + .HasForeignKey("FollowedId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Follower") + .WithMany("Following") + .HasForeignKey("FollowerId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Followed"); + + b.Navigation("Follower"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Notification", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("Notifications") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.SavedThreads", b => + { + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("SavedThreads") + .HasForeignKey("SavedThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("SavedThreads") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Asked") + .WithMany("ReceivedThreads") + .HasForeignKey("AskedId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Asker") + .WithMany("AskedThreads") + .HasForeignKey("AskerId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Asked"); + + b.Navigation("Asker"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ThreadLike", b => + { + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("ThreadLikes") + .HasForeignKey("ThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("ThreadLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => + { + b.Navigation("AskedThreads"); + + b.Navigation("CommentLikes"); + + b.Navigation("Comments"); + + b.Navigation("Followers"); + + b.Navigation("Following"); + + b.Navigation("Notifications"); + + b.Navigation("ReceivedThreads"); + + b.Navigation("SavedThreads"); + + b.Navigation("ThreadLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.Navigation("CommentLikes"); + + b.Navigation("Replies"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.Navigation("Comments"); + + b.Navigation("SavedThreads"); + + b.Navigation("ThreadLikes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AskFm/AskFm.DAL/Migrations/20250825225059_AddCreatedAtDefault.cs b/AskFm/AskFm.DAL/Migrations/20250825225059_AddCreatedAtDefault.cs new file mode 100644 index 0000000..475bfac --- /dev/null +++ b/AskFm/AskFm.DAL/Migrations/20250825225059_AddCreatedAtDefault.cs @@ -0,0 +1,163 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AskFm.DAL.Migrations +{ + /// + public partial class AddCreatedAtDefault : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "ThreadLikes", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Thread", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "SavedThreads", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Notifications", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Follows", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Comments", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "CommentLikes", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "AspNetUsers", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "ThreadLikes", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Thread", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "SavedThreads", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Notifications", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Follows", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "Comments", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "CommentLikes", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + table: "AspNetUsers", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + } + } +} diff --git a/AskFm/AskFm.DAL/Migrations/20250825230515_AddUpdatedAtAndDeletedAtDefault.Designer.cs b/AskFm/AskFm.DAL/Migrations/20250825230515_AddUpdatedAtAndDeletedAtDefault.Designer.cs new file mode 100644 index 0000000..443269e --- /dev/null +++ b/AskFm/AskFm.DAL/Migrations/20250825230515_AddUpdatedAtAndDeletedAtDefault.Designer.cs @@ -0,0 +1,811 @@ +// +using System; +using AskFm.DAL; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AskFm.DAL.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20250825230515_AddUpdatedAtAndDeletedAtDefault")] + partial class AddUpdatedAtAndDeletedAtDefault + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Proxies:ChangeTracking", false) + .HasAnnotation("Proxies:CheckEquality", false) + .HasAnnotation("Proxies:LazyLoading", true) + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("AvatarPath") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Bio") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FollowersCount") + .HasColumnType("int"); + + b.Property("FollowingCount") + .HasColumnType("int"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("LastSeen") + .HasColumnType("datetime2"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("LikeCount") + .HasColumnType("int"); + + b.Property("ParentCommentId") + .HasColumnType("int"); + + b.Property("ThreadId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ParentCommentId"); + + b.HasIndex("ThreadId"); + + b.HasIndex("UserId"); + + b.ToTable("Comments"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.CommentLike", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CommentId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.HasKey("UserId", "CommentId"); + + b.HasIndex("CommentId"); + + b.ToTable("CommentLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Follow", b => + { + b.Property("FollowerId") + .HasColumnType("int"); + + b.Property("FollowedId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.HasKey("FollowerId", "FollowedId"); + + b.HasIndex("FollowedId"); + + b.ToTable("Follows"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("IsRead") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("NVARCHAR"); + + b.Property("ResourceId") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.SavedThreads", b => + { + b.Property("SavedThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.HasKey("SavedThreadId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("SavedThreads"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AnswerContent") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("AskedId") + .HasColumnType("int"); + + b.Property("AskerId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("QuestionContent") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("isAnonymous") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("AskedId"); + + b.HasIndex("AskerId"); + + b.ToTable("Thread", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ThreadLike", b => + { + b.Property("ThreadId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("DeletedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("BIT") + .HasDefaultValue(false); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); + + b.HasKey("ThreadId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ThreadLikes"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.HasOne("AskFm.DAL.Models.Comment", "ParentComment") + .WithMany("Replies") + .HasForeignKey("ParentCommentId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("Comments") + .HasForeignKey("ThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("Comments") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("ParentComment"); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.CommentLike", b => + { + b.HasOne("AskFm.DAL.Models.Comment", "Comment") + .WithMany("CommentLikes") + .HasForeignKey("CommentId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("CommentLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Comment"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Follow", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Followed") + .WithMany("Followers") + .HasForeignKey("FollowedId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Follower") + .WithMany("Following") + .HasForeignKey("FollowerId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Followed"); + + b.Navigation("Follower"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Notification", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("Notifications") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.SavedThreads", b => + { + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("SavedThreads") + .HasForeignKey("SavedThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("SavedThreads") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Asked") + .WithMany("ReceivedThreads") + .HasForeignKey("AskedId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "Asker") + .WithMany("AskedThreads") + .HasForeignKey("AskerId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Asked"); + + b.Navigation("Asker"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ThreadLike", b => + { + b.HasOne("AskFm.DAL.Models.Thread", "Thread") + .WithMany("ThreadLikes") + .HasForeignKey("ThreadId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", "User") + .WithMany("ThreadLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Thread"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("AskFm.DAL.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AskFm.DAL.Models.ApplicationUser", b => + { + b.Navigation("AskedThreads"); + + b.Navigation("CommentLikes"); + + b.Navigation("Comments"); + + b.Navigation("Followers"); + + b.Navigation("Following"); + + b.Navigation("Notifications"); + + b.Navigation("ReceivedThreads"); + + b.Navigation("SavedThreads"); + + b.Navigation("ThreadLikes"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Comment", b => + { + b.Navigation("CommentLikes"); + + b.Navigation("Replies"); + }); + + modelBuilder.Entity("AskFm.DAL.Models.Thread", b => + { + b.Navigation("Comments"); + + b.Navigation("SavedThreads"); + + b.Navigation("ThreadLikes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AskFm/AskFm.DAL/Migrations/20250825230515_AddUpdatedAtAndDeletedAtDefault.cs b/AskFm/AskFm.DAL/Migrations/20250825230515_AddUpdatedAtAndDeletedAtDefault.cs new file mode 100644 index 0000000..5e8c59e --- /dev/null +++ b/AskFm/AskFm.DAL/Migrations/20250825230515_AddUpdatedAtAndDeletedAtDefault.cs @@ -0,0 +1,307 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AskFm.DAL.Migrations +{ + /// + public partial class AddUpdatedAtAndDeletedAtDefault : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "ThreadLikes", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "ThreadLikes", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "Thread", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "Thread", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "SavedThreads", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "SavedThreads", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "Notifications", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "Notifications", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "Follows", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "Follows", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "Comments", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "Comments", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "CommentLikes", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "CommentLikes", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "AspNetUsers", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "AspNetUsers", + type: "DATETIME", + nullable: false, + defaultValueSql: "GETUTCDATE()", + oldClrType: typeof(DateTime), + oldType: "DATETIME"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "ThreadLikes", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "ThreadLikes", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "Thread", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "Thread", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "SavedThreads", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "SavedThreads", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "Notifications", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "Notifications", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "Follows", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "Follows", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "Comments", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "Comments", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "CommentLikes", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "CommentLikes", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + table: "AspNetUsers", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + + migrationBuilder.AlterColumn( + name: "DeletedAt", + table: "AspNetUsers", + type: "DATETIME", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "DATETIME", + oldDefaultValueSql: "GETUTCDATE()"); + } + } +} diff --git a/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs b/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs index bdfe5a3..328f401 100644 --- a/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs +++ b/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs @@ -50,10 +50,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("nvarchar(max)"); b.Property("CreatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("DeletedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("Email") .IsRequired() @@ -113,7 +117,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("bit"); b.Property("UpdatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("UserName") .IsRequired() @@ -150,10 +156,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("nvarchar(1000)"); b.Property("CreatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("DeletedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("IsDeleted") .ValueGeneratedOnAdd() @@ -170,7 +180,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("UpdatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("UserId") .HasColumnType("int"); @@ -195,10 +207,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("CreatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("DeletedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("IsDeleted") .ValueGeneratedOnAdd() @@ -206,7 +222,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasDefaultValue(false); b.Property("UpdatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.HasKey("UserId", "CommentId"); @@ -224,10 +242,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("CreatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("DeletedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("IsActive") .HasColumnType("bit"); @@ -238,7 +260,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasDefaultValue(false); b.Property("UpdatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.HasKey("FollowerId", "FollowedId"); @@ -256,10 +280,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); b.Property("CreatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("DeletedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("IsDeleted") .ValueGeneratedOnAdd() @@ -280,7 +308,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("UpdatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("UserId") .HasColumnType("int"); @@ -301,10 +331,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("CreatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("DeletedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("IsDeleted") .ValueGeneratedOnAdd() @@ -312,7 +346,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasDefaultValue(false); b.Property("UpdatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.HasKey("SavedThreadId", "UserId"); @@ -341,10 +377,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("CreatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("DeletedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("IsDeleted") .ValueGeneratedOnAdd() @@ -361,7 +401,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("nvarchar(max)"); b.Property("UpdatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("isAnonymous") .HasColumnType("bit"); @@ -384,10 +426,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("CreatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("DeletedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.Property("IsDeleted") .ValueGeneratedOnAdd() @@ -395,7 +441,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasDefaultValue(false); b.Property("UpdatedAt") - .HasColumnType("DATETIME"); + .ValueGeneratedOnAdd() + .HasColumnType("DATETIME") + .HasDefaultValueSql("GETUTCDATE()"); b.HasKey("ThreadId", "UserId"); From dcc35e80ff5cb76b8b1fc0c260afad931494d873 Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Thu, 28 Aug 2025 04:08:24 +0300 Subject: [PATCH 17/20] Finish notification unit test and update notification hub --- AskFm/AskFm.API/Program.cs | 90 ++++- AskFm/AskFm.BLL/Hub/NotificationHub.cs | 20 +- .../AskFm.BLL/Services/NotificationService.cs | 22 +- AskFm/Tests/NotificationServiceTests.cs | 340 ++++++++++++++++++ AskFm/Tests/Tests.csproj | 5 + 5 files changed, 455 insertions(+), 22 deletions(-) create mode 100644 AskFm/Tests/NotificationServiceTests.cs diff --git a/AskFm/AskFm.API/Program.cs b/AskFm/AskFm.API/Program.cs index 34b6b1f..70e6f78 100644 --- a/AskFm/AskFm.API/Program.cs +++ b/AskFm/AskFm.API/Program.cs @@ -8,8 +8,14 @@ using DotNetEnv; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore.Proxies; -namespace AskFm.API; +using AskFm.BLL.Hub; +using AskFm.BLL.Services; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; +using AskFm.BLL.Services.UserIdentityService; +namespace AskFm.API; public class Program { @@ -18,7 +24,6 @@ public static void Main(string[] args) var builder = WebApplication.CreateBuilder(args); // Add services to the container. - builder.Services.AddControllers(); // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddOpenApi(); @@ -29,6 +34,7 @@ public static void Main(string[] args) { throw new Exception("Connection string is null"); } + builder.Services.AddHttpContextAccessor(); builder.Services.AddDbContext(options => options @@ -36,13 +42,14 @@ public static void Main(string[] args) .UseSqlServer(ConnectionString)); builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); - builder.Services.AddSwaggerGen(); - JwtOptions jwtOptions = new JwtOptions { Issuer = Environment.GetEnvironmentVariable("ISSUER"), @@ -56,6 +63,39 @@ public static void Main(string[] args) throw new Exception("jwtOptions is null"); } + // Enhanced SignalR Configuration + builder.Services.AddSignalR(options => + { + options.EnableDetailedErrors = true; + options.KeepAliveInterval = TimeSpan.FromSeconds(15); + options.ClientTimeoutInterval = TimeSpan.FromSeconds(30); + options.HandshakeTimeout = TimeSpan.FromSeconds(15); + }); + + // CORS Configuration for SignalR + builder.Services.AddCors(options => + { + options.AddPolicy("SignalRPolicy", policy => + { + // Option 1: Allow any origin (for development only) + policy.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader(); + + // Option 2: Specific origins (uncomment and modify when you know frontend URLs) + // policy.WithOrigins( + // "http://localhost:3000", // React default + // "http://localhost:4200", // Angular default + // "http://localhost:8080", // Vue default + // "http://localhost:5173", // Vite default + // "https://yourdomain.com" // Production domain + // ) + // .AllowAnyMethod() + // .AllowAnyHeader() + // .AllowCredentials(); + }); + }); + builder.Services.Configure(Options => { Options.Issuer = Environment.GetEnvironmentVariable("ISSUER"); @@ -69,9 +109,8 @@ public static void Main(string[] args) { options.DefaultAuthenticateScheme = "Bearer"; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; - }) - .AddJwtBearer( Options => + .AddJwtBearer(Options => { Options.TokenValidationParameters = new TokenValidationParameters { @@ -85,7 +124,23 @@ public static void Main(string[] args) ClockSkew = TimeSpan.FromMinutes(0) }; + // Enable JWT authentication for SignalR + Options.Events = new JwtBearerEvents + { + OnMessageReceived = context => + { + var accessToken = context.Request.Query["access_token"]; + var path = context.HttpContext.Request.Path; + + if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/notificationHub")) + { + context.Token = accessToken; + } + return Task.CompletedTask; + } + }; }); + builder.Services.AddAuthorization(); builder.Services.AddIdentity>(options => { @@ -108,30 +163,33 @@ public static void Main(string[] args) options.SignIn.RequireConfirmedEmail = false; options.SignIn.RequireConfirmedAccount = false; options.SignIn.RequireConfirmedPhoneNumber = false; - /* - * close confirmed email imediatly in register, - * but in other scenario we will block some action untill the user verify his email - */ }) .AddEntityFrameworkStores() .AddDefaultTokenProviders(); - - builder.Services.AddScoped(); - - var app = builder.Build(); // Configure the HTTP request pipeline. - if (app.Environment.IsDevelopment()) app.MapOpenApi(); + if (app.Environment.IsDevelopment()) + { + app.MapOpenApi(); + app.UseSwaggerUI(options => + { + options.SwaggerEndpoint("/openapi/v1.json", "api"); + }); + } app.UseHttpsRedirection(); + + // Apply CORS before authentication + app.UseCors("SignalRPolicy"); + app.UseAuthentication(); app.UseAuthorization(); - app.MapControllers(); + // Map SignalR Hub app.MapHub("/notificationHub"); app.Run(); diff --git a/AskFm/AskFm.BLL/Hub/NotificationHub.cs b/AskFm/AskFm.BLL/Hub/NotificationHub.cs index e881fcd..500526f 100644 --- a/AskFm/AskFm.BLL/Hub/NotificationHub.cs +++ b/AskFm/AskFm.BLL/Hub/NotificationHub.cs @@ -1,7 +1,9 @@ using Microsoft.AspNetCore.SignalR; +using Microsoft.AspNetCore.Authorization; namespace AskFm.BLL.Hub { + [Authorize] public class NotificationHub : Microsoft.AspNetCore.SignalR.Hub { public async Task JoinUserGroup(string userId) @@ -14,8 +16,24 @@ public async Task LeaveUserGroup(string userId) await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"user_{userId}"); } - public override async Task OnDisconnectedAsync(Exception exception) + public override async Task OnConnectedAsync() { + // Auto-join user to their group based on their ID from JWT token + var userId = Context.UserIdentifier; + if (!string.IsNullOrEmpty(userId)) + { + await Groups.AddToGroupAsync(Context.ConnectionId, $"user_{userId}"); + } + await base.OnConnectedAsync(); + } + + public override async Task OnDisconnectedAsync(Exception? exception) + { + var userId = Context.UserIdentifier; + if (!string.IsNullOrEmpty(userId)) + { + await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"user_{userId}"); + } await base.OnDisconnectedAsync(exception); } } diff --git a/AskFm/AskFm.BLL/Services/NotificationService.cs b/AskFm/AskFm.BLL/Services/NotificationService.cs index d94ea4f..13b78a5 100644 --- a/AskFm/AskFm.BLL/Services/NotificationService.cs +++ b/AskFm/AskFm.BLL/Services/NotificationService.cs @@ -111,20 +111,20 @@ public async Task MarkNotificationAsRead(int notificationId) notification.IsRead = true; _unitOfWork.Notifications.Update(notification); await _unitOfWork.SaveAsync(); - + return "notification has been read"; } public async Task MarkAllNotificationsAsRead(int userId) { var unreadNotifications = await _unitOfWork.Notifications.FindAllAsync(n => n.UserId == userId && !n.IsRead); - + foreach (var notification in unreadNotifications) { notification.IsRead = true; _unitOfWork.Notifications.Update(notification); } - + await _unitOfWork.SaveAsync(); return "All notifications marked as read"; } @@ -144,7 +144,10 @@ public async Task CreateNotification(int userId, NotificationStatus type, int re await _unitOfWork.Notifications.AddAsync(notification); await _unitOfWork.SaveAsync(); - + + // Get actor information for the notification + var actorUser = await _notificationRepository.GetActorUserByResourceId(resourceId, type); + var notificationDto = new NotificationDto { Id = notification.Id, @@ -153,10 +156,19 @@ public async Task CreateNotification(int userId, NotificationStatus type, int re ResourceId = notification.ResourceId, Message = notification.Message, IsRead = notification.IsRead, - CreatedAt = notification.CreatedAt + CreatedAt = notification.CreatedAt, + Actor = actorUser == null ? null : new ActorDto + { + Id = actorUser.Id, + Username = actorUser.UserName ?? "Unknown", + AvatarPath = actorUser.AvatarPath ?? string.Empty + } }; + // Send real-time notification to the specific user await _hubContext.Clients.Group($"user_{userId}") .SendAsync("ReceiveNotification", notificationDto); } + + } \ No newline at end of file diff --git a/AskFm/Tests/NotificationServiceTests.cs b/AskFm/Tests/NotificationServiceTests.cs new file mode 100644 index 0000000..6a16c64 --- /dev/null +++ b/AskFm/Tests/NotificationServiceTests.cs @@ -0,0 +1,340 @@ +using AskFm.BLL.DTO; +using AskFm.BLL.Hub; +using AskFm.DAL.Enums; +using AskFm.DAL.Interfaces; +using AskFm.DAL.Models; +using Microsoft.AspNetCore.Mvc.Diagnostics; +using Microsoft.AspNetCore.SignalR; +using Moq; +using Xunit; + +namespace AskFm.BLL.Tests.Services +{ + public class NotificationServiceTests + { + private readonly Mock _notificationRepositoryMock; + private readonly Mock _unitOfWorkMock; + private readonly Mock> _hubContextMock; + private readonly Mock _mockClients; + private readonly Mock _mockClientProxy; + private readonly Mock _mockGroups; + private readonly NotificationService _notificationService; + + public NotificationServiceTests() + { + _notificationRepositoryMock = new Mock(); + _unitOfWorkMock = new Mock(); + _hubContextMock = new Mock>(); + _mockClients = new Mock(); + _mockClientProxy = new Mock(); + _mockGroups = new Mock(); + + // Setup hub context relationships + _hubContextMock.Setup(p => p.Clients).Returns(_mockClients.Object); + _hubContextMock.Setup(p => p.Groups).Returns(_mockGroups.Object); + _mockClients.Setup(p => p.Group(It.IsAny())).Returns(_mockClientProxy.Object); + + _notificationService = new NotificationService( + _notificationRepositoryMock.Object, + _unitOfWorkMock.Object, + _hubContextMock.Object + ); + } + + [Fact] + public async Task GetUserNotifications_ReturnsCorrectDtoWithPagination() + { + // Arrange + + int userId = 1; + int pageNumber = 1; + int pageSize = 10; + var notifications = new List + { + new Notification + { + Id = 1, + UserId = userId, + Type = NotificationStatus.QUESTION, + ResourceId = 100, + Message = "Test notification", + IsRead = false, + CreatedAt = DateTime.UtcNow + } + }; + + var totalCount = 1; + var actorUser = new ApplicationUser { Id = 2, UserName = "test_user", AvatarPath = "test.jpg" }; + + _notificationRepositoryMock.Setup(p => p.GetAllNotifications(userId, pageNumber, pageSize)).ReturnsAsync((notifications, totalCount)); + _notificationRepositoryMock.Setup(p => p.GetActorUserByResourceId(100, NotificationStatus.QUESTION)).ReturnsAsync(actorUser); + + // Act + + var result = await _notificationService.GetUserNotifications(userId, pageNumber, pageSize); + + // Assert + + Assert.Single(result); + Assert.Equal(1, result[0].Id); + Assert.Equal(NotificationStatus.QUESTION.ToString(), result[0].Type); + Assert.Equal("Test notification", result[0].Message); + Assert.False(result[0].IsRead); + Assert.Equal("test_user", result[0].Actor.Username); + Assert.Equal("test.jpg", result[0].Actor.AvatarPath); + Assert.Equal(2, result[0].Actor.Id); + Assert.Equal(1, result[0].Pagination.TotalCount); + + } + + [Fact] + public async Task GetUserNotifications_WithNullActor_ReturnsCorrectDtoWithPagination() + { + // Arrange + + int userId = 1; + int pageNumber = 1; + int pageSize = 10; + var notifications = new List + { + new Notification + { + Id = 1, + UserId = userId, + Type = NotificationStatus.QUESTION, + ResourceId = 100, + Message = "Test notification", + IsRead = false, + CreatedAt = DateTime.UtcNow + } + }; + + var totalCount = 1; + + _notificationRepositoryMock.Setup(p => p.GetAllNotifications(userId, pageNumber, pageSize)).ReturnsAsync((notifications, totalCount)); + _notificationRepositoryMock.Setup(p => p.GetActorUserByResourceId(100, NotificationStatus.QUESTION)).ReturnsAsync((ApplicationUser)null); + + // Act + + var result = await _notificationService.GetUserNotifications(userId, pageNumber, pageSize); + + // Assert + + Assert.Single(result); + Assert.Equal(1, result[0].Id); + Assert.Equal(NotificationStatus.QUESTION.ToString(), result[0].Type); + Assert.Equal("Test notification", result[0].Message); + Assert.False(result[0].IsRead); + Assert.Null(result[0].Actor); + Assert.Equal(1, result[0].Pagination.TotalCount); + + } + + [Fact] + public async Task GetNotificationsByType_WithValidCategory_ReturnsFilteredNotifications() + { + // Arrange + + int userId = 1; + string category = "answer"; + int pageNumber = 1; + int pageSize = 10; + var notifications = new List + { + new Notification + { + Id = 1, + UserId = userId, + Type = NotificationStatus.ANSWER, + ResourceId = 100, + Message = "Test notification", + IsRead = false, + CreatedAt = DateTime.UtcNow + } + }; + + var totalCount = 1; + var actorUser = new ApplicationUser { Id = 2, UserName = "test_user", AvatarPath = "test.jpg" }; + + _notificationRepositoryMock.Setup(p => p.GetNotificationsByType(userId, NotificationStatus.ANSWER, pageNumber, pageSize)).ReturnsAsync((notifications, totalCount)); + _notificationRepositoryMock.Setup(p => p.GetActorUserByResourceId(100, NotificationStatus.ANSWER)).ReturnsAsync(actorUser); + + // Act + + var result = await _notificationService.GetNotificationsByType(userId, category, pageNumber, pageSize); + + // Assert + + Assert.Single(result); + Assert.Equal(1, result[0].Id); + Assert.Equal(category.ToUpper(), result[0].Type); + Assert.Equal("Test notification", result[0].Message); + Assert.False(result[0].IsRead); + Assert.Equal("test_user", result[0].Actor.Username); + Assert.Equal("test.jpg", result[0].Actor.AvatarPath); + Assert.Equal(2, result[0].Actor.Id); + Assert.Equal(1, result[0].Pagination.TotalCount); + + } + + [Fact] + public async Task GetNotificationsByType_WithINValidCategory_ReturnsFilteredNotifications() + { + // Arrang + int userId = 1; + string category = "InvalidCategory"; + // Act and Assert + + var ex = await Assert.ThrowsAsync(() => _notificationService.GetNotificationsByType(userId, category)); + Assert.Contains("Invalid notification category", ex.Message); + } + + [Fact] + public async Task MarkNotificationAsRead_WithValidId_UpdatesNotification() + { + // Arrange + + int notificationId = 1; + Notification notification = new Notification { Id = notificationId, UserId = 1, IsRead = false }; + _unitOfWorkMock.Setup(p => p.Notifications.GetByIdAsync(notificationId)).ReturnsAsync(notification); + _unitOfWorkMock.Setup(p => p.SaveAsync()).ReturnsAsync(1); + + // Act + var result = await _notificationService.MarkNotificationAsRead(notificationId); + + // Assert + + Assert.True(notification.IsRead); + Assert.Equal("notification has been read", result); + _unitOfWorkMock.Verify(p => p.Notifications.Update(notification), Times.Once); + _unitOfWorkMock.Verify(p => p.SaveAsync(), Times.Once); + } + + [Fact] + public async Task MarkNotificationAsRead_WithInvalidId_ThrowsInvalidOperationException() + { + // Arrange + var notificationId = 999; + _unitOfWorkMock.Setup(p => p.Notifications.GetByIdAsync(notificationId)).ReturnsAsync((Notification)null); + + // Act & Assert + var ex = await Assert.ThrowsAsync( + () => _notificationService.MarkNotificationAsRead(notificationId)); + + Assert.Contains("Notification with ID 999 not found", ex.Message); + } + + [Fact] + public async Task MarkAllNotificationsAsRead_UpdatesAllUnreadNotifications() + { + // Arrange + var userId = 1; + var unreadNotifications = new List + { + new Notification { Id = 1, UserId = userId, IsRead = false }, + new Notification { Id = 2, UserId = userId, IsRead = false } + }; + + _unitOfWorkMock.Setup(p => p.Notifications.FindAllAsync(It.IsAny>>(), It.IsAny())) + .ReturnsAsync(unreadNotifications); + _unitOfWorkMock.Setup(p => p.SaveAsync()).ReturnsAsync(1); + + // Act + var result = await _notificationService.MarkAllNotificationsAsRead(userId); + + // Assert + Assert.Equal("All notifications marked as read", result); + Assert.True(unreadNotifications.All(n => n.IsRead)); + _unitOfWorkMock.Verify(p => p.Notifications.Update(It.IsAny()), Times.Exactly(2)); + _unitOfWorkMock.Verify(p => p.SaveAsync(), Times.Once); + } + + [Fact] + public async Task CreateNotification_WithValidData_CreatesNotificationAndSendsSignalR() + { + // Arrange + var userId = 1; + var type = NotificationStatus.QUESTION; + var resourceId = 100; + var message = "Test notification message"; + var actorUser = new ApplicationUser + { + Id = 2, + UserName = "test_actor", + AvatarPath = "actor.jpg" + }; + + // Setup specific mock behavior for this test + _mockClients.Setup(p => p.Group($"user_{userId}")).Returns(_mockClientProxy.Object); + + _unitOfWorkMock.Setup(p => p.Notifications.AddAsync(It.IsAny())) + .Returns(Task.CompletedTask); + _unitOfWorkMock.Setup(p => p.SaveAsync()).ReturnsAsync(1); + + _notificationRepositoryMock.Setup(r => r.GetActorUserByResourceId(resourceId, type)) + .ReturnsAsync(actorUser); + + // Act + await _notificationService.CreateNotification(userId, type, resourceId, message); + + // Assert + // Verify notification was added to database + _unitOfWorkMock.Verify(p => p.Notifications.AddAsync(It.Is(n => + n.UserId == userId && + n.Type == type && + n.ResourceId == resourceId && + n.Message == message && + !n.IsRead && + n.CreatedAt != default && + n.UpdatedAt != default + )), Times.Once); + + // Verify database save was called + _unitOfWorkMock.Verify(p => p.SaveAsync(), Times.Once); + + // Verify actor user was fetched + _notificationRepositoryMock.Verify(r => r.GetActorUserByResourceId(resourceId, type), Times.Once); + + // Verify SignalR notification was sent to correct group + _mockClientProxy.Verify(cp => cp.SendCoreAsync( + "ReceiveNotification", + It.Is(args => args.Length == 1 && + ((NotificationDto)args[0]).UserId == userId && + ((NotificationDto)args[0]).Type == type.ToString() && + ((NotificationDto)args[0]).Message == message && + ((NotificationDto)args[0]).Actor.Username == "test_actor" && + ((NotificationDto)args[0]).Actor.AvatarPath == "actor.jpg" + ), + default(CancellationToken) + ), Times.Once); + } + + [Fact] + public async Task CreateNotification_WhenDatabaseSaveFails_ThrowsException() + { + // Arrange + var userId = 1; + var type = NotificationStatus.ANSWER; + var resourceId = 300; + var message = "Answer notification"; + + _unitOfWorkMock.Setup(p => p.Notifications.AddAsync(It.IsAny())) + .Returns(Task.CompletedTask); + _unitOfWorkMock.Setup(p => p.SaveAsync()) + .ThrowsAsync(new Exception("Database error")); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => _notificationService.CreateNotification(userId, type, resourceId, message) + ); + + Assert.Equal("Database error", exception.Message); + + // Verify notification was attempted to be added + _unitOfWorkMock.Verify(p => p.Notifications.AddAsync(It.IsAny()), Times.Once); + _unitOfWorkMock.Verify(p => p.SaveAsync(), Times.Once); + } + } + + +} \ No newline at end of file diff --git a/AskFm/Tests/Tests.csproj b/AskFm/Tests/Tests.csproj index 95d873d..ef29c88 100644 --- a/AskFm/Tests/Tests.csproj +++ b/AskFm/Tests/Tests.csproj @@ -10,6 +10,7 @@ + @@ -18,4 +19,8 @@ + + + + From 73b3fbaabbd7c28fe32deaa3b3fd6f6cdbae5d36 Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Thu, 28 Aug 2025 05:41:48 +0300 Subject: [PATCH 18/20] Apply Auth and update all files that depend on it --- .../Controllers/NotificationController.cs | 54 ++++-- .../Services/INotificationService.cs | 2 +- .../AskFm.BLL/Services/NotificationService.cs | 6 +- .../Interfaces/INotificationRepository.cs | 1 + .../Repositories/NotificationRepository.cs | 19 +- AskFm/Tests/NotificationServiceTests.cs | 169 +++++++----------- 6 files changed, 126 insertions(+), 125 deletions(-) diff --git a/AskFm/AskFm.API/Controllers/NotificationController.cs b/AskFm/AskFm.API/Controllers/NotificationController.cs index 5630476..9271ecd 100644 --- a/AskFm/AskFm.API/Controllers/NotificationController.cs +++ b/AskFm/AskFm.API/Controllers/NotificationController.cs @@ -1,11 +1,13 @@ using AskFm.BLL.Services; using AskFm.DAL.Enums; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace AskFm.API.Controllers { [ApiController] [Route("api/[controller]")] + [Authorize] public class NotificationController : ControllerBase { private readonly INotificationService _notificationService; @@ -15,11 +17,12 @@ public NotificationController(INotificationService notificationService) _notificationService = notificationService; } - [HttpGet("{userId}")] - public async Task GetUserNotifications(int userId, [FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 10) + [HttpGet] + public async Task GetUserNotifications([FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 10) { try { + var userId = GetCurrentUserId(); var notifications = await _notificationService.GetUserNotifications(userId, pageNumber, pageSize); return Ok(notifications); } @@ -29,11 +32,12 @@ public async Task GetUserNotifications(int userId, [FromQuery] in } } - [HttpGet("{userId}/type/{category}")] - public async Task GetNotificationsByType(int userId, string category, [FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 10) + [HttpGet("type/{category}")] + public async Task GetNotificationsByType(string category, [FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 10) { try { + var userId = GetCurrentUserId(); var response = await _notificationService.GetNotificationsByType(userId, category, pageNumber, pageSize); return Ok(response); } @@ -52,20 +56,30 @@ public async Task MarkNotificationAsRead(int notificationId) { try { - var result = await _notificationService.MarkNotificationAsRead(notificationId); + var userId = GetCurrentUserId(); + var result = await _notificationService.MarkNotificationAsRead(notificationId, userId); return Ok(new { message = result }); } + catch (InvalidOperationException ex) + { + return NotFound(ex.Message); + } + catch (UnauthorizedAccessException ex) + { + return Forbid(ex.Message); + } catch (Exception ex) { return BadRequest(ex.Message); } } - [HttpPut("{userId}/read-all")] - public async Task MarkAllNotificationsAsRead(int userId) + [HttpPut("read-all")] + public async Task MarkAllNotificationsAsRead() { try { + var userId = GetCurrentUserId(); var result = await _notificationService.MarkAllNotificationsAsRead(userId); return Ok(new { message = result }); } @@ -76,18 +90,36 @@ public async Task MarkAllNotificationsAsRead(int userId) } [HttpPost] - public async Task CreateNotification(int userId, NotificationStatus type, int resourceId, string message) + [Authorize(Roles = "Admin")] + public async Task CreateNotification([FromBody] CreateNotificationRequest request) { try { - await _notificationService.CreateNotification(userId, type, resourceId, message); - - return CreatedAtAction(nameof(GetUserNotifications), new { userId = userId }, new { message = "Notification created successfully" }); + await _notificationService.CreateNotification(request.UserId, request.Type, request.ResourceId, request.Message); + return Ok(new { message = "Notification created successfully" }); } catch (Exception ex) { return BadRequest(ex.Message); } } + + private int GetCurrentUserId() + { + var userIdClaim = User.FindFirst("UserId")?.Value; + if (string.IsNullOrEmpty(userIdClaim) || !int.TryParse(userIdClaim, out int userId)) + { + throw new UnauthorizedAccessException("Invalid user token"); + } + return userId; + } + } + + public class CreateNotificationRequest + { + public int UserId { get; set; } + public NotificationStatus Type { get; set; } + public int ResourceId { get; set; } + public string Message { get; set; } } } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/INotificationService.cs b/AskFm/AskFm.BLL/Services/INotificationService.cs index 4eb9d63..6f75ea9 100644 --- a/AskFm/AskFm.BLL/Services/INotificationService.cs +++ b/AskFm/AskFm.BLL/Services/INotificationService.cs @@ -7,7 +7,7 @@ public interface INotificationService { Task> GetUserNotifications(int userId, int pageNumber = 1, int pageSize = 10); Task> GetNotificationsByType(int userId, string category, int pageNumber = 1, int pageSize = 10); - Task MarkNotificationAsRead(int notificationId); + Task MarkNotificationAsRead(int notificationId, int userId); Task MarkAllNotificationsAsRead(int userId); Task CreateNotification(int userId, NotificationStatus type, int resourceId, string message); } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/NotificationService.cs b/AskFm/AskFm.BLL/Services/NotificationService.cs index 13b78a5..598f686 100644 --- a/AskFm/AskFm.BLL/Services/NotificationService.cs +++ b/AskFm/AskFm.BLL/Services/NotificationService.cs @@ -102,11 +102,11 @@ public async Task> GetNotificationsByType(int userId, stri return notificationDtos; } - public async Task MarkNotificationAsRead(int notificationId) + public async Task MarkNotificationAsRead(int notificationId, int userId) { - var notification = await _unitOfWork.Notifications.GetByIdAsync(notificationId); + var notification = await _notificationRepository.GetUserNotificationById(notificationId, userId); if (notification == null) - throw new InvalidOperationException($"Notification with ID {notificationId} not found."); + throw new InvalidOperationException("Notification not found or access denied."); notification.IsRead = true; _unitOfWork.Notifications.Update(notification); diff --git a/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs b/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs index 816c2f1..310ec78 100644 --- a/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs +++ b/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs @@ -8,4 +8,5 @@ public interface INotificationRepository Task<(IEnumerable notifications, int totalCount)> GetAllNotifications(int userId, int pageNumber, int pageSize); Task<(IEnumerable notifications, int totalCount)> GetNotificationsByType(int userId, NotificationStatus status, int pageNumber, int pageSize); Task GetActorUserByResourceId(int resourceId, NotificationStatus type); + Task GetUserNotificationById(int notificationId, int userId); } \ No newline at end of file diff --git a/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs b/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs index d49564d..56189c6 100644 --- a/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs +++ b/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs @@ -13,34 +13,34 @@ public NotificationRepository(IUnitOfWork unitOfWork) { _unitOfWork = unitOfWork; } - + public async Task<(IEnumerable notifications, int totalCount)> GetAllNotifications(int userId, int pageNumber, int pageSize) { var query = _unitOfWork.Notifications.FindAll(n => n.UserId == userId); - + var totalCount = await query.CountAsync(); - + var notifications = await query .OrderByDescending(n => n.CreatedAt) .Skip((pageNumber - 1) * pageSize) .Take(pageSize) .ToListAsync(); - + return (notifications, totalCount); } - + public async Task<(IEnumerable notifications, int totalCount)> GetNotificationsByType(int userId, NotificationStatus status, int pageNumber, int pageSize) { var query = _unitOfWork.Notifications.FindAll(n => n.UserId == userId && n.Type == status); var totalCount = await query.CountAsync(); - + var notifications = await query .OrderByDescending(n => n.CreatedAt) .Skip((pageNumber - 1) * pageSize) .Take(pageSize) .ToListAsync(); - + return (notifications, totalCount); } @@ -78,4 +78,9 @@ public NotificationRepository(IUnitOfWork unitOfWork) } return null; } + + public async Task GetUserNotificationById(int notificationId, int userId) + { + return await _unitOfWork.Notifications.FindAsync(n => n.Id == notificationId && n.UserId == userId); + } } \ No newline at end of file diff --git a/AskFm/Tests/NotificationServiceTests.cs b/AskFm/Tests/NotificationServiceTests.cs index 6a16c64..dc830ce 100644 --- a/AskFm/Tests/NotificationServiceTests.cs +++ b/AskFm/Tests/NotificationServiceTests.cs @@ -1,9 +1,9 @@ using AskFm.BLL.DTO; using AskFm.BLL.Hub; +using AskFm.BLL.Services; using AskFm.DAL.Enums; using AskFm.DAL.Interfaces; using AskFm.DAL.Models; -using Microsoft.AspNetCore.Mvc.Diagnostics; using Microsoft.AspNetCore.SignalR; using Moq; using Xunit; @@ -34,6 +34,10 @@ public NotificationServiceTests() _hubContextMock.Setup(p => p.Groups).Returns(_mockGroups.Object); _mockClients.Setup(p => p.Group(It.IsAny())).Returns(_mockClientProxy.Object); + // Setup unit of work notifications repository mock + var notificationRepoMock = new Mock>(); + _unitOfWorkMock.Setup(p => p.Notifications).Returns(notificationRepoMock.Object); + _notificationService = new NotificationService( _notificationRepositoryMock.Object, _unitOfWorkMock.Object, @@ -45,7 +49,6 @@ public NotificationServiceTests() public async Task GetUserNotifications_ReturnsCorrectDtoWithPagination() { // Arrange - int userId = 1; int pageNumber = 1; int pageSize = 10; @@ -70,11 +73,9 @@ public async Task GetUserNotifications_ReturnsCorrectDtoWithPagination() _notificationRepositoryMock.Setup(p => p.GetActorUserByResourceId(100, NotificationStatus.QUESTION)).ReturnsAsync(actorUser); // Act - var result = await _notificationService.GetUserNotifications(userId, pageNumber, pageSize); // Assert - Assert.Single(result); Assert.Equal(1, result[0].Id); Assert.Equal(NotificationStatus.QUESTION.ToString(), result[0].Type); @@ -84,14 +85,12 @@ public async Task GetUserNotifications_ReturnsCorrectDtoWithPagination() Assert.Equal("test.jpg", result[0].Actor.AvatarPath); Assert.Equal(2, result[0].Actor.Id); Assert.Equal(1, result[0].Pagination.TotalCount); - } [Fact] public async Task GetUserNotifications_WithNullActor_ReturnsCorrectDtoWithPagination() { // Arrange - int userId = 1; int pageNumber = 1; int pageSize = 10; @@ -115,11 +114,9 @@ public async Task GetUserNotifications_WithNullActor_ReturnsCorrectDtoWithPagina _notificationRepositoryMock.Setup(p => p.GetActorUserByResourceId(100, NotificationStatus.QUESTION)).ReturnsAsync((ApplicationUser)null); // Act - var result = await _notificationService.GetUserNotifications(userId, pageNumber, pageSize); // Assert - Assert.Single(result); Assert.Equal(1, result[0].Id); Assert.Equal(NotificationStatus.QUESTION.ToString(), result[0].Type); @@ -127,14 +124,12 @@ public async Task GetUserNotifications_WithNullActor_ReturnsCorrectDtoWithPagina Assert.False(result[0].IsRead); Assert.Null(result[0].Actor); Assert.Equal(1, result[0].Pagination.TotalCount); - } [Fact] public async Task GetNotificationsByType_WithValidCategory_ReturnsFilteredNotifications() { // Arrange - int userId = 1; string category = "answer"; int pageNumber = 1; @@ -160,11 +155,9 @@ public async Task GetNotificationsByType_WithValidCategory_ReturnsFilteredNotifi _notificationRepositoryMock.Setup(p => p.GetActorUserByResourceId(100, NotificationStatus.ANSWER)).ReturnsAsync(actorUser); // Act - var result = await _notificationService.GetNotificationsByType(userId, category, pageNumber, pageSize); // Assert - Assert.Single(result); Assert.Equal(1, result[0].Id); Assert.Equal(category.ToUpper(), result[0].Type); @@ -174,18 +167,19 @@ public async Task GetNotificationsByType_WithValidCategory_ReturnsFilteredNotifi Assert.Equal("test.jpg", result[0].Actor.AvatarPath); Assert.Equal(2, result[0].Actor.Id); Assert.Equal(1, result[0].Pagination.TotalCount); - } [Fact] - public async Task GetNotificationsByType_WithINValidCategory_ReturnsFilteredNotifications() + public async Task GetNotificationsByType_WithInvalidCategory_ThrowsArgumentException() { - // Arrang + // Arrange int userId = 1; string category = "InvalidCategory"; - // Act and Assert + int pageNumber = 1; + int pageSize = 10; - var ex = await Assert.ThrowsAsync(() => _notificationService.GetNotificationsByType(userId, category)); + // Act & Assert + var ex = await Assert.ThrowsAsync(() => _notificationService.GetNotificationsByType(userId, category, pageNumber, pageSize)); Assert.Contains("Invalid notification category", ex.Message); } @@ -193,17 +187,28 @@ public async Task GetNotificationsByType_WithINValidCategory_ReturnsFilteredNoti public async Task MarkNotificationAsRead_WithValidId_UpdatesNotification() { // Arrange - int notificationId = 1; - Notification notification = new Notification { Id = notificationId, UserId = 1, IsRead = false }; - _unitOfWorkMock.Setup(p => p.Notifications.GetByIdAsync(notificationId)).ReturnsAsync(notification); + int userId = 1; + Notification notification = new Notification + { + Id = notificationId, + UserId = userId, + IsRead = false, + Type = NotificationStatus.QUESTION, + ResourceId = 100, + Message = "Test notification", + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + _notificationRepositoryMock.Setup(p => p.GetUserNotificationById(notificationId, userId)) + .ReturnsAsync(notification); _unitOfWorkMock.Setup(p => p.SaveAsync()).ReturnsAsync(1); // Act - var result = await _notificationService.MarkNotificationAsRead(notificationId); + var result = await _notificationService.MarkNotificationAsRead(notificationId, userId); // Assert - Assert.True(notification.IsRead); Assert.Equal("notification has been read", result); _unitOfWorkMock.Verify(p => p.Notifications.Update(notification), Times.Once); @@ -215,27 +220,49 @@ public async Task MarkNotificationAsRead_WithInvalidId_ThrowsInvalidOperationExc { // Arrange var notificationId = 999; - _unitOfWorkMock.Setup(p => p.Notifications.GetByIdAsync(notificationId)).ReturnsAsync((Notification)null); + var userId = 1; + _notificationRepositoryMock.Setup(p => p.GetUserNotificationById(notificationId, userId)) + .ReturnsAsync((Notification)null); // Act & Assert var ex = await Assert.ThrowsAsync( - () => _notificationService.MarkNotificationAsRead(notificationId)); + () => _notificationService.MarkNotificationAsRead(notificationId, userId)); - Assert.Contains("Notification with ID 999 not found", ex.Message); + Assert.Contains("Notification not found or access denied", ex.Message); } [Fact] public async Task MarkAllNotificationsAsRead_UpdatesAllUnreadNotifications() { // Arrange - var userId = 1; + int userId = 1; var unreadNotifications = new List { - new Notification { Id = 1, UserId = userId, IsRead = false }, - new Notification { Id = 2, UserId = userId, IsRead = false } + new Notification + { + Id = 1, + UserId = userId, + IsRead = false, + Type = NotificationStatus.QUESTION, + ResourceId = 100, + Message = "Test notification 1", + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }, + new Notification + { + Id = 2, + UserId = userId, + IsRead = false, + Type = NotificationStatus.ANSWER, + ResourceId = 200, + Message = "Test notification 2", + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + } }; - _unitOfWorkMock.Setup(p => p.Notifications.FindAllAsync(It.IsAny>>(), It.IsAny())) + _unitOfWorkMock.Setup(p => p.Notifications.FindAllAsync(It.IsAny>>(), null)) .ReturnsAsync(unreadNotifications); _unitOfWorkMock.Setup(p => p.SaveAsync()).ReturnsAsync(1); @@ -244,97 +271,33 @@ public async Task MarkAllNotificationsAsRead_UpdatesAllUnreadNotifications() // Assert Assert.Equal("All notifications marked as read", result); - Assert.True(unreadNotifications.All(n => n.IsRead)); + Assert.All(unreadNotifications, n => Assert.True(n.IsRead)); _unitOfWorkMock.Verify(p => p.Notifications.Update(It.IsAny()), Times.Exactly(2)); _unitOfWorkMock.Verify(p => p.SaveAsync(), Times.Once); } [Fact] - public async Task CreateNotification_WithValidData_CreatesNotificationAndSendsSignalR() + public async Task CreateNotification_CreatesAndSendsNotification() { // Arrange - var userId = 1; - var type = NotificationStatus.QUESTION; - var resourceId = 100; - var message = "Test notification message"; - var actorUser = new ApplicationUser - { - Id = 2, - UserName = "test_actor", - AvatarPath = "actor.jpg" - }; - - // Setup specific mock behavior for this test - _mockClients.Setup(p => p.Group($"user_{userId}")).Returns(_mockClientProxy.Object); + int userId = 1; + var type = NotificationStatus.FOLLOW; + int resourceId = 100; + string message = "Test notification"; + var actorUser = new ApplicationUser { Id = 2, UserName = "test_user", AvatarPath = "test.jpg" }; - _unitOfWorkMock.Setup(p => p.Notifications.AddAsync(It.IsAny())) - .Returns(Task.CompletedTask); + _unitOfWorkMock.Setup(p => p.Notifications.AddAsync(It.IsAny())); _unitOfWorkMock.Setup(p => p.SaveAsync()).ReturnsAsync(1); - - _notificationRepositoryMock.Setup(r => r.GetActorUserByResourceId(resourceId, type)) + _notificationRepositoryMock.Setup(p => p.GetActorUserByResourceId(resourceId, type)) .ReturnsAsync(actorUser); // Act await _notificationService.CreateNotification(userId, type, resourceId, message); // Assert - // Verify notification was added to database - _unitOfWorkMock.Verify(p => p.Notifications.AddAsync(It.Is(n => - n.UserId == userId && - n.Type == type && - n.ResourceId == resourceId && - n.Message == message && - !n.IsRead && - n.CreatedAt != default && - n.UpdatedAt != default - )), Times.Once); - - // Verify database save was called - _unitOfWorkMock.Verify(p => p.SaveAsync(), Times.Once); - - // Verify actor user was fetched - _notificationRepositoryMock.Verify(r => r.GetActorUserByResourceId(resourceId, type), Times.Once); - - // Verify SignalR notification was sent to correct group - _mockClientProxy.Verify(cp => cp.SendCoreAsync( - "ReceiveNotification", - It.Is(args => args.Length == 1 && - ((NotificationDto)args[0]).UserId == userId && - ((NotificationDto)args[0]).Type == type.ToString() && - ((NotificationDto)args[0]).Message == message && - ((NotificationDto)args[0]).Actor.Username == "test_actor" && - ((NotificationDto)args[0]).Actor.AvatarPath == "actor.jpg" - ), - default(CancellationToken) - ), Times.Once); - } - - [Fact] - public async Task CreateNotification_WhenDatabaseSaveFails_ThrowsException() - { - // Arrange - var userId = 1; - var type = NotificationStatus.ANSWER; - var resourceId = 300; - var message = "Answer notification"; - - _unitOfWorkMock.Setup(p => p.Notifications.AddAsync(It.IsAny())) - .Returns(Task.CompletedTask); - _unitOfWorkMock.Setup(p => p.SaveAsync()) - .ThrowsAsync(new Exception("Database error")); - - // Act & Assert - var exception = await Assert.ThrowsAsync( - () => _notificationService.CreateNotification(userId, type, resourceId, message) - ); - - Assert.Equal("Database error", exception.Message); - - // Verify notification was attempted to be added _unitOfWorkMock.Verify(p => p.Notifications.AddAsync(It.IsAny()), Times.Once); _unitOfWorkMock.Verify(p => p.SaveAsync(), Times.Once); + _mockClientProxy.Verify(p => p.SendCoreAsync("ReceiveNotification", It.IsAny(), default), Times.Once); } } - - } \ No newline at end of file From ef94e309bdc6d244841b1dcf3befa6ec64edb914 Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Sun, 21 Sep 2025 01:12:51 +0300 Subject: [PATCH 19/20] Apply ServiceResult in Notifications Service --- AskFm/AskFm.API/AskFm.API.csproj | 2 +- .../Controllers/NotificationController.cs | 9 +- .../DTO/CreateNotificationRequest.cs | 16 + .../Services/INotificationService.cs | 10 +- .../AskFm.BLL/Services/NotificationService.cs | 274 ++++++++++-------- AskFm/Tests/NotificationServiceTests.cs | 106 ++++--- 6 files changed, 247 insertions(+), 170 deletions(-) create mode 100644 AskFm/AskFm.BLL/DTO/CreateNotificationRequest.cs diff --git a/AskFm/AskFm.API/AskFm.API.csproj b/AskFm/AskFm.API/AskFm.API.csproj index 2f95f72..60af690 100644 --- a/AskFm/AskFm.API/AskFm.API.csproj +++ b/AskFm/AskFm.API/AskFm.API.csproj @@ -23,7 +23,7 @@ - + diff --git a/AskFm/AskFm.API/Controllers/NotificationController.cs b/AskFm/AskFm.API/Controllers/NotificationController.cs index 9271ecd..fbd551a 100644 --- a/AskFm/AskFm.API/Controllers/NotificationController.cs +++ b/AskFm/AskFm.API/Controllers/NotificationController.cs @@ -1,3 +1,4 @@ +using AskFm.BLL.DTO; using AskFm.BLL.Services; using AskFm.DAL.Enums; using Microsoft.AspNetCore.Authorization; @@ -114,12 +115,4 @@ private int GetCurrentUserId() return userId; } } - - public class CreateNotificationRequest - { - public int UserId { get; set; } - public NotificationStatus Type { get; set; } - public int ResourceId { get; set; } - public string Message { get; set; } - } } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/DTO/CreateNotificationRequest.cs b/AskFm/AskFm.BLL/DTO/CreateNotificationRequest.cs new file mode 100644 index 0000000..1cd7eb2 --- /dev/null +++ b/AskFm/AskFm.BLL/DTO/CreateNotificationRequest.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AskFm.DAL.Enums; + +namespace AskFm.BLL.DTO +{ + public class CreateNotificationRequest + { + public int UserId { get; set; } + public NotificationStatus Type { get; set; } + public int ResourceId { get; set; } + public string Message { get; set; } + } +} \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/INotificationService.cs b/AskFm/AskFm.BLL/Services/INotificationService.cs index 6f75ea9..afa49ad 100644 --- a/AskFm/AskFm.BLL/Services/INotificationService.cs +++ b/AskFm/AskFm.BLL/Services/INotificationService.cs @@ -5,9 +5,9 @@ namespace AskFm.BLL.Services; public interface INotificationService { - Task> GetUserNotifications(int userId, int pageNumber = 1, int pageSize = 10); - Task> GetNotificationsByType(int userId, string category, int pageNumber = 1, int pageSize = 10); - Task MarkNotificationAsRead(int notificationId, int userId); - Task MarkAllNotificationsAsRead(int userId); - Task CreateNotification(int userId, NotificationStatus type, int resourceId, string message); + Task>> GetUserNotifications(int userId, int pageNumber = 1, int pageSize = 10); + Task>> GetNotificationsByType(int userId, string category, int pageNumber = 1, int pageSize = 10); + Task> MarkNotificationAsRead(int notificationId, int userId); + Task> MarkAllNotificationsAsRead(int userId); + Task> CreateNotification(int userId, NotificationStatus type, int resourceId, string message); } \ No newline at end of file diff --git a/AskFm/AskFm.BLL/Services/NotificationService.cs b/AskFm/AskFm.BLL/Services/NotificationService.cs index 598f686..cd3c90b 100644 --- a/AskFm/AskFm.BLL/Services/NotificationService.cs +++ b/AskFm/AskFm.BLL/Services/NotificationService.cs @@ -1,18 +1,18 @@ using AskFm.BLL.DTO; using AskFm.BLL.Hub; -using AskFm.BLL.Services; using AskFm.DAL.Enums; using AskFm.DAL.Interfaces; using AskFm.DAL.Models; using Microsoft.AspNetCore.SignalR; +namespace AskFm.BLL.Services; + public class NotificationService : INotificationService { private readonly INotificationRepository _notificationRepository; private readonly IUnitOfWork _unitOfWork; private readonly IHubContext _hubContext; - public NotificationService(INotificationRepository notificationRepository, IUnitOfWork unitOfWork, IHubContext hubContext) { _notificationRepository = notificationRepository; @@ -20,155 +20,191 @@ public NotificationService(INotificationRepository notificationRepository, IUnit _hubContext = hubContext; } - public async Task> GetUserNotifications(int userId, int pageNumber = 1, int pageSize = 10) + public async Task>> GetUserNotifications(int userId, int pageNumber = 1, int pageSize = 10) { - var (notifications, totalCount) = await _notificationRepository.GetAllNotifications(userId, pageNumber, pageSize); + try + { + var (notifications, totalCount) = await _notificationRepository.GetAllNotifications(userId, pageNumber, pageSize); - var totalPages = (int)Math.Ceiling((double)totalCount / pageSize); + var totalPages = (int)Math.Ceiling((double)totalCount / pageSize); - var notificationDtos = new List(); + var notificationDtos = new List(); - foreach (var notification in notifications) - { - var actorUser = await _notificationRepository.GetActorUserByResourceId(notification.ResourceId, notification.Type); - notificationDtos.Add(new NotificationDto + foreach (var notification in notifications) { - Id = notification.Id, - UserId = notification.UserId, - Type = notification.Type.ToString(), - ResourceId = notification.ResourceId, - Message = notification.Message, - IsRead = notification.IsRead, - CreatedAt = notification.CreatedAt, - Actor = actorUser == null ? null : new ActorDto - { - Id = actorUser.Id, - Username = actorUser?.UserName ?? "Unknown", - AvatarPath = actorUser?.AvatarPath ?? String.Empty - }, - Pagination = new PaginationDto + var actorUser = await _notificationRepository.GetActorUserByResourceId(notification.ResourceId, notification.Type); + notificationDtos.Add(new NotificationDto { - CurrentPage = pageNumber, - TotalPages = totalPages, - TotalCount = totalCount, - HasNext = pageNumber < totalPages, - HasPrevious = pageNumber > 1 - } - }); + Id = notification.Id, + UserId = notification.UserId, + Type = notification.Type.ToString(), + ResourceId = notification.ResourceId, + Message = notification.Message, + IsRead = notification.IsRead, + CreatedAt = notification.CreatedAt, + Actor = actorUser == null ? null : new ActorDto + { + Id = actorUser.Id, + Username = actorUser?.UserName ?? "Unknown", + AvatarPath = actorUser?.AvatarPath ?? String.Empty + }, + Pagination = new PaginationDto + { + CurrentPage = pageNumber, + TotalPages = totalPages, + TotalCount = totalCount, + HasNext = pageNumber < totalPages, + HasPrevious = pageNumber > 1 + } + }); + } + return await ServiceResult>.Success(notificationDtos); + } + catch (Exception ex) + { + return await ServiceResult>.Failure(new List { ex.Message }); } - return notificationDtos; } - public async Task> GetNotificationsByType(int userId, string category, int pageNumber = 1, int pageSize = 10) + public async Task>> GetNotificationsByType(int userId, string category, int pageNumber = 1, int pageSize = 10) { - // Convert category to uppercase and match with enum - if (!Enum.TryParse(category.ToUpper(), out var notificationType)) - throw new ArgumentException($"Invalid notification category: {category}"); + try + { + // Convert category to uppercase and match with enum + if (!Enum.TryParse(category.ToUpper(), out var notificationType)) + return await ServiceResult>.Failure(new List { $"Invalid notification category: {category}" }); - var (notifications, totalCount) = await _notificationRepository.GetNotificationsByType(userId, notificationType, pageNumber, pageSize); + var (notifications, totalCount) = await _notificationRepository.GetNotificationsByType(userId, notificationType, pageNumber, pageSize); - var totalPages = (int)Math.Ceiling((double)totalCount / pageSize); + var totalPages = (int)Math.Ceiling((double)totalCount / pageSize); - var notificationDtos = new List(); + var notificationDtos = new List(); - foreach (var notification in notifications) - { - var actorUser = await _notificationRepository.GetActorUserByResourceId(notification.ResourceId, notification.Type); - notificationDtos.Add(new NotificationDto + foreach (var notification in notifications) { - Id = notification.Id, - UserId = notification.UserId, - Type = notification.Type.ToString(), - ResourceId = notification.ResourceId, - Message = notification.Message, - IsRead = notification.IsRead, - CreatedAt = notification.CreatedAt, - Actor = actorUser == null ? null : new ActorDto - { - Id = actorUser.Id, - Username = actorUser?.UserName ?? "Unknown", - AvatarPath = actorUser?.AvatarPath ?? String.Empty - }, - Pagination = new PaginationDto + var actorUser = await _notificationRepository.GetActorUserByResourceId(notification.ResourceId, notification.Type); + notificationDtos.Add(new NotificationDto { - CurrentPage = pageNumber, - TotalPages = totalPages, - TotalCount = totalCount, - HasNext = pageNumber < totalPages, - HasPrevious = pageNumber > 1 - } - }); - } + Id = notification.Id, + UserId = notification.UserId, + Type = notification.Type.ToString(), + ResourceId = notification.ResourceId, + Message = notification.Message, + IsRead = notification.IsRead, + CreatedAt = notification.CreatedAt, + Actor = actorUser == null ? null : new ActorDto + { + Id = actorUser.Id, + Username = actorUser?.UserName ?? "Unknown", + AvatarPath = actorUser?.AvatarPath ?? String.Empty + }, + Pagination = new PaginationDto + { + CurrentPage = pageNumber, + TotalPages = totalPages, + TotalCount = totalCount, + HasNext = pageNumber < totalPages, + HasPrevious = pageNumber > 1 + } + }); + } - return notificationDtos; + return await ServiceResult>.Success(notificationDtos); + } + catch (Exception ex) + { + return await ServiceResult>.Failure(new List { ex.Message }); + } } - public async Task MarkNotificationAsRead(int notificationId, int userId) + + public async Task> MarkNotificationAsRead(int notificationId, int userId) { - var notification = await _notificationRepository.GetUserNotificationById(notificationId, userId); - if (notification == null) - throw new InvalidOperationException("Notification not found or access denied."); + try + { + var notification = await _notificationRepository.GetUserNotificationById(notificationId, userId); + if (notification == null) + return await ServiceResult.Failure(new List { "Notification not found or access denied." }); - notification.IsRead = true; - _unitOfWork.Notifications.Update(notification); - await _unitOfWork.SaveAsync(); + notification.IsRead = true; + _unitOfWork.Notifications.Update(notification); + await _unitOfWork.SaveAsync(); - return "notification has been read"; + return await ServiceResult.Success("notification has been read"); + } + catch (Exception ex) + { + return await ServiceResult.Failure(new List { ex.Message }); + } } - public async Task MarkAllNotificationsAsRead(int userId) + public async Task> MarkAllNotificationsAsRead(int userId) { - var unreadNotifications = await _unitOfWork.Notifications.FindAllAsync(n => n.UserId == userId && !n.IsRead); + try + { + var unreadNotifications = await _unitOfWork.Notifications.FindAllAsync(n => n.UserId == userId && !n.IsRead); - foreach (var notification in unreadNotifications) + foreach (var notification in unreadNotifications) + { + notification.IsRead = true; + _unitOfWork.Notifications.Update(notification); + } + + await _unitOfWork.SaveAsync(); + return await ServiceResult.Success("All notifications marked as read"); + } + catch (Exception ex) { - notification.IsRead = true; - _unitOfWork.Notifications.Update(notification); + return await ServiceResult.Failure(new List { ex.Message }); } - - await _unitOfWork.SaveAsync(); - return "All notifications marked as read"; } - public async Task CreateNotification(int userId, NotificationStatus type, int resourceId, string message) + public async Task> CreateNotification(int userId, NotificationStatus type, int resourceId, string message) { - var notification = new Notification - { - UserId = userId, - Type = type, - ResourceId = resourceId, - Message = message, - IsRead = false, - CreatedAt = DateTime.UtcNow, - UpdatedAt = DateTime.UtcNow - }; - - await _unitOfWork.Notifications.AddAsync(notification); - await _unitOfWork.SaveAsync(); - - // Get actor information for the notification - var actorUser = await _notificationRepository.GetActorUserByResourceId(resourceId, type); - - var notificationDto = new NotificationDto + try { - Id = notification.Id, - UserId = notification.UserId, - Type = notification.Type.ToString(), - ResourceId = notification.ResourceId, - Message = notification.Message, - IsRead = notification.IsRead, - CreatedAt = notification.CreatedAt, - Actor = actorUser == null ? null : new ActorDto + var notification = new Notification { - Id = actorUser.Id, - Username = actorUser.UserName ?? "Unknown", - AvatarPath = actorUser.AvatarPath ?? string.Empty - } - }; + UserId = userId, + Type = type, + ResourceId = resourceId, + Message = message, + IsRead = false, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + await _unitOfWork.Notifications.AddAsync(notification); + await _unitOfWork.SaveAsync(); + + // Get actor information for the notification + var actorUser = await _notificationRepository.GetActorUserByResourceId(resourceId, type); + + var notificationDto = new NotificationDto + { + Id = notification.Id, + UserId = notification.UserId, + Type = notification.Type.ToString(), + ResourceId = notification.ResourceId, + Message = notification.Message, + IsRead = notification.IsRead, + CreatedAt = notification.CreatedAt, + Actor = actorUser == null ? null : new ActorDto + { + Id = actorUser.Id, + Username = actorUser.UserName ?? "Unknown", + AvatarPath = actorUser.AvatarPath ?? string.Empty + } + }; - // Send real-time notification to the specific user - await _hubContext.Clients.Group($"user_{userId}") - .SendAsync("ReceiveNotification", notificationDto); + // Send real-time notification to the specific user + await _hubContext.Clients.Group($"user_{userId}") + .SendAsync("ReceiveNotification", notificationDto); + + return await ServiceResult.Success(notificationDto); + } + catch (Exception ex) + { + return await ServiceResult.Failure(new List { ex.Message }); + } } - - } \ No newline at end of file diff --git a/AskFm/Tests/NotificationServiceTests.cs b/AskFm/Tests/NotificationServiceTests.cs index dc830ce..9a701ef 100644 --- a/AskFm/Tests/NotificationServiceTests.cs +++ b/AskFm/Tests/NotificationServiceTests.cs @@ -76,15 +76,18 @@ public async Task GetUserNotifications_ReturnsCorrectDtoWithPagination() var result = await _notificationService.GetUserNotifications(userId, pageNumber, pageSize); // Assert - Assert.Single(result); - Assert.Equal(1, result[0].Id); - Assert.Equal(NotificationStatus.QUESTION.ToString(), result[0].Type); - Assert.Equal("Test notification", result[0].Message); - Assert.False(result[0].IsRead); - Assert.Equal("test_user", result[0].Actor.Username); - Assert.Equal("test.jpg", result[0].Actor.AvatarPath); - Assert.Equal(2, result[0].Actor.Id); - Assert.Equal(1, result[0].Pagination.TotalCount); + Assert.True(result.success); + Assert.Null(result.Errors); + Assert.NotNull(result.Data); + Assert.Single(result.Data); + Assert.Equal(1, result.Data[0].Id); + Assert.Equal(NotificationStatus.QUESTION.ToString(), result.Data[0].Type); + Assert.Equal("Test notification", result.Data[0].Message); + Assert.False(result.Data[0].IsRead); + Assert.Equal("test_user", result.Data[0].Actor.Username); + Assert.Equal("test.jpg", result.Data[0].Actor.AvatarPath); + Assert.Equal(2, result.Data[0].Actor.Id); + Assert.Equal(1, result.Data[0].Pagination.TotalCount); } [Fact] @@ -117,13 +120,16 @@ public async Task GetUserNotifications_WithNullActor_ReturnsCorrectDtoWithPagina var result = await _notificationService.GetUserNotifications(userId, pageNumber, pageSize); // Assert - Assert.Single(result); - Assert.Equal(1, result[0].Id); - Assert.Equal(NotificationStatus.QUESTION.ToString(), result[0].Type); - Assert.Equal("Test notification", result[0].Message); - Assert.False(result[0].IsRead); - Assert.Null(result[0].Actor); - Assert.Equal(1, result[0].Pagination.TotalCount); + Assert.True(result.success); + Assert.Null(result.Errors); + Assert.NotNull(result.Data); + Assert.Single(result.Data); + Assert.Equal(1, result.Data[0].Id); + Assert.Equal(NotificationStatus.QUESTION.ToString(), result.Data[0].Type); + Assert.Equal("Test notification", result.Data[0].Message); + Assert.False(result.Data[0].IsRead); + Assert.Null(result.Data[0].Actor); + Assert.Equal(1, result.Data[0].Pagination.TotalCount); } [Fact] @@ -158,19 +164,22 @@ public async Task GetNotificationsByType_WithValidCategory_ReturnsFilteredNotifi var result = await _notificationService.GetNotificationsByType(userId, category, pageNumber, pageSize); // Assert - Assert.Single(result); - Assert.Equal(1, result[0].Id); - Assert.Equal(category.ToUpper(), result[0].Type); - Assert.Equal("Test notification", result[0].Message); - Assert.False(result[0].IsRead); - Assert.Equal("test_user", result[0].Actor.Username); - Assert.Equal("test.jpg", result[0].Actor.AvatarPath); - Assert.Equal(2, result[0].Actor.Id); - Assert.Equal(1, result[0].Pagination.TotalCount); + Assert.True(result.success); + Assert.Null(result.Errors); + Assert.NotNull(result.Data); + Assert.Single(result.Data); + Assert.Equal(1, result.Data[0].Id); + Assert.Equal(category.ToUpper(), result.Data[0].Type); + Assert.Equal("Test notification", result.Data[0].Message); + Assert.False(result.Data[0].IsRead); + Assert.Equal("test_user", result.Data[0].Actor.Username); + Assert.Equal("test.jpg", result.Data[0].Actor.AvatarPath); + Assert.Equal(2, result.Data[0].Actor.Id); + Assert.Equal(1, result.Data[0].Pagination.TotalCount); } [Fact] - public async Task GetNotificationsByType_WithInvalidCategory_ThrowsArgumentException() + public async Task GetNotificationsByType_WithInvalidCategory_ReturnsFailureResult() { // Arrange int userId = 1; @@ -178,9 +187,14 @@ public async Task GetNotificationsByType_WithInvalidCategory_ThrowsArgumentExcep int pageNumber = 1; int pageSize = 10; - // Act & Assert - var ex = await Assert.ThrowsAsync(() => _notificationService.GetNotificationsByType(userId, category, pageNumber, pageSize)); - Assert.Contains("Invalid notification category", ex.Message); + // Act + var result = await _notificationService.GetNotificationsByType(userId, category, pageNumber, pageSize); + + // Assert + Assert.False(result.success); + Assert.NotNull(result.Errors); + Assert.Contains("Invalid notification category", result.Errors[0]); + Assert.Null(result.Data); } [Fact] @@ -209,14 +223,16 @@ public async Task MarkNotificationAsRead_WithValidId_UpdatesNotification() var result = await _notificationService.MarkNotificationAsRead(notificationId, userId); // Assert + Assert.True(result.success); + Assert.Null(result.Errors); + Assert.Equal("notification has been read", result.Data); Assert.True(notification.IsRead); - Assert.Equal("notification has been read", result); _unitOfWorkMock.Verify(p => p.Notifications.Update(notification), Times.Once); _unitOfWorkMock.Verify(p => p.SaveAsync(), Times.Once); } [Fact] - public async Task MarkNotificationAsRead_WithInvalidId_ThrowsInvalidOperationException() + public async Task MarkNotificationAsRead_WithInvalidId_ReturnsFailureResult() { // Arrange var notificationId = 999; @@ -224,11 +240,14 @@ public async Task MarkNotificationAsRead_WithInvalidId_ThrowsInvalidOperationExc _notificationRepositoryMock.Setup(p => p.GetUserNotificationById(notificationId, userId)) .ReturnsAsync((Notification)null); - // Act & Assert - var ex = await Assert.ThrowsAsync( - () => _notificationService.MarkNotificationAsRead(notificationId, userId)); + // Act + var result = await _notificationService.MarkNotificationAsRead(notificationId, userId); - Assert.Contains("Notification not found or access denied", ex.Message); + // Assert + Assert.False(result.success); + Assert.NotNull(result.Errors); + Assert.Contains("Notification not found or access denied.", result.Errors[0]); + Assert.Null(result.Data); } [Fact] @@ -270,7 +289,9 @@ public async Task MarkAllNotificationsAsRead_UpdatesAllUnreadNotifications() var result = await _notificationService.MarkAllNotificationsAsRead(userId); // Assert - Assert.Equal("All notifications marked as read", result); + Assert.True(result.success); + Assert.Null(result.Errors); + Assert.Equal("All notifications marked as read", result.Data); Assert.All(unreadNotifications, n => Assert.True(n.IsRead)); _unitOfWorkMock.Verify(p => p.Notifications.Update(It.IsAny()), Times.Exactly(2)); _unitOfWorkMock.Verify(p => p.SaveAsync(), Times.Once); @@ -292,9 +313,20 @@ public async Task CreateNotification_CreatesAndSendsNotification() .ReturnsAsync(actorUser); // Act - await _notificationService.CreateNotification(userId, type, resourceId, message); + var result = await _notificationService.CreateNotification(userId, type, resourceId, message); // Assert + Assert.True(result.success); + Assert.Null(result.Errors); + Assert.NotNull(result.Data); + Assert.Equal(userId, result.Data.UserId); + Assert.Equal(type.ToString(), result.Data.Type); + Assert.Equal(resourceId, result.Data.ResourceId); + Assert.Equal(message, result.Data.Message); + Assert.False(result.Data.IsRead); + Assert.Equal("test_user", result.Data.Actor.Username); + Assert.Equal("test.jpg", result.Data.Actor.AvatarPath); + Assert.Equal(2, result.Data.Actor.Id); _unitOfWorkMock.Verify(p => p.Notifications.AddAsync(It.IsAny()), Times.Once); _unitOfWorkMock.Verify(p => p.SaveAsync(), Times.Once); _mockClientProxy.Verify(p => p.SendCoreAsync("ReceiveNotification", It.IsAny(), default), Times.Once); From 97620e4bab13e11316730c635ad3e98e202f177d Mon Sep 17 00:00:00 2001 From: Mahmoud Ayman Date: Sun, 21 Sep 2025 01:45:07 +0300 Subject: [PATCH 20/20] fix(auth): Use standard JWT NameIdentifier claim for user ID extraction --- AskFm/AskFm.API/Controllers/NotificationController.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/AskFm/AskFm.API/Controllers/NotificationController.cs b/AskFm/AskFm.API/Controllers/NotificationController.cs index fbd551a..a7855da 100644 --- a/AskFm/AskFm.API/Controllers/NotificationController.cs +++ b/AskFm/AskFm.API/Controllers/NotificationController.cs @@ -1,3 +1,4 @@ +using System.Security.Claims; using AskFm.BLL.DTO; using AskFm.BLL.Services; using AskFm.DAL.Enums; @@ -107,7 +108,8 @@ public async Task CreateNotification([FromBody] CreateNotificatio private int GetCurrentUserId() { - var userIdClaim = User.FindFirst("UserId")?.Value; + // Use the standard NameIdentifier claim + var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; if (string.IsNullOrEmpty(userIdClaim) || !int.TryParse(userIdClaim, out int userId)) { throw new UnauthorizedAccessException("Invalid user token");