diff --git a/AskFm/AskFm.API/AskFm.API.csproj b/AskFm/AskFm.API/AskFm.API.csproj
index 59c3c20..60af690 100644
--- a/AskFm/AskFm.API/AskFm.API.csproj
+++ b/AskFm/AskFm.API/AskFm.API.csproj
@@ -21,7 +21,10 @@
-
+
+
+
+
@@ -29,4 +32,28 @@
+
+ net9.0
+ enable
+ enable
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AskFm/AskFm.API/Controllers/NotificationController.cs b/AskFm/AskFm.API/Controllers/NotificationController.cs
new file mode 100644
index 0000000..a7855da
--- /dev/null
+++ b/AskFm/AskFm.API/Controllers/NotificationController.cs
@@ -0,0 +1,120 @@
+using System.Security.Claims;
+using AskFm.BLL.DTO;
+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;
+
+ public NotificationController(INotificationService notificationService)
+ {
+ _notificationService = notificationService;
+ }
+
+ [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);
+ }
+ catch (Exception ex)
+ {
+ return BadRequest(ex.Message);
+ }
+ }
+
+ [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);
+ }
+ 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 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("read-all")]
+ public async Task MarkAllNotificationsAsRead()
+ {
+ try
+ {
+ var userId = GetCurrentUserId();
+ var result = await _notificationService.MarkAllNotificationsAsRead(userId);
+ return Ok(new { message = result });
+ }
+ catch (Exception ex)
+ {
+ return BadRequest(ex.Message);
+ }
+ }
+
+ [HttpPost]
+ [Authorize(Roles = "Admin")]
+ public async Task CreateNotification([FromBody] CreateNotificationRequest request)
+ {
+ try
+ {
+ 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()
+ {
+ // 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");
+ }
+ return userId;
+ }
+ }
+}
\ No newline at end of file
diff --git a/AskFm/AskFm.API/Program.cs b/AskFm/AskFm.API/Program.cs
index efe32e8..06e84f6 100644
--- a/AskFm/AskFm.API/Program.cs
+++ b/AskFm/AskFm.API/Program.cs
@@ -1,5 +1,4 @@
using System.Text;
-using AskFm.BLL.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Identity;
using AskFm.DAL;
@@ -9,10 +8,13 @@
using DotNetEnv;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore.Proxies;
+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;
+using Swashbuckle.AspNetCore.SwaggerGen;
using Castle.Components.DictionaryAdapter.Xml;
namespace AskFm.API;
@@ -43,6 +45,9 @@ 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.AddScoped();
@@ -91,6 +96,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");
@@ -120,6 +158,21 @@ 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 =>
@@ -167,17 +220,26 @@ public static void Main(string[] args)
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
- app.UseSwaggerUI();
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();
}
}
\ No newline at end of file
diff --git a/AskFm/AskFm.API/Properties/launchSettings.json b/AskFm/AskFm.API/Properties/launchSettings.json
index 280f933..75f6fa6 100644
--- a/AskFm/AskFm.API/Properties/launchSettings.json
+++ b/AskFm/AskFm.API/Properties/launchSettings.json
@@ -5,7 +5,7 @@
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
- "launchUrl": "swagger",
+ "launchUrl": "swagger",
"applicationUrl": "http://localhost:5180",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
diff --git a/AskFm/AskFm.BLL/AskFm.BLL.csproj b/AskFm/AskFm.BLL/AskFm.BLL.csproj
index 09d480a..b92a98e 100644
--- a/AskFm/AskFm.BLL/AskFm.BLL.csproj
+++ b/AskFm/AskFm.BLL/AskFm.BLL.csproj
@@ -1,19 +1,19 @@
-
-
- net9.0
- enable
- enable
-
-
-
-
-
-
-
-
-
+
+ net9.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
-
+
+
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/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/DTO/NotificationDto.cs b/AskFm/AskFm.BLL/DTO/NotificationDto.cs
new file mode 100644
index 0000000..91c1fc3
--- /dev/null
+++ b/AskFm/AskFm.BLL/DTO/NotificationDto.cs
@@ -0,0 +1,19 @@
+using AskFm.BLL.DTO;
+
+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; }
+ public ActorDto? Actor { get; set; }
+ public PaginationDto Pagination { 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.BLL/Hub/NotificationHub.cs b/AskFm/AskFm.BLL/Hub/NotificationHub.cs
new file mode 100644
index 0000000..500526f
--- /dev/null
+++ b/AskFm/AskFm.BLL/Hub/NotificationHub.cs
@@ -0,0 +1,40 @@
+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)
+ {
+ await Groups.AddToGroupAsync(Context.ConnectionId, $"user_{userId}");
+ }
+
+ public async Task LeaveUserGroup(string userId)
+ {
+ await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"user_{userId}");
+ }
+
+ 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);
+ }
+ }
+}
\ 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..afa49ad
--- /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>> 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
new file mode 100644
index 0000000..cd3c90b
--- /dev/null
+++ b/AskFm/AskFm.BLL/Services/NotificationService.cs
@@ -0,0 +1,210 @@
+using AskFm.BLL.DTO;
+using AskFm.BLL.Hub;
+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;
+ _unitOfWork = unitOfWork;
+ _hubContext = hubContext;
+ }
+
+ public async Task>> GetUserNotifications(int userId, int pageNumber = 1, int pageSize = 10)
+ {
+ try
+ {
+ var (notifications, totalCount) = await _notificationRepository.GetAllNotifications(userId, 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(),
+ 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 });
+ }
+ }
+
+ public async Task>> GetNotificationsByType(int userId, string category, int pageNumber = 1, int pageSize = 10)
+ {
+ 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 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(),
+ 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 });
+ }
+ }
+
+ public async Task> MarkNotificationAsRead(int notificationId, int userId)
+ {
+ 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();
+
+ 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)
+ {
+ try
+ {
+ 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 await ServiceResult.Success("All notifications marked as read");
+ }
+ catch (Exception ex)
+ {
+ return await ServiceResult.Failure(new List { ex.Message });
+ }
+ }
+
+ public async Task> CreateNotification(int userId, NotificationStatus type, int resourceId, string message)
+ {
+ try
+ {
+ 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
+ {
+ 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);
+
+ 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/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
diff --git a/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs b/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs
index d2883fd..310ec78 100644
--- a/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs
+++ b/AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs
@@ -1,6 +1,12 @@
+using AskFm.DAL.Enums;
+using AskFm.DAL.Models;
+
namespace AskFm.DAL.Interfaces;
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/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 832c22c..dfccc1a 100644
--- a/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs
+++ b/AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs
@@ -296,9 +296,19 @@ 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")
.ValueGeneratedOnAdd()
.HasColumnType("DATETIME")
@@ -307,13 +317,6 @@ protected override void BuildModel(ModelBuilder modelBuilder)
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 0ea01ef..2807d59 100644
--- a/AskFm/AskFm.DAL/Models/Notification.cs
+++ b/AskFm/AskFm.DAL/Models/Notification.cs
@@ -1,17 +1,18 @@
using System.Runtime.InteropServices.JavaScript;
+using AskFm.DAL.Enums;
namespace AskFm.DAL.Models;
public class Notification : ITrackable
{
- public GCNotificationStatus Type;
+ public NotificationStatus Type { get; set; }
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();
diff --git a/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs b/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs
index 7fa79f1..56189c6 100644
--- a/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs
+++ b/AskFm/AskFm.DAL/Repositories/NotificationRepository.cs
@@ -1,6 +1,86 @@
+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 IUnitOfWork _unitOfWork;
+
+ 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);
+ }
+
+ public async Task GetActorUserByResourceId(int resourceId, NotificationStatus type)
+ {
+ if (type == NotificationStatus.FOLLOW)
+ {
+ var follow = await _unitOfWork.Follows.FindAsync(f => f.FollowedId == resourceId, new[] { "Follower" });
+ return follow?.Follower;
+ }
+ else if (type == NotificationStatus.QUESTION)
+ {
+ var thread = await _unitOfWork.Threads.FindAsync(t => t.Id == resourceId, new[] { "Asker" });
+ return thread?.Asker;
+ }
+ else if (type == NotificationStatus.ANSWER)
+ {
+ var thread = await _unitOfWork.Threads.FindAsync(t => t.Id == resourceId, new[] { "Asked" });
+ return thread?.Asked;
+ }
+ else if (type == NotificationStatus.COMMENT_LIKE)
+ {
+ var commentLike = await _unitOfWork.CommentLikes.FindAsync(cl => cl.CommentId == resourceId, new[] { "User" });
+ return commentLike?.User;
+ }
+ else if (type == NotificationStatus.QUESTION_LIKE)
+ {
+ var threadLike = await _unitOfWork.ThreadLikes.FindAsync(tl => tl.ThreadId == resourceId, new[] { "User" });
+ return threadLike?.User;
+ }
+ else if (type == NotificationStatus.REPLAY)
+ {
+ var comment = await _unitOfWork.Comments.FindAsync(c => c.Id == resourceId, new[] { "User" });
+ return comment?.User;
+ }
+ 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
new file mode 100644
index 0000000..9a701ef
--- /dev/null
+++ b/AskFm/Tests/NotificationServiceTests.cs
@@ -0,0 +1,335 @@
+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;
+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);
+
+ // 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,
+ _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.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]
+ 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.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]
+ 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.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_ReturnsFailureResult()
+ {
+ // Arrange
+ int userId = 1;
+ string category = "InvalidCategory";
+ int pageNumber = 1;
+ int pageSize = 10;
+
+ // 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]
+ public async Task MarkNotificationAsRead_WithValidId_UpdatesNotification()
+ {
+ // Arrange
+ int notificationId = 1;
+ 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, userId);
+
+ // Assert
+ Assert.True(result.success);
+ Assert.Null(result.Errors);
+ Assert.Equal("notification has been read", result.Data);
+ Assert.True(notification.IsRead);
+ _unitOfWorkMock.Verify(p => p.Notifications.Update(notification), Times.Once);
+ _unitOfWorkMock.Verify(p => p.SaveAsync(), Times.Once);
+ }
+
+ [Fact]
+ public async Task MarkNotificationAsRead_WithInvalidId_ReturnsFailureResult()
+ {
+ // Arrange
+ var notificationId = 999;
+ var userId = 1;
+ _notificationRepositoryMock.Setup(p => p.GetUserNotificationById(notificationId, userId))
+ .ReturnsAsync((Notification)null);
+
+ // Act
+ var result = await _notificationService.MarkNotificationAsRead(notificationId, userId);
+
+ // 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]
+ public async Task MarkAllNotificationsAsRead_UpdatesAllUnreadNotifications()
+ {
+ // Arrange
+ int userId = 1;
+ var unreadNotifications = new List
+ {
+ 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>>(), null))
+ .ReturnsAsync(unreadNotifications);
+ _unitOfWorkMock.Setup(p => p.SaveAsync()).ReturnsAsync(1);
+
+ // Act
+ var result = await _notificationService.MarkAllNotificationsAsRead(userId);
+
+ // Assert
+ 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);
+ }
+
+ [Fact]
+ public async Task CreateNotification_CreatesAndSendsNotification()
+ {
+ // Arrange
+ 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()));
+ _unitOfWorkMock.Setup(p => p.SaveAsync()).ReturnsAsync(1);
+ _notificationRepositoryMock.Setup(p => p.GetActorUserByResourceId(resourceId, type))
+ .ReturnsAsync(actorUser);
+
+ // Act
+ 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