Skip to content

Notification System Implementation#20

Merged
Mahmoud-Ayman-Saleh merged 22 commits into
Developfrom
mahmoud-notif-patch
Sep 20, 2025
Merged

Notification System Implementation#20
Mahmoud-Ayman-Saleh merged 22 commits into
Developfrom
mahmoud-notif-patch

Conversation

@Mahmoud-Ayman-Saleh
Copy link
Copy Markdown

@Mahmoud-Ayman-Saleh Mahmoud-Ayman-Saleh commented Aug 24, 2025

Notification System Implementation

What's Implemented

This branch (mahmoud-notif-patch) implements a complete notification system for the AskFm clone with the following features:

Core Features

  • Create Notifications: POST endpoint to create new notifications
  • Get User Notifications: Paginated retrieval of user notifications
  • Filter by Type: Get notifications by category (FOLLOW, QUESTION, ANSWER, etc.)
  • Mark as Read: Mark individual notifications as read
  • Mark All as Read: Bulk mark all user notifications as read
  • Real-time Delivery: SignalR hub for instant notification delivery

Notification Types Supported

  • ANSWER - When someone answers your question
  • QUESTION - When someone asks you a question
  • FOLLOW - When someone follows you
  • COMMENT_LIKE - When someone likes your comment
  • QUESTION_LIKE - When someone likes your question
  • REPLAY - When someone replies to your comment

Technical Implementation

  • Repository Pattern: Clean data access layer
  • Unit of Work Pattern: Transaction management
  • SignalR Integration: Real-time notifications via WebSocket
  • Actor Resolution: Automatically resolves who performed actions
  • Pagination Support: Efficient handling of large notification lists
  • Dependency Injection: Properly configured services

API Endpoints

GET    /api/notification/{userId}                    - Get user notifications
GET    /api/notification/{userId}/type/{category}    - Get notifications by type
PUT    /api/notification/{notificationId}/read       - Mark notification as read
PUT    /api/notification/{userId}/read-all           - Mark all as read
POST   /api/notification                             - Create notification

Real-time Features

  • SignalR Hub at /notificationHub
  • User-specific notification groups
  • Instant delivery when notifications are created

Implementation Phases

Phase 1: Foundation Setup

  • Commit: 888cfb3 - Add SignalR Package
  • Commit: ad90d92 - Add Notification Hub
  • Commit: 47f9e2b - Implement NotificationDto

Phase 2: Data Layer Implementation

  • Commit: fe6259a - Implement Notification Repository
  • Commit: 6c8330e - Fix notification model and repository for API compliance
  • Commit: 0f1a29c - Implement Notification Services

Phase 3: Service Layer & API

  • Commit: e65c410 - Fix NotificationDto and implement manual mapping
  • Commit: 7432403 - Implement Notification Controller
  • Commit: 9125016 - Implement notification hub
  • Commit: 065fdac - Add Dependency injection

Phase 4: Architecture Refinement

  • Commit: b2dca6d - Remove redundant actor fields and resolve actors via ResourceId
  • Commit: fda6755 - Remove NotificationResponseType (frontend optimization)
  • Commit: bf3cefd - Refactor to use UnitOfWork and Repository pattern
  • Commit: d7bd9cf - Update Naming in Notification model and apply migrations for it
  • Commit: 3e53d29 - Enable Swagger

Phase 5: Testing & Service Integration

  • Commit: dcc35e8 - Finish notification unit test and update notification hub
  • Commit: ef94e30 - Apply ServiceResult in Notifications Service
  • Commit: 97620e4 - Fix(auth): Use standard JWT NameIdentifier claim for user ID extraction

Testing Coverage

The notification system includes comprehensive unit tests covering:

  • Service layer functionality
  • Repository pattern implementation
  • SignalR hub integration
  • Error handling scenarios
  • Data mapping and validation

Integration Notes

This notification system is fully integrated with:

  • Authentication system (JWT-based)
  • User management system
  • Comment and question systems
  • Real-time communication via SignalR

Summary by CodeRabbit

  • New Features

    • Real-time in-app notifications via live updates.
    • Notifications API: view paginated notifications, filter by type, mark single or all as read.
  • Documentation

    • Updated API docs UI; OpenAPI/Swagger now launches automatically when running locally.
  • Tests

    • Added comprehensive unit tests for notification retrieval, filtering, read status, and real-time delivery.
  • Chores

    • Updated packages and configurations to support notifications and API documentation improvements.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Aug 24, 2025

Walkthrough

Adds a real-time notifications feature: new NotificationController, service, repository methods, DTOs, SignalR hub, and tests. Updates Program.cs to wire SignalR, CORS, JWT for hub, OpenAPI, and DI. Renames Notification model fields, adds Type/CreatedAt/UpdatedAt, and includes an EF migration/snapshot. Test project gains Moq and BLL reference.

Changes

Cohort / File(s) Summary
API configuration and dependencies
AskFm/AskFm.API/AskFm.API.csproj, AskFm/AskFm.API/Program.cs, AskFm/AskFm.API/Properties/launchSettings.json
Adds SignalR dependency and configuration, CORS policy, JWT query-string token handling for hub, DI for notification services, maps /notificationHub, switches Swagger to OpenAPI endpoints/UI, and sets launch to Swagger.
Notification API controller
AskFm/AskFm.API/Controllers/NotificationController.cs
New controller with authenticated endpoints: list user notifications (paginated), filter by type, mark one/all as read, and admin create. Uses userId from claims.
BLL DTOs
AskFm/AskFm.BLL/DTO/ActorDto.cs, .../NotificationDto.cs, .../PaginationDto.cs
Introduces DTOs for actor, notification payloads, and pagination metadata.
SignalR hub
AskFm/AskFm.BLL/Hub/NotificationHub.cs
New authorized hub managing per-user groups (user_{userId}) on connect/disconnect; join/leave methods exposed.
Notification service layer
AskFm/AskFm.BLL/Services/INotificationService.cs, .../NotificationService.cs
Adds service contract and implementation: paginated queries, filter by category, mark read, mark all read, create notification with real-time push via SignalR.
BLL project deps
AskFm/AskFm.BLL/AskFm.BLL.csproj
Adds Microsoft.AspNetCore.SignalR, AutoMapper, and AutoMapper.Extensions.Microsoft.DependencyInjection.
DAL model and migrations
AskFm/AskFm.DAL/Models/Notification.cs, .../ModelsConfigrations/NotificationConfigration.cs, .../Migrations/20250824204202_NotificationModelNaming*.cs, .../Migrations/AppDbContextModelSnapshot.cs, AskFm/AskFm.DAL/AskFm.DAL.csproj
Renames isReadIsRead, jsonContentMessage, changes Type to enum property, adds CreatedAt/UpdatedAt. Adds migration and updates snapshot; adds SignalR package reference.
DAL repositories and UoW
AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs, .../Repositories/NotificationRepository.cs, .../Interfaces/IUnitOfWork.cs, .../UnitOfWork.cs
Extends repository interface and implements paginated queries/lookups, actor resolution, single-notification fetch. Adjusts Thread typing to Models.Thread; adds explicit interface member in UnitOfWork.
Tests
AskFm/Tests/NotificationServiceTests.cs, AskFm/Tests/Tests.csproj
Adds unit tests for NotificationService, mocking repo/UoW/hub; adds Moq and BLL project reference.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Admin as Admin Client
  participant API as API (NotificationController)
  participant SVC as NotificationService
  participant REPO as NotificationRepository
  participant DB as Database
  participant HUB as SignalR NotificationHub
  participant Users as user_{userId} group

  Admin->>API: POST /api/Notification (JWT)
  API->>SVC: CreateNotification(userId, type, resourceId, message)
  SVC->>REPO: Add Notification + GetActorUserByResourceId(...)
  REPO->>DB: Insert notification, query actor
  DB-->>REPO: Persisted entity + actor
  SVC->>HUB: Clients.Group("user_{userId}").Send(ReceiveNotification, payload)
  HUB-->>Users: ReceiveNotification(payload)
  SVC-->>API: Completed
  API-->>Admin: 200 OK
