Skip to content

feat: HTTP 缓存#2682

Open
LinQingYuu wants to merge 7 commits intodevfrom
feat/http-cache-support
Open

feat: HTTP 缓存#2682
LinQingYuu wants to merge 7 commits intodevfrom
feat/http-cache-support

Conversation

@LinQingYuu
Copy link
Copy Markdown
Member

@LinQingYuu LinQingYuu commented Apr 6, 2026

重做了缓存

时间仓促还没做测试,之后再补 orz

Sourcery 总结

引入一个基于 SQLite 的缓存仓库作为 HTTP 响应缓存层,并通过专用的带缓存功能的 HttpClient 将其接入网络服务。

新功能:

  • 新增基于 SQLite 的 HttpCacheRepository,使用惰性 GC 风格的元数据管理和基于文件的内容存储。
  • 引入 HttpCacheHandler,用于透明地提供缓存响应、执行条件请求,并根据 HTTP 头更新缓存条目。
  • 添加流式写入缓存的辅助组件(CacheStreamBlockingStreamHttpCacheUpdateHandleHttpCacheDetailsHttpCacheStatus),在正常请求处理过程中捕获并管理响应体。
  • NetworkService 中注册一个新的 "cache" HttpClient,它使用 HTTP 缓存处理程序和共享缓存仓库来处理带缓存的 HTTP 流量。
Original summary in English

Sourcery 总结

引入一个基于 SQLite 的 HTTP 响应缓存层,并通过专用的带缓存功能的 HttpClient 将其接入网络栈。

新功能:

  • 添加 HttpCacheRepository,使用 SQLite 存储 HTTP 缓存元数据、在磁盘上存储负载,并支持惰性失效和全局过期策略。
  • 引入 HttpCacheHandler 委托处理程序,用于提供缓存响应、执行条件请求,并根据 HTTP 头更新缓存条目。
  • 提供流式缓存写入辅助工具(CacheStreamBlockingStreamHttpCacheUpdateHandleHttpCacheDetailsHttpCacheStatus),以在正常请求处理过程中捕获并持久化响应体。
  • NetworkService 中注册一个新的 "cache" HttpClient,使其使用 HTTP 缓存处理程序和共享仓库来处理带缓存的 HTTP 流量。
Original summary in English

Summary by Sourcery

引入由 SQLite 支持的 HTTP 响应缓存层,并通过专用的缓存 HttpClient 将其接入网络栈。

新功能:

  • 添加基于 SQLite 的 HttpCacheRepository,用于在磁盘上持久化 HTTP 缓存元数据和响应负载。
  • 引入 HttpCacheHandler,用于提供缓存响应、执行条件请求,并根据 HTTP 语义更新缓存条目。
  • 提供流式辅助工具(CacheStreamBlockingStreamHttpCacheUpdateHandleHttpCacheDetailsHttpCacheStatus),在正常请求处理过程中捕获并写入响应体到缓存。
  • NetworkService 中注册新的 "cache" HttpClient,使用共享的缓存仓库和 HTTP 缓存处理程序来处理带缓存的流量。
Original summary in English

Summary by Sourcery

Introduce an HTTP response caching layer backed by SQLite and wire it into the networking stack via a dedicated cached HttpClient.

New Features:

  • Add a SQLite-backed HttpCacheRepository to persist HTTP cache metadata and response payloads on disk.
  • Introduce HttpCacheHandler to serve cached responses, perform conditional requests, and update cache entries based on HTTP semantics.
  • Provide streaming helpers (CacheStream, BlockingStream, HttpCacheUpdateHandle, HttpCacheDetails, HttpCacheStatus) to capture and write response bodies to the cache during normal request processing.
  • Register a new "cache" HttpClient in NetworkService that uses the shared cache repository and HTTP cache handler for cached traffic.

新功能:

  • 添加一个基于 SQLite 的 HttpCacheRepository,用于管理 HTTP 缓存元数据以及将响应负载存储到磁盘。
  • 添加一个 HttpCacheHandler 委托处理程序,用于提供缓存响应、执行条件请求,并根据 HTTP 头更新缓存条目。
  • 引入流式缓存写入辅助工具(CacheStreamBlockingStreamHttpCacheUpdateHandleHttpCacheDetailsHttpCacheStatus),以在正常请求处理过程中捕获并持久化响应主体。
  • NetworkService 中注册一个新的 "cache" HttpClient,它使用 HTTP 缓存处理程序和共享存储库来处理缓存的 HTTP 流量。
Original summary in English

Summary by Sourcery

引入由 SQLite 支持的 HTTP 响应缓存层,并通过专用的缓存 HttpClient 将其接入网络栈。

新功能:

  • 添加基于 SQLite 的 HttpCacheRepository,用于在磁盘上持久化 HTTP 缓存元数据和响应负载。
  • 引入 HttpCacheHandler,用于提供缓存响应、执行条件请求,并根据 HTTP 语义更新缓存条目。
  • 提供流式辅助工具(CacheStreamBlockingStreamHttpCacheUpdateHandleHttpCacheDetailsHttpCacheStatus),在正常请求处理过程中捕获并写入响应体到缓存。
  • NetworkService 中注册新的 "cache" HttpClient,使用共享的缓存仓库和 HTTP 缓存处理程序来处理带缓存的流量。
Original summary in English

Summary by Sourcery

Introduce an HTTP response caching layer backed by SQLite and wire it into the networking stack via a dedicated cached HttpClient.

New Features:

  • Add a SQLite-backed HttpCacheRepository to persist HTTP cache metadata and response payloads on disk.
  • Introduce HttpCacheHandler to serve cached responses, perform conditional requests, and update cache entries based on HTTP semantics.
  • Provide streaming helpers (CacheStream, BlockingStream, HttpCacheUpdateHandle, HttpCacheDetails, HttpCacheStatus) to capture and write response bodies to the cache during normal request processing.
  • Register a new "cache" HttpClient in NetworkService that uses the shared cache repository and HTTP cache handler for cached traffic.

@pcl-ce-automation pcl-ce-automation bot added 🛠️ 等待审查 Pull Request 已完善,等待维护者或负责人进行代码审查 size: XL PR 大小评估:超大型 labels Apr 6, 2026
@LinQingYuu LinQingYuu changed the title feat: HTTTP 缓存 feat: HTTP 缓存 Apr 6, 2026
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Apr 6, 2026

审阅者指南

引入一个基于 SQLite 的 HTTP 响应缓存,并通过一个新的带缓存的 HttpClient 将其接入 NetworkService。该 HttpClient 使用自定义的 HttpCacheHandler 和流式工具,将响应体存储到磁盘。

使用 HttpCacheHandler 的带缓存 HttpClient 请求流程时序图

sequenceDiagram
    actor App
    participant NetworkService
    participant HttpClientFactory
    participant HttpClient_cache
    participant HttpCacheHandler
    participant HttpCacheRepository
    participant SocketsHttpHandler
    participant HttpCacheUpdateHandle
    participant CacheStream
    participant BlockingStream

    App->>NetworkService: Get HttpClient("cache")
    NetworkService->>HttpClientFactory: CreateClient("cache")
    HttpClientFactory-->>NetworkService: HttpClient_cache
    NetworkService-->>App: HttpClient_cache

    App->>HttpClient_cache: SendAsync(HttpRequestMessage)
    HttpClient_cache->>HttpCacheHandler: SendAsync(request)

    HttpCacheHandler->>HttpCacheRepository: TryGetCacheData(request.RequestUri)
    alt cached and fresh and EnsureValidate is false
        HttpCacheRepository->>HttpCacheRepository: TryGetCacheResponse(request)
        HttpCacheRepository-->>HttpCacheHandler: HttpResponseMessage(cache hit)
        HttpCacheHandler-->>HttpClient_cache: HttpResponseMessage
        HttpClient_cache-->>App: HttpResponseMessage
    else not cached or expired or updating
        HttpCacheHandler->>HttpCacheRepository: TryGetCacheData(request.RequestUri)
        HttpCacheRepository-->>HttpCacheHandler: HttpCacheDetails or null
        opt conditional request
            HttpCacheHandler->>HttpRequestMessage: Set IfNoneMatch and IfModifiedSince
        end
        HttpCacheHandler->>SocketsHttpHandler: SendAsync(request)
        SocketsHttpHandler-->>HttpCacheHandler: HttpResponseMessage(response)
        alt response.StatusCode is 304 and cache exists
            HttpCacheHandler->>HttpCacheRepository: TryGetCacheResponse(request)
            HttpCacheRepository-->>HttpCacheHandler: HttpResponseMessage(cache hit)
            HttpCacheHandler-->>HttpClient_cache: HttpResponseMessage
            HttpClient_cache-->>App: HttpResponseMessage
        else response has body and CacheControl.NoStore is false
            HttpCacheHandler->>HttpCacheRepository: TryBeginUpdateAsync(request.RequestUri)
            HttpCacheRepository-->>HttpCacheUpdateHandle: HttpCacheUpdateHandle
            HttpCacheUpdateHandle-->>BlockingStream: GetOutputStream()
            HttpCacheHandler->>CacheStream: new CacheStream(handle, response.ContentStream)
            CacheStream->>BlockingStream: Write(bytes)
            CacheStream->>HttpCacheUpdateHandle: Dispose()
            HttpCacheUpdateHandle->>HttpCacheRepository: TryEndUpdateAsync(handle)
            HttpCacheRepository-->>HttpCacheUpdateHandle: update completed
            HttpCacheHandler-->>HttpClient_cache: HttpResponseMessage(with CacheStream)
            HttpClient_cache-->>App: HttpResponseMessage
        end
    end
Loading

新 HTTP 缓存组件的类图

classDiagram
    class HttpCacheRepository {
        -Func~SqliteConnection~ _connectionFactory
        -HashStorage _store
        +HttpCacheRepository(string dbPath, string destLocation)
        +void Initialize()
        +bool TryGetCacheData(string uri, out HttpCacheDetails details)
        +bool TryGetCacheResponse(HttpRequestMessage request, out HttpResponseMessage response)
        +ValueTask~HttpCacheUpdateHandle~ TryBeginUpdateAsync(string uri)
        +ValueTask~bool~ TryEndUpdateAsync(HttpCacheUpdateHandle handle)
        +bool TryRemove(HttpRequestMessage request)
        +ValueTask~bool~ TryRemoveAsync(HttpRequestMessage request)
        +void MarkAllObjectAsExpired()
        -SqliteCommand _InsertDatabase(HttpCacheDetails details, SqliteConnection conn)
        -SqliteCommand _DeleteTable(string uri, SqliteConnection conn)
        -SqliteCommand _FindTableWithUri(string uri, SqliteConnection conn)
        -SqliteCommand _UpdateTable(HttpCacheDetails details, SqliteConnection conn)
    }

    class HttpCacheHandler {
        -HttpCacheRepository _repository
        +HttpCacheHandler(HttpMessageHandler invoker, HttpCacheRepository repo)
        +HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)
        +Task~HttpResponseMessage~ SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    }

    class HttpCacheDetails {
        -HttpCacheRepository repo
        +DateTimeOffset LastUpdate
        +string RequestUri
        +string Tag
        +string LastModify
        +int ExpiredAt
        +bool EnsureValidate
        +string Hash
        +HttpCacheStatus Status
        +HttpCacheDetails(HttpCacheRepository repo)
        +HttpCacheUpdateHandle GetUpdateHandle()
    }

    class HttpCacheUpdateHandle {
        -HttpCacheRepository repo
        -BlockingStream _fileStream
        -bool _disposed
        +HttpCacheDetails Details
        +HttpCacheUpdateHandle(HttpCacheRepository repo)
        +BlockingStream GetOutputStream()
        +void Dispose()
    }

    class CacheStream {
        -Stream _responseStream
        -BlockingStream _destStream
        -HttpCacheUpdateHandle _handle
        +CacheStream(HttpCacheUpdateHandle handle, byte[] data)
        +CacheStream(HttpCacheUpdateHandle handle, Stream responseStream)
        +void Flush()
        +int Read(byte[] buffer, int offset, int count)
        +long Seek(long offset, SeekOrigin origin)
        +void SetLength(long value)
        +void Write(byte[] buffer, int offset, int count)
        +bool CanRead
        +bool CanSeek
        +bool CanWrite
        +long Length
        +long Position
        +void Dispose()
    }

    class BlockingStream {
        -SemaphoreSlim _lock
        +BlockingStream()
        +int Read(Span~byte~ buffer, CancellationToken token)
        +ValueTask~int~ ReadAsync(Memory~byte~ buffer, CancellationToken cancellationToken)
        +void Readable()
        +void Dispose()
    }

    class HttpCacheStatus {
        <<enumeration>>
        Invalid
        Ok
        Expired
        Updating
    }

    class NetworkService {
        +static HttpCacheRepository _repo
        +static ServiceProvider _provider
        +static IHttpClientFactory _factory
        +static void _Start()
    }

    HttpCacheHandler --* HttpCacheRepository : uses
    HttpCacheRepository o--> HttpCacheDetails : creates
    HttpCacheDetails o--> HttpCacheUpdateHandle : GetUpdateHandle
    HttpCacheUpdateHandle o--> BlockingStream : GetOutputStream
    CacheStream o--> HttpCacheUpdateHandle : wraps
    CacheStream o--> BlockingStream : writes
    HttpCacheDetails --> HttpCacheStatus : has
    BlockingStream --|> MemoryStream
    CacheStream --|> Stream
    HttpCacheHandler --|> DelegatingHandler
    NetworkService --> HttpCacheRepository : static field _repo
    NetworkService --> HttpCacheHandler : configures HttpClient cache
Loading

文件级变更

变更 详情 文件
添加一个基于 SQLite 的 HTTP 缓存仓库,在 Sqlite 数据库中存储元数据,并通过 HashStorage 存储响应体,并提供读取、更新和删除缓存条目的操作。
  • 定义 HttpCacheRepository,使用指向本地 SQLite 数据库的连接工厂以及基于 HashStorage 的负载存储
  • 实现初始化逻辑,在需要时创建缓存目录和 HttpCache 表
  • 提供方法以获取缓存元数据,并从存储的负载构建 HttpResponseMessage 对象
  • 通过 TryBeginUpdateAsync/TryEndUpdateAsync 实现异步更新生命周期,包含 Updating 状态和惰性失效机制
  • 添加删除 API(同步/异步)以及批量的 MarkAllObjectAsExpired 辅助方法
  • 实现插入、删除、查找以及条件更新缓存行的内部 SQL 帮助方法
PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs
引入 HTTP 缓存处理程序和流式原语,在将响应传递给调用方的同时捕获响应内容。
  • 添加 HttpCacheHandler 作为 DelegatingHandler,使用 HttpCacheRepository 进行缓存命中查询,驱动条件请求(ETag/Last-Modified),并用可更新缓存的流包装响应
  • 实现 CacheStream,将读取操作镜像到底层 BlockingStream 以进行持久化,同时向使用方暴露只读 Stream
  • 实现 BlockingStream,作为带信号量阻塞读取的 MemoryStream,以协调生产者/消费者访问
  • 添加 HttpCacheUpdateHandle,用于管理一次缓存更新会话,并确保在释放时调用 TryEndUpdateAsync
  • 定义 HttpCacheDetails 和 HttpCacheStatus,用于承载每个 URI 的缓存状态,并通过 HttpCacheRepository 驱动更新
PCL.Core/IO/Net/Http/Cache/HttpCacheHandler.cs
PCL.Core/IO/Net/Http/Cache/Models/CacheStream.cs
PCL.Core/IO/Net/Http/Cache/Models/BlockingStream.cs
PCL.Core/IO/Net/Http/Cache/Models/HttpCacheUpdateHandle.cs
PCL.Core/IO/Net/Http/Cache/Models/HttpCacheDetails.cs
PCL.Core/IO/Net/Http/Cache/Models/HttpCacheStatus.cs
通过新的带缓存 HttpClient 和共享仓库实例,将 HTTP 缓存接入 NetworkService。
  • 在临时路径下实例化一个静态 HttpCacheRepository,并在 NetworkService 启动期间对其进行初始化
  • 注册一个名为 "cache" 的新 HttpClient,由包装在 SocketsHttpHandler 外层的 HttpCacheHandler 驱动,并复用现有 HTTP 配置(代理、Cookie、解压、DoH 连接回调)
  • 在新增带缓存客户端的同时保留现有的 "default" HttpClient 配置
PCL.Core/IO/Net/NetworkService.cs

提示与命令

与 Sourcery 交互

  • 触发新的审查: 在 Pull Request 上评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的审查评论。
  • 从审查评论生成 GitHub issue: 在某条审查评论下回复,请求 Sourcery 从该评论创建一个 issue。你也可以回复 @sourcery-ai issue 来从该评论创建 issue。
  • 生成 Pull Request 标题: 在 Pull Request 标题的任意位置写上 @sourcery-ai 即可随时生成标题。你也可以在 Pull Request 中评论 @sourcery-ai title 来(重新)生成标题。
  • 生成 Pull Request 摘要: 在 Pull Request 正文任意位置写上 @sourcery-ai summary,即可在该位置生成 PR 摘要。你也可以在 Pull Request 中评论 @sourcery-ai summary 来(重新)生成摘要。
  • 生成审阅者指南: 在 Pull Request 上评论 @sourcery-ai guide,即可随时(重新)生成审阅者指南。
  • 批量解决所有 Sourcery 评论: 在 Pull Request 上评论 @sourcery-ai resolve,即可将所有 Sourcery 评论标记为已解决。如果你已经处理了所有评论且不希望再看到它们,这非常有用。
  • 批量撤销所有 Sourcery 审查: 在 Pull Request 上评论 @sourcery-ai dismiss,即可撤销所有已有的 Sourcery 审查。特别适用于你希望从一次全新的审查开始——别忘了随后再评论 @sourcery-ai review 来触发新审查!

自定义你的体验

访问你的 控制面板 以:

  • 启用或禁用诸如 Sourcery 生成的 Pull Request 摘要、审阅者指南等审查功能。
  • 更改审查语言。
  • 添加、移除或编辑自定义审查说明。
  • 调整其他审查设置。

获取帮助

Original review guide in English

Reviewer's Guide

Introduce a SQLite-backed HTTP response cache and wire it into NetworkService via a new cached HttpClient that uses a custom HttpCacheHandler and streaming helpers to store response bodies on disk.

Sequence diagram for cached HttpClient request flow with HttpCacheHandler

sequenceDiagram
    actor App
    participant NetworkService
    participant HttpClientFactory
    participant HttpClient_cache
    participant HttpCacheHandler
    participant HttpCacheRepository
    participant SocketsHttpHandler
    participant HttpCacheUpdateHandle
    participant CacheStream
    participant BlockingStream

    App->>NetworkService: Get HttpClient("cache")
    NetworkService->>HttpClientFactory: CreateClient("cache")
    HttpClientFactory-->>NetworkService: HttpClient_cache
    NetworkService-->>App: HttpClient_cache

    App->>HttpClient_cache: SendAsync(HttpRequestMessage)
    HttpClient_cache->>HttpCacheHandler: SendAsync(request)

    HttpCacheHandler->>HttpCacheRepository: TryGetCacheData(request.RequestUri)
    alt cached and fresh and EnsureValidate is false
        HttpCacheRepository->>HttpCacheRepository: TryGetCacheResponse(request)
        HttpCacheRepository-->>HttpCacheHandler: HttpResponseMessage(cache hit)
        HttpCacheHandler-->>HttpClient_cache: HttpResponseMessage
        HttpClient_cache-->>App: HttpResponseMessage
    else not cached or expired or updating
        HttpCacheHandler->>HttpCacheRepository: TryGetCacheData(request.RequestUri)
        HttpCacheRepository-->>HttpCacheHandler: HttpCacheDetails or null
        opt conditional request
            HttpCacheHandler->>HttpRequestMessage: Set IfNoneMatch and IfModifiedSince
        end
        HttpCacheHandler->>SocketsHttpHandler: SendAsync(request)
        SocketsHttpHandler-->>HttpCacheHandler: HttpResponseMessage(response)
        alt response.StatusCode is 304 and cache exists
            HttpCacheHandler->>HttpCacheRepository: TryGetCacheResponse(request)
            HttpCacheRepository-->>HttpCacheHandler: HttpResponseMessage(cache hit)
            HttpCacheHandler-->>HttpClient_cache: HttpResponseMessage
            HttpClient_cache-->>App: HttpResponseMessage
        else response has body and CacheControl.NoStore is false
            HttpCacheHandler->>HttpCacheRepository: TryBeginUpdateAsync(request.RequestUri)
            HttpCacheRepository-->>HttpCacheUpdateHandle: HttpCacheUpdateHandle
            HttpCacheUpdateHandle-->>BlockingStream: GetOutputStream()
            HttpCacheHandler->>CacheStream: new CacheStream(handle, response.ContentStream)
            CacheStream->>BlockingStream: Write(bytes)
            CacheStream->>HttpCacheUpdateHandle: Dispose()
            HttpCacheUpdateHandle->>HttpCacheRepository: TryEndUpdateAsync(handle)
            HttpCacheRepository-->>HttpCacheUpdateHandle: update completed
            HttpCacheHandler-->>HttpClient_cache: HttpResponseMessage(with CacheStream)
            HttpClient_cache-->>App: HttpResponseMessage
        end
    end
Loading

Class diagram for new HTTP cache components

classDiagram
    class HttpCacheRepository {
        -Func~SqliteConnection~ _connectionFactory
        -HashStorage _store
        +HttpCacheRepository(string dbPath, string destLocation)
        +void Initialize()
        +bool TryGetCacheData(string uri, out HttpCacheDetails details)
        +bool TryGetCacheResponse(HttpRequestMessage request, out HttpResponseMessage response)
        +ValueTask~HttpCacheUpdateHandle~ TryBeginUpdateAsync(string uri)
        +ValueTask~bool~ TryEndUpdateAsync(HttpCacheUpdateHandle handle)
        +bool TryRemove(HttpRequestMessage request)
        +ValueTask~bool~ TryRemoveAsync(HttpRequestMessage request)
        +void MarkAllObjectAsExpired()
        -SqliteCommand _InsertDatabase(HttpCacheDetails details, SqliteConnection conn)
        -SqliteCommand _DeleteTable(string uri, SqliteConnection conn)
        -SqliteCommand _FindTableWithUri(string uri, SqliteConnection conn)
        -SqliteCommand _UpdateTable(HttpCacheDetails details, SqliteConnection conn)
    }

    class HttpCacheHandler {
        -HttpCacheRepository _repository
        +HttpCacheHandler(HttpMessageHandler invoker, HttpCacheRepository repo)
        +HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)
        +Task~HttpResponseMessage~ SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    }

    class HttpCacheDetails {
        -HttpCacheRepository repo
        +DateTimeOffset LastUpdate
        +string RequestUri
        +string Tag
        +string LastModify
        +int ExpiredAt
        +bool EnsureValidate
        +string Hash
        +HttpCacheStatus Status
        +HttpCacheDetails(HttpCacheRepository repo)
        +HttpCacheUpdateHandle GetUpdateHandle()
    }

    class HttpCacheUpdateHandle {
        -HttpCacheRepository repo
        -BlockingStream _fileStream
        -bool _disposed
        +HttpCacheDetails Details
        +HttpCacheUpdateHandle(HttpCacheRepository repo)
        +BlockingStream GetOutputStream()
        +void Dispose()
    }

    class CacheStream {
        -Stream _responseStream
        -BlockingStream _destStream
        -HttpCacheUpdateHandle _handle
        +CacheStream(HttpCacheUpdateHandle handle, byte[] data)
        +CacheStream(HttpCacheUpdateHandle handle, Stream responseStream)
        +void Flush()
        +int Read(byte[] buffer, int offset, int count)
        +long Seek(long offset, SeekOrigin origin)
        +void SetLength(long value)
        +void Write(byte[] buffer, int offset, int count)
        +bool CanRead
        +bool CanSeek
        +bool CanWrite
        +long Length
        +long Position
        +void Dispose()
    }

    class BlockingStream {
        -SemaphoreSlim _lock
        +BlockingStream()
        +int Read(Span~byte~ buffer, CancellationToken token)
        +ValueTask~int~ ReadAsync(Memory~byte~ buffer, CancellationToken cancellationToken)
        +void Readable()
        +void Dispose()
    }

    class HttpCacheStatus {
        <<enumeration>>
        Invalid
        Ok
        Expired
        Updating
    }

    class NetworkService {
        +static HttpCacheRepository _repo
        +static ServiceProvider _provider
        +static IHttpClientFactory _factory
        +static void _Start()
    }

    HttpCacheHandler --* HttpCacheRepository : uses
    HttpCacheRepository o--> HttpCacheDetails : creates
    HttpCacheDetails o--> HttpCacheUpdateHandle : GetUpdateHandle
    HttpCacheUpdateHandle o--> BlockingStream : GetOutputStream
    CacheStream o--> HttpCacheUpdateHandle : wraps
    CacheStream o--> BlockingStream : writes
    HttpCacheDetails --> HttpCacheStatus : has
    BlockingStream --|> MemoryStream
    CacheStream --|> Stream
    HttpCacheHandler --|> DelegatingHandler
    NetworkService --> HttpCacheRepository : static field _repo
    NetworkService --> HttpCacheHandler : configures HttpClient cache
Loading

File-Level Changes

Change Details Files
Add a SQLite-backed HTTP cache repository that stores metadata in a Sqlite DB and response bodies via HashStorage, with operations to read, update, and delete cache entries.
  • Define HttpCacheRepository with a connection factory to a local SQLite DB and a HashStorage-based payload store
  • Implement initialization logic that creates the cache directory and HttpCache table if needed
  • Provide methods to fetch cache metadata and build HttpResponseMessage objects from stored payloads
  • Implement async update lifecycle via TryBeginUpdateAsync/TryEndUpdateAsync with an Updating status and lazy invalidation
  • Add deletion APIs (sync/async) and a bulk MarkAllObjectAsExpired helper
  • Implement internal SQL helpers for insert, delete, lookup, and conditional updates of cache rows
PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs
Introduce HTTP cache handler and streaming primitives that capture responses while passing them through to callers.
  • Add HttpCacheHandler as a DelegatingHandler that consults HttpCacheRepository for hits, drives conditional requests (ETag/Last-Modified), and wraps responses in cache-updating streams
  • Implement CacheStream that mirrors reads to an underlying BlockingStream for persistence while exposing a read-only Stream to consumers
  • Implement BlockingStream as a MemoryStream with semaphore-based blocking reads to coordinate producer/consumer access
  • Add HttpCacheUpdateHandle to manage a cache update session and ensure TryEndUpdateAsync is called on dispose
  • Define HttpCacheDetails and HttpCacheStatus to carry per-URI cache state and drive updates via HttpCacheRepository
PCL.Core/IO/Net/Http/Cache/HttpCacheHandler.cs
PCL.Core/IO/Net/Http/Cache/Models/CacheStream.cs
PCL.Core/IO/Net/Http/Cache/Models/BlockingStream.cs
PCL.Core/IO/Net/Http/Cache/Models/HttpCacheUpdateHandle.cs
PCL.Core/IO/Net/Http/Cache/Models/HttpCacheDetails.cs
PCL.Core/IO/Net/Http/Cache/Models/HttpCacheStatus.cs
Wire the HTTP cache into NetworkService via a new cached HttpClient and a shared repository instance.
  • Instantiate a static HttpCacheRepository under the temp path and initialize it during NetworkService startup
  • Register a new named HttpClient "cache" backed by HttpCacheHandler wrapped around SocketsHttpHandler, mirroring the existing HTTP configuration (proxy, cookies, decompression, DoH connect callback)
  • Retain existing "default" HttpClient configuration alongside the new cached client
PCL.Core/IO/Net/NetworkService.cs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

sourcery-ai[bot]

This comment was marked as outdated.

@LinQingYuu

This comment was marked as resolved.

@LinQingYuu LinQingYuu marked this pull request as draft April 6, 2026 06:37
@LinQingYuu LinQingYuu marked this pull request as draft April 6, 2026 06:37
@pcl-ce-automation pcl-ce-automation bot added 🚧 正在处理 开发人员正在对该内容进行开发、测试或修复,进展中 and removed 🛠️ 等待审查 Pull Request 已完善,等待维护者或负责人进行代码审查 labels Apr 6, 2026
sourcery-ai[bot]

This comment was marked as outdated.

@LinQingYuu
Copy link
Copy Markdown
Member Author

@sourcery-ai review

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - 我发现了 4 个安全问题、2 个其他问题,并且给出了一些整体反馈:

Security issues:

  • 在 SQL 语句中检测到使用格式化字符串。如果语句中的变量没有被正确清理,这可能导致 SQL 注入。请改用预编译语句。你可以通过 SqlCommandSqlParameter 获取 PreparedStatement。(link)
  • 在 SQL 语句中检测到使用格式化字符串。如果语句中的变量没有被正确清理,这可能导致 SQL 注入。请改用预编译语句。你可以通过 SqlCommandSqlParameter 获取 PreparedStatement。(link)
  • 在 SQL 语句中检测到使用格式化字符串。如果语句中的变量没有被正确清理,这可能导致 SQL 注入。请改用预编译语句。你可以通过 SqlCommandSqlParameter 获取 PreparedStatement。(link)
  • 在 SQL 语句中检测到使用格式化字符串。如果语句中的变量没有被正确清理,这可能导致 SQL 注入。请改用预编译语句。你可以通过 SqlCommandSqlParameter 获取 PreparedStatement。(link)

General comments:

  • HttpCacheRepository 中,_InsertDatabase_UpdateTable 等方法在 using 块中创建 SqliteConnection,然后返回一个绑定到已释放连接的 SqliteCommand;连接的生命周期应该长于命令的使用周期(例如返回一个“已打开的 connection + command”对,或者在调用方使用完命令之前不要释放连接)。
  • Initialize 中,destLocation 被当作目录使用,但在 IOException 处理逻辑中调用了 File.Delete(destLocation);当 destLocation 实际上是目录时,这会失败或造成误导,这里应改用 Directory.Delete 或者对已有目录内容进行安全清理。
  • HttpCacheHandler 重写了 Send,内部调用 SendAsync(...).GetAwaiter().GetResult(),这在某些同步上下文下可能导致死锁,并且会阻塞线程;建议要么不要重写 Send,要么让该 handler 完全异步,只提供 async 接口,让框架自行处理同步调用。
给 AI Agent 的提示
Please address the comments from this code review:

## Overall Comments
- In HttpCacheRepository, methods like _InsertDatabase and _UpdateTable create a SqliteConnection in a using block and then return a SqliteCommand that holds a disposed connection; the connection lifetime should outlive the command usage (e.g., return an open connection+command pair or avoid disposing the connection before the caller uses the command).
- In Initialize, destLocation is treated as a directory but in the IOException handler File.Delete(destLocation) is called; this will fail or be misleading when destLocation is a directory and should be handled with Directory.Delete or a safe cleanup of existing directory contents.
- HttpCacheHandler overrides Send to call SendAsync(...).GetAwaiter().GetResult(), which can deadlock on certain synchronization contexts and blocks threads; consider either not overriding Send or making the handler fully async-only and letting the framework handle sync calls.

## Individual Comments

### Comment 1
<location path="PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs" line_range="260-269" />
<code_context>
+    #region "SQL 执行函数"
+
+    
+    private SqliteCommand _InsertDatabase(HttpCacheDetails details)
+    {
+        using var conn = _connectionFactory.Invoke();
+        var cmd = conn.CreateCommand();
+        cmd.CommandText = InsertTable;
+        cmd.Parameters.AddWithValue("@Uri", details.RequestUri);
+        cmd.Parameters.AddWithValue("@Tag", details.Tag);
+        cmd.Parameters.AddWithValue("@LastModify", details.LastModify);
+        cmd.Parameters.AddWithValue("@ExpiredAt", details.ExpiredAt);
+        cmd.Parameters.AddWithValue("@EnsureValidate", details.EnsureValidate);
+        cmd.Parameters.AddWithValue("@Status", (int)details.Status);
+        cmd.Parameters.AddWithValue("@Hash", details.Hash);
+        return cmd;
+    }
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Returning a SqliteCommand built on a disposed connection will cause runtime failures.

In `_InsertDatabase`, `conn` is disposed at method exit due to `using var conn = _connectionFactory.Invoke();`, but the returned `SqliteCommand` still references this disposed connection, so any later `ExecuteNonQuery()` will throw `ObjectDisposedException`.

Align `_InsertDatabase` with `_DeleteTable` and `_FindTableWithUri`: don’t `using` the connection here; instead, keep it alive and dispose it when the command is disposed (e.g., by attaching a disposal handler to the command).
</issue_to_address>

### Comment 2
<location path="PCL.Core/IO/Net/Http/Cache/HttpCacheHandler.cs" line_range="50" />
<code_context>
+        newDetails?.RequestUri = request.RequestUri.ToString();
+        newDetails?.LastUpdate = DateTimeOffset.Now;
+        newDetails?.EnsureValidate = response.Headers.CacheControl?.NoCache ?? false;
+        newDetails?.LastModify = response.Content.Headers.LastModified.ToString();
+        newDetails?.Tag = response.Headers.ETag?.Tag;
+        if (handle is not null)
</code_context>
<issue_to_address>
**issue (bug_risk):** Using ToString() on LastModified without null checks can throw or produce culture-dependent values.

`LastModified` is a `DateTimeOffset?`; when it's null, `.ToString()` returns an empty string, which will later cause `DateTimeOffset.Parse` to throw.

Recommend:
- Preserve nulls instead of empty strings: `newDetails.LastModify = response.Content.Headers.LastModified?.ToString("O");`
- Use an invariant, round-trip format (e.g. `"O"`) so parsing is culture-independent.
</issue_to_address>

### Comment 3
<location path="PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs" line_range="89" />
<code_context>
        cmd.CommandText = CreateTable;
</code_context>
<issue_to_address>
**security (csharp.lang.security.sqli.csharp-sqli):** Detected a formatted string in a SQL statement. This could lead to SQL injection if variables in the SQL statement are not properly sanitized. Use a prepared statements instead. You can obtain a PreparedStatement using 'SqlCommand' and 'SqlParameter'.

*Source: opengrep*
</issue_to_address>

### Comment 4
<location path="PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs" line_range="264" />
<code_context>
        cmd.CommandText = InsertTable;
</code_context>
<issue_to_address>
**security (csharp.lang.security.sqli.csharp-sqli):** Detected a formatted string in a SQL statement. This could lead to SQL injection if variables in the SQL statement are not properly sanitized. Use a prepared statements instead. You can obtain a PreparedStatement using 'SqlCommand' and 'SqlParameter'.

*Source: opengrep*
</issue_to_address>

### Comment 5
<location path="PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs" line_range="279" />
<code_context>
        cmd.CommandText = DeleteTable;
</code_context>
<issue_to_address>
**security (csharp.lang.security.sqli.csharp-sqli):** Detected a formatted string in a SQL statement. This could lead to SQL injection if variables in the SQL statement are not properly sanitized. Use a prepared statements instead. You can obtain a PreparedStatement using 'SqlCommand' and 'SqlParameter'.

*Source: opengrep*
</issue_to_address>

### Comment 6
<location path="PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs" line_range="289" />
<code_context>
        queryCmd.CommandText = FindTable;
</code_context>
<issue_to_address>
**security (csharp.lang.security.sqli.csharp-sqli):** Detected a formatted string in a SQL statement. This could lead to SQL injection if variables in the SQL statement are not properly sanitized. Use a prepared statements instead. You can obtain a PreparedStatement using 'SqlCommand' and 'SqlParameter'.

*Source: opengrep*
</issue_to_address>

Sourcery 对开源项目免费使用——如果你觉得这些评审有帮助,欢迎分享 ✨
帮我变得更有用!请在每条评论上点击 👍 或 👎,我会根据你的反馈改进后续的评审。
Original comment in English

Hey - I've found 4 security issues, 2 other issues, and left some high level feedback:

Security issues:

  • Detected a formatted string in a SQL statement. This could lead to SQL injection if variables in the SQL statement are not properly sanitized. Use a prepared statements instead. You can obtain a PreparedStatement using 'SqlCommand' and 'SqlParameter'. (link)
  • Detected a formatted string in a SQL statement. This could lead to SQL injection if variables in the SQL statement are not properly sanitized. Use a prepared statements instead. You can obtain a PreparedStatement using 'SqlCommand' and 'SqlParameter'. (link)
  • Detected a formatted string in a SQL statement. This could lead to SQL injection if variables in the SQL statement are not properly sanitized. Use a prepared statements instead. You can obtain a PreparedStatement using 'SqlCommand' and 'SqlParameter'. (link)
  • Detected a formatted string in a SQL statement. This could lead to SQL injection if variables in the SQL statement are not properly sanitized. Use a prepared statements instead. You can obtain a PreparedStatement using 'SqlCommand' and 'SqlParameter'. (link)

General comments:

  • In HttpCacheRepository, methods like _InsertDatabase and _UpdateTable create a SqliteConnection in a using block and then return a SqliteCommand that holds a disposed connection; the connection lifetime should outlive the command usage (e.g., return an open connection+command pair or avoid disposing the connection before the caller uses the command).
  • In Initialize, destLocation is treated as a directory but in the IOException handler File.Delete(destLocation) is called; this will fail or be misleading when destLocation is a directory and should be handled with Directory.Delete or a safe cleanup of existing directory contents.
  • HttpCacheHandler overrides Send to call SendAsync(...).GetAwaiter().GetResult(), which can deadlock on certain synchronization contexts and blocks threads; consider either not overriding Send or making the handler fully async-only and letting the framework handle sync calls.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In HttpCacheRepository, methods like _InsertDatabase and _UpdateTable create a SqliteConnection in a using block and then return a SqliteCommand that holds a disposed connection; the connection lifetime should outlive the command usage (e.g., return an open connection+command pair or avoid disposing the connection before the caller uses the command).
- In Initialize, destLocation is treated as a directory but in the IOException handler File.Delete(destLocation) is called; this will fail or be misleading when destLocation is a directory and should be handled with Directory.Delete or a safe cleanup of existing directory contents.
- HttpCacheHandler overrides Send to call SendAsync(...).GetAwaiter().GetResult(), which can deadlock on certain synchronization contexts and blocks threads; consider either not overriding Send or making the handler fully async-only and letting the framework handle sync calls.

## Individual Comments

### Comment 1
<location path="PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs" line_range="260-269" />
<code_context>
+    #region "SQL 执行函数"
+
+    
+    private SqliteCommand _InsertDatabase(HttpCacheDetails details)
+    {
+        using var conn = _connectionFactory.Invoke();
+        var cmd = conn.CreateCommand();
+        cmd.CommandText = InsertTable;
+        cmd.Parameters.AddWithValue("@Uri", details.RequestUri);
+        cmd.Parameters.AddWithValue("@Tag", details.Tag);
+        cmd.Parameters.AddWithValue("@LastModify", details.LastModify);
+        cmd.Parameters.AddWithValue("@ExpiredAt", details.ExpiredAt);
+        cmd.Parameters.AddWithValue("@EnsureValidate", details.EnsureValidate);
+        cmd.Parameters.AddWithValue("@Status", (int)details.Status);
+        cmd.Parameters.AddWithValue("@Hash", details.Hash);
+        return cmd;
+    }
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Returning a SqliteCommand built on a disposed connection will cause runtime failures.

In `_InsertDatabase`, `conn` is disposed at method exit due to `using var conn = _connectionFactory.Invoke();`, but the returned `SqliteCommand` still references this disposed connection, so any later `ExecuteNonQuery()` will throw `ObjectDisposedException`.

Align `_InsertDatabase` with `_DeleteTable` and `_FindTableWithUri`: don’t `using` the connection here; instead, keep it alive and dispose it when the command is disposed (e.g., by attaching a disposal handler to the command).
</issue_to_address>

### Comment 2
<location path="PCL.Core/IO/Net/Http/Cache/HttpCacheHandler.cs" line_range="50" />
<code_context>
+        newDetails?.RequestUri = request.RequestUri.ToString();
+        newDetails?.LastUpdate = DateTimeOffset.Now;
+        newDetails?.EnsureValidate = response.Headers.CacheControl?.NoCache ?? false;
+        newDetails?.LastModify = response.Content.Headers.LastModified.ToString();
+        newDetails?.Tag = response.Headers.ETag?.Tag;
+        if (handle is not null)
</code_context>
<issue_to_address>
**issue (bug_risk):** Using ToString() on LastModified without null checks can throw or produce culture-dependent values.

`LastModified` is a `DateTimeOffset?`; when it's null, `.ToString()` returns an empty string, which will later cause `DateTimeOffset.Parse` to throw.

Recommend:
- Preserve nulls instead of empty strings: `newDetails.LastModify = response.Content.Headers.LastModified?.ToString("O");`
- Use an invariant, round-trip format (e.g. `"O"`) so parsing is culture-independent.
</issue_to_address>

### Comment 3
<location path="PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs" line_range="89" />
<code_context>
        cmd.CommandText = CreateTable;
</code_context>
<issue_to_address>
**security (csharp.lang.security.sqli.csharp-sqli):** Detected a formatted string in a SQL statement. This could lead to SQL injection if variables in the SQL statement are not properly sanitized. Use a prepared statements instead. You can obtain a PreparedStatement using 'SqlCommand' and 'SqlParameter'.

*Source: opengrep*
</issue_to_address>

### Comment 4
<location path="PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs" line_range="264" />
<code_context>
        cmd.CommandText = InsertTable;
</code_context>
<issue_to_address>
**security (csharp.lang.security.sqli.csharp-sqli):** Detected a formatted string in a SQL statement. This could lead to SQL injection if variables in the SQL statement are not properly sanitized. Use a prepared statements instead. You can obtain a PreparedStatement using 'SqlCommand' and 'SqlParameter'.

*Source: opengrep*
</issue_to_address>

### Comment 5
<location path="PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs" line_range="279" />
<code_context>
        cmd.CommandText = DeleteTable;
</code_context>
<issue_to_address>
**security (csharp.lang.security.sqli.csharp-sqli):** Detected a formatted string in a SQL statement. This could lead to SQL injection if variables in the SQL statement are not properly sanitized. Use a prepared statements instead. You can obtain a PreparedStatement using 'SqlCommand' and 'SqlParameter'.

*Source: opengrep*
</issue_to_address>

### Comment 6
<location path="PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs" line_range="289" />
<code_context>
        queryCmd.CommandText = FindTable;
</code_context>
<issue_to_address>
**security (csharp.lang.security.sqli.csharp-sqli):** Detected a formatted string in a SQL statement. This could lead to SQL injection if variables in the SQL statement are not properly sanitized. Use a prepared statements instead. You can obtain a PreparedStatement using 'SqlCommand' and 'SqlParameter'.

*Source: opengrep*
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs Outdated
newDetails?.RequestUri = request.RequestUri.ToString();
newDetails?.LastUpdate = DateTimeOffset.Now;
newDetails?.EnsureValidate = response.Headers.CacheControl?.NoCache ?? false;
newDetails?.LastModify = response.Content.Headers.LastModified.ToString();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk):LastModified 调用 ToString() 而不进行空检查,可能抛出异常或生成与区域设置相关的值。

LastModified 是一个 DateTimeOffset?;当它为 null 时,.ToString() 会返回空字符串,这在之后调用 DateTimeOffset.Parse 时会导致异常。

建议:

  • 保留 null 而不是空字符串:newDetails.LastModify = response.Content.Headers.LastModified?.ToString("O");
  • 使用不依赖区域设置的往返格式(例如 "O"),以保证解析与区域设置无关。
Original comment in English

issue (bug_risk): Using ToString() on LastModified without null checks can throw or produce culture-dependent values.

LastModified is a DateTimeOffset?; when it's null, .ToString() returns an empty string, which will later cause DateTimeOffset.Parse to throw.

Recommend:

  • Preserve nulls instead of empty strings: newDetails.LastModify = response.Content.Headers.LastModified?.ToString("O");
  • Use an invariant, round-trip format (e.g. "O") so parsing is culture-independent.

Comment thread PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs
Comment thread PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs
Comment thread PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs
Comment thread PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs
@LinQingYuu
Copy link
Copy Markdown
Member Author

LinQingYuu commented Apr 12, 2026

@sourcery-ai review

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Apr 12, 2026

Sure! I'm generating a new review now.

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - 我发现了 4 个安全问题、3 个其他问题,并提供了一些高层次反馈:

安全问题

  • 在 SQL 语句中检测到使用格式化字符串。如果其中的变量没有被正确清理,这可能导致 SQL 注入。请改用预编译语句。你可以通过 SqlCommandSqlParameter 来获取 PreparedStatement。(link)
  • 在 SQL 语句中检测到使用格式化字符串。如果其中的变量没有被正确清理,这可能导致 SQL 注入。请改用预编译语句。你可以通过 SqlCommandSqlParameter 来获取 PreparedStatement。(link)
  • 在 SQL 语句中检测到使用格式化字符串。如果其中的变量没有被正确清理,这可能导致 SQL 注入。请改用预编译语句。你可以通过 SqlCommandSqlParameter 来获取 PreparedStatement。(link)
  • 在 SQL 语句中检测到使用格式化字符串。如果其中的变量没有被正确清理,这可能导致 SQL 注入。请改用预编译语句。你可以通过 SqlCommandSqlParameter 来获取 PreparedStatement。(link)

总体评论

  • HttpCacheRepository 中的 SQLite 模式与访问代码不一致:InsertTable 期望 8 列,但 _InsertDatabase 只绑定了 7 个参数(缺少 LastUpdate,并与 ExpiredAt/Status 的顺序不一致),而 TryGetCacheData 使用 result.GetInt16(6) 作为 Status,但第 6 列实际是 LastUpdate,这会导致运行时错误以及错误的缓存状态处理。
  • HttpCacheRepository._UpdateTable 通过为每个变更列重复前置 "SET" 来构造 UPDATE 语句(例如:UPDATE HttpCache SET RequestUri=..., SET Tag=...),并且直接对可能为 null 的列调用 GetString/GetInt32,这可能生成无效的 SQL 并在遇到 NULL 值时抛异常;建议构建一个单一的 SET 子句,使用逗号分隔的赋值表达式,并安全地处理 DBNull
  • HttpCacheHandler.SendAsync 中的缓存校验逻辑似乎被反转了:在 ExpiredAt 的检查中,当 LastUpdate + ExpiredAt 早于当前时间(即条目已过期)时依然返回缓存响应,而通常只有在缓存仍然是“新鲜”的情况下才应返回缓存,否则应重新验证或获取新的响应。
给 AI 代理的提示
Please address the comments from this code review:

## Overall Comments
- The SQLite schema and access code in HttpCacheRepository are inconsistent: InsertTable expects 8 columns but _InsertDatabase only binds 7 parameters (missing LastUpdate and misaligned with ExpiredAt/Status), and TryGetCacheData uses result.GetInt16(6) as Status even though column 6 is LastUpdate, which will cause runtime errors and incorrect cache status handling.
- HttpCacheRepository._UpdateTable builds an UPDATE statement by prepending "SET" for each changed column (e.g., "UPDATE HttpCache SET RequestUri=..., SET Tag=..."), and directly calling GetString/GetInt32 on potentially nullable columns, which can generate invalid SQL and throw on NULL values; consider building a single SET clause with comma-separated assignments and handling DBNull safely.
- The cache validation logic in HttpCacheHandler.SendAsync appears inverted: in the ExpiredAt check, it serves the cached response when LastUpdate + ExpiredAt is in the past (i.e., when the entry is expired), whereas typically you want to serve cache only when it is still fresh and otherwise revalidate or fetch a new response.

## Individual Comments

### Comment 1
<location path="PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs" line_range="77-83" />
<code_context>
+    /// </summary>
+    public void Initialize()
+    {
+        try
+        {
+            if (!Directory.Exists(destLocation)) Directory.CreateDirectory(destLocation);
+        }
+        catch (IOException)
+        {
+            File.Delete(destLocation);
+            Directory.CreateDirectory(destLocation);
+        }
</code_context>
<issue_to_address>
**issue (bug_risk):** 在目录路径上使用 File.Delete 是很容易出错的

在 catch 块中,`destLocation` 被当作文件路径并传给 `File.Delete`,但在其他地方它被视为目录路径。对目录调用 `File.Delete` 本身会抛出异常(例如 `UnauthorizedAccessException`),而这些异常没有被处理。建议分别使用 `File.Exists(destLocation)``Directory.Exists(destLocation)` 进行检查,并根据实际类型执行删除/创建操作,这样就只会对文件执行删除、对目录执行创建。
</issue_to_address>

### Comment 2
<location path="PCL.Core/IO/Net/Http/Cache/Models/CacheStream.cs" line_range="61-65" />
<code_context>
+        _lock.Release();
+    }
+
+    protected override void Dispose(bool disposing)
+    {
+        _lock.Dispose();
+        base.Dispose(disposing);
+    }
+}
</code_context>
<issue_to_address>
**suggestion (bug_risk):** CacheStream 没有释放被包装的响应流

`Dispose(bool)` 只释放了 `_lock`(以及基类),但没有释放被包装的响应流。如果该流拥有网络/文件资源(例如 `HttpContent`),这些资源可能永远不会被释放。可以在此处同时释放 `_responseStream`,或者明确约定其所有权,并避免包装你不拥有的流。

建议实现如下:

```csharp
    public override void Flush() { }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            _responseStream?.Dispose();
            _lock?.Dispose();
        }

        base.Dispose(disposing);
    }

```

这假定 `CacheStream` 完全拥有 `_responseStream`,并对其生命周期负责。如果并非所有调用点都如此,你可能需要:
1. 添加一个构造函数参数(例如 `bool leaveResponseStreamOpen = false`),并将其存储到字段中;
2. 仅在明确表明拥有所有权时才有条件地释放响应流:`if (disposing && !_leaveResponseStreamOpen) _responseStream?.Dispose();`3. 相应地更新所有 `CacheStream` 的实例化代码,以显式标明所有权。
</issue_to_address>

### Comment 3
<location path="PCL.Core/IO/Net/Http/Cache/HttpCacheHandler.cs" line_range="24-26" />
<code_context>
+        _repository = repo;
+    }
+
+    protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)
+    {
+        return SendAsync(request, cancellationToken).GetAwaiter().GetResult();
+    }
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** 在 Send 中同步阻塞 SendAsync 会带来死锁和线程池耗尽的风险

在 `Send` 中使用 `SendAsync(...).GetAwaiter().GetResult()`,在存在同步上下文时可能导致死锁,并在高负载下加剧线程池耗尽问题。更好的做法是将同步路径委托给 `InnerHandler.Send`(如果可用),或者移除该同步重写,仅依赖异步管线。

建议实现如下:

```csharp

```

通过移除这个同步重写,`HttpCacheHandler` 将依赖基类 `DelegatingHandler`/`HttpMessageHandler` 对同步调用的默认实现,从而避免显式调用 `SendAsync(...).GetAwaiter().GetResult()` 带来的死锁/线程池耗尽风险。除非有其它代码直接依赖此重写的特定行为,否则不需要进一步修改。
</issue_to_address>

### Comment 4
<location path="PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs" line_range="89" />
<code_context>
        cmd.CommandText = CreateTable;
</code_context>
<issue_to_address>
**security (csharp.lang.security.sqli.csharp-sqli):** 在 SQL 语句中检测到使用格式化字符串。如果其中的变量没有被正确清理,这可能导致 SQL 注入。请改用预编译语句。你可以通过 `SqlCommand``SqlParameter` 来获取 `PreparedStatement`*Source: opengrep*
</issue_to_address>

### Comment 5
<location path="PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs" line_range="268" />
<code_context>
        cmd.CommandText = InsertTable;
</code_context>
<issue_to_address>
**security (csharp.lang.security.sqli.csharp-sqli):** 在 SQL 语句中检测到使用格式化字符串。如果其中的变量没有被正确清理,这可能导致 SQL 注入。请改用预编译语句。你可以通过 `SqlCommand``SqlParameter` 来获取 `PreparedStatement`*Source: opengrep*
</issue_to_address>

### Comment 6
<location path="PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs" line_range="282" />
<code_context>
        cmd.CommandText = DeleteTable;
</code_context>
<issue_to_address>
**security (csharp.lang.security.sqli.csharp-sqli):** 在 SQL 语句中检测到使用格式化字符串。如果其中的变量没有被正确清理,这可能导致 SQL 注入。请改用预编译语句。你可以通过 `SqlCommand``SqlParameter` 来获取 `PreparedStatement`*Source: opengrep*
</issue_to_address>

### Comment 7
<location path="PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs" line_range="290" />
<code_context>
        queryCmd.CommandText = FindTable;
</code_context>
<issue_to_address>
**security (csharp.lang.security.sqli.csharp-sqli):** 在 SQL 语句中检测到使用格式化字符串。如果其中的变量没有被正确清理,这可能导致 SQL 注入。请改用预编译语句。你可以通过 `SqlCommand``SqlParameter` 来获取 `PreparedStatement`*Source: opengrep*
</issue_to_address>

Sourcery 对开源项目是免费的 —— 如果你喜欢我们的评审,请考虑分享 ✨
帮我变得更有用!请在每条评论上点选 👍 或 👎,我会根据反馈改进后续的评审质量。
Original comment in English

Hey - I've found 4 security issues, 3 other issues, and left some high level feedback:

Security issues:

  • Detected a formatted string in a SQL statement. This could lead to SQL injection if variables in the SQL statement are not properly sanitized. Use a prepared statements instead. You can obtain a PreparedStatement using 'SqlCommand' and 'SqlParameter'. (link)
  • Detected a formatted string in a SQL statement. This could lead to SQL injection if variables in the SQL statement are not properly sanitized. Use a prepared statements instead. You can obtain a PreparedStatement using 'SqlCommand' and 'SqlParameter'. (link)
  • Detected a formatted string in a SQL statement. This could lead to SQL injection if variables in the SQL statement are not properly sanitized. Use a prepared statements instead. You can obtain a PreparedStatement using 'SqlCommand' and 'SqlParameter'. (link)
  • Detected a formatted string in a SQL statement. This could lead to SQL injection if variables in the SQL statement are not properly sanitized. Use a prepared statements instead. You can obtain a PreparedStatement using 'SqlCommand' and 'SqlParameter'. (link)

General comments:

  • The SQLite schema and access code in HttpCacheRepository are inconsistent: InsertTable expects 8 columns but _InsertDatabase only binds 7 parameters (missing LastUpdate and misaligned with ExpiredAt/Status), and TryGetCacheData uses result.GetInt16(6) as Status even though column 6 is LastUpdate, which will cause runtime errors and incorrect cache status handling.
  • HttpCacheRepository._UpdateTable builds an UPDATE statement by prepending "SET" for each changed column (e.g., "UPDATE HttpCache SET RequestUri=..., SET Tag=..."), and directly calling GetString/GetInt32 on potentially nullable columns, which can generate invalid SQL and throw on NULL values; consider building a single SET clause with comma-separated assignments and handling DBNull safely.
  • The cache validation logic in HttpCacheHandler.SendAsync appears inverted: in the ExpiredAt check, it serves the cached response when LastUpdate + ExpiredAt is in the past (i.e., when the entry is expired), whereas typically you want to serve cache only when it is still fresh and otherwise revalidate or fetch a new response.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The SQLite schema and access code in HttpCacheRepository are inconsistent: InsertTable expects 8 columns but _InsertDatabase only binds 7 parameters (missing LastUpdate and misaligned with ExpiredAt/Status), and TryGetCacheData uses result.GetInt16(6) as Status even though column 6 is LastUpdate, which will cause runtime errors and incorrect cache status handling.
- HttpCacheRepository._UpdateTable builds an UPDATE statement by prepending "SET" for each changed column (e.g., "UPDATE HttpCache SET RequestUri=..., SET Tag=..."), and directly calling GetString/GetInt32 on potentially nullable columns, which can generate invalid SQL and throw on NULL values; consider building a single SET clause with comma-separated assignments and handling DBNull safely.
- The cache validation logic in HttpCacheHandler.SendAsync appears inverted: in the ExpiredAt check, it serves the cached response when LastUpdate + ExpiredAt is in the past (i.e., when the entry is expired), whereas typically you want to serve cache only when it is still fresh and otherwise revalidate or fetch a new response.

## Individual Comments

### Comment 1
<location path="PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs" line_range="77-83" />
<code_context>
+    /// </summary>
+    public void Initialize()
+    {
+        try
+        {
+            if (!Directory.Exists(destLocation)) Directory.CreateDirectory(destLocation);
+        }
+        catch (IOException)
+        {
+            File.Delete(destLocation);
+            Directory.CreateDirectory(destLocation);
+        }
</code_context>
<issue_to_address>
**issue (bug_risk):** Using File.Delete on a directory path is error-prone

In the catch block, `destLocation` is assumed to be a file and passed to `File.Delete`, but elsewhere it’s treated as a directory. Calling `File.Delete` on a directory will itself throw (e.g., `UnauthorizedAccessException`), which isn’t handled. Consider checking `File.Exists(destLocation)` vs `Directory.Exists(destLocation)` and deleting/creating accordingly, so you only delete files and only create directories at this path.
</issue_to_address>

### Comment 2
<location path="PCL.Core/IO/Net/Http/Cache/Models/CacheStream.cs" line_range="61-65" />
<code_context>
+        _lock.Release();
+    }
+
+    protected override void Dispose(bool disposing)
+    {
+        _lock.Dispose();
+        base.Dispose(disposing);
+    }
+}
</code_context>
<issue_to_address>
**suggestion (bug_risk):** CacheStream does not dispose the wrapped response stream

`Dispose(bool)` only disposes `_lock` (and base), but not the wrapped response stream. If that stream owns network/file resources (e.g., `HttpContent`), they may never be released. Either dispose `_responseStream` here as well, or make its ownership contract explicit and avoid wrapping streams you don’t own.

Suggested implementation:

```csharp
    public override void Flush() { }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            _responseStream?.Dispose();
            _lock?.Dispose();
        }

        base.Dispose(disposing);
    }

```

This assumes that `CacheStream` fully owns `_responseStream` and is responsible for its lifetime. If that’s not true for all call sites, you may need to:
1. Add a constructor parameter (e.g., `bool leaveResponseStreamOpen = false`) and store it in a field.
2. Conditionally dispose the response stream only when ownership is indicated: `if (disposing && !_leaveResponseStreamOpen) _responseStream?.Dispose();`.
3. Update all `CacheStream` instantiations accordingly to explicitly signal ownership.
</issue_to_address>

### Comment 3
<location path="PCL.Core/IO/Net/Http/Cache/HttpCacheHandler.cs" line_range="24-26" />
<code_context>
+        _repository = repo;
+    }
+
+    protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)
+    {
+        return SendAsync(request, cancellationToken).GetAwaiter().GetResult();
+    }
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Blocking on SendAsync in Send risks deadlocks and thread-pool starvation

Using `SendAsync(...).GetAwaiter().GetResult()` in `Send` can deadlock under a synchronization context and contribute to thread-pool starvation under load. Prefer delegating the sync path to `InnerHandler.Send` (if available) or removing the sync override and relying on the async pipeline instead.

Suggested implementation:

```csharp

```

By removing this synchronous override, `HttpCacheHandler` will rely on the base `DelegatingHandler`/`HttpMessageHandler` implementation for synchronous calls, avoiding the explicit `SendAsync(...).GetAwaiter().GetResult()` and its deadlock/thread-pool starvation risks. No further changes are required unless other code depends directly on this override’s specific behavior.
</issue_to_address>

### Comment 4
<location path="PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs" line_range="89" />
<code_context>
        cmd.CommandText = CreateTable;
</code_context>
<issue_to_address>
**security (csharp.lang.security.sqli.csharp-sqli):** Detected a formatted string in a SQL statement. This could lead to SQL injection if variables in the SQL statement are not properly sanitized. Use a prepared statements instead. You can obtain a PreparedStatement using 'SqlCommand' and 'SqlParameter'.

*Source: opengrep*
</issue_to_address>

### Comment 5
<location path="PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs" line_range="268" />
<code_context>
        cmd.CommandText = InsertTable;
</code_context>
<issue_to_address>
**security (csharp.lang.security.sqli.csharp-sqli):** Detected a formatted string in a SQL statement. This could lead to SQL injection if variables in the SQL statement are not properly sanitized. Use a prepared statements instead. You can obtain a PreparedStatement using 'SqlCommand' and 'SqlParameter'.

*Source: opengrep*
</issue_to_address>

### Comment 6
<location path="PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs" line_range="282" />
<code_context>
        cmd.CommandText = DeleteTable;
</code_context>
<issue_to_address>
**security (csharp.lang.security.sqli.csharp-sqli):** Detected a formatted string in a SQL statement. This could lead to SQL injection if variables in the SQL statement are not properly sanitized. Use a prepared statements instead. You can obtain a PreparedStatement using 'SqlCommand' and 'SqlParameter'.

*Source: opengrep*
</issue_to_address>

### Comment 7
<location path="PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs" line_range="290" />
<code_context>
        queryCmd.CommandText = FindTable;
</code_context>
<issue_to_address>
**security (csharp.lang.security.sqli.csharp-sqli):** Detected a formatted string in a SQL statement. This could lead to SQL injection if variables in the SQL statement are not properly sanitized. Use a prepared statements instead. You can obtain a PreparedStatement using 'SqlCommand' and 'SqlParameter'.

*Source: opengrep*
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs
Comment thread PCL.Core/IO/Net/Http/Cache/Models/CacheStream.cs
Comment thread PCL.Core/IO/Net/Http/Cache/HttpCacheHandler.cs Outdated
Comment thread PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs
private static SqliteCommand _InsertDatabase(HttpCacheDetails details, SqliteConnection conn)
{
var cmd = conn.CreateCommand();
cmd.CommandText = InsertTable;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security (csharp.lang.security.sqli.csharp-sqli): 在 SQL 语句中检测到使用格式化字符串。如果其中的变量没有被正确清理,这可能导致 SQL 注入。请改用预编译语句。你可以通过 SqlCommandSqlParameter 来获取 PreparedStatement

Source: opengrep

Original comment in English

security (csharp.lang.security.sqli.csharp-sqli): Detected a formatted string in a SQL statement. This could lead to SQL injection if variables in the SQL statement are not properly sanitized. Use a prepared statements instead. You can obtain a PreparedStatement using 'SqlCommand' and 'SqlParameter'.

Source: opengrep

Comment thread PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs
private static SqliteCommand _FindTableWithUri(string uri, SqliteConnection conn)
{
var queryCmd = conn.CreateCommand();
queryCmd.CommandText = FindTable;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security (csharp.lang.security.sqli.csharp-sqli): 在 SQL 语句中检测到使用格式化字符串。如果其中的变量没有被正确清理,这可能导致 SQL 注入。请改用预编译语句。你可以通过 SqlCommandSqlParameter 来获取 PreparedStatement

Source: opengrep

Original comment in English

security (csharp.lang.security.sqli.csharp-sqli): Detected a formatted string in a SQL statement. This could lead to SQL injection if variables in the SQL statement are not properly sanitized. Use a prepared statements instead. You can obtain a PreparedStatement using 'SqlCommand' and 'SqlParameter'.

Source: opengrep

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Apr 12, 2026

Hey @LinQingYuu, I've posted a new review for you!

Copy link
Copy Markdown

@SourceryAI SourceryAI left a comment

Choose a reason for hiding this comment

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

Hey - 我发现了 4 个安全问题、2 个其他问题,并给出了一些高层次的反馈:

安全问题

  • 在 SQL 语句中检测到格式化字符串。如果 SQL 语句中的变量没有正确清理,这可能导致 SQL 注入。请改用预编译语句。你可以通过 SqlCommandSqlParameter 来获取 PreparedStatement。(link)
  • 在 SQL 语句中检测到格式化字符串。如果 SQL 语句中的变量没有正确清理,这可能导致 SQL 注入。请改用预编译语句。你可以通过 SqlCommandSqlParameter 来获取 PreparedStatement。(link)
  • 在 SQL 语句中检测到格式化字符串。如果 SQL 语句中的变量没有正确清理,这可能导致 SQL 注入。请改用预编译语句。你可以通过 SqlCommandSqlParameter 来获取 PreparedStatement。(link)
  • 在 SQL 语句中检测到格式化字符串。如果 SQL 语句中的变量没有正确清理,这可能导致 SQL 注入。请改用预编译语句。你可以通过 SqlCommandSqlParameter 来获取 PreparedStatement。(link)

总体评论

  • HttpCacheRepository._UpdateTable 中,生成的 SQL 是无效的,因为每个变更列都会追加一个单独的 SET 子句(例如 SET RequestUri = ... SET Tag = ...);应改为构建一个单一的 SET 子句,并使用逗号分隔的赋值列表。同时要注意对可空列使用 IsDBNull 来处理,而不是在可能为 null 的值上调用 GetString
  • HttpCacheRepository.Initialize 中,destLocation 被当作目录来处理,但是 IOException 的处理分支中调用了 File.Delete(destLocation),这在目录已存在时会失败;建议确保区分目录和文件,并避免在瞬时 IO 错误时删除整个缓存目录。
  • HttpCacheHandler 中,同步的 Send 实现调用了 SendAsync().GetAwaiter().GetResult(),在某些环境下可能导致死锁,并且当前的过期逻辑(LastUpdate.AddSeconds(ExpiredAt) < Now)在缓存项已经过期时仍然返回缓存响应;建议只使用 SendAsync,并反转过期条件,使新鲜条目从缓存中返回,而过期条目触发重新验证/重新获取。
面向 AI Agent 的提示
请处理这次代码评审中的所有评论:

## 总体评论
-`HttpCacheRepository._UpdateTable` 中,生成的 SQL 是无效的,因为每个变更列都会追加一个单独的 `SET` 子句(例如 `SET RequestUri = ... SET Tag = ...`);应改为构建一个单一的 `SET` 子句,并使用逗号分隔的赋值列表。同时要注意对可空列使用 `IsDBNull` 来处理,而不是在可能为 null 的值上调用 `GetString`-`HttpCacheRepository.Initialize` 中,`destLocation` 被当作目录来处理,但是 IOException 的处理分支中调用了 `File.Delete(destLocation)`,这在目录已存在时会失败;建议确保区分目录和文件,并避免在瞬时 IO 错误时删除整个缓存目录。
-`HttpCacheHandler` 中,同步的 `Send` 实现调用了 `SendAsync().GetAwaiter().GetResult()`,在某些环境下可能导致死锁,并且当前的过期逻辑(`LastUpdate.AddSeconds(ExpiredAt) < Now`)在缓存项已经过期时仍然返回缓存响应;建议只使用 `SendAsync`,并反转过期条件,使新鲜条目从缓存中返回,而过期条目触发重新验证/重新获取。

## 单独评论

### 评论 1
<location path="PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs" line_range="75-81" />
<code_context>
+    /// <summary>
+    /// 初始化数据库
+    /// </summary>
+    public void Initialize()
+    {
+        try
+        {
+            if (!Directory.Exists(destLocation)) Directory.CreateDirectory(destLocation);
+        }
+        catch (IOException)
+        {
+            File.Delete(destLocation);
</code_context>
<issue_to_address>
**issue (bug_risk):** 使用 File.Delete 删除目录路径是不正确的,并且可能抛出异常。

这里的 destLocation 是一个目录,但在 Directory.Exists/CreateDirectory 之后的 catch 中使用了 File.Delete(destLocation)。对目录路径调用 File.Delete 会抛出异常。如果需要清理损坏的目录,请使用 Directory.Delete(destLocation, recursive: true),或者调整异常处理逻辑,对目录故障采用不同的处理方式。
</issue_to_address>

### 评论 2
<location path="PCL.Core/IO/Net/Http/Cache/HttpCacheHandler.cs" line_range="24" />
<code_context>
+        _repository = repo;
+    }
+
+    protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)
+    {
+        return SendAsync(request, cancellationToken).GetAwaiter().GetResult();
</code_context>
<issue_to_address>
**suggestion (bug_risk):** 在 Send 中使用 sync-over-async 模式可能导致死锁和线程池耗尽。

在 `Send` 中调用 `SendAsync(...).GetAwaiter().GetResult()` 是一种 sync-over-async 模式,在存在同步上下文时可能导致死锁,并且会不必要地阻塞线程池线程。建议使用真正的同步路径,或者(如果可能的话)只重写 `SendAsync`,并依赖基类实现来处理同步调用,而不是在异步方法上进行阻塞。

建议实现如下:

```csharp
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)

```

通过移除 `Send` 的重写,同步调用方将使用基类的 `HttpMessageHandler.Send` 实现,而该实现会调用这个重写后的 `SendAsync`。这可以避免在你的处理程序中引入额外的 sync-over-async 层,同时保持同步与异步流程的既有行为。
</issue_to_address>

### 评论 3
<location path="PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs" line_range="89" />
<code_context>
        cmd.CommandText = CreateTable;
</code_context>
<issue_to_address>
**security (csharp.lang.security.sqli.csharp-sqli):** 在 SQL 语句中检测到格式化字符串。如果 SQL 语句中的变量没有正确清理,这可能导致 SQL 注入。请改用预编译语句。你可以通过 `SqlCommand``SqlParameter` 来获取 `PreparedStatement`*Source: opengrep*
</issue_to_address>

### 评论 4
<location path="PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs" line_range="268" />
<code_context>
        cmd.CommandText = InsertTable;
</code_context>
<issue_to_address>
**security (csharp.lang.security.sqli.csharp-sqli):** 在 SQL 语句中检测到格式化字符串。如果 SQL 语句中的变量没有正确清理,这可能导致 SQL 注入。请改用预编译语句。你可以通过 `SqlCommand``SqlParameter` 来获取 `PreparedStatement`*Source: opengrep*
</issue_to_address>

### 评论 5
<location path="PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs" line_range="282" />
<code_context>
        cmd.CommandText = DeleteTable;
</code_context>
<issue_to_address>
**security (csharp.lang.security.sqli.csharp-sqli):** 在 SQL 语句中检测到格式化字符串。如果 SQL 语句中的变量没有正确清理,这可能导致 SQL 注入。请改用预编译语句。你可以通过 `SqlCommand``SqlParameter` 来获取 `PreparedStatement`*Source: opengrep*
</issue_to_address>

### 评论 6
<location path="PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs" line_range="290" />
<code_context>
        queryCmd.CommandText = FindTable;
</code_context>
<issue_to_address>
**security (csharp.lang.security.sqli.csharp-sqli):** 在 SQL 语句中检测到格式化字符串。如果 SQL 语句中的变量没有正确清理,这可能导致 SQL 注入。请改用预编译语句。你可以通过 `SqlCommand``SqlParameter` 来获取 `PreparedStatement`*Source: opengrep*
</issue_to_address>

Hi @LinQingYuu! 👋

感谢你通过评论 @sourcery-ai review 来尝试使用 Sourcery!🚀

安装 sourcery-ai bot 就可以在每个 Pull Request 上自动获得代码评审 ✨

帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据这些反馈来改进之后的评审。
Original comment in English

Hey - I've found 4 security issues, 2 other issues, and left some high level feedback:

Security issues:

  • Detected a formatted string in a SQL statement. This could lead to SQL injection if variables in the SQL statement are not properly sanitized. Use a prepared statements instead. You can obtain a PreparedStatement using 'SqlCommand' and 'SqlParameter'. (link)
  • Detected a formatted string in a SQL statement. This could lead to SQL injection if variables in the SQL statement are not properly sanitized. Use a prepared statements instead. You can obtain a PreparedStatement using 'SqlCommand' and 'SqlParameter'. (link)
  • Detected a formatted string in a SQL statement. This could lead to SQL injection if variables in the SQL statement are not properly sanitized. Use a prepared statements instead. You can obtain a PreparedStatement using 'SqlCommand' and 'SqlParameter'. (link)
  • Detected a formatted string in a SQL statement. This could lead to SQL injection if variables in the SQL statement are not properly sanitized. Use a prepared statements instead. You can obtain a PreparedStatement using 'SqlCommand' and 'SqlParameter'. (link)

General comments:

  • In HttpCacheRepository._UpdateTable, the generated SQL is invalid because each changed column appends a separate SET clause (e.g., SET RequestUri = ... SET Tag = ...); instead, build a single SET with comma‑separated assignments and be careful to handle nullable columns via IsDBNull rather than using GetString on potentially null values.
  • In HttpCacheRepository.Initialize, destLocation is treated as a directory but the IOException handler calls File.Delete(destLocation), which will fail for an existing directory; consider ensuring you distinguish between directories and files and avoid deleting the whole cache directory on a transient IO error.
  • In HttpCacheHandler, the sync Send implementation calls SendAsync().GetAwaiter().GetResult() which can deadlock in certain environments, and the expiration logic (LastUpdate.AddSeconds(ExpiredAt) < Now) currently returns the cached response when it is already expired; consider using only SendAsync and inverting the expiry condition so fresh entries are served from cache and stale ones trigger a revalidation/fetch.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `HttpCacheRepository._UpdateTable`, the generated SQL is invalid because each changed column appends a separate `SET` clause (e.g., `SET RequestUri = ... SET Tag = ...`); instead, build a single `SET` with comma‑separated assignments and be careful to handle nullable columns via `IsDBNull` rather than using `GetString` on potentially null values.
- In `HttpCacheRepository.Initialize`, `destLocation` is treated as a directory but the IOException handler calls `File.Delete(destLocation)`, which will fail for an existing directory; consider ensuring you distinguish between directories and files and avoid deleting the whole cache directory on a transient IO error.
- In `HttpCacheHandler`, the sync `Send` implementation calls `SendAsync().GetAwaiter().GetResult()` which can deadlock in certain environments, and the expiration logic (`LastUpdate.AddSeconds(ExpiredAt) < Now`) currently returns the cached response when it is already expired; consider using only `SendAsync` and inverting the expiry condition so fresh entries are served from cache and stale ones trigger a revalidation/fetch.

## Individual Comments

### Comment 1
<location path="PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs" line_range="75-81" />
<code_context>
+    /// <summary>
+    /// 初始化数据库
+    /// </summary>
+    public void Initialize()
+    {
+        try
+        {
+            if (!Directory.Exists(destLocation)) Directory.CreateDirectory(destLocation);
+        }
+        catch (IOException)
+        {
+            File.Delete(destLocation);
</code_context>
<issue_to_address>
**issue (bug_risk):** Deleting a directory path with File.Delete is incorrect and can throw.

Here destLocation is a directory, but the catch uses File.Delete(destLocation) after Directory.Exists/CreateDirectory. File.Delete on a directory path will throw. If you need to clean up a bad directory, use Directory.Delete(destLocation, recursive: true) or adjust the exception handling to treat directory failures differently.
</issue_to_address>

### Comment 2
<location path="PCL.Core/IO/Net/Http/Cache/HttpCacheHandler.cs" line_range="24" />
<code_context>
+        _repository = repo;
+    }
+
+    protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)
+    {
+        return SendAsync(request, cancellationToken).GetAwaiter().GetResult();
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Using sync-over-async in Send can cause deadlocks and thread pool starvation.

Calling `SendAsync(...).GetAwaiter().GetResult()` from `Send` is a sync-over-async pattern that can deadlock with a synchronization context and unnecessarily block thread-pool threads. Prefer a truly synchronous path or, if possible, only override `SendAsync` and rely on the base implementation for sync calls instead of blocking on the async method.

Suggested implementation:

```csharp
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)

```

By removing the `Send` override, synchronous callers will use the base `HttpMessageHandler.Send` implementation, which in turn calls this overridden `SendAsync`. This avoids introducing an additional sync-over-async layer in your handler while preserving existing behavior for both sync and async flows.
</issue_to_address>

### Comment 3
<location path="PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs" line_range="89" />
<code_context>
        cmd.CommandText = CreateTable;
</code_context>
<issue_to_address>
**security (csharp.lang.security.sqli.csharp-sqli):** Detected a formatted string in a SQL statement. This could lead to SQL injection if variables in the SQL statement are not properly sanitized. Use a prepared statements instead. You can obtain a PreparedStatement using 'SqlCommand' and 'SqlParameter'.

*Source: opengrep*
</issue_to_address>

### Comment 4
<location path="PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs" line_range="268" />
<code_context>
        cmd.CommandText = InsertTable;
</code_context>
<issue_to_address>
**security (csharp.lang.security.sqli.csharp-sqli):** Detected a formatted string in a SQL statement. This could lead to SQL injection if variables in the SQL statement are not properly sanitized. Use a prepared statements instead. You can obtain a PreparedStatement using 'SqlCommand' and 'SqlParameter'.

*Source: opengrep*
</issue_to_address>

### Comment 5
<location path="PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs" line_range="282" />
<code_context>
        cmd.CommandText = DeleteTable;
</code_context>
<issue_to_address>
**security (csharp.lang.security.sqli.csharp-sqli):** Detected a formatted string in a SQL statement. This could lead to SQL injection if variables in the SQL statement are not properly sanitized. Use a prepared statements instead. You can obtain a PreparedStatement using 'SqlCommand' and 'SqlParameter'.

*Source: opengrep*
</issue_to_address>

### Comment 6
<location path="PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs" line_range="290" />
<code_context>
        queryCmd.CommandText = FindTable;
</code_context>
<issue_to_address>
**security (csharp.lang.security.sqli.csharp-sqli):** Detected a formatted string in a SQL statement. This could lead to SQL injection if variables in the SQL statement are not properly sanitized. Use a prepared statements instead. You can obtain a PreparedStatement using 'SqlCommand' and 'SqlParameter'.

*Source: opengrep*
</issue_to_address>

Hi @LinQingYuu! 👋

Thanks for trying out Sourcery by commenting with @sourcery-ai review! 🚀

Install the sourcery-ai bot to get automatic code reviews on every pull request ✨

Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs
Comment thread PCL.Core/IO/Net/Http/Cache/HttpCacheHandler.cs Outdated
Comment thread PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs
Comment thread PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs
Comment thread PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs
Comment thread PCL.Core/IO/Net/Http/Cache/HttpCacheRepository.cs
@LinQingYuu LinQingYuu marked this pull request as ready for review April 12, 2026 06:38
@pcl-ce-automation pcl-ce-automation bot added 🛠️ 等待审查 Pull Request 已完善,等待维护者或负责人进行代码审查 and removed 🚧 正在处理 开发人员正在对该内容进行开发、测试或修复,进展中 labels Apr 12, 2026
@LinQingYuu LinQingYuu requested a review from a team April 12, 2026 06:39
Copy link
Copy Markdown
Member

@lhx077 lhx077 left a comment

Choose a reason for hiding this comment

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

LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size: XL PR 大小评估:超大型 🛠️ 等待审查 Pull Request 已完善,等待维护者或负责人进行代码审查

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants