Skip to content

Finishing Comment Section in API Design#21

Merged
ziad-ashraf7 merged 22 commits into
Askfm-clone:Developfrom
ziad-ashraf7:Comments
Sep 3, 2025
Merged

Finishing Comment Section in API Design#21
ziad-ashraf7 merged 22 commits into
Askfm-clone:Developfrom
ziad-ashraf7:Comments

Conversation

@ziad-ashraf7
Copy link
Copy Markdown
Contributor

@ziad-ashraf7 ziad-ashraf7 commented Aug 24, 2025

  • Added CommentController (Adding 3 endpoints mentioned on API Design issue API Design #9 )
  • Added ICommentService, CommentService (With one function to get the comment by id)
  • Added ICommentLikeService and CommentLikeService to handle the core logic function for the Comment likes.
  • Added CommentLikeServiceTest to write Unit tests for the CommentlikeService Class.

Summary by CodeRabbit

  • New Features
    • Added comment like functionality: like/unlike a comment and view all likes for a comment.
    • Introduced a “profile” endpoint returning streamlined user information.
  • Bug Fixes
    • Authentication endpoints now return clearer error messages on failure.
  • Tests
    • Added unit tests covering add/delete/retrieve comment likes.
  • Chores
    • Swagger now opens automatically on app start for easier API exploration.
    • Database now auto-sets UTC timestamps for created/updated/deleted fields.
    • Updated dependencies to support new features.

hadeer-r and others added 3 commits July 29, 2025 01:17
allow running all branches and PRs
Updating the main to the Develop branch
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Aug 24, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

Introduces comment-like functionality: new CommentController with GET/POST/DELETE endpoints for comment likes; corresponding BLL services and interfaces for comments and comment likes; DI registrations; DTO for likes; unit tests; user identity return type change; minor API controller response tweaks; EF Core defaults added for CreatedAt/UpdatedAt/DeletedAt via migrations; Thread alias fix in DAL.

Changes

Cohort / File(s) Summary
API Controllers
AskFm.API/Controllers/CommentController.cs, AskFm.API/Controllers/AuthController.cs, AskFm.API/Controllers/UserController.cs
Adds CommentController (get/add/delete likes) using IUserService/ICommentLikeService/ICommentService with structured error handling and logging; AuthController now returns BadRequest with Errors; UserController changes route to profile and maps current user to ReadUserDTO.
API Composition & Settings
AskFm.API/Program.cs, AskFm.API/AskFm.API.csproj, AskFm.API/Properties/launchSettings.json
Registers ICommentLikeService and ICommentService; adds AutoMapper package; enables browser launch to Swagger.
BLL Services: Comments & Likes
AskFm.BLL/Services/ICommentLikeService.cs, AskFm.BLL/Services/CommentLikeService.cs, AskFm.BLL/Services/ICommentService.cs, AskFm.BLL/Services/CommentService.cs, AskFm.BLL/DTO/CommentLikeDto.cs
Introduces comment-like DTO and services: list/add/delete likes and fetch comment; interfaces added; service implementations use UnitOfWork; logs operations.
Identity Service Contract Change
AskFm.BLL/Services/UserIdentityService/IUserService.cs, AskFm.BLL/Services/UserIdentityService/UserService.cs
GetCurrentUserAsync now returns ApplicationUser instead of ReadUserDTO; implementation updated accordingly.
DAL: UnitOfWork Thread Alias
AskFm.DAL/Interfaces/IUnitOfWork.cs, AskFm.DAL/UnitOfWork.cs
Changes Thread alias to domain model AskFm.DAL.Models.Thread.
EF Core Timestamp Defaults
AskFm.DAL/AppDbContext.cs, AskFm.DAL/Migrations/20250825225059_AddCreatedAtDefault*.cs, AskFm.DAL/Migrations/20250825230515_AddUpdatedAtAndDeletedAtDefault*.cs, AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs
Adds GETUTCDATE() defaults for CreatedAt/UpdatedAt/DeletedAt across entities via two migrations; updates model configuration and snapshot.
Tests
Tests/CommentLikeServiceTest.cs, Tests/Tests.csproj
Adds unit tests for CommentLikeService (add/delete/list); adds Moq and references API project.
BLL Project
AskFm.BLL/AskFm.BLL.csproj
Adds AutoMapper package reference.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Client
  participant API as CommentController
  participant U as IUserService
  participant CLS as ICommentLikeService
  participant CS as ICommentService
  participant UoW as UnitOfWork/Repos

  rect rgb(245,248,252)
    Note over Client,API: GET /api/comment/{id}/likes
    Client->>API: GetAllLikes(id)
    API->>CLS: GetLikesForCommentAsync(id)
    CLS->>UoW: Validate comment, fetch likes
    UoW-->>CLS: Likes (DTOs)
    CLS-->>API: ServiceResult<IEnumerable<CommentLikeDto>>
    API-->>Client: 200/400/404/500
  end

  rect rgb(245,252,245)
    Note over Client,API: POST /api/comment/{id}/likes
    Client->>API: AddLike(id)
    API->>U: GetCurrentUserAsync()
    U-->>API: ServiceResult<ApplicationUser>
    API->>CLS: AddLikeAsync(id, userId)
    CLS->>UoW: Load comment/user, upsert like, save
    UoW-->>CLS: Persisted like
    CLS-->>API: ServiceResult<CommentLikeDto>
    API-->>Client: 201/400/409/500
  end

  rect rgb(252,245,245)
    Note over Client,API: DELETE /api/comment/{id}/likes
    Client->>API: DeleteLike(id)
    API->>U: GetCurrentUserAsync()
    U-->>API: ServiceResult<ApplicationUser>
    API->>CS: GetCommentAsync(id)
    CS->>UoW: GetByIdAsync(id)
    UoW-->>CS: Comment or null
    CS-->>API: ServiceResult<Comment>
    API->>CLS: DeleteLikeAsync(id, userId)
    CLS->>UoW: Find like, remove, save
    UoW-->>CLS: Success/failure
    CLS-->>API: ServiceResult
    API-->>Client: 204/403/404/500
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • hadeer-r

Poem

A tap on a comment: like, like, hooray!
I thump my paws—new routes lead the way.
Timestamps sprout, UTC dew,
Migrations hop the schema through.
With mocks and logs, I sniff the trail—
Approve this PR, and twitch my tail! 🐇✨

✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@ziad-ashraf7
Copy link
Copy Markdown
Contributor Author

@CodeRabbit review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Aug 24, 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: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
AskFm/AskFm.API/Program.cs (2)

31-33: UseLazyLoadingProxies() requires the Proxies namespace/package import.

Without using Microsoft.EntityFrameworkCore.Proxies; the extension method is out of scope and compilation will fail.

Apply:

+using Microsoft.EntityFrameworkCore.Proxies;
 ...
 builder.Services.AddDbContext<AppDbContext>(options =>
     options
         .UseLazyLoadingProxies()
         .UseSqlServer(ConnectionString));

49-52: Critical: Configure and invoke authentication before authorization

Your API currently calls app.UseAuthorization() without having first registered or enabled authentication. This means HttpContext.User will never be populated. To fix:

• In your service configuration (typically in Program.cs before building the app), register an authentication scheme. For example:

builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        // configure token validation parameters here
    });

• In the middleware pipeline (around lines 49–52 of Program.cs), insert UseAuthentication() before UseAuthorization():

     app.UseHttpsRedirection();

+    app.UseAuthentication();
     app.UseAuthorization();

These changes are mandatory to ensure your controllers see an authenticated User and that any [Authorize] attributes work as intended.

🧹 Nitpick comments (11)
AskFm/AskFm.BLL/DTO/CommentLikeDto.cs (1)

3-9: Make the DTO immutable, fix non-nullable warnings, and prefer DateTimeOffset for timestamps.

  • With enable, string UserName { get; set; } is non-nullable but uninitialized, triggering CS8618 and risking nulls in serialization.
  • DTOs benefit from immutability; init-only or a record reduce accidental mutations.
  • For cross-time-zone correctness, DateTimeOffset is a better default than DateTime for persisted/serialized timestamps.

Suggested refactor (record with required/init and DateTimeOffset):

-namespace AskFm.BLL.DTO;
-
-public class CommentLikeDto
-{
-    public int CommentId { get; set; }
-    public int UserId { get; set; }
-    public string UserName { get; set; }
-    public DateTime CreatedAt { get; set; }
-}
+namespace AskFm.BLL.DTO;
+
+public sealed record CommentLikeDto
+{
+    public required int CommentId { get; init; }
+    public required int UserId { get; init; }
+    public required string UserName { get; init; }
+    public DateTimeOffset CreatedAt { get; init; }
+}

If you prefer to keep a class:

-public class CommentLikeDto
-{
-    public int CommentId { get; set; }
-    public int UserId { get; set; }
-    public string UserName { get; set; }
-    public DateTime CreatedAt { get; set; }
-}
+public class CommentLikeDto
+{
+    public int CommentId { get; init; }
+    public int UserId { get; init; }
+    public required string UserName { get; init; }
+    public DateTimeOffset CreatedAt { get; init; }
+}
AskFm/AskFm.BLL/Services/ICommentService.cs (2)

1-1: Decouple BLL service contracts from DAL model types if possible.

ICommentService exposes AskFm.DAL.Models.Comment. Consider returning a BLL-facing DTO/view model to avoid leaking DAL entities across layers and to keep the BLL independent of persistence concerns. This also simplifies serialization concerns at the API boundary.


5-7: Enable async comment retrieval with nullability and optional cancellation

To align with EF Core’s asynchronous data access and clearly model “not found,” refactor the service method from synchronous Comment GetComment(int) to Task<Comment?> GetCommentAsync(int, CancellationToken). This change will require updates in the interface, its implementation, and all call sites.

Affected locations:

  • ICommentService.cs (lines 5–7): change return type, rename method, add CancellationToken.
  • CommentService.cs (around line 14): implement GetCommentAsync and invoke the async repository method (e.g., GetByIdAsync).
  • CommentController.cs (around lines 94–100): update calls to await _commentService.GetCommentAsync(id, cancellationToken) and handle the null result.

Suggested diff for the interface:

 namespace AskFm.BLL.Services;
 
-public interface ICommentService
-{
-    Comment GetComment(int commentId);
-}
+using System.Threading;
+using System.Threading.Tasks;
+
+public interface ICommentService
+{
+    Task<Comment?> GetCommentAsync(int commentId, CancellationToken cancellationToken = default);
+}
AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs (2)

2-2: Avoid aliasing Thread to minimize collisions with System.Threading.Thread.

Using Thread as an alias invites accidental confusion/import collisions in other files. Prefer a more explicit alias or fully-qualify the domain type.

Apply:

-using Thread = AskFm.DAL.Models.Thread;
+using DomainThread = AskFm.DAL.Models.Thread;

And update the property:

-    IRepository<Thread>  Threads { get; }
+    IRepository<DomainThread>  Threads { get; }

Also applies to: 9-9


17-18: Async-first standardization with cancellation support – minimal sync API surface detected

We ran the ripgrep search and found only two references to the synchronous Save() API:

  • Implementation in AskFm/AskFm.DAL/UnitOfWork.cs (line 128):
public int Save() => _context.SaveChanges();
  • One test in AskFm/Tests/CommentLikeServiceTest.cs (line 87):
_mockUnitOfWork.Verify(uow => uow.Save(), Times.Never);

Since the sync API surface is small, migrating toward an async-first model with cancellation support should be straightforward.

Key action items:

  • Add a cancellation-aware overload to the interface:
     interface IUnitOfWork
     {
  •  Task<int> SaveAsync();
    
  •  Task<int> SaveAsync(CancellationToken cancellationToken = default);
    
    }
  • Update the implementation in UnitOfWork.cs:
     public Task<int> SaveAsync()
  •  => _context.SaveChangesAsync();
    
  •  => _context.SaveChangesAsync(cancellationToken);
    
  • Modify any callers (e.g., in CommentLikeServiceTest.cs) to verify the new async method instead of the sync one.
  • Optionally mark int Save() as [Obsolete("Use SaveAsync with CancellationToken")] or remove it in the next major release to discourage mixed usage.

This refactor remains optional but will improve scalability and consistency if adopted.

AskFm/AskFm.BLL/Services/CommentService.cs (2)

8-12: Make the dependency readonly and guard against null.

Minor polish for immutability and safety.

Apply:

-    private IUnitOfWork _unitOfWork;
+    private readonly IUnitOfWork _unitOfWork;
     public CommentService(IUnitOfWork unitOfWork)
     {
-        _unitOfWork = unitOfWork;
+        ArgumentNullException.ThrowIfNull(unitOfWork);
+        _unitOfWork = unitOfWork;
     }

14-18: Consider async or explicit not-found behavior.

GetComment returns null when absent; either:

  • Make it async if the repo supports it (GetByIdAsync) and add CancellationToken, or
  • Throw a domain-specific exception when not found, or
  • Rename to FindComment/TryGetComment to reflect nullable return.

If async exists:

-    public Comment GetComment(int commentId)
+    public Task<Comment?> GetCommentAsync(int commentId, CancellationToken ct = default)
     {
-        var comment = _unitOfWork.Comments.GetById(commentId);
-        return comment;
+        return _unitOfWork.Comments.GetByIdAsync(commentId, ct);
     }
AskFm/AskFm.API/Controllers/CommentController.cs (1)

42-46: Consider not echoing raw exception messages to clients

Returning ex.Message can leak internal details. Map to consistent, client-friendly messages, and keep details in logs.

AskFm/AskFm.BLL/Services/CommentLikeService.cs (1)

121-123: Structured logging token name

Prefer consistent casing for tokens: use {UserId} not {userid} to keep property names uniform across logs.

AskFm/Tests/CommentLikeServiceTest.cs (2)

37-72: Good happy-path coverage for AddLike, but consider asserting LikeCount increment

You verify AddAsync and SaveAsync; also assert comment.LikeCount == 1 to ensure bookkeeping is updated.

Example:

Assert.Equal(1, comment.LikeCount);

172-197: Assertion strategy: be explicit on fields or use custom comparer

Assert.Equivalent can be lenient or brittle depending on versions. To avoid flakiness (extra fields like CreatedAt/UserName), assert per-field or provide a comparer ignoring those fields.

Example:

Assert.All(result, dto => Assert.Equal(commentId, dto.CommentId));
Assert.Equal(new[] {4,5,6,8,9}, result.Select(r => r.UserId));
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3750097 and 7522177.

📒 Files selected for processing (13)
  • AskFm/AskFm.API/Controllers/CommentController.cs (1 hunks)
  • AskFm/AskFm.API/Program.cs (2 hunks)
  • AskFm/AskFm.BLL/AskFm.BLL.csproj (1 hunks)
  • AskFm/AskFm.BLL/DTO/CommentLikeDto.cs (1 hunks)
  • AskFm/AskFm.BLL/DTO/CreateCommentLikeDto.cs (1 hunks)
  • AskFm/AskFm.BLL/Services/CommentLikeService.cs (1 hunks)
  • AskFm/AskFm.BLL/Services/CommentService.cs (1 hunks)
  • AskFm/AskFm.BLL/Services/ICommentLikeService.cs (1 hunks)
  • AskFm/AskFm.BLL/Services/ICommentService.cs (1 hunks)
  • AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs (1 hunks)
  • AskFm/AskFm.DAL/UnitOfWork.cs (1 hunks)
  • AskFm/Tests/CommentLikeServiceTest.cs (1 hunks)
  • AskFm/Tests/Tests.csproj (2 hunks)
🔇 Additional comments (3)
AskFm/Tests/Tests.csproj (1)

13-13: LGTM: Moq reference is appropriate for service-level unit tests.

AskFm/AskFm.DAL/UnitOfWork.cs (1)

4-4: LGTM: Alias resolves ambiguity with System.Threading.Thread and aligns repository type.

This change prevents accidental usage of the threading type in repository definitions in this file.

AskFm/Tests/CommentLikeServiceTest.cs (1)

34-35: Constructor wiring OK

Mocks and service under test are constructed correctly.

Comment thread AskFm/AskFm.API/Controllers/CommentController.cs Outdated
Comment thread AskFm/AskFm.API/Controllers/CommentController.cs
Comment thread AskFm/AskFm.API/Controllers/CommentController.cs
Comment on lines +40 to +49
builder.Services.AddScoped<ICommentLikeService, CommentLikeService>();
builder.Services.AddScoped<ICommentService, CommentService>();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Good DI registrations; ensure AutoMapper (if used) is registered too.

If CommentLikeService/CommentService use IMapper, you need AddAutoMapper(...) in DI.

Search for IMapper usage:

If found, add:

+builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());

The scoped registrations for the services look correct alongside IUnitOfWork.


🏁 Script executed:

#!/bin/bash
rg -nP --type cs -C2 '\bIMapper\b|AutoMapper'

Length of output: 1565


Ensure AutoMapper is registered and injected for all IMapper usages

We’ve confirmed IMapper is used in both the BLL service and the API controller but neither the DI registration nor the constructors reflect that:

  • AskFm/AskFm.BLL/Services/CommentLikeService.cs declares
    private readonly IMapper _mapper;
    public CommentLikeService(IUnitOfWork unitOfWork, ILogger<CommentLikeService> logger) {}
    → Missing constructor parameter and assignment for IMapper _mapper.
  • AskFm/AskFm.API/Controllers/CommentController.cs includes
    using AutoMapper;
    public class CommentController : ControllerBase
    {
        private readonly IMapper _mapper;}
    → Likewise needs IMapper in its constructor.

Please apply the following updates:

• In Program.cs (AskFm/AskFm.API/Program.cs), before building the host, register AutoMapper:

 builder.Services.AddScoped<ICommentLikeService, CommentLikeService>();
 builder.Services.AddScoped<ICommentService, CommentService>();
+builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());

• Update CommentLikeService constructor:

- public CommentLikeService(IUnitOfWork unitOfWork, ILogger<CommentLikeService> logger)
+ public CommentLikeService(IUnitOfWork unitOfWork,
+                           ILogger<CommentLikeService> logger,
+                           IMapper mapper)
 {
     _unitOfWork = unitOfWork;
     _logger = logger;
+    _mapper = mapper;
 }

• Update CommentController constructor similarly to accept and assign IMapper.

These changes will ensure AutoMapper is available in DI and that each consuming class receives its IMapper instance.

🤖 Prompt for AI Agents
In AskFm/AskFm.API/Program.cs around lines 40-41, AutoMapper is not registered
in DI while IMapper is required by services and controllers; register AutoMapper
(e.g., builder.Services.AddAutoMapper(typeof(Startup) or an assembly marker)
before building the host. In AskFm/AskFm.BLL/Services/CommentLikeService.cs add
an IMapper parameter to the constructor and assign it to the existing private
readonly IMapper _mapper field (and import AutoMapper namespace), preserving
existing parameters and logger. In
AskFm/AskFm.API/Controllers/CommentController.cs update the controller
constructor to accept IMapper and assign it to the controller's private readonly
IMapper _mapper field (add using AutoMapper if missing). Ensure constructor
parameter order and DI registrations remain consistent so IMapper is injected
where used.

<ItemGroup>
<Folder Include="DTO\" />
<Folder Include="Services\" />
<PackageReference Include="AutoMapper" Version="15.0.1" />
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Confirm AutoMapper DI integration point; add DI package in the API if needed.

Adding AutoMapper here is fine if BLL uses IMapper internally. If the mapping is configured/registered in the API via services.AddAutoMapper(...), ensure the API project references AutoMapper.Extensions.Microsoft.DependencyInjection; that DI package is typically added to the startup project rather than BLL.

Run to verify usage and registration points:


🏁 Script executed:

#!/bin/bash
# Find AutoMapper registrations and usages
rg -n --type=cs '\bAddAutoMapper\b|IMapper\b|MapperConfiguration\b|Profile\b' -C3

Length of output: 702


🏁 Script executed:

#!/bin/bash
# Search for DI package reference in all csproj files
rg -n "AutoMapper.Extensions.Microsoft.DependencyInjection" -g "*.csproj"
# Search for DI registration code (services.AddAutoMapper) in C# files
rg -n --type=cs "AddAutoMapper" -C3

Length of output: 109


Add AutoMapper DI registration in the API project

The BLL layer is injecting IMapper (e.g., in CommentLikeService), but I didn’t find any DI package reference or services.AddAutoMapper(...) calls across the solution. Without registering AutoMapper in your startup project, those injections will fail at runtime.

Please address the following:

  • In your API project’s .csproj (e.g. AskFm/AskFm.API/AskFm.API.csproj), add:
    <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="[latest-compatible-version]" />
  • In your startup configuration (Program.cs or Startup.cs), register AutoMapper, for example:
    services.AddAutoMapper(typeof(Startup).Assembly);
    // or specify assemblies containing your Profile classes:
    // services.AddAutoMapper(typeof(CommentMappingProfile).Assembly);

This will ensure that IMapper is properly configured and available to the BLL.

🤖 Prompt for AI Agents
In AskFm/AskFm.BLL/AskFm.BLL.csproj around line 15, the project references
AutoMapper but the API project lacks the DI integration; add the
AutoMapper.Extensions.Microsoft.DependencyInjection package reference to your
API project's .csproj (AskFm/AskFm.API/AskFm.API.csproj) and then register
AutoMapper during startup (in Program.cs or Startup.cs) by calling the
AddAutoMapper extension and pointing it to the assembly or assemblies that
contain your AutoMapper Profile classes (for example the assembly containing
your mapping profiles or Startup class) so that IMapper can be resolved by the
BLL at runtime.

Comment on lines +5 to +10
public interface ICommentLikeService
{
Task<IEnumerable<CommentLikeDto>> GetLikesForCommentAsync(int commentId);
Task<CommentLikeDto> AddLikeAsync(int commentId, int userId);
Task DeleteLikeAsync(int commentId, int userId);
} No newline at end of file
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Add CancellationToken and define idempotent semantics.

Async service methods should accept CancellationToken. Also clarify whether adding a like is idempotent (no-op if already liked) or should error.

Consider:

 public interface ICommentLikeService
 {
-    Task<IEnumerable<CommentLikeDto>> GetLikesForCommentAsync(int commentId);
-    Task<CommentLikeDto> AddLikeAsync(int commentId, int userId);
-    Task DeleteLikeAsync(int commentId, int userId);
+    Task<IEnumerable<CommentLikeDto>> GetLikesForCommentAsync(int commentId, CancellationToken ct = default);
+    Task<CommentLikeDto> AddLikeAsync(int commentId, int userId, CancellationToken ct = default);
+    Task<bool> DeleteLikeAsync(int commentId, int userId, CancellationToken ct = default); // return false if not found
 }

Scan implementation to ensure alignment if you adopt CTs/return type:


🏁 Script executed:

#!/bin/bash
rg -nP --type cs 'class\s+CommentLikeService\b|ICommentLikeService'

Length of output: 583


🏁 Script executed:

#!/bin/bash
# Print the first 200 lines of the CommentLikeService implementation to inspect method signatures
sed -n '1,200p' AskFm/AskFm.BLL/Services/CommentLikeService.cs

Length of output: 4893


Improve CancellationToken support, idempotency semantics, and correct DeleteLikeAsync implementation

  • All three interface methods lack a CancellationToken parameter – the implementation’s GetByIdAsync, FindAsync, etc., all accept a token. Adding CancellationToken ct = default ensures upstream callers can cancel long-running operations.
  • AddLikeAsync currently throws if a duplicate like exists. Decide and document whether this should remain an error or become an idempotent no-op.
  • DeleteLikeAsync signature and behavior both need adjustment:
    • It blocks on GetByIdAsync via .Result instead of awaiting, risking deadlocks.
    • It marks the comment (comment.Result.IsDeleted = true) rather than the like as deleted – this will delete the comment itself.
    • It throws when no like exists; consider returning false instead of throwing, and return true on success.

Locations to update:

  • AskFm/AskFm.BLL/Services/ICommentLikeService.cs – add CancellationToken params and change DeleteLikeAsync return type.
  • AskFm/AskFm.BLL/Services/CommentLikeService.cs – update method signatures, await all async calls, delete the correct entity, and return a success flag on delete.

Suggested interface diff:

 public interface ICommentLikeService
 {
-    Task<IEnumerable<CommentLikeDto>> GetLikesForCommentAsync(int commentId);
-    Task<CommentLikeDto> AddLikeAsync(int commentId, int userId);
-    Task DeleteLikeAsync(int commentId, int userId);
+    Task<IEnumerable<CommentLikeDto>> GetLikesForCommentAsync(int commentId, CancellationToken ct = default);
+    /// <summary>
+    /// Adds a like; throws if already liked unless idempotent semantics chosen.
+    /// </summary>
+    Task<CommentLikeDto> AddLikeAsync(int commentId, int userId, CancellationToken ct = default);
+    /// <summary>
+    /// Deletes a like; returns false if no like existed.
+    /// </summary>
+    Task<bool> DeleteLikeAsync(int commentId, int userId, CancellationToken ct = default);
 }

And in CommentLikeService.cs, for DeleteLikeAsync:

-   public async Task DeleteLikeAsync(int commentId, int userId)
+   public async Task<bool> DeleteLikeAsync(int commentId, int userId, CancellationToken ct = default)
    {
-       var commentLike = await _unitOfWork.CommentLikes.FindAsync(cl => cl.CommentId == commentId && cl.UserId == userId && !cl.IsDeleted);
+       var commentLike = await _unitOfWork.CommentLikes.FindAsync(
+           predicate: cl => cl.CommentId == commentId && cl.UserId == userId && !cl.IsDeleted,
+           cancellationToken: ct);
        // …
-       var comment = _unitOfWork.Comments.GetByIdAsync(commentId);
-       Console.WriteLine(comment.Result.Content);
-       // if the user didn't like  this comment before 
-       if(commentLike == null)
-           throw new ArgumentException($"User didn't like this comment");
-       
-       await _unitOfWork.CommentLikes.RemoveAsync(commentLike);
-       
-       comment.Result.IsDeleted = true;
-       comment.Result.CommentLikes.Remove(commentLike);
-       comment.Result.LikeCount--;
-       
-       await _unitOfWork.SaveAsync();
-       
-   }
+       if (commentLike == null)
+           return false;
+
+       var comment = await _unitOfWork.Comments.GetByIdAsync(commentId, ct)
+                       ?? throw new ArgumentException($"Comment {commentId} not found");
+
+       // Mark the like as deleted rather than the comment
+       commentLike.IsDeleted = true;
+       comment.LikeCount--;
+       _unitOfWork.CommentLikes.Update(commentLike);
+       _unitOfWork.Comments.Update(comment);
+
+       await _unitOfWork.SaveAsync(ct);
+       return true;
+   }
📝 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 interface ICommentLikeService
{
Task<IEnumerable<CommentLikeDto>> GetLikesForCommentAsync(int commentId);
Task<CommentLikeDto> AddLikeAsync(int commentId, int userId);
Task DeleteLikeAsync(int commentId, int userId);
}
// AskFm/AskFm.BLL/Services/ICommentLikeService.cs
public interface ICommentLikeService
{
Task<IEnumerable<CommentLikeDto>> GetLikesForCommentAsync(int commentId, CancellationToken ct = default);
/// <summary>
/// Adds a like; throws if already liked unless idempotent semantics chosen.
/// </summary>
Task<CommentLikeDto> AddLikeAsync(int commentId, int userId, CancellationToken ct = default);
/// <summary>
/// Deletes a like; returns false if no like existed.
/// </summary>
Task<bool> DeleteLikeAsync(int commentId, int userId, CancellationToken ct = default);
}
Suggested change
public interface ICommentLikeService
{
Task<IEnumerable<CommentLikeDto>> GetLikesForCommentAsync(int commentId);
Task<CommentLikeDto> AddLikeAsync(int commentId, int userId);
Task DeleteLikeAsync(int commentId, int userId);
}
// AskFm/AskFm.BLL/Services/CommentLikeService.cs
public async Task<bool> DeleteLikeAsync(int commentId, int userId, CancellationToken ct = default)
{
var commentLike = await _unitOfWork.CommentLikes.FindAsync(
predicate: cl => cl.CommentId == commentId && cl.UserId == userId && !cl.IsDeleted,
cancellationToken: ct);
if (commentLike == null)
return false;
var comment = await _unitOfWork.Comments.GetByIdAsync(commentId, ct)
?? throw new ArgumentException($"Comment {commentId} not found");
// Mark the like as deleted rather than the comment
commentLike.IsDeleted = true;
comment.LikeCount--;
_unitOfWork.CommentLikes.Update(commentLike);
_unitOfWork.Comments.Update(comment);
await _unitOfWork.SaveAsync(ct);
return true;
}
🤖 Prompt for AI Agents
In AskFm/AskFm.BLL/Services/ICommentLikeService.cs around lines 5-10, update the
API to accept CancellationToken on all methods and make DeleteLikeAsync return a
success flag: change signatures to include CancellationToken ct = default for
GetLikesForCommentAsync and AddLikeAsync, and change DeleteLikeAsync to
Task<bool> DeleteLikeAsync(int commentId, int userId, CancellationToken ct =
default). In the implementation (CommentLikeService.cs) update method signatures
accordingly, await all async calls using the passed ct (no .Result), make
AddLikeAsync idempotent by returning the existing CommentLikeDto if a like
already exists instead of throwing (or document alternative if you choose to
keep throwing), and fix DeleteLikeAsync to locate the like entity, return false
if not found, set like.IsDeleted = true, persist changes, and return true on
success while passing ct through to repository calls.

Comment thread AskFm/Tests/CommentLikeServiceTest.cs Outdated
Comment thread AskFm/Tests/CommentLikeServiceTest.cs Outdated
Comment thread AskFm/Tests/CommentLikeServiceTest.cs Outdated
Comment thread AskFm/Tests/Tests.csproj
Comment on lines +22 to +24
<ItemGroup>
<ProjectReference Include="..\AskFm.API\AskFm.API.csproj" />
</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.

💡 Verification agent

❓ Verification inconclusive

Avoid coupling unit tests to the API layer; reference BLL directly.

For unit-testing CommentLikeService, a reference to AskFm.API is unnecessary and tightens coupling. Prefer a project reference to AskFm.BLL (and DAL/Shared if truly required). Keep API references only for API integration tests.

Apply:

-  <ProjectReference Include="..\AskFm.API\AskFm.API.csproj" />
+  <ProjectReference Include="..\AskFm.BLL\AskFm.BLL.csproj" />

🏁 Script executed:

#!/bin/bash
# Check if tests are actually doing API/integration work; if none, drop API ref.
rg -n 'WebApplicationFactory|TestServer|HttpClient|ControllerBase|Controller\s' AskFm/Tests -C2

Length of output: 97


Avoid coupling Tests to the API layer
Verified: no API/integration constructs (WebApplicationFactory, TestServer, HttpClient, Controller, etc.) are used in AskFm/Tests, so the API reference is unnecessary and introduces tight coupling. Tests should target the BLL (and DAL/Shared if they depend on those types) directly.

Please update the project reference in AskFm/Tests/Tests.csproj:

• File: AskFm/Tests/Tests.csproj (lines 22–24)
• Replace the API reference with the BLL reference:

--- a/AskFm/Tests/Tests.csproj
@@ 22,24
-  <ProjectReference Include="..\AskFm.API\AskFm.API.csproj" />
+  <ProjectReference Include="..\AskFm.BLL\AskFm.BLL.csproj" />

Ensure you also add references to AskFm.DAL or AskFm.Shared if any test cases need domain or shared types.

🤖 Prompt for AI Agents
In AskFm/Tests/Tests.csproj around lines 22 to 24, the tests currently reference
the API project which introduces unnecessary coupling; remove the
ProjectReference to ..\AskFm.API\AskFm.API.csproj and add a ProjectReference to
the BLL project (e.g. ..\AskFm.BLL\AskFm.BLL.csproj) instead; also inspect tests
for any domain/shared types and add ProjectReference(s) to AskFm.DAL and/or
AskFm.Shared as required so tests depend only on BLL (and DAL/Shared when
needed).

- Added CommentController (Adding 3 endpoints mentioned on API Design
  issue)
- Added ICommentService, CommentService (With one function to get the
    comment by id)
- Added ICommentLikeService and CommentLikeService to handle the core
  logic function for the Comment likes.
- Added CommentLikeServiceTest to write Unit tests for the
  CommentlikeService Class.

# Conflicts:
#	AskFm/AskFm.API/Program.cs
#	AskFm/AskFm.BLL/AskFm.BLL.csproj
ziad-ashraf7 and others added 8 commits August 25, 2025 20:10
- Deleted CreateCommentLikeDto , no need for it after using the
  GetCurrentUser method
  - Marked CommentsController as Authorize
- Applied the ServiceResult on CommentService and CommentLikeService
@ziad-ashraf7
Copy link
Copy Markdown
Contributor Author

@CodeRabbit review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Aug 28, 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: 15

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
AskFm/AskFm.DAL/AppDbContext.cs (1)

70-76: Don’t set DeletedAt on Added; this flags new entities as deleted

On insert you set DeletedAt = UtcNow. That’s incorrect for soft-delete and contradicts the mapping.

                 case EntityState.Added:
                     entry.Entity.CreatedAt = DateTime.UtcNow;
-                    entry.Entity.DeletedAt = DateTime.UtcNow;;
                     entry.Entity.UpdatedAt = DateTime.UtcNow;
                     entry.Entity.IsDeleted = false;
                     break;
AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs (1)

55-69: NRE risk when reading claims; also return failure when user not found.

FindFirst(ClaimTypes.Email).Value will throw if the claim/context is missing. Additionally, Success(null) propagates null to controllers.

Apply this diff:

-    public async Task<ServiceResult<ApplicationUser>> GetCurrentUserAsync()
+    public async Task<ServiceResult<ApplicationUser>> GetCurrentUserAsync()
     {
-        string email = _httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.Email).Value;
-        if (string.IsNullOrEmpty(email))
+        var email = _httpContextAccessor.HttpContext?.User?.FindFirstValue(ClaimTypes.Email);
+        if (string.IsNullOrWhiteSpace(email))
         {
             var errors = new List<string>()
             {
-                "Can't Access Current user"
+                "Cannot access current user (missing email claim)."
             };
-            return await ServiceResult<ApplicationUser>.Failure(errors);
+            return await ServiceResult<ApplicationUser>.Failure(errors);
         }
-        var currentAppUser = await _userManager.FindByEmailAsync(email);
-
-        return await ServiceResult<ApplicationUser>.Success(currentAppUser);
+        var currentAppUser = await _userManager.FindByEmailAsync(email);
+        if (currentAppUser is null)
+        {
+            var errors = new List<string>() { "Current user not found." };
+            return await ServiceResult<ApplicationUser>.Failure(errors);
+        }
+        return await ServiceResult<ApplicationUser>.Success(currentAppUser);
     }

Optionally, fall back to ClaimTypes.NameIdentifier if email is absent, and consider returning a distinct unauthorized failure to allow the controller to emit 401.

♻️ Duplicate comments (3)
AskFm/AskFm.API/Program.cs (1)

48-49: Register AutoMapper in DI and ensure constructors inject IMapper

IMapper is used in services/controllers; register it. Also verify constructors accept and assign IMapper.

         builder.Services.AddScoped<IUserService, UserService>();
         builder.Services.AddScoped<ICommentLikeService, CommentLikeService>();
         builder.Services.AddScoped<ICommentService, CommentService>();
+        builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
#!/bin/bash
# Ensure constructors inject IMapper and assignments exist
rg -nP --type cs -C2 'class\s+\w+\s*:\s*.*Controller|Service' | sed -n '1,120p' >/dev/null
rg -nP --type cs -C2 '\bIMapper\b'
rg -nP --type cs -C2 'public\s+\w+\([^)]*IMapper[^)]*\)'
rg -nP --type cs -C2 '_mapper\s*='
AskFm/AskFm.BLL/Services/ICommentLikeService.cs (1)

5-10: Add CancellationToken to all methods and fix Delete return type to reflect no payload

Expose cancellation and return a simple success flag for delete. Clarify idempotency in XML docs.

 public interface ICommentLikeService
 {
-    Task<ServiceResult<IEnumerable<CommentLikeDto>>> GetLikesForCommentAsync(int commentId);
-    Task<ServiceResult<CommentLikeDto>> AddLikeAsync(int commentId, int userId);
-    Task<ServiceResult<CommentLikeDto>> DeleteLikeAsync(int commentId, int userId);
+    Task<ServiceResult<IEnumerable<CommentLikeDto>>> GetLikesForCommentAsync(int commentId, CancellationToken ct = default);
+    Task<ServiceResult<CommentLikeDto>> AddLikeAsync(int commentId, int userId, CancellationToken ct = default);
+    // Returns true if a like was removed; false if none existed (idempotent delete).
+    Task<ServiceResult<bool>> DeleteLikeAsync(int commentId, int userId, CancellationToken ct = default);
 }
AskFm/AskFm.BLL/Services/CommentLikeService.cs (1)

14-21: Fix compile error: unused readonly _mapper and non-readonly _unitOfWork

remove IMapper (unused) and mark _unitOfWork readonly. This currently fails to compile.

-using AutoMapper;
+// using AutoMapper;

 public class CommentLikeService :  ICommentLikeService
 {
-    
-    private IUnitOfWork _unitOfWork;
+    private readonly IUnitOfWork _unitOfWork;
     private readonly ILogger<CommentLikeService> _logger;
-    private readonly IMapper _mapper;
-    public CommentLikeService(IUnitOfWork unitOfWork,  ILogger<CommentLikeService> logger)
+    public CommentLikeService(IUnitOfWork unitOfWork, ILogger<CommentLikeService> logger)
     {
         _unitOfWork = unitOfWork;
         _logger = logger;
     }
🧹 Nitpick comments (27)
AskFm/AskFm.DAL/AppDbContext.cs (2)

64-66: Fix typo and references: _fillTrackableInfromation_fillTrackableInformation

Improves readability and consistency.

-        _fillTrackableInfromation();
+        _fillTrackableInformation();
...
-        _fillTrackableInfromation();
+        _fillTrackableInformation();
...
-    private void _fillTrackableInfromation()
+    private void _fillTrackableInformation()

Also applies to: 55-62


25-51: Consider moving all timestamps to DB defaults or EF layer, but not both

Right now both DB defaults and EF code set timestamps. Pick one strategy to avoid drift. If DB-side, add an UPDATE trigger for UpdatedAt; if EF-side, drop DB defaults (except CreatedAt).

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

50-50: Duplicate AddControllers() registration

This call appears twice. Remove the second one.

-        builder.Services.AddControllers();

63-66: Redundant null-check after new

jwtOptions cannot be null. Remove dead code; add guards for missing env vars instead.

-        if (jwtOptions == null)
-        {
-            throw new Exception("jwtOptions is null");
-        }
+        if (string.IsNullOrEmpty(jwtOptions.SigningKey)
+            || string.IsNullOrEmpty(jwtOptions.Issuer)
+            || string.IsNullOrEmpty(jwtOptions.Audience))
+            throw new Exception("JWT environment variables are missing");

77-83: Use the same scheme constant for DefaultAuthenticateScheme

Minor consistency/readability improvement.

-        builder.Services.AddAuthentication(options =>
+        builder.Services.AddAuthentication(options =>
             {
-                options.DefaultAuthenticateScheme = "Bearer";
+                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                 options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
AskFm/AskFm.API/Controllers/AuthController.cs (4)

33-34: Returning only Errors changes the response shape; ensure clients can handle it

Success returns ServiceResult<T>, error now returns List<string>. Consider a consistent envelope (e.g., RFC7807 ProblemDetails) or always return ServiceResult<T>.

-            return BadRequest(result.Errors);
+            return Problem(title: "Registration failed",
+                           detail: string.Join("; ", result.Errors ?? new List<string>()),
+                           statusCode: StatusCodes.Status400BadRequest);

50-51: Same response-shape concern for Login

Align with the chosen error contract.


73-74: Same response-shape concern for RefreshToken

Consider unified error responses.


94-95: Same response-shape concern for Logout

Ensure consistency for API consumers.

AskFm/AskFm.API/Controllers/UserController.cs (1)

49-58: Prefer AutoMapper (or a mapper method) over manual projection; also fix DTO casing for followerCount.

Manual mapping here drifts from conventions and risks divergence. If ReadUserDTO uses PascalCase (FollowerCount), align property name; if it intentionally uses lower camel-case, reconsider to match .NET naming.

Apply within this block:

-        ReadUserDTO readUserDTO = new ReadUserDTO()
-        {
-            Name = result.Data.Name,
-            Email = result.Data.Email,
-            AvatarPath = result.Data.AvatarPath,
-            Bio = result.Data.Bio,
-            followerCount = result.Data.FollowersCount,
-            LastSeen = result.Data.LastSeen,
-        };
-        return Ok(readUserDTO);
+        var dto = new ReadUserDTO
+        {
+            Name = result.Data.Name,
+            Email = result.Data.Email,
+            AvatarPath = result.Data.AvatarPath,
+            Bio = result.Data.Bio,
+            FollowerCount = result.Data.FollowersCount, // ensure DTO uses PascalCase
+            LastSeen = result.Data.LastSeen,
+        };
+        return Ok(dto);

Outside these lines, inject and use IMapper if already configured:

// ctor add: IMapper mapper
private readonly IMapper _mapper;
// assign in ctor
return Ok(_mapper.Map<ReadUserDTO>(result.Data));
AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs (1)

12-22: Mark injected dependencies as readonly.

Makes intent explicit and prevents accidental reassignment.

-    private IUnitOfWork _unitOfWork;
-    private readonly UserManager<ApplicationUser> _userManager;
-    private readonly IHttpContextAccessor _httpContextAccessor;
+    private readonly IUnitOfWork _unitOfWork;
+    private readonly UserManager<ApplicationUser> _userManager;
+    private readonly IHttpContextAccessor _httpContextAccessor;
AskFm/AskFm.DAL/Migrations/20250825225059_AddCreatedAtDefault.cs (1)

14-85: LGTM: sensible CreatedAt defaults across tables

Defaults to GETUTCDATE() on insert are appropriate. Consider DATETIME2 in future for precision, but not blocking.

Also applies to: 87-161

AskFm/AskFm.BLL/Services/CommentService.cs (1)

8-12: Make _unitOfWork readonly

Field is never reassigned; mark as readonly to communicate intent and thread-safety.

-    private IUnitOfWork _unitOfWork;
+    private readonly IUnitOfWork _unitOfWork;
AskFm/AskFm.BLL/Services/CommentLikeService.cs (4)

122-143: Pass CancellationToken, prefer UTC, and ensure SaveAsync uses ct

Minor consistency and cancellation improvements.

-                var newLike = new CommentLike
+                var newLike = new CommentLike
                 {
                     CommentId = commentId,
                     UserId = userId,
                 };

-                await _unitOfWork.CommentLikes.AddAsync(newLike);
+                await _unitOfWork.CommentLikes.AddAsync(newLike, ct);
                 
                 comment.LikeCount++;
                 _unitOfWork.Comments.Update(comment);

-                await _unitOfWork.SaveAsync();
+                await _unitOfWork.SaveAsync(ct);

                 _logger.LogInformation("Like added successfully for comment id: {CommentId}", commentId);
                 return await ServiceResult<CommentLikeDto>.Success(new CommentLikeDto
                 {
                     CommentId = newLike.CommentId,
                     UserId = newLike.UserId,
                     UserName = userName,
                     CreatedAt = newLike.CreatedAt
                 });

1-8: Remove unnecessary usings

System.Runtime.InteropServices.JavaScript and Microsoft.VisualBasic are not used here.

-using System.Runtime.InteropServices.JavaScript;
-using Microsoft.VisualBasic;

86-101: Decide on idempotent add semantics

Consider returning Success with existing like when already liked (idempotent) instead of Failure; this simplifies client logic and aligns with HTTP PUT-like behavior.

If you choose idempotency, replace the failure branch with:

-                        var errors = new List<String>()
-                        {
-                            "User has already liked this comment"
-                        };
-                        return await ServiceResult<CommentLikeDto>.Failure(errors);
+                        return await ServiceResult<CommentLikeDto>.Success(new CommentLikeDto
+                        {
+                            CommentId = existingLike.CommentId,
+                            UserId = existingLike.UserId,
+                            UserName = userName,
+                            CreatedAt = existingLike.CreatedAt
+                        });

Update tests accordingly.


120-131: Enforce uniqueness at the DB level to prevent duplicate likes under concurrency

Add a unique index on (CommentId, UserId) for CommentLikes to avoid races; with soft-delete, reuse the same row.

I can generate a migration adding:

  • Unique index on (CommentId, UserId)
  • Optional filtered index if you intend to physically delete instead of soft-delete.
AskFm/AskFm.API/Controllers/CommentController.cs (4)

13-13: Use JwtBearerDefaults instead of hard-coded scheme string

Avoid magic strings for auth schemes.

+using Microsoft.AspNetCore.Authentication.JwtBearer;
@@
-[Authorize(AuthenticationSchemes = "Bearer")]
+[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]

55-65: Normalize error payloads to a standard shape

Some endpoints return { message }, others return raw arrays. Prefer a single schema, e.g., { errors: [] } or RFC7807 ProblemDetails.


90-93: Minor: simplify route values

Tiny nit, but new { id } is cleaner and equivalent.

-                new { id = id }, 
+                new { id }, 

137-143: Consider 404 for "like not found" to make DELETE idempotent-friendly

If the service signals "like not found", respond 404 (or 204 for idempotency). Today all failures map to 400.

-            if (!result.success)
-            {
-                return BadRequest(result.Errors);
-            }
+            if (!result.success)
+            {
+                var errors = result.Errors ?? Array.Empty<string>();
+                if (errors.Any(e => e.Contains("not found", StringComparison.OrdinalIgnoreCase)))
+                    return NotFound(new { errors });
+                return BadRequest(new { errors });
+            }
AskFm/AskFm.DAL/Migrations/20250825225059_AddCreatedAtDefault.Designer.cs (1)

55-60: Prefer datetime2 over DATETIME

For SQL Server, datetime2 has better precision and range. Consider switching all timestamp columns to datetime2.

Also applies to: 157-161, 204-208, 235-239, 269-273, 316-320, 358-362, 404-407

AskFm/Tests/CommentLikeServiceTest.cs (5)

37-72: Strengthen assertions in AddLike test

Also assert success and LikeCount increment.

-        // Act
-        await _commentLikeService.AddLikeAsync(commentId, userId);
+        // Arrange initial like count
+        comment.LikeCount = 0;
+        // Act
+        var result = await _commentLikeService.AddLikeAsync(commentId, userId);
@@
-        _mockUnitOfWork.Verify(uow => uow.SaveAsync(), Times.Once);
+        _mockUnitOfWork.Verify(uow => uow.SaveAsync(), Times.Once);
+        Assert.True(result.success);
+        Assert.Equal(1, comment.LikeCount);

74-97: Test name says "ThrowException" but the service returns a failure result

Rename for accuracy and assert the error payload shape.

-    public async Task AddLike_WhenCommentNotFoundOrDeleted_ThrowException()
+    public async Task AddLike_WhenCommentNotFoundOrDeleted_ReturnsFailure()
@@
-        Assert.False(result.success); 
+        Assert.False(result.success);
+        Assert.NotNull(result.Errors);

100-151: Misleading test name; it deletes a like, not the comment

Rename and keep assertions. Consider asserting the comment itself is not marked deleted once the service bug (if any) is settled.

-    public async Task DeleteLike_WhenDeleteByAuthor_CommentIsDeleted()
+    public async Task DeleteLike_WhenDeleteByAuthor_LikeIsRemoved()

153-207: Add minimal verification of DTO mapping (username/createdAt) if exposed

Current assertions only cover ids. If DTO includes UserName/CreatedAt, assert at least non-null where applicable.


209-241: Also assert success flag for empty result

Tiny completeness tweak.

-        Assert.Empty(result.Data);
+        Assert.True(result.success);
+        Assert.Empty(result.Data);
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7522177 and 462e88c.

📒 Files selected for processing (24)
  • AskFm/AskFm.API/AskFm.API.csproj (1 hunks)
  • AskFm/AskFm.API/Controllers/AuthController.cs (4 hunks)
  • AskFm/AskFm.API/Controllers/CommentController.cs (1 hunks)
  • AskFm/AskFm.API/Controllers/UserController.cs (1 hunks)
  • AskFm/AskFm.API/Program.cs (2 hunks)
  • AskFm/AskFm.API/Properties/launchSettings.json (2 hunks)
  • AskFm/AskFm.BLL/AskFm.BLL.csproj (1 hunks)
  • AskFm/AskFm.BLL/DTO/CommentLikeDto.cs (1 hunks)
  • AskFm/AskFm.BLL/Services/CommentLikeService.cs (1 hunks)
  • AskFm/AskFm.BLL/Services/CommentService.cs (1 hunks)
  • AskFm/AskFm.BLL/Services/ICommentLikeService.cs (1 hunks)
  • AskFm/AskFm.BLL/Services/ICommentService.cs (1 hunks)
  • AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs (2 hunks)
  • AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs (2 hunks)
  • AskFm/AskFm.DAL/AppDbContext.cs (1 hunks)
  • AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs (1 hunks)
  • AskFm/AskFm.DAL/Migrations/20250825225059_AddCreatedAtDefault.Designer.cs (1 hunks)
  • AskFm/AskFm.DAL/Migrations/20250825225059_AddCreatedAtDefault.cs (1 hunks)
  • AskFm/AskFm.DAL/Migrations/20250825230515_AddUpdatedAtAndDeletedAtDefault.Designer.cs (1 hunks)
  • AskFm/AskFm.DAL/Migrations/20250825230515_AddUpdatedAtAndDeletedAtDefault.cs (1 hunks)
  • AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs (13 hunks)
  • AskFm/AskFm.DAL/UnitOfWork.cs (1 hunks)
  • AskFm/Tests/CommentLikeServiceTest.cs (1 hunks)
  • AskFm/Tests/Tests.csproj (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • AskFm/Tests/Tests.csproj
  • AskFm/AskFm.BLL/DTO/CommentLikeDto.cs
  • AskFm/AskFm.BLL/AskFm.BLL.csproj
  • AskFm/AskFm.DAL/Interfaces/IUnitOfWork.cs
  • AskFm/AskFm.DAL/UnitOfWork.cs
🔇 Additional comments (6)
AskFm/AskFm.API/AskFm.API.csproj (1)

10-10: No separate AutoMapper.Extensions.Microsoft.DependencyInjection package needed
AddAutoMapper is now included in the AutoMapper core package since v13.0; skip adding the legacy DI extension package. (docs.automapper.io, stackoverflow.com)

Likely an incorrect or invalid review comment.

AskFm/AskFm.API/Properties/launchSettings.json (1)

7-9: Good DX improvement: auto-launch Swagger

Enabling launchBrowser with launchUrl: "swagger" eases manual testing.

Also applies to: 17-19

AskFm/AskFm.API/Controllers/UserController.cs (1)

40-40: Route change to "profile" is a breaking API change—coordinate clients and docs.

If this replaces "current-user", update API docs and client calls; consider adding an [Obsolete] alias route temporarily to avoid breaking consumers.

AskFm/AskFm.DAL/Migrations/20250825230515_AddUpdatedAtAndDeletedAtDefault.cs (2)

14-21: No action needed: the SaveChanges overrides invoke _fillTrackableInfromation, which sets UpdatedAt on Modified entries in AppDbContext—updates are properly stamped.


159-305: No changes required for DeletedAt in Down migration

The Down method already includes oldDefaultValueSql: "GETUTCDATE()" on each DeletedAt column, which correctly reverts the default constraint and restores prior state—no further edits needed.

Likely an incorrect or invalid review comment.

AskFm/AskFm.API/Controllers/CommentController.cs (1)

45-51: Return only the Data payload for GET likes
Modify GetAllLikes to return Ok(likes.Data) and wrap failures as BadRequest(new { errors = likes.Errors }) so the GET response shape matches POST.

Comment on lines +77 to +83
var user = await _userService.GetCurrentUserAsync();

if (!user.success)
{
return BadRequest(user.Errors);
}

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

Map "no current user" to 401, not 400

If the current user cannot be resolved, respond with Unauthorized.

-            if (!user.success)
-            {
-                return BadRequest(user.Errors);
-            }
+            if (!user.success)
+                return Unauthorized(new { errors = user.Errors });
📝 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
var user = await _userService.GetCurrentUserAsync();
if (!user.success)
{
return BadRequest(user.Errors);
}
var user = await _userService.GetCurrentUserAsync();
if (!user.success)
return Unauthorized(new { errors = user.Errors });
🤖 Prompt for AI Agents
In AskFm/AskFm.API/Controllers/CommentController.cs around lines 77 to 83, the
code returns BadRequest when the current user cannot be resolved; change this to
return an Unauthorized (401) response instead. Replace the
BadRequest(user.Errors) return with Unauthorized(user.Errors) (or at minimum
Unauthorized()) so the API maps "no current user" to HTTP 401.

Comment on lines +124 to +126
if (user==null || !user.success)
return BadRequest(user.Errors);

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

NRE risk when user is null; also wrong status code

You dereference user.Errors even when user == null. Return 401 for null user, and 400 only for a valid result that failed validation.

-            if (user==null || !user.success)
-                return BadRequest(user.Errors);
+            if (user is null)
+                return Unauthorized();
+            if (!user.success)
+                return BadRequest(new { errors = user.Errors });
📝 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
if (user==null || !user.success)
return BadRequest(user.Errors);
if (user is null)
return Unauthorized();
if (!user.success)
return BadRequest(new { errors = user.Errors });
🤖 Prompt for AI Agents
In AskFm/AskFm.API/Controllers/CommentController.cs around lines 124-126, the
code dereferences user.Errors even when user is null and uses the wrong status
code; change the logic to first check if user == null and return Unauthorized()
(401) in that case, then only if user is non-null and user.success is false
return BadRequest(user.Errors); ensure the null check occurs before accessing
user.Errors so no NRE can occur.

Comment on lines +131 to +133
if (comment == null || !user.success)
return BadRequest(user.Errors);

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

Incorrect condition; comment null should yield 404, not reuse user.success

You re-check user.success and return user.Errors. Return NotFound for a missing comment.

-            if (comment == null || !user.success)
-                return BadRequest(user.Errors);
+            if (comment is null)
+                return NotFound(new { message = $"Comment with id {id} not found" });

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

🤖 Prompt for AI Agents
In AskFm/AskFm.API/Controllers/CommentController.cs around lines 131 to 133, the
current combined condition incorrectly treats a null comment as a BadRequest
tied to user.success; split the checks so that if comment == null you return
NotFound(), and separately if !user.success you return BadRequest(user.Errors);
update the control flow accordingly to ensure the correct response codes are
returned for each case.

Comment on lines +25 to +56
public async Task<ServiceResult<IEnumerable<CommentLikeDto>>> GetLikesForCommentAsync(int commentId)
{
try
{
_logger.LogInformation("Retrieving likes for comment id: {CommentId}", commentId);

var comment = await _unitOfWork.Comments.GetByIdAsync(commentId);
if (comment == null)
{
throw new ArgumentException($"Comment with id {commentId} not found");
}

var likes = await _unitOfWork.CommentLikes.FindAllAsync(
predicate: cl => cl.CommentId == commentId && !cl.IsDeleted
);

var likeDtos = likes.Select(like => new CommentLikeDto
{
CommentId = like.CommentId,
UserId = like.UserId,
CreatedAt = like.CreatedAt,
UserName = like.User?.UserName
});

return await ServiceResult<IEnumerable<CommentLikeDto>>.Success(likeDtos);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error retrieving likes for comment id: {CommentId}", commentId);
return await ServiceResult<IEnumerable<CommentLikeDto>>.Failure(new List<string>() { ex.Message });
}
}
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

Load usernames reliably and add CancellationToken passthrough

If lazy-loading is off, like.User may be null. Project the username via a join/include or fetch it explicitly. Also add CT.

-    public async Task<ServiceResult<IEnumerable<CommentLikeDto>>> GetLikesForCommentAsync(int commentId)
+    public async Task<ServiceResult<IEnumerable<CommentLikeDto>>> GetLikesForCommentAsync(int commentId, CancellationToken ct = default)
     {
         try
         {
             _logger.LogInformation("Retrieving likes for comment id: {CommentId}", commentId);

-            var comment = await _unitOfWork.Comments.GetByIdAsync(commentId);
+            var comment = await _unitOfWork.Comments.GetByIdAsync(commentId, ct);
             if (comment == null)
             {
                 throw new ArgumentException($"Comment with id {commentId} not found");
             }

-            var likes = await _unitOfWork.CommentLikes.FindAllAsync(
-                predicate: cl => cl.CommentId == commentId && !cl.IsDeleted
-            );
+            var likes = await _unitOfWork.CommentLikes.FindAllAsync(
+                predicate: cl => cl.CommentId == commentId && !cl.IsDeleted,
+                include: q => q.Include(x => x.User),
+                cancellationToken: ct
+            );
📝 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 async Task<ServiceResult<IEnumerable<CommentLikeDto>>> GetLikesForCommentAsync(int commentId)
{
try
{
_logger.LogInformation("Retrieving likes for comment id: {CommentId}", commentId);
var comment = await _unitOfWork.Comments.GetByIdAsync(commentId);
if (comment == null)
{
throw new ArgumentException($"Comment with id {commentId} not found");
}
var likes = await _unitOfWork.CommentLikes.FindAllAsync(
predicate: cl => cl.CommentId == commentId && !cl.IsDeleted
);
var likeDtos = likes.Select(like => new CommentLikeDto
{
CommentId = like.CommentId,
UserId = like.UserId,
CreatedAt = like.CreatedAt,
UserName = like.User?.UserName
});
return await ServiceResult<IEnumerable<CommentLikeDto>>.Success(likeDtos);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error retrieving likes for comment id: {CommentId}", commentId);
return await ServiceResult<IEnumerable<CommentLikeDto>>.Failure(new List<string>() { ex.Message });
}
}
public async Task<ServiceResult<IEnumerable<CommentLikeDto>>> GetLikesForCommentAsync(int commentId, CancellationToken ct = default)
{
try
{
_logger.LogInformation("Retrieving likes for comment id: {CommentId}", commentId);
var comment = await _unitOfWork.Comments.GetByIdAsync(commentId, ct);
if (comment == null)
{
throw new ArgumentException($"Comment with id {commentId} not found");
}
var likes = await _unitOfWork.CommentLikes.FindAllAsync(
predicate: cl => cl.CommentId == commentId && !cl.IsDeleted,
include: q => q.Include(x => x.User),
cancellationToken: ct
);
var likeDtos = likes.Select(like => new CommentLikeDto
{
CommentId = like.CommentId,
UserId = like.UserId,
CreatedAt = like.CreatedAt,
UserName = like.User?.UserName
});
return await ServiceResult<IEnumerable<CommentLikeDto>>.Success(likeDtos);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error retrieving likes for comment id: {CommentId}", commentId);
return await ServiceResult<IEnumerable<CommentLikeDto>>.Failure(new List<string> { ex.Message });
}
}

Comment on lines +58 to +120
public async Task<ServiceResult<CommentLikeDto>> AddLikeAsync(int commentId, int userId)
{
try
{
_logger.LogInformation("Adding like for comment id: {CommentId} by user id: {UserId}",
commentId, userId);

var comment = await _unitOfWork.Comments.GetByIdAsync(commentId);
var user = await _unitOfWork.Users.GetByIdAsync(userId);
if (user == null)
{
var errors = new List<String>()
{
$"User with id {userId} not found"
};
return await ServiceResult<CommentLikeDto>.Failure(errors);
}

string userName = user.UserName;
if (comment == null)
{
var errors = new List<String>()
{
$"Comment with id {commentId} not found"
};
return await ServiceResult<CommentLikeDto>.Failure(errors);
}

var existingLike = await _unitOfWork.CommentLikes.FindAsync(
cl => cl.CommentId == commentId && cl.UserId == userId
);

// if the CommentLike already exit in the DB
if (existingLike != null)
{
// if the The use already liked this comment
if (!existingLike.IsDeleted)
{
var errors = new List<String>()
{
"User has already liked this comment"
};
return await ServiceResult<CommentLikeDto>.Failure(errors);
}

// otherwise , the user liked the commend , then unliked it , and then wants to like it again
existingLike.IsDeleted = false;
comment.LikeCount++;
_unitOfWork.Comments.Update(comment);
await _unitOfWork.SaveAsync();
_logger.LogInformation("Like added successfully for comment id: {CommentId}", commentId);


// updating the createdAt column to Now , ignoring the first time the user liked the comment
existingLike.CreatedAt = DateTime.Now;
return await ServiceResult<CommentLikeDto>.Success(new CommentLikeDto
{
CommentId = existingLike.CommentId,
UserId = existingLike.UserId,
UserName = userName,
CreatedAt = existingLike.CreatedAt
});
}
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

Reactivation path doesn’t persist IsDeleted/CreatedAt; use UtcNow and update the like entity

When re-liking, you never call Update(existingLike), and you set CreatedAt after SaveAsync (not persisted). Also prefer UTC.

-    public async Task<ServiceResult<CommentLikeDto>> AddLikeAsync(int commentId, int userId)
+    public async Task<ServiceResult<CommentLikeDto>> AddLikeAsync(int commentId, int userId, CancellationToken ct = default)
     {
         try
             {
                 _logger.LogInformation("Adding like for comment id: {CommentId} by user id: {UserId}", 
                     commentId, userId);

-                var comment = await _unitOfWork.Comments.GetByIdAsync(commentId);
-                var user = await _unitOfWork.Users.GetByIdAsync(userId);
+                var comment = await _unitOfWork.Comments.GetByIdAsync(commentId, ct);
+                var user = await _unitOfWork.Users.GetByIdAsync(userId, ct);
                 if (user == null)
                 {
                     var errors = new List<String>()
                     {
                         $"User with id {userId} not found"
                     };
                     return await ServiceResult<CommentLikeDto>.Failure(errors);
                 }
                 
                 string userName = user.UserName;
                 if (comment == null)
                 {
                     var errors = new List<String>()
                     {
                         $"Comment with id {commentId} not found"
                     };
                     return await ServiceResult<CommentLikeDto>.Failure(errors);
                 }

                 var existingLike = await _unitOfWork.CommentLikes.FindAsync(
-                    cl => cl.CommentId == commentId && cl.UserId == userId
+                    cl => cl.CommentId == commentId && cl.UserId == userId,
+                    ct
                 );

                 // if the CommentLike already exit in the DB
                 if (existingLike != null)
                 {
                     // if the The use already liked this comment
                     if (!existingLike.IsDeleted)
                     {
                         var errors = new List<String>()
                         {
                             "User has already liked this comment"
                         };
                         return await ServiceResult<CommentLikeDto>.Failure(errors);
                     }

                     // otherwise , the user liked the commend , then unliked it , and then wants to like it again
-                    existingLike.IsDeleted = false;
+                    existingLike.IsDeleted = false;
+                    existingLike.CreatedAt = DateTime.UtcNow;
+                    _unitOfWork.CommentLikes.Update(existingLike);
                     comment.LikeCount++;
                     _unitOfWork.Comments.Update(comment);
-                    await _unitOfWork.SaveAsync();
+                    await _unitOfWork.SaveAsync(ct);
                     _logger.LogInformation("Like added successfully for comment id: {CommentId}", commentId);
-                    
-                    
-                    // updating the createdAt column to Now , ignoring the first time the user liked the comment
-                    existingLike.CreatedAt = DateTime.Now;
                     return await ServiceResult<CommentLikeDto>.Success(new CommentLikeDto
                     {
                         CommentId = existingLike.CommentId,
                         UserId = existingLike.UserId,
                         UserName = userName,
                         CreatedAt = existingLike.CreatedAt
                     });
                 }

Comment on lines +283 to +289
.HasColumnType("bit");

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" without length for Notification.Message

SQL Server requires NVARCHAR(n) or NVARCHAR(MAX). Current mapping risks truncation or invalid DDL.

-                    b.Property<string>("Message")
-                        .IsRequired()
-                        .HasColumnType("NVARCHAR");
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasColumnType("NVARCHAR(MAX)");

Fix in your entity configuration (preferred), then regenerate migrations:

// In Notification configuration
builder.Property(n => n.Message)
       .HasColumnType("nvarchar(max)")
       .IsRequired();
🤖 Prompt for AI Agents
In AskFm/AskFm.DAL/Migrations/20250825225059_AddCreatedAtDefault.Designer.cs
around lines 283-289 the Notification.Message column is configured with
HasColumnType("NVARCHAR") which is invalid without a length; update the
Notification entity configuration to specify a valid SQL Server type (for
example set HasColumnType("nvarchar(max)") or use .IsUnicode().HasMaxLength(...)
instead) and then regenerate the migration so the designer file gets the
corrected column type.

Comment on lines +41 to +48
migrationBuilder.AlterColumn<DateTime>(
name: "DeletedAt",
table: "Thread",
type: "DATETIME",
nullable: false,
defaultValueSql: "GETUTCDATE()",
oldClrType: typeof(DateTime),
oldType: "DATETIME");
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 default DeletedAt to GETUTCDATE() and make it nullable (soft-delete semantics)

Current migration sets DeletedAt as non-null with a UTC default, which marks every row as “deleted” at insert time. DeletedAt should be nullable with no default.

Apply the pattern below to all DeletedAt alterations in this migration:

-            migrationBuilder.AlterColumn<DateTime>(
+            migrationBuilder.AlterColumn<DateTime>(
                 name: "DeletedAt",
                 table: "Thread",
                 type: "DATETIME",
-                nullable: false,
-                defaultValueSql: "GETUTCDATE()",
+                nullable: true,
                 oldClrType: typeof(DateTime),
-                oldType: "DATETIME");
+                oldType: "DATETIME",
+                oldNullable: false,
+                oldDefaultValueSql: "GETUTCDATE()");

And for Comments/CommentLikes similarly:

-            migrationBuilder.AlterColumn<DateTime>(
+            migrationBuilder.AlterColumn<DateTime>(
                 name: "DeletedAt",
                 table: "Comments",
                 type: "DATETIME",
-                nullable: false,
-                defaultValueSql: "GETUTCDATE()",
+                nullable: true,
                 oldClrType: typeof(DateTime),
-                oldType: "DATETIME");
+                oldType: "DATETIME",
+                oldNullable: false,
+                oldDefaultValueSql: "GETUTCDATE()");

Also applies to: 113-121, 131-139, 149-157

🤖 Prompt for AI Agents
In AskFm/AskFm.DAL/Migrations/20250825230515_AddUpdatedAtAndDeletedAtDefault.cs
around lines 41 to 48 (and also for the other DeletedAt alterations at 113-121,
131-139, 149-157), the migration currently changes DeletedAt to non-nullable
with defaultValueSql "GETUTCDATE()", which effectively marks all new rows as
deleted; change the AlterColumn calls so DeletedAt is nullable (nullable: true),
remove any defaultValue or defaultValueSql for DeletedAt, and update
oldClrType/oldType/oldNullable fields accordingly so the migration represents
nullable soft-delete semantics; apply the same change pattern to the Comment and
CommentLike DeletedAt alterations in the listed line ranges.

Comment on lines +60 to +64
b.Property<DateTime>("DeletedAt")
.ValueGeneratedOnAdd()
.HasColumnType("DATETIME")
.HasDefaultValueSql("GETUTCDATE()");

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

Critical: DeletedAt defaulting to GETUTCDATE() marks new rows as deleted

Soft-delete semantics break if DeletedAt is auto-populated on insert. Remove the default and allow null until deletion.

-                    b.Property<DateTime>("DeletedAt")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("DATETIME")
-                        .HasDefaultValueSql("GETUTCDATE()");
+                    b.Property<DateTime?>("DeletedAt")
+                        .HasColumnType("datetime2");

Apply to all entities above. Change CLR type to nullable in your models and fluent config, then add a corrective migration to drop defaults and set nulls.

Also applies to: 166-170, 217-221, 252-256, 290-294, 341-345, 387-391, 436-440

🤖 Prompt for AI Agents
In
AskFm/AskFm.DAL/Migrations/20250825230515_AddUpdatedAtAndDeletedAtDefault.Designer.cs
around lines 60-64 (and similarly at 166-170, 217-221, 252-256, 290-294,
341-345, 387-391, 436-440), the DeletedAt property is incorrectly given a
GETUTCDATE() default causing new rows to be marked deleted; remove the
.HasDefaultValueSql("GETUTCDATE()") from the migration and from any model
builder configuration, change the CLR type for DeletedAt properties in your
entity models to DateTime? (nullable) and update the fluent configuration to
allow nulls (no default), then create a corrective migration that drops the
default constraint(s) on the DeletedAt columns and updates existing rows if
needed (set unintended non-null defaults to NULL), and apply the new migration
so the database and EF model are consistent.

Comment on lines +122 to +126
b.Property<DateTime>("UpdatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("DATETIME")
.HasDefaultValueSql("GETUTCDATE()");

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

UpdatedAt default only on insert; ensure it changes on update

ValueGeneratedOnAdd + default sets UpdatedAt once. Use an EF interceptor/SaveChanges override or a DB trigger to update on modification; alternatively configure ValueGeneratedOnAddOrUpdate and set the value in code.

Example SaveChanges override:

public override Task<int> SaveChangesAsync(CancellationToken ct = default)
{
    var now = DateTime.UtcNow;
    foreach (var e in ChangeTracker.Entries().Where(e => e.State is EntityState.Added or EntityState.Modified))
    {
        if (e.Properties.Any(p => p.Metadata.Name == "UpdatedAt"))
            e.CurrentValues["UpdatedAt"] = now;
        if (e.State == EntityState.Added && e.Properties.Any(p => p.Metadata.Name == "CreatedAt"))
            e.CurrentValues["CreatedAt"] = now;
    }
    return base.SaveChangesAsync(ct);
}

Also applies to: 185-189, 228-231, 265-269, 313-317, 406-410, 446-450

🤖 Prompt for AI Agents
In
AskFm/AskFm.DAL/Migrations/20250825230515_AddUpdatedAtAndDeletedAtDefault.Designer.cs
around lines 122-126 (and also update occurrences at 185-189, 228-231, 265-269,
313-317, 406-410, 446-450), the UpdatedAt property is configured with
ValueGeneratedOnAdd and a default GETUTCDATE(), which only sets the timestamp on
insert; change this so UpdatedAt is refreshed on every update by either (A)
replacing ValueGeneratedOnAdd with ValueGeneratedOnAddOrUpdate and ensure
application code sets UpdatedAt on SaveChanges/SaveChangesAsync before saving,
or (B) implement an EF SaveChanges override or interceptor that sets UpdatedAt =
DateTime.UtcNow for Added and Modified entries (and sets CreatedAt only for
Added), or (C) add a database trigger to update the column on UPDATE; pick one
approach and apply it consistently to all listed locations.

Comment on lines 57 to 61
b.Property<DateTime>("DeletedAt")
.HasColumnType("DATETIME");
.ValueGeneratedOnAdd()
.HasColumnType("DATETIME")
.HasDefaultValueSql("GETUTCDATE()");

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

Defaulting DeletedAt to GETUTCDATE() and treating UpdatedAt as ValueGeneratedOnAdd is incorrect.

  • DeletedAt should be nullable and default to NULL; setting a non-null default implies every row is “deleted” at insert time.
  • UpdatedAt needs to change on updates; GETUTCDATE() as a default only runs on insert. ValueGeneratedOnAdd prevents updates.

Action:

  • Make DeletedAt nullable with no default.
  • Keep CreatedAt default GETUTCDATE().
  • For UpdatedAt, either:
    • Update in SaveChanges/SaveChangesAsync, or
    • Use a computed column or trigger (if DB-driven updates are required).

Example model config (adjusted in entity configurations, then regenerate migrations):

// CreatedAt
b.Property(x => x.CreatedAt)
 .HasColumnType("datetime2")
 .HasDefaultValueSql("GETUTCDATE()")
 .ValueGeneratedOnAdd();

// UpdatedAt
b.Property(x => x.UpdatedAt)
 .HasColumnType("datetime2")
 .ValueGeneratedOnAddOrUpdate();

// DeletedAt
b.Property(x => x.DeletedAt)
 .HasColumnType("datetime2")
 .IsRequired(false); // nullable, no default

Example SaveChanges override:

public override Task<int> SaveChangesAsync(CancellationToken ct = default)
{
    var now = DateTime.UtcNow;
    foreach (var e in ChangeTracker.Entries().Where(e => e.State is EntityState.Added or EntityState.Modified))
    {
        if (e.Metadata.FindProperty("UpdatedAt") != null)
            e.CurrentValues["UpdatedAt"] = now;
    }
    return base.SaveChangesAsync(ct);
}

Follow up with new migrations to drop defaults on DeletedAt and fix UpdatedAt semantics.

Also applies to: 119-123, 163-167, 182-186, 214-228, 249-268, 289-306, 337-351, 383-406, 432-446

🤖 Prompt for AI Agents
AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs lines 57-61 (and
similarly at 119-123, 163-167, 182-186, 214-228, 249-268, 289-306, 337-351,
383-406, 432-446): DeletedAt is incorrectly non-nullable with a GETUTCDATE()
default and UpdatedAt is incorrectly configured as ValueGeneratedOnAdd; change
DeletedAt to be nullable with no default, keep CreatedAt as ValueGeneratedOnAdd
with GETUTCDATE(), and change UpdatedAt to be updated on each update (either
mark as ValueGeneratedOnAddOrUpdate if using a DB-side computed/trigger solution
or remove ValueGeneratedOnAdd and set UpdatedAt in SaveChanges/SaveChangesAsync
by assigning DateTime.UtcNow on Added/Modified entries). After adjusting the
entity configuration, regenerate migrations to remove the GETUTCDATE() default
from DeletedAt and correct UpdatedAt semantics across all affected model
snapshot locations.

@ziad-ashraf7 ziad-ashraf7 merged commit bd4ec9e into Askfm-clone:Develop Sep 3, 2025
2 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Sep 4, 2025
@coderabbitai coderabbitai Bot mentioned this pull request Oct 8, 2025
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.

4 participants