Loading
sequenceDiagram
  autonumber
  actor User as App Client
  participant HUB as SignalR NotificationHub
  participant Auth as JWT Bearer

  User->>HUB: Connect /notificationHub?access_token=JWT
  HUB->>Auth: Validate token (OnMessageReceived)
  Auth-->>HUB: Principal with UserIdentifier
  HUB->>HUB: OnConnectedAsync -> AddToGroup("user_{userId}")
  HUB-->>User: Connected
  User->>HUB: JoinUserGroup(userId) [optional]
  HUB-->>User: Ack
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

I thump with joy—new signals ring,
A hub that hums, alerts that sing!
Messages hop from code to ear,
Read or unread—now crystal clear.
With queues of carrots (DTOs),
I stream the news—on bunny toes.
Ping! A nibble; off I goes. 🐇📣

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.38% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title "Notification System Implementation" is concise and directly reflects the primary change set — implementing a notification system (SignalR hub, controller, services, repository/DTO updates, and migrations); it clearly communicates the main purpose for someone scanning PR history.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch mahmoud-notif-patch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@hadeer-r
Copy link
Copy Markdown
Contributor

@CodeRabbit review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Sep 16, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🧹 Nitpick comments (41)
AskFm/AskFm.API/Properties/launchSettings.json (1)

17-18: Mirror the correct launchUrl for the HTTPS profile as well.

Once you confirm the actual UI/doc endpoint, apply the same path here to avoid inconsistent behavior between profiles.

AskFm/AskFm.BLL/DTO/ActorDto.cs (2)

5-7: Fix nullable/required contract for DTO strings.

With Nullable enabled, non-nullable strings must be initialized or marked required.

Apply this diff:

-public class ActorDto
+public class ActorDto
 {
     public int Id { get; set; }
-    public string Username { get; set; }
-    public string AvatarPath { get; set; }
+    public required string Username { get; set; }
+    public string? AvatarPath { get; set; }
 }

3-8: Optional: prefer immutable DTOs for safer mapping/serialization.

Consider a record with init-only setters.

-public class ActorDto
+public record ActorDto
 {
-    public int Id { get; set; }
-    public required string Username { get; set; }
-    public string? AvatarPath { get; set; }
+    public int Id { get; init; }
+    public required string Username { get; init; }
+    public string? AvatarPath { get; init; }
 }
AskFm/AskFm.BLL/DTO/PaginationDto.cs (1)

3-10: Optional: make pagination DTO immutable to avoid accidental mutation.

Use a record with init-only properties.

-public class PaginationDto
+public record 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; }
+    public int CurrentPage { get; init; }
+    public int TotalPages { get; init; }
+    public int TotalCount { get; init; }
+    public bool HasNext { get; init; }
+    public bool HasPrevious { get; init; }
 }
AskFm/AskFm.DAL/ModelsConfigrations/NotificationConfigration.cs (1)

5-8: Nit: fix typos in names.

Rename folder and class to “ModelsConfigurations/NotificationConfiguration” for consistency.

No functional change—purely readability.

AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs (2)

299-301: Set a default for IsRead at the database level

To avoid accidental NULLs or needing to always send a value, add a default of false in the model configuration.

Example in NotificationConfiguration:

builder.Property(n => n.IsRead)
  .HasDefaultValue(false);

309-311: Add a supporting index for common notification queries

If you list notifications by user and unread-first/newest-first, add a composite index on (UserId, IsRead, CreatedAt DESC).

Follow-up migration:

migrationBuilder.CreateIndex(
  name: "IX_Notifications_UserId_IsRead_CreatedAt",
  table: "Notifications",
  columns: new[] { "UserId", "IsRead", "CreatedAt" });
AskFm/AskFm.DAL/UnitOfWork.cs (1)

12-12: Clarify Thread type usage to avoid System.Threading.Thread confusion

Using Models.Thread is fine; consider an alias for readability across the class.

Example:

using ThreadEntity = AskFm.DAL.Models.Thread;
// ...
private IRepository<ThreadEntity> _threads;
// ...
_threads = new Repository<ThreadEntity>(_context);

Also applies to: 38-47, 43-44

AskFm/AskFm.BLL/DTO/NotificationDto.cs (3)

1-1: Remove self-referential using

The using AskFm.BLL.DTO is unnecessary here and can be dropped.

-using AskFm.BLL.DTO;

16-16: Pagination per-item is awkward; return a paged envelope instead

Carrying PaginationDto on each NotificationDto duplicates metadata n times. Prefer an envelope like PagedResult with Items + Pagination.

Example:

public sealed class PagedResult<T>
{
    public required IReadOnlyList<T> Items { get; init; }
    public required PaginationDto Pagination { get; init; }
}

And update service signatures to return PagedResult.


6-7: Consider using the enum type for Type in the DTO

If clients can handle enum names, expose NotificationStatus (serialized as string) to avoid stringly-typed bugs.

Example:

public NotificationStatus Type { get; set; }

And configure System.Text.Json to serialize enums as strings.

AskFm/AskFm.DAL/Models/Notification.cs (2)

1-1: Remove unused import

System.Runtime.InteropServices.JavaScript is unrelated in DAL.

-using System.Runtime.InteropServices.JavaScript;

15-15: Initialize non-nullable Message

Prevent nullability warnings and ensure EF treats it as required.

-    public string Message { get; set; }
+    public string Message { get; set; } = default!;
AskFm/AskFm.BLL/Services/INotificationService.cs (3)

8-9: Return a paged envelope, not a bare List

Expose totalCount/page metadata once via a PagedResult.

-Task<List<NotificationDto>> GetUserNotifications(int userId, int pageNumber = 1, int pageSize = 10);
-Task<List<NotificationDto>> GetNotificationsByType(int userId, string category, int pageNumber = 1, int pageSize = 10);
+Task<PagedResult<NotificationDto>> GetUserNotifications(int userId, int pageNumber = 1, int pageSize = 10, CancellationToken ct = default);
+Task<PagedResult<NotificationDto>> GetNotificationsByType(int userId, string category, int pageNumber = 1, int pageSize = 10, CancellationToken ct = default);

(Adjust implementations and controller accordingly.)


8-13: Add CancellationToken parameters

Standard for async service interfaces; bubbles cancellation from HTTP requests.

-Task<string> MarkNotificationAsRead(int notificationId, int userId);
-Task<string> MarkAllNotificationsAsRead(int userId);
-Task CreateNotification(int userId, NotificationStatus type, int resourceId, string message);
+Task<string> MarkNotificationAsRead(int notificationId, int userId, CancellationToken ct = default);
+Task<string> MarkAllNotificationsAsRead(int userId, CancellationToken ct = default);
+Task CreateNotification(int userId, NotificationStatus type, int resourceId, string message, CancellationToken ct = default);

12-12: Consider returning the created notification (or Id) from CreateNotification

Improves ergonomics for callers and tests.

-Task CreateNotification(int userId, NotificationStatus type, int resourceId, string message);
+Task<NotificationDto> CreateNotification(int userId, NotificationStatus type, int resourceId, string message, CancellationToken ct = default);
AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs (1)

8-11: Add CancellationToken and document ordering

Repository methods should accept CancellationToken and define sort order (e.g., CreatedAt DESC).

-Task<(IEnumerable<Notification> notifications, int totalCount)> GetAllNotifications(int userId, int pageNumber, int pageSize);
-Task<(IEnumerable<Notification> notifications, int totalCount)> GetNotificationsByType(int userId, NotificationStatus status, int pageNumber, int pageSize);
-Task<ApplicationUser?> GetActorUserByResourceId(int resourceId, NotificationStatus type);
-Task<Notification?> GetUserNotificationById(int notificationId, int userId);
+Task<(IEnumerable<Notification> notifications, int totalCount)> GetAllNotifications(int userId, int pageNumber, int pageSize, CancellationToken ct = default);
+Task<(IEnumerable<Notification> notifications, int totalCount)> GetNotificationsByType(int userId, NotificationStatus status, int pageNumber, int pageSize, CancellationToken ct = default);
+Task<ApplicationUser?> GetActorUserByResourceId(int resourceId, NotificationStatus type, CancellationToken ct = default);
+Task<Notification?> GetUserNotificationById(int notificationId, int userId, CancellationToken ct = default);
AskFm/AskFm.BLL/Services/NotificationService.cs (7)

23-31: Validate pagination inputs and guard against division by zero.

pageSize can be zero/negative and pageNumber < 1, leading to exceptions or odd paging. Clamp/sanitize before querying.

 public async Task<List<NotificationDto>> 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);
+    pageNumber = Math.Max(1, pageNumber);
+    pageSize = pageSize <= 0 ? 10 : Math.Min(pageSize, 100);
+    var (notifications, totalCount) = await _notificationRepository.GetAllNotifications(userId, pageNumber, pageSize);
+    var totalPages = totalCount == 0 ? 0 : (int)Math.Ceiling((double)totalCount / pageSize);

31-58: Avoid N+1 + serial awaits for actor resolution.

You await per-item actor lookups in a loop. Batch with Task.WhenAll to cut latency.

-        var notificationDtos = new List<NotificationDto>();
-
-        foreach (var notification in notifications)
-        {
-            var actorUser = await _notificationRepository.GetActorUserByResourceId(notification.ResourceId, notification.Type);
-            notificationDtos.Add(new NotificationDto
+        var actorTasks = notifications.Select(n => _notificationRepository.GetActorUserByResourceId(n.ResourceId, n.Type)).ToList();
+        var actors = await Task.WhenAll(actorTasks);
+        var notificationDtos = new List<NotificationDto>(actors.Length);
+        var i = 0;
+        foreach (var notification in notifications)
+        {
+            var actorUser = actors[i++];
+            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
+                Actor = actorUser == null ? null : new ActorDto
                 {
-                    Id = actorUser.Id,
-                    Username = actorUser?.UserName ?? "Unknown",
-                    AvatarPath = actorUser?.AvatarPath ?? String.Empty
+                    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
                 }
             });
         }

Apply the same pattern to GetNotificationsByType.


65-66: Case-insensitive enum parse without ToUpper and null-safety.

Using ToUpper() can throw on null and is unnecessary. Use the ignoreCase overload and trim input.

-        if (!Enum.TryParse<NotificationStatus>(category.ToUpper(), out var notificationType))
+        if (string.IsNullOrWhiteSpace(category) ||
+            !Enum.TryParse<NotificationStatus>(category.Trim(), ignoreCase: true, out var notificationType))
             throw new ArgumentException($"Invalid notification category: {category}");

68-71: Repeat pagination validation here too.

-        var (notifications, totalCount) = await _notificationRepository.GetNotificationsByType(userId, notificationType, pageNumber, pageSize);
-
-        var totalPages = (int)Math.Ceiling((double)totalCount / pageSize);
+        pageNumber = Math.Max(1, pageNumber);
+        pageSize = pageSize <= 0 ? 10 : Math.Min(pageSize, 100);
+        var (notifications, totalCount) = await _notificationRepository.GetNotificationsByType(userId, notificationType, pageNumber, pageSize);
+        var totalPages = totalCount == 0 ? 0 : (int)Math.Ceiling((double)totalCount / pageSize);

86-91: Nit: remove redundant null-conditionals inside a non-null branch.

Inside the actorUser != null branch, operator ?. is redundant. Also prefer string.Empty consistently.

-                    Username = actorUser?.UserName ?? "Unknown",
-                    AvatarPath = actorUser?.AvatarPath ?? String.Empty
+                    Username = actorUser.UserName ?? "Unknown",
+                    AvatarPath = actorUser.AvatarPath ?? string.Empty

111-116: Update UpdatedAt when marking a notification as read.

Keeps metadata accurate for clients relying on sorting/recency.

         notification.IsRead = true;
+        notification.UpdatedAt = DateTime.UtcNow;
         _unitOfWork.Notifications.Update(notification);
         await _unitOfWork.SaveAsync();

120-129: Use a single bulk update for “mark all as read”.

Current approach loads all rows and updates one-by-one. Prefer a DB-side update (EF Core ExecuteUpdateAsync) for scale.

-        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();
+        await _unitOfWork.Notifications
+            .Where(n => n.UserId == userId && !n.IsRead)
+            .ExecuteUpdateAsync(s => s
+                .SetProperty(n => n.IsRead, true)
+                .SetProperty(n => n.UpdatedAt, DateTime.UtcNow));
         return "All notifications marked as read";

If your repository abstraction doesn’t expose IQueryable/ExecuteUpdateAsync, add a repository method to perform this bulk update.

AskFm/AskFm.API/Program.cs (2)

50-50: Remove duplicate AddControllers registration.

Called at Line 27 already.

-        builder.Services.AddControllers();

53-61: Validate JWT env vars and parsing to fail fast with clear errors.

int.Parse on missing/invalid envs will throw unhelpful exceptions; also SigningKey/Issuer/Audience can be null.

-        JwtOptions jwtOptions = new JwtOptions
-        {
-            Issuer = Environment.GetEnvironmentVariable("ISSUER"),
-            Audience = Environment.GetEnvironmentVariable("AUDIENCE"),
-            SigningKey = Environment.GetEnvironmentVariable("SIGNINGKEY"),
-            AccessExpiration = int.Parse(Environment.GetEnvironmentVariable("TOKEN_EXP")),
-            AccessRefreshTokenExpiration = int.Parse(Environment.GetEnvironmentVariable("REFRESH_TOKEN_EXP")),
-        };
-        if (jwtOptions == null)
-        {
-            throw new Exception("jwtOptions is null");
-        }
+        var issuer = Environment.GetEnvironmentVariable("ISSUER");
+        var audience = Environment.GetEnvironmentVariable("AUDIENCE");
+        var signingKey = Environment.GetEnvironmentVariable("SIGNINGKEY");
+        if (!int.TryParse(Environment.GetEnvironmentVariable("TOKEN_EXP"), out var accessExp) ||
+            !int.TryParse(Environment.GetEnvironmentVariable("REFRESH_TOKEN_EXP"), out var refreshExp) ||
+            string.IsNullOrWhiteSpace(issuer) || string.IsNullOrWhiteSpace(audience) || string.IsNullOrWhiteSpace(signingKey))
+        {
+            throw new InvalidOperationException("JWT configuration is invalid or missing required environment variables.");
+        }
+        JwtOptions jwtOptions = new JwtOptions
+        {
+            Issuer = issuer,
+            Audience = audience,
+            SigningKey = signingKey,
+            AccessExpiration = accessExp,
+            AccessRefreshTokenExpiration = refreshExp,
+        };
AskFm/AskFm.DAL/Repositories/NotificationRepository.cs (2)

23-27: Prefer AsNoTracking() on read-only paged queries.

These lists are not modified; skip change tracking for lower overhead.

-        var notifications = await query
+        var notifications = await query
+            .AsNoTracking()
             .OrderByDescending(n => n.CreatedAt)
             .Skip((pageNumber - 1) * pageSize)
             .Take(pageSize)
             .ToListAsync();

38-42: Apply AsNoTracking() here as well.

-        var notifications = await query
+        var notifications = await query
+            .AsNoTracking()
             .OrderByDescending(n => n.CreatedAt)
             .Skip((pageNumber - 1) * pageSize)
             .Take(pageSize)
             .ToListAsync();
AskFm/AskFm.API/Controllers/NotificationController.cs (4)

98-100: Return 201 Created with location/payload for POST.

Use CreatedAtAction to align with REST and help clients.

-                return Ok(new { message = "Notification created successfully" });
+                return CreatedAtAction(nameof(GetUserNotifications), null, new { message = "Notification created successfully" });

67-70: Use 401 Unauthorized instead of 403 Forbid for invalid/missing user claim.

Forbid implies authenticated-but-not-allowed; this is an authn failure.

-                return Forbid(ex.Message);
+                return Unauthorized(ex.Message);

118-124: Add request validation attributes to prevent bad data reaching the service/DB.

Message is required in the model; validate early. Also constrain lengths.

 public class CreateNotificationRequest
 {
-    public int UserId { get; set; }
-    public NotificationStatus Type { get; set; }
-    public int ResourceId { get; set; }
-    public string Message { get; set; }
+    [System.ComponentModel.DataAnnotations.Range(1, int.MaxValue)]
+    public int UserId { get; set; }
+    public NotificationStatus Type { get; set; }
+    [System.ComponentModel.DataAnnotations.Range(1, int.MaxValue)]
+    public int ResourceId { get; set; }
+    [System.ComponentModel.DataAnnotations.Required, System.ComponentModel.DataAnnotations.StringLength(500)]
+    public string Message { get; set; }
 }

21-33: Avoid returning raw exception messages to clients.

Prefer ProblemDetails with a generic message to prevent information disclosure.

Example:

catch (Exception)
{
    return Problem(title: "Failed to fetch notifications.", statusCode: StatusCodes.Status500InternalServerError);
}

Also applies to: 77-90

AskFm/Tests/NotificationServiceTests.cs (9)

15-21: Capture the hub group for assertions.

Add a field to capture the SignalR group name used by the service so tests can assert correct targeting.

     private readonly Mock<IClientProxy> _mockClientProxy;
     private readonly Mock<IGroupManager> _mockGroups;
     private readonly NotificationService _notificationService;
+    private string _capturedGroup = string.Empty;

32-36: Assert the message is sent to the expected group (userId).

Capture the group name during setup and later assert it equals userId.ToString().

-            _mockClients.Setup(p => p.Group(It.IsAny<string>())).Returns(_mockClientProxy.Object);
+            _mockClients
+                .Setup(p => p.Group(It.IsAny<string>()))
+                .Callback<string>(g => _capturedGroup = g)
+                .Returns(_mockClientProxy.Object);

289-291: Return a completed Task from AddAsync to avoid awaiting null.

If NotificationService awaits AddAsync, this setup must return a completed task.

-            _unitOfWorkMock.Setup(p => p.Notifications.AddAsync(It.IsAny<Notification>()));
+            _unitOfWorkMock
+                .Setup(p => p.Notifications.AddAsync(It.IsAny<Notification>()))
+                .Returns(Task.CompletedTask);

295-301: Assert the notification is sent to the correct group.

After CreateNotification, assert the captured group equals the target user’s group.

             await _notificationService.CreateNotification(userId, type, resourceId, message);
 
             // Assert
+            Assert.Equal(userId.ToString(), _capturedGroup);
             _unitOfWorkMock.Verify(p => p.Notifications.AddAsync(It.IsAny<Notification>()), Times.Once);
             _unitOfWorkMock.Verify(p => p.SaveAsync(), Times.Once);

289-296: Strengthen AddAsync verification by validating the entity contents.

Ensure the created Notification has expected fields before persisting.

-            _unitOfWorkMock
-                .Setup(p => p.Notifications.AddAsync(It.IsAny<Notification>()))
-                .Returns(Task.CompletedTask);
+            _unitOfWorkMock
+                .Setup(p => p.Notifications.AddAsync(It.Is<Notification>(n =>
+                    n.UserId == userId &&
+                    n.Type == type &&
+                    n.ResourceId == resourceId &&
+                    n.Message == message &&
+                    !n.IsRead)))
+                .Returns(Task.CompletedTask);

208-216: Also assert UpdatedAt is advanced on mark-as-read.

Capture the previous UpdatedAt and assert it increases after the operation.

-            // Act
+            var prevUpdatedAt = notification.UpdatedAt;
+            // Act
             var result = await _notificationService.MarkNotificationAsRead(notificationId, userId);
 
             // Assert
             Assert.True(notification.IsRead);
+            Assert.True(notification.UpdatedAt > prevUpdatedAt);
             Assert.Equal("notification has been read", result);

181-184: Avoid brittle checks on exception messages.

Asserting on the exception type is sufficient; message text can change.

-            var ex = await Assert.ThrowsAsync<ArgumentException>(() => _notificationService.GetNotificationsByType(userId, category, pageNumber, pageSize));
-            Assert.Contains("Invalid notification category", ex.Message);
+            await Assert.ThrowsAsync<ArgumentException>(() =>
+                _notificationService.GetNotificationsByType(userId, category, pageNumber, pageSize));

79-88: Pagination embedded per item is awkward; prefer a paged wrapper.

Consider returning a PagedResult with Items + Pagination instead of duplicating pagination on each item. Tests would then assert the wrapper’s Pagination.TotalCount and item fields separately.

Also applies to: 119-127, 161-170


234-277: Add a mixed-state test to ensure only unread notifications are updated.

Current test uses all-unread inputs. Add a case with a pre-read item to verify it remains unchanged.

Example test to add:

[Fact]
public async Task MarkAllNotificationsAsRead_MixedStates_OnlyUnreadAreUpdated()
{
    int userId = 1;
    var notifications = new List<Notification>
    {
        new() { Id = 1, UserId = userId, IsRead = false, Type = NotificationStatus.QUESTION, ResourceId = 100, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow },
        new() { Id = 2, UserId = userId, IsRead = true,  Type = NotificationStatus.ANSWER,   ResourceId = 200, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }
    };

    _unitOfWorkMock.Setup(p => p.Notifications.FindAllAsync(It.IsAny<System.Linq.Expressions.Expression<System.Func<Notification, bool>>>(), null))
        .ReturnsAsync(notifications);
    _unitOfWorkMock.Setup(p => p.SaveAsync()).ReturnsAsync(1);

    var result = await _notificationService.MarkAllNotificationsAsRead(userId);

    Assert.Equal("All notifications marked as read", result);
    Assert.True(notifications[0].IsRead);
    Assert.True(notifications[1].IsRead); // service may idempotently set true; verify Update called only for previously unread:
    _unitOfWorkMock.Verify(p => p.Notifications.Update(It.Is<Notification>(n => n.Id == 1)), Times.Once);
    _unitOfWorkMock.Verify(p => p.Notifications.Update(It.Is<Notification>(n => n.Id == 2)), Times.Never);
}
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 208976b and 73b3fba.

📒 Files selected for processing (23)
  • AskFm/AskFm.API/AskFm.API.csproj (1 hunks)
  • AskFm/AskFm.API/Controllers/NotificationController.cs (1 hunks)
  • AskFm/AskFm.API/Program.cs (6 hunks)
  • AskFm/AskFm.API/Properties/launchSettings.json (2 hunks)
  • AskFm/AskFm.BLL/AskFm.BLL.csproj (1 hunks)
  • AskFm/AskFm.BLL/DTO/ActorDto.cs (1 hunks)
  • AskFm/AskFm.BLL/DTO/NotificationDto.cs (1 hunks)
  • AskFm/AskFm.BLL/DTO/PaginationDto.cs (1 hunks)
  • AskFm/AskFm.BLL/Hub/NotificationHub.cs (1 hunks)
  • AskFm/AskFm.BLL/Services/INotificationService.cs (1 hunks)
  • AskFm/AskFm.BLL/Services/NotificationService.cs (1 hunks)
  • AskFm/AskFm.DAL/AskFm.DAL.csproj (1 hunks)
  • AskFm/AskFm.DAL/Interfaces/INotificationRepository.cs (1 hunks)
  • AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs (1 hunks)
  • AskFm/AskFm.DAL/Migrations/20250824204202_NotificationModelNaming.Designer.cs (1 hunks)
  • AskFm/AskFm.DAL/Migrations/20250824204202_NotificationModelNaming.cs (1 hunks)
  • AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs (1 hunks)
  • AskFm/AskFm.DAL/Models/Notification.cs (1 hunks)
  • AskFm/AskFm.DAL/ModelsConfigrations/NotificationConfigration.cs (1 hunks)
  • AskFm/AskFm.DAL/Repositories/NotificationRepository.cs (1 hunks)
  • AskFm/AskFm.DAL/UnitOfWork.cs (3 hunks)
  • AskFm/Tests/NotificationServiceTests.cs (1 hunks)
  • AskFm/Tests/Tests.csproj (2 hunks)
🔇 Additional comments (10)
AskFm/AskFm.API/Properties/launchSettings.json (1)

7-8: Verify launchUrl matches the actual OpenAPI/Swagger UI route.

If Program.cs uses only app.MapOpenApi() (no UI), /swagger will 404; use /openapi/v1.json. If Scalar UI is enabled, use /scalar. If Swashbuckle UI is enabled, /swagger is correct. Please confirm and align.

Example diffs depending on what’s configured:

-      "launchUrl": "swagger",
+      "launchUrl": "openapi/v1.json",
-      "launchUrl": "swagger",
+      "launchUrl": "scalar",
AskFm/Tests/Tests.csproj (2)

13-13: LGTM: Moq addition is appropriate for hub/service mocking.

No issues spotted.


22-24: LGTM: BLL reference for integration-style unit tests.

This aligns with testing NotificationService behavior.

AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs (1)

8-8: LGTM: Fully-qualifying Thread resolves ambiguity with System.Threading.Thread.

This avoids name collisions and clarifies intent.

AskFm/AskFm.DAL/AskFm.DAL.csproj (1)

22-22: Blocker: remove Microsoft.AspNetCore.SignalR from DAL (legacy, incompatible with net9.0)

Microsoft.AspNetCore.SignalR 1.2.0 targets legacy ASP.NET Core and must not be referenced from the DAL layer.

Location: AskFm/AskFm.DAL/AskFm.DAL.csproj:22

-    <PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0"/>

Verify no other projects reference this package (run from repo root):
rg -n --glob '*.csproj' 'Microsoft.AspNetCore.SignalR' || true

AskFm/AskFm.BLL/Hub/NotificationHub.cs (2)

19-28: LGTM: auto-join on connect

Auto-joining the caller to their own group based on UserIdentifier is correct.


30-38: LGTM: cleanup on disconnect

Leaving the group on disconnect is correct and avoids stale memberships.

AskFm/AskFm.DAL/Migrations/20250824204202_NotificationModelNaming.cs (1)

23-29: Confirm default mapping for Notifications.Type (0 → ANSWER)

AskFm/AskFm.DAL/Enums/NotificationStatus.cs defines ANSWER as the first enum member (implicitly 0). The migration AddColumn adds a non-null Notifications.Type with defaultValue: 0 (AskFm/AskFm.DAL/Migrations/20250824204202_NotificationModelNaming.cs). If ANSWER is not the intended default, either add an explicit Unknown = 0 to the enum or change the migration's defaultValue to the intended enum numeric value.

AskFm/Tests/NotificationServiceTests.cs (2)

41-46: Service instantiation and dependency wiring look solid.

Mocks are composed correctly; constructor DI path is exercised.


297-301: Verify hub method and loosen CancellationToken matcher

Use It.IsAny() when verifying the hub call and confirm whether the service invokes SendCoreAsync or SendAsync; update the test to verify the exact method.

  • Suggested change (if service uses SendCoreAsync):
-            _mockClientProxy.Verify(p => p.SendCoreAsync("ReceiveNotification", It.IsAny<object[]>(), default), Times.Once);
+            _mockClientProxy.Verify(
+                p => p.SendCoreAsync("ReceiveNotification", It.IsAny<object[]>(), It.IsAny<CancellationToken>()),
+                Times.Once);
  • If the service calls SendAsync, verify SendAsync(...) with It.IsAny() instead.
  • To locate the call in the codebase run: rg -nP --type=cs -C2 'ReceiveNotification|.?(SendCoreAsync|SendAsync)\s*('

Comment thread AskFm/AskFm.API/AskFm.API.csproj Outdated
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="9.0.0"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0"/>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0"/>
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0"/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Blocker: Remove obsolete SignalR package; rely on ASP.NET Core shared framework.

Web SDK already brings ASP.NET Core; the 1.2.0 package is incompatible with net9.0.

Apply this diff:

-    <PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0"/>

Confirm Program.cs uses MapHub/AddSignalR without extra package refs:


🏁 Script executed:

#!/bin/bash
rg -nP 'AddSignalR|MapHub<|MapOpenApi|UseSwaggerUI' AskFm/AskFm.API/Program.cs -C2

Length of output: 525


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="AskFm/AskFm.API/AskFm.API.csproj"
if [ ! -f "$file" ]; then
  echo "MISSING: $file"
  rg -nP 'Microsoft\.AspNetCore\.SignalR' -S || true
  exit 0
fi

echo "=== $file ==="
nl -ba "$file" | sed -n '1,240p'

echo
echo "=== PackageReference lines matching SignalR ==="
rg -nP '<PackageReference\b[^>]*Microsoft\.AspNetCore\.SignalR' -S "$file" || true

echo
echo "=== Project SDK / TargetFramework ==="
rg -nP '<Project\s+Sdk=' -n "$file" || true
rg -nP '<TargetFramework>' -n "$file" || true

Length of output: 82


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="AskFm/AskFm.API/AskFm.API.csproj"
if [ ! -f "$file" ]; then
  echo "MISSING: $file"
  rg -nP 'Microsoft\.AspNetCore\.SignalR' -S || true
  exit 0
fi

echo "=== $file ==="
sed -n '1,240p' "$file"

echo
echo "=== PackageReference lines matching SignalR ==="
rg -nP '<PackageReference\b[^>]*Include="Microsoft\.AspNetCore\.SignalR"[^>]*>' -n "$file" || true

echo
echo "=== All PackageReference lines ==="
rg -nP '<PackageReference\b' -n "$file" || true

echo
echo "=== Project SDK / TargetFramework ==="
rg -nP '<Project\s+Sdk=' -n "$file" || true
rg -nP '<TargetFramework>' -n "$file" || true

Length of output: 2614


Remove Microsoft.AspNetCore.SignalR (v1.2.0) PackageReference from AskFm.API.csproj

Remove the PackageReference — this project targets net9.0 using Microsoft.NET.Sdk.Web and the SignalR server APIs (AddSignalR / MapHub) are provided by the ASP.NET Core shared framework; the explicit Microsoft.AspNetCore.SignalR v1.2.0 NuGet is unnecessary and can cause assembly/version conflicts. (learn.microsoft.com)

Apply this diff:

-    <PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0"/>

Location: AskFm/AskFm.API/AskFm.API.csproj (line 19). Program.cs already uses builder.Services.AddSignalR(...) (~line 67) and app.MapHub("/notificationHub") (~line 193) — keep those calls and remove only the explicit PackageReference.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0"/>
🤖 Prompt for AI Agents
In AskFm/AskFm.API/AskFm.API.csproj around line 19, remove the explicit
PackageReference to Microsoft.AspNetCore.SignalR Version="1.2.0" because the
project targets net9.0 and ASP.NET Core's shared framework already provides
SignalR server APIs; keep the existing calls to builder.Services.AddSignalR(...)
and app.MapHub<NotificationHub>("/notificationHub") in Program.cs unchanged and
only delete that single PackageReference line to avoid assembly/version
conflicts.

Comment on lines +75 to +97
// 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();
});
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Don’t ship AllowAnyOrigin; gate CORS by environment and allow credentials for known origins.

As-is, any site can open a WebSocket to your hub in production. Tighten CORS.

-        builder.Services.AddCors(options =>
-        {
-            options.AddPolicy("SignalRPolicy", policy =>
-            {
-                // Option 1: Allow any origin (for development only)
-                policy.AllowAnyOrigin()
-                      .AllowAnyMethod()
-                      .AllowAnyHeader();
+        builder.Services.AddCors(options =>
+        {
+            options.AddPolicy("SignalRPolicy", policy =>
+            {
+                if (builder.Environment.IsDevelopment())
+                {
+                    policy.AllowAnyOrigin()
+                          .AllowAnyMethod()
+                          .AllowAnyHeader();
+                }
+                else
+                {
+                    var origins = (Environment.GetEnvironmentVariable("FRONTEND_ORIGINS") ?? string.Empty)
+                        .Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+                    policy.WithOrigins(origins)
+                          .AllowAnyMethod()
+                          .AllowAnyHeader()
+                          .AllowCredentials();
+                }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 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();
});
});
// CORS Configuration for SignalR
builder.Services.AddCors(options =>
{
options.AddPolicy("SignalRPolicy", policy =>
{
if (builder.Environment.IsDevelopment())
{
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
}
else
{
var origins = (Environment.GetEnvironmentVariable("FRONTEND_ORIGINS") ?? string.Empty)
.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
policy.WithOrigins(origins)
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
}
});
});
🤖 Prompt for AI Agents
In AskFm/AskFm.API/Program.cs around lines 75 to 97, the CORS policy currently
uses AllowAnyOrigin which is unsafe for production; update the policy to gate
behavior by environment: in non-production (Development) allow permissive CORS
for local dev, but in Production read a configured list of allowed origins
(e.g., from appsettings or an environment variable) and call
policy.WithOrigins(...).AllowAnyMethod().AllowAnyHeader().AllowCredentials();
ensure you remove AllowAnyOrigin in production, apply the "SignalRPolicy" to the
SignalR hub endpoints, and validate the origins list is parsed and empty-check
guarded to avoid accidentally falling back to AllowAnyOrigin.

Comment thread AskFm/AskFm.BLL/AskFm.BLL.csproj Outdated
Comment on lines +12 to +15
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0"/>
<PackageReference Include="AutoMapper" Version="15.0.1"/>
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1"/>
</ItemGroup>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Blocker: Replace obsolete SignalR package with FrameworkReference for ASP.NET Core.

  • Microsoft.AspNetCore.SignalR 1.2.0 is not compatible with net9.0.
  • For IHubContext/Hub types in a class library, use the shared framework Reference.

Apply this diff:

   <ItemGroup>
-    <PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0"/>
     <PackageReference Include="AutoMapper" Version="15.0.1"/>
     <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1"/>
   </ItemGroup>
+  <ItemGroup>
+    <FrameworkReference Include="Microsoft.AspNetCore.App" />
+  </ItemGroup>

Check that BLL compiles without the package after adding the FrameworkReference.


🏁 Script executed:

#!/bin/bash
rg -n 'IHubContext|Hub<' -g 'AskFm/AskFm.BLL/**.cs'

Length of output: 0


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== AskFm.BLL.csproj (first 300 lines) ==="
if [ -f AskFm/AskFm.BLL/AskFm.BLL.csproj ]; then
  sed -n '1,300p' AskFm/AskFm.BLL/AskFm.BLL.csproj || true
else
  echo "File not found: AskFm/AskFm.BLL/AskFm.BLL.csproj"
fi

echo
echo "=== Search for Microsoft.AspNetCore.SignalR package/reference in repo ==="
rg -n "Microsoft.AspNetCore.SignalR" -S || true

echo
echo "=== Search for IHubContext usage in AskFm.BLL (.cs files) ==="
rg -n --type cs '\bIHubContext\b' AskFm/AskFm.BLL -S -C3 || true

echo
echo "=== Search for Hub usage in AskFm.BLL (.cs files) ==="
rg -n --type cs '\bHub\b' AskFm/AskFm.BLL -S -C3 || true

echo
echo "=== Search for FrameworkReference occurrences in repo ==="
rg -n "FrameworkReference" -S || true

echo
echo "=== Show TargetFramework in AskFm.BLL.csproj ==="
rg -n "<TargetFramework" AskFm/AskFm.BLL/AskFm.BLL.csproj || true

Length of output: 3823


Blocker: Replace obsolete SignalR PackageReference with FrameworkReference (Microsoft.AspNetCore.App)

AskFm.BLL targets net9.0 and contains server SignalR types (AskFm/AskFm.BLL/Hub/NotificationHub.cs and AskFm/AskFm.BLL/Services/NotificationService.cs). Server-side SignalR assemblies are provided by the ASP.NET Core shared framework — remove the Microsoft.AspNetCore.SignalR package reference and add a FrameworkReference to Microsoft.AspNetCore.App. (github.com)

   <ItemGroup>
-    <PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0"/>
     <PackageReference Include="AutoMapper" Version="15.0.1"/>
     <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1"/>
   </ItemGroup>
+  <ItemGroup>
+    <FrameworkReference Include="Microsoft.AspNetCore.App" />
+  </ItemGroup>
  • Remove Microsoft.AspNetCore.SignalR from other projects that reference it (AskFm/AskFm.API/AskFm.API.csproj, AskFm/AskFm.DAL/AskFm.DAL.csproj, tests) unless a package-targeted scenario drives a different decision.
  • Verify: dotnet build AskFm/AskFm.BLL/AskFm.BLL.csproj (or dotnet build AskFm.sln) and fix any remaining references.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0"/>
<PackageReference Include="AutoMapper" Version="15.0.1"/>
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1"/>
</ItemGroup>
<PackageReference Include="AutoMapper" Version="15.0.1"/>
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1"/>
</ItemGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
🤖 Prompt for AI Agents
In AskFm/AskFm.BLL/AskFm.BLL.csproj around lines 12 to 15, the project
references the obsolete Microsoft.AspNetCore.SignalR package while targeting
net9.0 and using server SignalR types; remove the PackageReference for
Microsoft.AspNetCore.SignalR and add a FrameworkReference to
Microsoft.AspNetCore.App instead, then remove the same package reference from
other projects (AskFm/AskFm.API/AskFm.API.csproj,
AskFm/AskFm.DAL/AskFm.DAL.csproj and test projects) unless they have a specific
package-targeted need, update any using/assembly references if necessary, and
run dotnet build AskFm/AskFm.BLL/AskFm.BLL.csproj (or dotnet build AskFm.sln) to
resolve and fix any remaining reference or compile errors.

Comment on lines 17 to -18
</ItemGroup>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

🧩 Analysis chain

Layering concern: JwtBearer belongs in API, not BLL.

Authentication hosting concerns should live in the web layer. Consider removing from BLL and adding to AskFm.API if needed.

-  <ItemGroup>
-      <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.0" />
-    </ItemGroup>
+  <!-- Consider moving JwtBearer to AskFm.API -->

Verify if any BLL files directly use JwtBearer types.


🏁 Script executed:

#!/bin/bash
rg -nP --type cs 'Microsoft\.AspNetCore\.Authentication\.JwtBearer' AskFm/AskFm.BLL

Length of output: 0


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Search for PackageReference to JwtBearer in repository (csproj files)"
rg -n --hidden -S "<PackageReference[^>]*Include=[\"']Microsoft\.AspNetCore\.Authentication\.JwtBearer[\"']" || true

echo
echo "2) Search for JwtBearer usages (AddJwtBearer, JwtBearerDefaults, JwtBearerOptions, using ...JwtBearer, JwtBearer types) in AskFm source"
rg -nP -S -C2 '(AddJwtBearer|JwtBearerDefaults|JwtBearerOptions|using\s+Microsoft\.AspNetCore\.Authentication\.JwtBearer|Microsoft\.AspNetCore\.Authentication\.JwtBearer|JwtBearer)' AskFm || true

echo
echo "3) Show AskFm/AskFm.BLL/AskFm.BLL.csproj (if present)"
if [ -f "AskFm/AskFm.BLL/AskFm.BLL.csproj" ]; then
  sed -n '1,240p' AskFm/AskFm.BLL/AskFm.BLL.csproj
else
  echo "AskFm/AskFm.BLL/AskFm.BLL.csproj not found"
fi

echo
echo "4) Show AskFm/AskFm.API csproj (if present) for comparison"
if [ -f "AskFm/AskFm.API/AskFm.API.csproj" ]; then
  sed -n '1,240p' AskFm/AskFm.API/AskFm.API.csproj
else
  echo "AskFm/AskFm.API/AskFm.API.csproj not found"
fi

Length of output: 4144


Move JwtBearer package reference from AskFm.BLL to AskFm.API

AskFm/AskFm.BLL/AskFm.BLL.csproj contains the PackageReference (line 17) but JwtBearer types are only used in AskFm/AskFm.API/Program.cs — remove the reference from AskFm.BLL and add it to AskFm/AskFm.API.csproj.

-  <ItemGroup>
-      <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.0" />
-    </ItemGroup>
+  <!-- Move JwtBearer to AskFm.API -->

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In AskFm/AskFm.BLL/AskFm.BLL.csproj around lines 17-18, there is a
PackageReference to Microsoft.AspNetCore.Authentication.JwtBearer which is
unused in the BLL project; remove that PackageReference from AskFm.BLL.csproj
and add the same PackageReference (matching Version="9.0.0") into
AskFm/AskFm.API/AskFm.API.csproj (or the API project file where Program.cs
lives) so the JwtBearer package is referenced only in the API project that
actually uses those types; update project files and run a restore/build to
verify no missing references.

Comment on lines +3 to +19
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; }


}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Missing namespace; class is in the global namespace

This will break consumers expecting AskFm.BLL.DTO.NotificationDto.

Apply:

-public class NotificationDto
-{
+namespace AskFm.BLL.DTO;
+
+public class NotificationDto
+{

Also initialize non-nullable strings to satisfy nullability:

-    public string Type { get; set; }
+    public string Type { get; set; } = default!;
-    public string Message { get; set; }
+    public string Message { get; set; } = default!;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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; }
}
namespace AskFm.BLL.DTO;
public class NotificationDto
{
public int Id { get; set; }
public string Type { get; set; } = default!;
public string Message { get; set; } = default!;
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; }
}
🤖 Prompt for AI Agents
In AskFm/AskFm.BLL/DTO/NotificationDto.cs around lines 3 to 19, the class is
declared in the global namespace and non-nullable string properties are not
initialized; move the class into the AskFm.BLL.DTO namespace and initialize
non-nullable reference properties (at minimum Type and Message) with safe
defaults (e.g. empty strings). Also ensure any other non-nullable reference
properties (like Pagination) are either made nullable or given sensible default
instances so the DTO honors nullability annotations.

Comment on lines +275 to +279
b.Property<string>("Message")
.IsRequired()
.HasColumnType("NVARCHAR");

b.Property<int>("ResourceId")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Invalid SQL type: NVARCHAR requires a length (e.g., nvarchar(500) or nvarchar(max)). Migration will fail.

Configure Message with a max length or nvarchar(max) in the model and re-scaffold the migration; don’t hand-edit the designer.

Proposed model configuration (Fluent API):

builder.Entity<Notification>()
    .Property(n => n.Message)
    .IsRequired()
    .HasMaxLength(500); // or .HasColumnType("nvarchar(max)")

Then re-generate the migration to emit nvarchar(500) or nvarchar(max).

🤖 Prompt for AI Agents
In AskFm/AskFm.DAL/Migrations/20250824204202_NotificationModelNaming.Designer.cs
around lines 275 to 279, the migration uses HasColumnType("NVARCHAR") which is
invalid because NVARCHAR requires a length; fix by updating the Notification
entity configuration in the model (not the designer) to specify a length or
nvarchar(max) (e.g., configure Message as IsRequired() and either
HasMaxLength(500) or HasColumnType("nvarchar(max)")), then
re-scaffold/re-generate the migration so the designer emits nvarchar(500) or
nvarchar(max) instead of the invalid NVARCHAR; do not hand-edit the designer
file.

Comment on lines +302 to +305
b.Property<string>("Message")
.IsRequired()
.HasColumnType("NVARCHAR");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Invalid SQL type for Message: use nvarchar(max) or a bounded length

Column type is set to "NVARCHAR" without length, which is invalid on SQL Server. Prefer a bounded length or nvarchar(max) via model configuration so future migrations/snapshots are consistent.

Suggested fix in NotificationConfiguration (not this snapshot):

builder.Property(n => n.Message)
  .IsRequired()
  .HasMaxLength(1000); // lets EF emit nvarchar(1000)
// or explicitly:
// .HasColumnType("nvarchar(max)");
🤖 Prompt for AI Agents
In AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs around lines 302 to
305, the snapshot shows
b.Property<string>("Message").IsRequired().HasColumnType("NVARCHAR"); which uses
an invalid/unspecified NVARCHAR SQL type; update the entity configuration
(NotificationConfiguration) instead of editing the snapshot: set a bounded
length via .HasMaxLength(1000) (or another appropriate length) so EF emits
nvarchar(1000), or explicitly set .HasColumnType("nvarchar(max)"); then
regenerate the migration/snapshot so the snapshot reflects the proper SQL type.

Comment on lines +13 to 17
builder.Property(n => n.Message)
.HasColumnType("NVARCHAR");

builder.Property(n => n.isRead)
builder.Property(n => n.IsRead)
.IsRequired();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Blocker: NVARCHAR without length is invalid SQL Server type.

Specify length or use nvarchar(max); otherwise migrations will generate an invalid column type.

Apply this diff:

-        builder.Property(n => n.Message)
-            .HasColumnType("NVARCHAR");
+        builder.Property(n => n.Message)
+            .HasColumnType("nvarchar(max)");

Optionally add an index for common queries:

+        builder.HasIndex(n => new { n.UserId, n.IsRead });

Search for queries filtering by UserId/IsRead to justify the index:


🏁 Script executed:

#!/bin/bash
rg -nP --type cs 'Notifications?\.(Where|Any|Count)\(.*UserId|IsRead' -C2

Length of output: 12751


Blocker: NVARCHAR without length — set Message to nvarchar(max).

File: AskFm/AskFm.DAL/ModelsConfigrations/NotificationConfigration.cs (lines 13–17)

-        builder.Property(n => n.Message)
-            .HasColumnType("NVARCHAR");
+        builder.Property(n => n.Message)
+            .HasColumnType("nvarchar(max)");

Optional: add index for common queries (justified by FindAllAsync(n => n.UserId == userId && !n.IsRead) in AskFm/AskFm.BLL/Services/NotificationService.cs:120):

+        builder.HasIndex(n => new { n.UserId, n.IsRead });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
builder.Property(n => n.Message)
.HasColumnType("NVARCHAR");
builder.Property(n => n.isRead)
builder.Property(n => n.IsRead)
.IsRequired();
builder.Property(n => n.Message)
.HasColumnType("nvarchar(max)");
builder.Property(n => n.IsRead)
.IsRequired();
builder.HasIndex(n => new { n.UserId, n.IsRead });
🤖 Prompt for AI Agents
In AskFm/AskFm.DAL/ModelsConfigrations/NotificationConfigration.cs around lines
13-17, the Message property is configured as NVARCHAR without a length which
causes EF/Core to generate an invalid column type; change the configuration to
use nvarchar(max) explicitly (e.g. HasColumnType("NVARCHAR(MAX)")) so large
messages are supported and migrations are valid, and optionally add a DB index
on UserId and IsRead (composite index) to optimize queries like FindAllAsync(n
=> n.UserId == userId && !n.IsRead).

Comment on lines +47 to +79
public async Task<ApplicationUser?> 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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Actor resolution is ambiguous for FOLLOW and LIKE events — wrong user may be returned.

Using only resourceId (e.g., CommentId/ThreadId/FollowedId) can match many rows; FindAsync will return an arbitrary record, not the actual actor. Persist ActorId on Notification and resolve by that, or include a unique composite key in resource metadata.

Proposed direction:

  • Update Notification model to include ActorId (int, FK to ApplicationUser).
  • Change CreateNotification(...) signature to accept actorId and persist it.
  • Rewrite GetActorUserByResourceId(...) to GetActorUserAsync(int actorId) and drop resource lookups for FOLLOW/LIKE.

I can draft the model/migration/service/controller changes if you want.

🤖 Prompt for AI Agents
In AskFm/AskFm.DAL/Repositories/NotificationRepository.cs around lines 47 to 79,
the current GetActorUserByResourceId implementation resolves actor users by
querying resources (Follow/Thread/Comment/Like) using only a resourceId, which
can return arbitrary rows; change the design to use an explicit ActorId stored
on the Notification and stop inferring the actor from resource lookups: add an
ActorId (int FK) to the Notification model and migration, update any
CreateNotification signatures/usage to accept and persist actorId, and replace
GetActorUserByResourceId with a simple GetActorUserAsync(int actorId) that loads
and returns the ApplicationUser by that FK (remove FOLLOW/LIKE resource-based
queries).

Comment thread AskFm/AskFm.DAL/UnitOfWork.cs Outdated
}
}

IRepository<Models.Thread> IUnitOfWork.Threads => throw new NotImplementedException();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Do not throw from the interface implementation of Threads

This breaks consumers typed as IUnitOfWork and is a runtime landmine.

Apply one of the following:

-IRepository<Models.Thread> IUnitOfWork.Threads => throw new NotImplementedException();
+IRepository<Models.Thread> IUnitOfWork.Threads => Threads;

Or simply remove the explicit implementation if the public property already satisfies the interface.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
IRepository<Models.Thread> IUnitOfWork.Threads => throw new NotImplementedException();
IRepository<Models.Thread> IUnitOfWork.Threads => Threads;
🤖 Prompt for AI Agents
In AskFm/AskFm.DAL/UnitOfWork.cs around line 121 the explicit interface
implementation IRepository<Models.Thread> IUnitOfWork.Threads currently throws
NotImplementedException which causes a runtime failure for consumers using
IUnitOfWork; replace this throwing implementation with a proper getter that
returns the concrete/backing Threads repository (or delegate to the existing
public Threads property), or if the class already exposes a public Threads
property that matches the interface, remove the explicit implementation so the
public property satisfies IUnitOfWork; ensure the implementation returns the
repository instance and does not throw.

@Mahmoud-Ayman-Saleh Mahmoud-Ayman-Saleh merged commit 97620e4 into Develop Sep 20, 2025
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants