Skip to content

Conversation

@omgbeez
Copy link

@omgbeez omgbeez commented Jan 2, 2026

User description

Fixes an issue where adding more jobs to RDTClient causes the per-second UI update via websocket to get very very large as it sends far more data, and null fields, over the channel.

With about 100 download jobs, I was getting about 35MiB/sec of transfer, with this change that's now 200KiB/sec.

This change implements a DTO object to minimal represent data needed for the UI and transfer only that.


PR Type

Enhancement


Description

  • Replace Torrent model with TorrentDto to reduce websocket data transfer

  • Eliminate null fields and unnecessary data from API responses

  • Implement DTO mapping in RemoteService and TorrentsController endpoints

  • Update logging and database paths to relative paths in configuration


Diagram Walkthrough

flowchart LR
  Torrent["Torrent Model<br/>Full entity with all fields"]
  TorrentDto["TorrentDto<br/>Minimal DTO representation"]
  DownloadDto["DownloadDto<br/>Minimal download data"]
  WebSocket["WebSocket/API<br/>Optimized transfer"]
  
  Torrent -- "Map to DTO" --> TorrentDto
  Torrent -- "Map to DTO" --> DownloadDto
  TorrentDto -- "Send via" --> WebSocket
  DownloadDto -- "Include in" --> TorrentDto
Loading

File Walkthrough

Relevant files
Enhancement
RemoteService.cs
Implement TorrentDto mapping in websocket updates               

server/RdtClient.Service/Services/RemoteService.cs

  • Added import for RdtClient.Data.Models.Internal namespace
  • Replaced null assignment workaround with explicit TorrentDto and
    DownloadDto mapping
  • Maps all relevant torrent and download properties to DTOs before
    sending via websocket
  • Eliminates circular reference issues and reduces payload size
    significantly
+62/-6   
TorrentsController.cs
Convert controller endpoints to return TorrentDto objects

server/RdtClient.Web/Controllers/TorrentsController.cs

  • Updated GetAll() return type from IList to IList
  • Updated GetById() return type from Torrent to TorrentDto
  • Replaced null assignment workaround with explicit DTO mapping in both
    endpoints
  • Maps all torrent and download properties to DTOs for API responses
+125/-9 
Configuration changes
appsettings.json
Update configuration paths to relative locations                 

server/RdtClient.Web/appsettings.json

  • Changed logging file path from /data/db/rdtclient.log to
    ./rdtclient.log
  • Changed database path from /data/db/rdtclient.db to ./rdtclient.db
  • Converts absolute paths to relative paths for better portability
+2/-2     

@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Jan 2, 2026

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Sensitive data exposure

Description: The new DTO responses include potentially sensitive fields like DownloadDto.Path and
DownloadDto.Link, which may disclose local filesystem layout and/or private download
URLs/tokens to any caller of the endpoint (and similar data is also broadcast via
websocket), so access control and data-minimization should be verified.
TorrentsController.cs [61-80]

Referred Code
Downloads = torrent.Downloads.Select(download => new DownloadDto
{
    DownloadId = download.DownloadId,
    TorrentId = download.TorrentId,
    Path = download.Path,
    Link = download.Link,
    Added = download.Added,
    DownloadQueued = download.DownloadQueued,
    DownloadStarted = download.DownloadStarted,
    DownloadFinished = download.DownloadFinished,
    UnpackingQueued = download.UnpackingQueued,
    UnpackingStarted = download.UnpackingStarted,
    UnpackingFinished = download.UnpackingFinished,
    Completed = download.Completed,
    RetryCount = download.RetryCount,
    Error = download.Error,
    BytesTotal = download.BytesTotal,
    BytesDone = download.BytesDone,
    Speed = download.Speed
}).ToList()
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Null torrent not handled: GetById constructs a TorrentDto from torrent without handling the case where
torrents.GetById(torrentId) returns null, which can lead to a null reference instead of
returning a 404/meaningful error.

Referred Code
public async Task<ActionResult<TorrentDto>> GetById(Guid torrentId)
{
    var torrent = await torrents.GetById(torrentId);

    if (torrent?.Downloads != null)
    {
        foreach (var file in torrent.Downloads)
        {
            file.Torrent = null;
        }
    }

    var torrentDto = new TorrentDto
    {
        TorrentId = torrent.TorrentId,
        Hash = torrent.Hash,
        Category = torrent.Category,
        DownloadAction = torrent.DownloadAction,
        FinishedAction = torrent.FinishedAction,
        FinishedActionDelay = torrent.FinishedActionDelay,
        HostDownloadAction = torrent.HostDownloadAction,


 ... (clipped 54 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
Missing audit context: The new/updated endpoints and websocket update path do not add any audit logging (e.g.,
user identity, action, outcome), so it is not verifiable from the diff whether critical
access/events are properly captured.

Referred Code
public async Task<ActionResult<IList<TorrentDto>>> GetAll()
{
    var results = await torrents.Get();

    var torrentDtos = results.Select(torrent => new TorrentDto
    {
        TorrentId = torrent.TorrentId,
        Hash = torrent.Hash,
        Category = torrent.Category,
        DownloadAction = torrent.DownloadAction,
        FinishedAction = torrent.FinishedAction,
        FinishedActionDelay = torrent.FinishedActionDelay,
        HostDownloadAction = torrent.HostDownloadAction,
        DownloadMinSize = torrent.DownloadMinSize,
        IncludeRegex = torrent.IncludeRegex,
        ExcludeRegex = torrent.ExcludeRegex,
        DownloadManualFiles = torrent.DownloadManualFiles,
        DownloadClient = torrent.DownloadClient,
        Added = torrent.Added,
        FilesSelected = torrent.FilesSelected,
        Completed = torrent.Completed,


 ... (clipped 124 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status:
Potential exception exposure: Because GetById may throw a null reference when torrent is null, the resulting runtime
error behavior (and whether exception details are exposed to clients) cannot be confirmed
from the diff and requires verification of global exception handling/middleware
configuration.

Referred Code
public async Task<ActionResult<TorrentDto>> GetById(Guid torrentId)
{
    var torrent = await torrents.GetById(torrentId);

    if (torrent?.Downloads != null)
    {
        foreach (var file in torrent.Downloads)
        {
            file.Torrent = null;
        }
    }

    var torrentDto = new TorrentDto
    {
        TorrentId = torrent.TorrentId,
        Hash = torrent.Hash,
        Category = torrent.Category,
        DownloadAction = torrent.DownloadAction,
        FinishedAction = torrent.FinishedAction,
        FinishedActionDelay = torrent.FinishedActionDelay,
        HostDownloadAction = torrent.HostDownloadAction,


 ... (clipped 54 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Log format unclear: The change updates the log file path but the diff does not show whether logs are
structured (e.g., JSON) or whether sensitive fields (e.g., download Link/Path) might be
logged elsewhere, so compliance cannot be fully verified from the provided hunks.

Referred Code
{
  "Logging": {
    "File": {
      "Path": "./rdtclient.log",
      "FileSizeLimitBytes": 5242880,
      "MaxRollingFiles": 5
    }
  },
  "Database": {
    "Path": "./rdtclient.db"
  },
  "Port": "6500",
  "BasePath": null
}

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Authorization not shown: The updated endpoints return DTOs containing potentially sensitive fields (e.g., download
Link and Path) but the diff does not show any authentication/authorization attributes or
checks, so secure access control and data exposure constraints require human verification.

Referred Code
public async Task<ActionResult<IList<TorrentDto>>> GetAll()
{
    var results = await torrents.Get();

    var torrentDtos = results.Select(torrent => new TorrentDto
    {
        TorrentId = torrent.TorrentId,
        Hash = torrent.Hash,
        Category = torrent.Category,
        DownloadAction = torrent.DownloadAction,
        FinishedAction = torrent.FinishedAction,
        FinishedActionDelay = torrent.FinishedActionDelay,
        HostDownloadAction = torrent.HostDownloadAction,
        DownloadMinSize = torrent.DownloadMinSize,
        IncludeRegex = torrent.IncludeRegex,
        ExcludeRegex = torrent.ExcludeRegex,
        DownloadManualFiles = torrent.DownloadManualFiles,
        DownloadClient = torrent.DownloadClient,
        Added = torrent.Added,
        FilesSelected = torrent.FilesSelected,
        Completed = torrent.Completed,


 ... (clipped 124 lines)

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Jan 2, 2026

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Add null check to prevent crash
Suggestion Impact:The commit implemented the requested null check and returns NotFound() when the torrent is null, preventing a potential crash. However, it did not follow the suggestion to remove the loop that sets file.Torrent = null; instead it moved/kept that loop unconditionally after the null check.

code diff:

         var torrent = await torrents.GetById(torrentId);
 
-        if (torrent?.Downloads != null)
-        {
-            foreach (var file in torrent.Downloads)
-            {
-                file.Torrent = null;
-            }
+        if (torrent == null)
+        {
+            return NotFound();
+        }
+
+        foreach (var file in torrent.Downloads)
+        {
+            file.Torrent = null;
         }

In GetById, add a null check for the torrent object after it's fetched to
prevent a potential NullReferenceException. If it is null, return NotFound().
Also, remove the obsolete loop that sets file.Torrent = null.

server/RdtClient.Web/Controllers/TorrentsController.cs [88-162]

 public async Task<ActionResult<TorrentDto>> GetById(Guid torrentId)
 {
     var torrent = await torrents.GetById(torrentId);
 
-    if (torrent?.Downloads != null)
+    if (torrent == null)
     {
-        foreach (var file in torrent.Downloads)
-        {
-            file.Torrent = null;
-        }
+        return NotFound();
     }
 
     var torrentDto = new TorrentDto
     {
         TorrentId = torrent.TorrentId,
         Hash = torrent.Hash,
         Category = torrent.Category,
         DownloadAction = torrent.DownloadAction,
         FinishedAction = torrent.FinishedAction,
         FinishedActionDelay = torrent.FinishedActionDelay,
         HostDownloadAction = torrent.HostDownloadAction,
         DownloadMinSize = torrent.DownloadMinSize,
         IncludeRegex = torrent.IncludeRegex,
         ExcludeRegex = torrent.ExcludeRegex,
         DownloadManualFiles = torrent.DownloadManualFiles,
         DownloadClient = torrent.DownloadClient,
         Added = torrent.Added,
         FilesSelected = torrent.FilesSelected,
         Completed = torrent.Completed,
         Type = torrent.Type,
         IsFile = torrent.IsFile,
         Priority = torrent.Priority,
         RetryCount = torrent.RetryCount,
         DownloadRetryAttempts = torrent.DownloadRetryAttempts,
         TorrentRetryAttempts = torrent.TorrentRetryAttempts,
         DeleteOnError = torrent.DeleteOnError,
         Lifetime = torrent.Lifetime,
         Error = torrent.Error,
         RdId = torrent.RdId,
         RdName = torrent.RdName,
         RdSize = torrent.RdSize,
         RdHost = torrent.RdHost,
         RdSplit = torrent.RdSplit,
         RdProgress = torrent.RdProgress,
         RdStatus = torrent.RdStatus,
         RdStatusRaw = torrent.RdStatusRaw,
         RdAdded = torrent.RdAdded,
         RdEnded = torrent.RdEnded,
         RdSpeed = torrent.RdSpeed,
         RdSeeders = torrent.RdSeeders,
         Files = torrent.Files,
         Downloads = torrent.Downloads.Select(download => new DownloadDto
         {
             DownloadId = download.DownloadId,
             TorrentId = download.TorrentId,
             Path = download.Path,
             Link = download.Link,
             Added = download.Added,
             DownloadQueued = download.DownloadQueued,
             DownloadStarted = download.DownloadStarted,
             DownloadFinished = download.DownloadFinished,
             UnpackingQueued = download.UnpackingQueued,
             UnpackingStarted = download.UnpackingStarted,
             UnpackingFinished = download.UnpackingFinished,
             Completed = download.Completed,
             RetryCount = download.RetryCount,
             Error = download.Error,
             BytesTotal = download.BytesTotal,
             BytesDone = download.BytesDone,
             Speed = download.Speed
         }).ToList()
     };
 
     return Ok(torrentDto);
 }

[Suggestion processed]

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a potential NullReferenceException if torrent is null and provides a valid fix by adding a null check and returning NotFound(), which prevents a runtime crash.

Medium
General
Centralize DTO mapping to reduce duplication

Centralize the duplicated Torrent to TorrentDto mapping logic found in
TorrentsController.cs and RemoteService.cs. Create a ToDto() extension method to
encapsulate this logic, reduce code duplication, and improve maintainability.

server/RdtClient.Web/Controllers/TorrentsController.cs [22-81]

-var torrentDtos = results.Select(torrent => new TorrentDto
+// In a new file, e.g., Mappers/TorrentMapper.cs
+public static class TorrentMapper
 {
-    TorrentId = torrent.TorrentId,
-    Hash = torrent.Hash,
-    Category = torrent.Category,
-    DownloadAction = torrent.DownloadAction,
-    FinishedAction = torrent.FinishedAction,
-    FinishedActionDelay = torrent.FinishedActionDelay,
-    HostDownloadAction = torrent.HostDownloadAction,
-    DownloadMinSize = torrent.DownloadMinSize,
-    IncludeRegex = torrent.IncludeRegex,
-    ExcludeRegex = torrent.ExcludeRegex,
-    DownloadManualFiles = torrent.DownloadManualFiles,
-    DownloadClient = torrent.DownloadClient,
-    Added = torrent.Added,
-    FilesSelected = torrent.FilesSelected,
-    Completed = torrent.Completed,
-    Type = torrent.Type,
-    IsFile = torrent.IsFile,
-    Priority = torrent.Priority,
-    RetryCount = torrent.RetryCount,
-    DownloadRetryAttempts = torrent.DownloadRetryAttempts,
-    TorrentRetryAttempts = torrent.TorrentRetryAttempts,
-    DeleteOnError = torrent.DeleteOnError,
-    Lifetime = torrent.Lifetime,
-    Error = torrent.Error,
-    RdId = torrent.RdId,
-    RdName = torrent.RdName,
-    RdSize = torrent.RdSize,
-    RdHost = torrent.RdHost,
-    RdSplit = torrent.RdSplit,
-    RdProgress = torrent.RdProgress,
-    RdStatus = torrent.RdStatus,
-    RdStatusRaw = torrent.RdStatusRaw,
-    RdAdded = torrent.RdAdded,
-    RdEnded = torrent.RdEnded,
-    RdSpeed = torrent.RdSpeed,
-    RdSeeders = torrent.RdSeeders,
-    Files = torrent.Files,
-    Downloads = torrent.Downloads.Select(download => new DownloadDto
+    public static TorrentDto ToDto(this Torrent torrent)
     {
-        DownloadId = download.DownloadId,
-        TorrentId = download.TorrentId,
-        Path = download.Path,
-        Link = download.Link,
-        Added = download.Added,
-        DownloadQueued = download.DownloadQueued,
-        DownloadStarted = download.DownloadStarted,
-        DownloadFinished = download.DownloadFinished,
-        UnpackingQueued = download.UnpackingQueued,
-        UnpackingStarted = download.UnpackingStarted,
-        UnpackingFinished = download.UnpackingFinished,
-        Completed = download.Completed,
-        RetryCount = download.RetryCount,
-        Error = download.Error,
-        BytesTotal = download.BytesTotal,
-        BytesDone = download.BytesDone,
-        Speed = download.Speed
-    }).ToList()
-}).ToList();
+        return new TorrentDto
+        {
+            TorrentId = torrent.TorrentId,
+            // ... all other properties mapped
+            Downloads = torrent.Downloads.Select(download => download.ToDto()).ToList()
+        };
+    }
 
+    public static DownloadDto ToDto(this Download download)
+    {
+        return new DownloadDto
+        {
+            DownloadId = download.DownloadId,
+            // ... all other properties mapped
+        };
+    }
+
+    public static List<TorrentDto> ToDto(this IEnumerable<Torrent> torrents)
+    {
+        return torrents.Select(t => t.ToDto()).ToList();
+    }
+}
+
+// In TorrentsController.cs
+var torrentDtos = results.ToDto();
+
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies significant code duplication in DTO mapping across multiple files and proposes a valid refactoring using extension methods, which would greatly improve maintainability and adherence to the DRY principle.

Medium
  • Update

@omgbeez omgbeez force-pushed the upstream/websocket-perf branch from bd50741 to 7ca05bc Compare January 2, 2026 22:00
@omgbeez omgbeez force-pushed the upstream/websocket-perf branch from 6fbf80c to 9454ea1 Compare January 17, 2026 17:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant