Skip to content

[FEATURE] Update cachemanager and cacherepository implementations to async#715

Open
HarryKambo wants to merge 12 commits intoguibranco:mainfrom
HarryKambo:feature/cacheasync
Open

[FEATURE] Update cachemanager and cacherepository implementations to async#715
HarryKambo wants to merge 12 commits intoguibranco:mainfrom
HarryKambo:feature/cacheasync

Conversation

@HarryKambo
Copy link
Copy Markdown

@HarryKambo HarryKambo commented Jun 27, 2025

User description

Closes #88

πŸ“‘ Description

βœ… Checks

  • My pull request adheres to the code style of this project
  • My code requires changes to the documentation
  • I have updated the documentation as required
  • All the tests have passed

☒️ Does this introduce a breaking change?

  • Yes
  • No

β„Ή Additional Information

Summary by Sourcery

Convert cache management APIs and repository implementations to asynchronous patterns, adding cancellation support, timeouts, and enhanced logging throughout the caching layers, and update dependent tests and SmtpMailer to use the new async methods.

New Features:

  • Support async cache operations in CacheManager with CancellationToken and default timeout logic.

Enhancements:

  • Refactor ICacheRepository to define async ValueTask/Task methods.
  • Update MemoryCacheRepository, RedisCacheRepository, and CouchDBCacheRepository to implement the new async API with cancellation tokens, exception handling, and logging.
  • Enhance CacheManager to run repository calls in parallel, track success counts, handle exceptions per repository, and promote retrieved values to memory cache.
  • Introduce default timeout for cache Get calls and unify cancellation handling in all cache operations.
  • Modify SmtpMailer to use async cache methods for error throttling.

Tests:

  • Revise cache-related tests to use the async API with await, renaming methods to SetAsync/GetAsync/TryGetAsync, and update assertions accordingly.

Note

I'm currently writing a description for your pull request. I should be done shortly (<1 minute). Please don't edit the description field until I'm finished, or we may overwrite each other. If I find nothing to write about, I'll delete this message.


Description

  • Migrated the entire caching layer to asynchronous APIs, enhancing performance and responsiveness.
  • Introduced cancellation token support across all cache operations to allow for better control during long-running tasks.
  • Improved logging and error handling mechanisms to provide better insights during cache operations.
  • Updated all relevant repository classes and interfaces to support the new asynchronous patterns.
  • Refactored tests to ensure they validate the new asynchronous behavior and cancellation handling.

Changes walkthrough πŸ“

Relevant files
Enhancement
CacheManager.cs
Migrate CacheManager to Asynchronous OperationsΒ  Β  Β  Β  Β  Β  Β  Β  Β  Β 

Src/CrispyWaffle/Cache/CacheManager.cs

  • Migrated cache operations to asynchronous methods.
  • Added cancellation token support for cache operations.
  • Improved logging and error handling in cache methods.
  • +732/-162
    CouchDBCacheRepository.cs
    Enhance CouchDBCacheRepository with Async SupportΒ  Β  Β  Β  Β  Β  Β  Β 

    Src/CrispyWaffle.CouchDB/Cache/CouchDBCacheRepository.cs

  • Updated methods to support asynchronous operations.
  • Added cancellation token support for database operations.
  • +279/-92
    RedisCacheRepository.cs
    Refactor RedisCacheRepository for Asynchronous Operations

    Src/CrispyWaffle.Redis/Cache/RedisCacheRepository.cs

  • Refactored methods to be asynchronous.
  • Integrated cancellation token for Redis operations.
  • +362/-58
    MemoryCacheRepository.cs
    Update MemoryCacheRepository for Async MethodsΒ  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β 

    Src/CrispyWaffle/Cache/MemoryCacheRepository.cs

  • Converted cache methods to asynchronous.
  • Implemented cancellation token handling.
  • +109/-29
    ICacheRepository.cs
    Modify ICacheRepository for Async Method SupportΒ  Β  Β  Β  Β  Β  Β  Β  Β 

    Src/CrispyWaffle/Cache/ICacheRepository.cs

  • Updated interface to include asynchronous method signatures.
  • Added cancellation token parameters to methods.
  • +26/-12Β 
    CacheManagerTests.cs
    Refactor CacheManager Tests for Asynchronous MethodsΒ  Β  Β  Β  Β 

    Tests/CrispyWaffle.Tests/Cache/CacheManagerTests.cs

  • Updated tests to use asynchronous methods.
  • Ensured cancellation tokens are utilized in tests.
  • +35/-32Β 
    MemoryCacheRepositoryTests.cs
    Update MemoryCacheRepository Tests for Async SupportΒ  Β  Β  Β  Β 

    Tests/CrispyWaffle.Tests/Cache/MemoryCacheRepositoryTests.cs

  • Converted tests to support asynchronous operations.
  • Ensured proper handling of cancellation tokens in tests.
  • +12/-11Β 

    πŸ’‘ Penify usage:
    Comment /help on the PR to get a list of all available Penify tools and their descriptions

    Summary by CodeRabbit

    • New Features

      • All cache operations across memory, Redis, and CouchDB repositories are now fully asynchronous and support cancellation, providing improved responsiveness and scalability.
      • Asynchronous cache methods are available throughout the application, including set, get, remove, clear, and TTL operations.
    • Bug Fixes

      • Enhanced error and cancellation handling for cache operations, with improved logging and more robust exception management.
    • Tests

      • All cache-related tests have been updated to use async/await patterns, ensuring reliable testing of asynchronous cache behavior.

    @sourcery-ai
    Copy link
    Copy Markdown

    sourcery-ai Bot commented Jun 27, 2025

    Reviewer's Guide

    This PR overhauls the caching layer by migrating all CacheManager methods, repository interfaces, and implementations to asynchronous APIs with CancellationToken support, built-in default timeouts, enhanced logging and error handling, and automatic promotion of fetched entries into the in-memory cache. Tests have been updated to exercise the new async methods.

    Sequence diagram for async GetAsync with automatic memory cache promotion

    sequenceDiagram
        participant Client
        participant CacheManager
        participant MemoryCacheRepository
        participant RedisCacheRepository
        participant CouchDBCacheRepository
        Client->>CacheManager: GetAsync<T>(key, cancellationToken)
        CacheManager->>MemoryCacheRepository: GetAsync<T>(key, cancellationToken)
        alt Found in memory
            MemoryCacheRepository-->>CacheManager: value
            CacheManager-->>Client: value
        else Not found in memory
            CacheManager->>RedisCacheRepository: GetAsync<T>(key, cancellationToken)
            alt Found in Redis
                RedisCacheRepository-->>CacheManager: value
                CacheManager->>MemoryCacheRepository: SetAsync<T>(value, key, CancellationToken.None)
                CacheManager-->>Client: value
            else Not found in Redis
                CacheManager->>CouchDBCacheRepository: GetAsync<T>(key, cancellationToken)
                alt Found in CouchDB
                    CouchDBCacheRepository-->>CacheManager: value
                    CacheManager->>MemoryCacheRepository: SetAsync<T>(value, key, CancellationToken.None)
                    CacheManager-->>Client: value
                else Not found in any
                    CouchDBCacheRepository-->>CacheManager: (throws)
                    CacheManager-->>Client: (throws InvalidOperationException)
                end
            end
        end
    
    Loading

    Class diagram for updated ICacheRepository interface and implementations

    classDiagram
        class ICacheRepository {
            <<interface>>
            +ValueTask SetAsync<T>(T value, string key, TimeSpan? ttl = null, CancellationToken cancellationToken = default)
            +ValueTask SetAsync<T>(T value, string key, string subKey, CancellationToken cancellationToken = default)
            +ValueTask<T> GetAsync<T>(string key, CancellationToken cancellationToken = default)
            +ValueTask<T> GetAsync<T>(string key, string subKey, CancellationToken cancellationToken = default)
            +ValueTask<(bool Success, T Value)> TryGetAsync<T>(string key, CancellationToken cancellationToken = default)
            +ValueTask<(bool Success, T Value)> TryGetAsync<T>(string key, string subKey, CancellationToken cancellationToken = default)
            +Task RemoveAsync(string key, CancellationToken cancellationToken = default)
            +Task RemoveAsync(string key, string subKey, CancellationToken cancellationToken = default)
            +Task<TimeSpan> TTLAsync(string key, CancellationToken cancellationToken = default)
            +Task Clear()
        }
        class MemoryCacheRepository {
            +ValueTask SetAsync<T>(T value, string key, TimeSpan? ttl = null, CancellationToken cancellationToken = default)
            +ValueTask SetAsync<T>(T value, string key, string subKey, CancellationToken cancellationToken = default)
            +ValueTask<T> GetAsync<T>(string key, CancellationToken cancellationToken = default)
            +ValueTask<T> GetAsync<T>(string key, string subKey, CancellationToken cancellationToken = default)
            +ValueTask<(bool Success, T Value)> TryGetAsync<T>(string key, CancellationToken cancellationToken = default)
            +ValueTask<(bool Success, T Value)> TryGetAsync<T>(string key, string subKey, CancellationToken cancellationToken = default)
            +Task RemoveAsync(string key, CancellationToken cancellationToken = default)
            +Task RemoveAsync(string key, string subKey, CancellationToken cancellationToken = default)
            +Task<TimeSpan> TTLAsync(string key, CancellationToken cancellationToken = default)
            +Task Clear()
        }
        class RedisCacheRepository {
            +ValueTask SetAsync<T>(T value, string key, TimeSpan? ttl = null, CancellationToken cancellationToken = default)
            +ValueTask SetAsync<T>(T value, string key, string subKey, CancellationToken cancellationToken = default)
            +ValueTask<T> GetAsync<T>(string key, CancellationToken cancellationToken = default)
            +ValueTask<T> GetAsync<T>(string key, string subKey, CancellationToken cancellationToken = default)
            +ValueTask<(bool Success, T Value)> TryGetAsync<T>(string key, CancellationToken cancellationToken = default)
            +ValueTask<(bool Success, T Value)> TryGetAsync<T>(string key, string subKey, CancellationToken cancellationToken = default)
            +Task RemoveAsync(string key, CancellationToken cancellationToken = default)
            +Task RemoveAsync(string key, string subKey, CancellationToken cancellationToken = default)
            +Task<TimeSpan> TTLAsync(string key, CancellationToken cancellationToken = default)
            +Task Clear()
        }
        class CouchDBCacheRepository {
            +ValueTask SetAsync<T>(T value, string key, TimeSpan? ttl = null, CancellationToken cancellationToken = default)
            +ValueTask SetAsync<T>(T value, string key, string subKey, CancellationToken cancellationToken = default)
            +ValueTask<T> GetAsync<T>(string key, CancellationToken cancellationToken = default)
            +ValueTask<T> GetAsync<T>(string key, string subKey, CancellationToken cancellationToken = default)
            +ValueTask<(bool Success, T Value)> TryGetAsync<T>(string key, CancellationToken cancellationToken = default)
            +ValueTask<(bool Success, T Value)> TryGetAsync<T>(string key, string subKey, CancellationToken cancellationToken = default)
            +Task RemoveAsync(string key, CancellationToken cancellationToken = default)
            +Task RemoveAsync(string key, string subKey, CancellationToken cancellationToken = default)
            +Task<TimeSpan> TTLAsync(string key, CancellationToken cancellationToken = default)
            +Task Clear()
        }
        ICacheRepository <|.. MemoryCacheRepository
        ICacheRepository <|.. RedisCacheRepository
        ICacheRepository <|.. CouchDBCacheRepository
    
    Loading

    Class diagram for updated CacheManager async API

    classDiagram
        class CacheManager {
            <<static>>
            +ValueTask SetAsync<T>(T value, string key, CancellationToken cancellationToken = default)
            +ValueTask SetAsync<T>(T value, string key, string subKey, CancellationToken cancellationToken = default)
            +ValueTask SetAsync<T>(T value, string key, TimeSpan ttl, CancellationToken cancellationToken = default)
            +ValueTask SetToAsync<TCacheRepository, TValue>(TValue value, string key, CancellationToken cancellationToken = default)
            +ValueTask SetToAsync<TCacheRepository, TValue>(TValue value, string key, string subKey, CancellationToken cancellationToken = default)
            +ValueTask SetToAsync<TCacheRepository, TValue>(TValue value, string key, TimeSpan ttl, CancellationToken cancellationToken = default)
            +ValueTask SetToAsync<TCacheRepository, TValue>(TValue value, string key, string subKey, TimeSpan ttl, CancellationToken cancellationToken = default)
            +ValueTask<T> GetAsync<T>(string key, CancellationToken cancellationToken = default)
            +ValueTask<T> GetAsync<T>(string key, string subKey, CancellationToken cancellationToken = default)
            +ValueTask<TValue> GetFrom<TCacheRepository, TValue>(string key, CancellationToken cancellationToken = default)
            +ValueTask<TValue> GetFromAsync<TCacheRepository, TValue>(string key, string subKey, CancellationToken cancellationToken = default)
            +ValueTask<(bool Success, T Value)> TryGetAsync<T>(string key, CancellationToken cancellationToken = default)
            +ValueTask<bool> TryGetAsync<T>(string key, string subKey, CancellationToken cancellationToken = default)
            +Task<TimeSpan> TTLAsync(string key)
            +ValueTask Remove(string key, CancellationToken cancellationToken = default)
            +ValueTask Remove(string key, string subKey, CancellationToken cancellationToken = default)
            +ValueTask RemoveFrom<TCacheRepository>(string key, CancellationToken cancellationToken = default)
            +ValueTask RemoveFrom<TCacheRepository>(string key, string subKey, CancellationToken cancellationToken = default)
        }
    
    Loading

    File-Level Changes

    Change Details Files
    Migrate CacheManager to async/ValueTask with cancellation and timeout handling
    • Convert all CacheManager methods from sync to async/ValueTask signatures with optional CancellationToken
    • Introduce DefaultTimeout and wrap get/set/remove operations in timeout-aware CancellationTokenSource
    • Parallelize Set operations across repositories with Task.WhenAll and count successes
    • Handle OperationCanceledException and TimeoutException with logging
    • Automatically promote items from non-memory repositories into the memory cache asynchronously
    Src/CrispyWaffle/Cache/CacheManager.cs
    Convert ICacheRepository interface to async contract
    • Replace sync Set/Get/TryGet/Remove/TTL/Clear methods with async ValueTask/Task variants
    • Add CancellationToken parameter to all repository interface methods
    Src/CrispyWaffle/Cache/ICacheRepository.cs
    Refactor repository implementations for async, cancellation and robust error handling
    • Implement async Set/Get/TryGet/Remove/TTL/Clear in MemoryCacheRepository with CancellationToken
    • Refactor RedisCacheRepository to ValueTask/Task methods, use Task.WhenAny for cancellation support and propagate exceptions based on configuration
    • Update CouchDBCacheRepository to async/Task methods, add cancellation checks, and unify database resolution to async
    Src/CrispyWaffle/Cache/MemoryCacheRepository.cs
    Src/CrispyWaffle.Redis/Cache/RedisCacheRepository.cs
    Src/CrispyWaffle.CouchDB/Cache/CouchDBCacheRepository.cs
    Adapt unit and integration tests to the new async APIs
    • Change test methods to async Task and await calls to SetAsync/GetAsync/RemoveAsync/TTLAsync
    • Update assertions to use FluentAssertions’ Should().ThrowAsync or Received().SetAsync
    • Use Task.WhenAll in integration tests where multiple async operations are issued
    Tests/CrispyWaffle.Tests/Cache/CacheManagerTests.cs
    Tests/CrispyWaffle.Tests/Cache/MemoryCacheRepositoryTests.cs
    Tests/CrispyWaffle.Tests/Cache/CouchDBCacheRepositoryTests.cs
    Tests/CrispyWaffle.IntegrationTests/Cache/CouchDBCacheRepositoryTests.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

    @korbit-ai
    Copy link
    Copy Markdown

    korbit-ai Bot commented Jun 27, 2025

    Based on your review schedule, I'll hold off on reviewing this PR until it's marked as ready for review. If you'd like me to take a look now, comment /korbit-review.

    Your admin can change your review schedule in the Korbit Console

    @gstraccini gstraccini Bot requested a review from guibranco June 27, 2025 00:54
    @gstraccini gstraccini Bot added the 🚦 awaiting triage Items that are awaiting triage or categorization label Jun 27, 2025
    @coderabbitai
    Copy link
    Copy Markdown

    coderabbitai Bot commented Jun 27, 2025

    Walkthrough

    This change set refactors the entire caching infrastructure from synchronous to asynchronous patterns across all repositories, the cache manager, and related utilities. All cache operations now use async/await, return Task or ValueTask, and support cancellation tokens. Corresponding tests and interface definitions are updated for asynchronous execution and cancellation support.

    Changes

    Cohort / File(s) Change Summary
    CouchDB Cache Repository Refactor
    Src/CrispyWaffle.CouchDB/Cache/CouchDBCacheRepository.cs
    All cache methods converted to async/await with cancellation token support; exception and logging improvements; method signatures updated to Task/ValueTask; new TTLAsync method added.
    Redis Cache Repository Refactor
    Src/CrispyWaffle.Redis/Cache/RedisCacheRepository.cs
    All cache methods refactored to async/await with cancellation token support; exception handling improved; TryGet now returns tuple; TTL renamed to TTLAsync; Clear is now async.
    Memory Cache Repository Refactor
    Src/CrispyWaffle/Cache/MemoryCacheRepository.cs
    All cache methods refactored to async/await with cancellation token support; TryGet returns tuple; improved cancellation and exception handling; method signatures updated.
    Cache Manager Refactor
    Src/CrispyWaffle/Cache/CacheManager.cs
    All cache manager operations refactored to async/await and cancellation token support; timeouts implemented; improved logging and error handling; synchronous methods replaced with async versions.
    Cache Repository Interface Update
    Src/CrispyWaffle/Cache/ICacheRepository.cs
    Interface methods converted to async/await with cancellation token parameters; TryGet returns tuple; XML docs updated.
    SMTP Mailer Async Handling
    Src/CrispyWaffle.Utils/Communications/SmtpMailer.cs
    Exception handling and cache operations made asynchronous; HandleExtension and related calls now use async/await.
    CouchDB Cache Repository Tests
    Tests/CrispyWaffle.IntegrationTests/Cache/CouchDBCacheRepositoryTests.cs,
    Tests/CrispyWaffle.Tests/Cache/CouchDBCacheRepositoryTests.cs
    All tests converted to async/await; cache operations and assertions updated for asynchronous execution.
    Memory Cache Repository Tests
    Tests/CrispyWaffle.Tests/Cache/MemoryCacheRepositoryTests.cs
    All tests converted to async/await; cache operations and assertions updated for asynchronous execution.
    Cache Manager Tests
    Tests/CrispyWaffle.Tests/Cache/CacheManagerTests.cs
    All tests converted to async/await; cache operations and exception assertions updated for asynchronous execution.

    Sequence Diagram(s)

    sequenceDiagram
        participant Caller
        participant CacheManager
        participant Repo1 as ICacheRepository (async)
        participant Repo2 as ICacheRepository (async)
        Note over CacheManager: Example: SetAsync flow
    
        Caller->>CacheManager: SetAsync(value, key, token)
        CacheManager->>Repo1: SetAsync(value, key, token)
        CacheManager->>Repo2: SetAsync(value, key, token)
        Repo1-->>CacheManager: (await) Success/Failure
        Repo2-->>CacheManager: (await) Success/Failure
        CacheManager-->>Caller: (await) Complete
    
    Loading
    sequenceDiagram
        participant Caller
        participant CacheManager
        participant Repo1 as ICacheRepository (async)
        participant Repo2 as ICacheRepository (async)
        Note over CacheManager: Example: GetAsync flow
    
        Caller->>CacheManager: GetAsync(key, token)
        CacheManager->>Repo1: GetAsync(key, token)
        alt Value found
            Repo1-->>CacheManager: (await) Value
            CacheManager-->>Caller: (await) Value
        else Value not found
            Repo1-->>CacheManager: (await) null
            CacheManager->>Repo2: GetAsync(key, token)
            Repo2-->>CacheManager: (await) Value/null
            CacheManager-->>Caller: (await) Value/null
        end
    
    Loading

    Estimated code review effort

    🎯 4 (Complex) | ⏱️ ~45 minutes

    Poem

    A rabbit hops through async fields,
    Where once were waits, now Task-y yieldsβ€”
    With tokens for a gentle stop,
    And logs for every cachey hop.
    Now tests and code both run with glee,
    In parallel, as fast as can be!
    πŸ‡βœ¨

    Note

    ⚑️ Unit Test Generation is now available in beta!

    Learn more here, or try it out under "Finishing Touches" below.

    ✨ Finishing Touches
    • πŸ“ Generate Docstrings
    πŸ§ͺ Generate unit tests
    • Create PR with unit tests
    • Post copyable unit tests in a comment

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

    ❀️ Share
    πŸͺ§ Tips

    Chat

    There are 3 ways to chat with CodeRabbit:

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

    Support

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

    Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

    CodeRabbit Commands (Invoked using PR comments)

    • @coderabbitai pause to pause the reviews on a PR.
    • @coderabbitai resume to resume the paused reviews.
    • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
    • @coderabbitai full review to do a full review from scratch and review all the files again.
    • @coderabbitai summary to regenerate the summary of the PR.
    • @coderabbitai generate docstrings to generate docstrings for this PR.
    • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
    • @coderabbitai generate unit tests to generate unit tests for this PR.
    • @coderabbitai resolve resolve all the CodeRabbit review comments.
    • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
    • @coderabbitai help to get help.

    Other keywords and placeholders

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

    CodeRabbit Configuration File (.coderabbit.yaml)

    • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
    • Please see the configuration documentation for more information.
    • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

    Documentation and Community

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

    @HarryKambo
    Copy link
    Copy Markdown
    Author

    Hi I request an early feedback before I update Test cases.

    @AppVeyorBot
    Copy link
    Copy Markdown

    ❌ Build CrispyWaffle 10.0.352 failed (commit 3e9349bd33 by @HarryKambo)

    @AppVeyorBot
    Copy link
    Copy Markdown

    @github-advanced-security
    Copy link
    Copy Markdown

    This pull request sets up GitHub code scanning for this repository. Once the scans have completed and the checks have passed, the analysis results for this pull request branch will appear on this overview. Once you merge this pull request, the 'Security' tab will show more code scanning analysis results (for example, for the default branch). Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results. For more information about GitHub code scanning, check out the documentation.

    Copy link
    Copy Markdown

    @github-advanced-security github-advanced-security AI left a comment

    Choose a reason for hiding this comment

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

    Sonarcsharp (reported by Codacy) found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

    @guibranco
    Copy link
    Copy Markdown
    Owner

    Hi @HarryKambo πŸ‘‹,

    Thank you so much for your pull request! πŸ™Œ

    I appreciate the time and effort you put into this contribution.
    I'll review it shortly, and if everything looks good, I'll approve it as soon as possible.

    Thanks again for your valuable contribution! πŸš€

    @HarryKambo
    Copy link
    Copy Markdown
    Author

    Hi @guibranco
    Do you want me seperate out the Asycn function into seperate files and restore other. Any changes required kindly let me know.

    @AppVeyorBot
    Copy link
    Copy Markdown

    Copy link
    Copy Markdown
    Owner

    @guibranco guibranco left a comment

    Choose a reason for hiding this comment

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

    Hi @HarryKambo πŸ‘‹, this PR looks good to me, do you plan to do more work over it? Or it's ready to approve and merge?

    @guibranco guibranco added πŸ“ documentation Tasks related to writing or updating documentation enhancement New feature or request cache 🎲 database Database-related operations .NET Pull requests that update .net code and removed 🚦 awaiting triage Items that are awaiting triage or categorization labels Jul 15, 2025
    @guibranco guibranco added Redis and removed RabbitMQ RabbitMQ labels Jul 15, 2025
    @guibranco
    Copy link
    Copy Markdown
    Owner

    @gstraccini csharpier

    @gstraccini
    Copy link
    Copy Markdown
    Contributor

    gstraccini Bot commented Jul 15, 2025

    Running CSharpier on this branch! πŸ”§

    @gstraccini
    Copy link
    Copy Markdown
    Contributor

    gstraccini Bot commented Jul 15, 2025

    ❌ CSharpier failed!

    @AppVeyorBot
    Copy link
    Copy Markdown

    @HarryKambo
    Copy link
    Copy Markdown
    Author

    Hi @HarryKambo πŸ‘‹, this PR looks good to me, do you plan to do more work over it? Or it's ready to approve and merge?

    Please merge

    @penify-dev
    Copy link
    Copy Markdown
    Contributor

    penify-dev Bot commented Jul 29, 2025

    PR Review πŸ”

    ⏱️ Estimated effort to review [1-5]

    4, because the PR introduces significant changes to the caching layer, migrating to asynchronous operations across multiple files. The complexity of ensuring that all asynchronous methods are correctly implemented and tested adds to the review effort.

    πŸ§ͺΒ Relevant tests

    Yes

    ⚑ Possible issues

    Possible Bug: Ensure that all cancellation tokens are properly handled in all asynchronous methods to avoid unintentional task cancellations.

    Performance Concern: The use of Task.WhenAll in SetAsync could lead to high concurrency issues if many tasks are executed simultaneously. Consider implementing throttling if necessary.

    πŸ”’Β Security concerns

    No

    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 @HarryKambo - I've reviewed your changes - here's some feedback:

    • There’s a lot of repeated async/cancellation/timeout boilerplate in CacheManager and repository implementationsβ€”consider extracting the common patterns (cancellation/token linking, logging, error handling, Task.WhenAll) into shared helper methods to reduce duplication and improve maintainability.
    • The DefaultTimeout of 10 seconds is currently hard‐codedβ€”make this configurable (e.g. via DI or settings) so callers can adjust timeouts per scenario instead of relying on a single static value.
    • The fire‐and‐forget promotion to memory cache uses the original cancellation token and ignores exceptionsβ€”switch to CancellationToken.None for background tasks, await or explicitly log any failures, and apply ConfigureAwait(false) to avoid unobserved exceptions or deadlocks.
    Prompt for AI Agents
    Please address the comments from this code review:
    ## Overall Comments
    - There’s a lot of repeated async/cancellation/timeout boilerplate in CacheManager and repository implementationsβ€”consider extracting the common patterns (cancellation/token linking, logging, error handling, Task.WhenAll) into shared helper methods to reduce duplication and improve maintainability.
    - The `DefaultTimeout` of 10 seconds is currently hard‐codedβ€”make this configurable (e.g. via DI or settings) so callers can adjust timeouts per scenario instead of relying on a single static value.
    - The fire‐and‐forget promotion to memory cache uses the original cancellation token and ignores exceptionsβ€”switch to `CancellationToken.None` for background tasks, await or explicitly log any failures, and apply `ConfigureAwait(false)` to avoid unobserved exceptions or deadlocks.
    
    ## Individual Comments
    
    ### Comment 1
    <location> `Src/CrispyWaffle/Cache/CacheManager.cs:138` </location>
    <code_context>
    +            }
    +            catch (OperationCanceledException)
    +            {
    +                Console.WriteLine("Cache operation was cancelled");
    +                return false;
    +            }
    </code_context>
    
    <issue_to_address>
    Console.WriteLine for cancellation is inconsistent with logging strategy.
    
    Use LogConsumer.Info or LogConsumer.Warning for cancellation events to align with the project's logging approach.
    </issue_to_address>
    
    <suggested_fix>
    <<<<<<< SEARCH
                    Console.WriteLine("Cache operation was cancelled");
    =======
                    LogConsumer.Info("Cache operation was cancelled");
    >>>>>>> REPLACE
    
    </suggested_fix>
    
    ### Comment 2
    <location> `Src/CrispyWaffle/Cache/CacheManager.cs:148` </location>
    <code_context>
    +            }
    +        });
    +
    +        var results = await Task.WhenAll(tasks);
    +        var successCount = results.Count(r => r);
    +
    +        LogConsumer.Info("Successfully set {0} in {1} out of {2} repositories", key, successCount, _repositories.Count);
         }
    
    </code_context>
    
    <issue_to_address>
    Success count may be misleading if all repositories fail.
    
    Log a warning or error when successCount is zero or less than the total repository count to highlight partial or total failures.
    </issue_to_address>
    
    <suggested_fix>
    <<<<<<< SEARCH
            var results = await Task.WhenAll(tasks);
            var successCount = results.Count(r => r);
    
            LogConsumer.Info("Successfully set {0} in {1} out of {2} repositories", key, successCount, _repositories.Count);
         }
    =======
            var results = await Task.WhenAll(tasks);
            var successCount = results.Count(r => r);
    
            if (successCount == 0)
            {
                LogConsumer.Error("Failed to set {0} in all {1} repositories", key, _repositories.Count);
            }
            else if (successCount < _repositories.Count)
            {
                LogConsumer.Warning("Successfully set {0} in only {1} out of {2} repositories", key, successCount, _repositories.Count);
            }
            else
            {
                LogConsumer.Info("Successfully set {0} in all {1} repositories", key, _repositories.Count);
            }
         }
    >>>>>>> REPLACE
    
    </suggested_fix>
    
    ### Comment 3
    <location> `Src/CrispyWaffle/Cache/CacheManager.cs:463` </location>
    <code_context>
    +            );
    +
    +            // Create timeout for the entire operation
    +            using var timeoutCts = new CancellationTokenSource(DefaultTimeout);
    +            using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
    
    -            if (_isMemoryRepositoryInList && repository.GetType() != typeof(MemoryCacheRepository))
    </code_context>
    
    <issue_to_address>
    Timeout is hardcoded and not configurable.
    
    Allow the timeout value to be set by the caller or made configurable to better accommodate different use cases and environments.
    </issue_to_address>
    
    ### Comment 4
    <location> `Src/CrispyWaffle/Cache/CacheManager.cs:503` </location>
    <code_context>
    +                        }, cancellationToken);
    +                    }
    +
    +                    LogConsumer.Debug("Key {0} not found in repository {1} ({2}ms)",  key, repositoryName, repositoryStopwatch.ElapsedMilliseconds);
    +                    return result;
    +                }
    </code_context>
    
    <issue_to_address>
    Debug log message may be misleading for successful gets.
    
    The log statement should only be executed when the key is not found to avoid confusion.
    </issue_to_address>
    
    <suggested_fix>
    <<<<<<< SEARCH
                        LogConsumer.Debug("Key {0} not found in repository {1} ({2}ms)",  key, repositoryName, repositoryStopwatch.ElapsedMilliseconds);
                        return result;
                    }
    =======
                        if (result == null)
                        {
                            LogConsumer.Debug("Key {0} not found in repository {1} ({2}ms)", key, repositoryName, repositoryStopwatch.ElapsedMilliseconds);
                        }
                        return result;
                    }
    >>>>>>> REPLACE
    
    </suggested_fix>
    
    ### Comment 5
    <location> `Src/CrispyWaffle/Cache/MemoryCacheRepository.cs:49` </location>
    <code_context>
    +    {
    +        cancellationToken.ThrowIfCancellationRequested();
    +
    +        if (_data.Count >= int.MaxValue)
    +        {
    +            throw new OverflowException("The dictionary already contains the maximum number of elements.");
    </code_context>
    
    <issue_to_address>
    Check for int.MaxValue is unnecessary for most practical scenarios.
    
    System memory will be exhausted before reaching int.MaxValue. Consider removing this check or using a more practical resource constraint.
    </issue_to_address>
    
    ### Comment 6
    <location> `Src/CrispyWaffle/Cache/MemoryCacheRepository.cs:227` </location>
    <code_context>
         {
    +        cancellationToken.ThrowIfCancellationRequested();
             var finalKey = $"{key}-{subKey}";
             if (_data.ContainsKey(finalKey))
             {
                 _hash.TryRemove(finalKey, out _);
    </code_context>
    
    <issue_to_address>
    Inconsistent use of _data and _hash for subKey removal.
    
    The method checks _data but removes from _hash. Please verify that both the check and removal target the correct dictionary for subKey operations.
    </issue_to_address>
    
    ### Comment 7
    <location> `Src/CrispyWaffle.Redis/Cache/RedisCacheRepository.cs:248` </location>
    <code_context>
    +            throw new ArgumentException("SubKey cannot be null or empty", nameof(subKey));
    +        }
    +
    +        if (EqualityComparer<T>.Default.Equals(value, default(T)) && !typeof(T).IsValueType)
    +        {
    +            throw new ArgumentNullException(nameof(value));
    </code_context>
    
    <issue_to_address>
    Null value check for reference types may not be sufficient.
    
    This approach may miss cases like nullable value types. Consider using 'value == null' for reference types or clarify your intent.
    </issue_to_address>
    
    <suggested_fix>
    <<<<<<< SEARCH
            if (EqualityComparer<T>.Default.Equals(value, default(T)) && !typeof(T).IsValueType)
            {
                throw new ArgumentNullException(nameof(value));
            }
    =======
            if (!typeof(T).IsValueType || Nullable.GetUnderlyingType(typeof(T)) != null)
            {
                if (value == null)
                {
                    throw new ArgumentNullException(nameof(value));
                }
            }
    >>>>>>> REPLACE
    
    </suggested_fix>
    
    ### Comment 8
    <location> `Src/CrispyWaffle.CouchDB/Cache/CouchDBCacheRepository.cs:50` </location>
    <code_context>
    +    public async Task<int> GetDocCount<T>()
    +        where T : CouchDBCacheDocument
    +    {
    +        var db = await ResolveDatabase<T>().ConfigureAwait(false);
    +        return db.Where(x => x.Id != null).ToList().Count;
    +    }
    </code_context>
    
    <issue_to_address>
    ToList().Count is inefficient for counting documents.
    
    Consider using a database-level count operation instead to avoid loading all documents into memory, which is inefficient for large datasets.
    </issue_to_address>
    
    <suggested_fix>
    <<<<<<< SEARCH
            return db.Where(x => x.Id != null).ToList().Count;
    =======
            return db.Count(x => x.Id != null);
    >>>>>>> REPLACE
    
    </suggested_fix>
    
    ### Comment 9
    <location> `Src/CrispyWaffle.CouchDB/Cache/CouchDBCacheRepository.cs:203` </location>
    <code_context>
    +            cancellationToken.ThrowIfCancellationRequested();
    +
    +            var client = await ResolveDatabase<T>().ConfigureAwait(false);
    +            var doc = client
                     .Where(x => x.Key == key && x.SubKey == subKey)
                     .FirstOrDefault();
    </code_context>
    
    <issue_to_address>
    Synchronous LINQ query on async database may block.
    
    Consider replacing the synchronous query with an async alternative to prevent potential thread blocking.
    
    Suggested implementation:
    
    ```csharp
                var doc = await client
                    .Where(x => x.Key == key && x.SubKey == subKey)
                    .FirstOrDefaultAsync(cancellationToken)
                    .ConfigureAwait(false);
    
    ```
    
    - Ensure that `using System.Linq;` is replaced or supplemented with `using System.Linq.Async;` or the correct namespace for async LINQ extensions, depending on your database client and async support.
    - If your database client does not support async LINQ, you may need to use its own async query API.
    </issue_to_address>
    
    ### Comment 10
    <location> `Tests/CrispyWaffle.Tests/Cache/CacheManagerTests.cs:81` </location>
    <code_context>
    
         [Fact]
    -    public void GetShouldThrowWhenItemNotFound()
    +    public async Task SetAsyncShouldSetValueWithSubKey()
         {
             // Arrange
             var key = "testKey";
    -        _mockCacheRepository.TryGet(key, out Arg.Any<object>()).Returns(false);
    +        var subKey = "testSubKey";
    +        var value = new { Name = "Test" };
    +        CacheManager.AddRepository(_mockCacheRepository);       
    
             // Act
    -        Action act = () => CacheManager.Get<dynamic>(key);
    +        await CacheManager.SetAsync(value, key, subKey);
    
             // Assert
    -        act.Should()
    -            .Throw<InvalidOperationException>()
    -            .WithMessage("Unable to get the item with key testKey");
    +        await _mockCacheRepository.Received(1).SetAsync(value, key, subKey, Arg.Any<CancellationToken>());
         }
    
    </code_context>
    
    <issue_to_address>
    Missing test for SetAsync with TTL and subKey together.
    
    Please add a test for SetAsync that uses both subKey and TTL to confirm the method handles both parameters together correctly.
    </issue_to_address>
    
    <suggested_fix>
    <<<<<<< SEARCH
    
        [Fact]
        public async Task SetAsyncShouldSetValueInAllRepositories()
    =======
    
        [Fact]
        public async Task SetAsyncShouldSetValueWithSubKeyAndTtl()
        {
            // Arrange
            var key = "testKey";
            var subKey = "testSubKey";
            var value = new { Name = "Test" };
            var ttl = TimeSpan.FromMinutes(5);
            CacheManager.AddRepository(_mockCacheRepository);
    
            // Act
            await CacheManager.SetAsync(value, key, subKey, ttl);
    
            // Assert
            await _mockCacheRepository.Received(1).SetAsync(value, key, subKey, ttl, Arg.Any<CancellationToken>());
        }
    
        [Fact]
        public async Task SetAsyncShouldSetValueInAllRepositories()
    >>>>>>> REPLACE
    
    </suggested_fix>
    
    ### Comment 11
    <location> `Tests/CrispyWaffle.Tests/Cache/CacheManagerTests.cs:97` </location>
    <code_context>
    
         [Fact]
    -    public void SetToShouldThrowWhenRepositoryNotFound()
    +    public async Task GetAsyncShouldThrowWhenItemNotFound()
         {
             // Arrange
             var key = "testKey";
    -        var value = new { Name = "Test" };
             CacheManager.AddRepository(_mockCacheRepository);
    -
             // Act
    -        Action act = () => CacheManager.SetTo<ICacheRepository, object>(value, key);
    +        Func<Task> act = async () => await CacheManager.GetAsync<dynamic>(key);
    
             // Assert
    -        act.Should()
    -            .Throw<InvalidOperationException>()
    -            .WithMessage(
    -                "The repository of type CrispyWaffle.Cache.ICacheRepository isn't available in the repositories providers list"
    -            );
    +        await act.Should()
    +            .ThrowAsync<InvalidOperationException>()
    +            .WithMessage("Unable to get the item with key testKey");
         }
    
    </code_context>
    
    <issue_to_address>
    No test for GetAsync with subKey when item is not found.
    
    Add a test for GetAsync with both key and subKey when the item is not found to verify the correct exception and message are thrown.
    </issue_to_address>
    
    ### Comment 12
    <location> `Tests/CrispyWaffle.Tests/Cache/CacheManagerTests.cs:131` </location>
    <code_context>
    
         [Fact]
    -    public void TTLShouldReturnCorrectTTLFromRepositories()
    +    public async Task TTLShouldReturnCorrectTTLFromRepositories()
         {
             // Arrange
             var key = "testKey";
             var expectedTTL = TimeSpan.FromMinutes(10);
    -        CacheManager.AddRepository(_mockCacheRepository);
    -        _mockCacheRepository.TTL(key).Returns(expectedTTL);
    +        CacheManager.AddRepository(_mockCacheRepository); 
    +        await CacheManager.SetAsync(new { Name = "Test" }, key, expectedTTL, CancellationToken.None); // Set value with TTL
    
             // Act
    -        var result = CacheManager.TTL(key);
    +        var result = await CacheManager.TTLAsync(key);
    
             // Assert
             result.Should().Be(expectedTTL);
    </code_context>
    
    <issue_to_address>
    No test for TTLAsync when key does not exist.
    
    Please add a test to verify that TTLAsync returns TimeSpan.Zero when the key is missing from all repositories.
    </issue_to_address>
    
    ### Comment 13
    <location> `Tests/CrispyWaffle.Tests/Cache/CacheManagerTests.cs:112` </location>
    <code_context>
    
         [Fact]
    -    public void RemoveShouldRemoveKeyFromAllRepositories()
    +    public async Task SetToShouldThrowWhenRepositoryNotFound()
         {
             // Arrange
             var key = "testKey";
    +        var value = new { Name = "Test" };
             CacheManager.AddRepository(_mockCacheRepository);
    
             // Act
    -        CacheManager.Remove(key);
    +        Func<Task> act = async () => await CacheManager.SetToAsync<ICacheRepository, object>(value, key);
    
             // Assert
    -        _mockCacheRepository.Received().Remove(key);
    -    }
    +        await act.Should()
    +            .ThrowAsync<InvalidOperationException>()
    +            .WithMessage(
    +                "The repository of type CrispyWaffle.Cache.ICacheRepository isn't available in the repositories providers list"
    +            );
    +    }   
    
    </code_context>
    
    <issue_to_address>
    No test for RemoveFromAsync with subKey when repository is not found.
    
    Add a test for RemoveFromAsync with both key and subKey when the repository is missing to verify the correct exception is thrown.
    </issue_to_address>
    
    <suggested_fix>
    <<<<<<< SEARCH
            await act.Should()
                .ThrowAsync<InvalidOperationException>()
                .WithMessage("Unable to get the item with key testKey");
        }
    
        [Fact]
    =======
            await act.Should()
                .ThrowAsync<InvalidOperationException>()
                .WithMessage("Unable to get the item with key testKey");
        }
    
        [Fact]
        public async Task RemoveFromAsyncShouldThrowWhenRepositoryNotFound()
        {
            // Arrange
            var key = "testKey";
            var subKey = "testSubKey";
            // Do not add any repository to CacheManager
    
            // Act
            Func<Task> act = async () => await CacheManager.RemoveFromAsync<ICacheRepository>(key, subKey);
    
            // Assert
            await act.Should()
                .ThrowAsync<InvalidOperationException>()
                .WithMessage(
                    "The repository of type CrispyWaffle.Cache.ICacheRepository isn't available in the repositories providers list"
                );
        }
    
        [Fact]
    >>>>>>> REPLACE
    
    </suggested_fix>
    
    ### Comment 14
    <location> `Tests/CrispyWaffle.IntegrationTests/Cache/CouchDBCacheRepositoryTests.cs:27` </location>
    <code_context>
    
         [Fact]
    -    public void GetAndSetCouchDocTest()
    +    public async Task GetAndSetAsyncCouchDocTest()
         {
             var doc = new CouchDBCacheDocument();
    
    -        _repository.Set(doc, Guid.NewGuid().ToString());
    +        await _repository.SetAsync(doc, Guid.NewGuid().ToString());
    
    -        var docDB = _repository.Get<CouchDBCacheDocument>(doc.Key);
    +        var docDB = await _repository.GetAsync<CouchDBCacheDocument>(doc.Key);
    
             Assert.True(doc.Key == docDB.Key);
    
    -        _repository.Remove(doc.Key);
    +        await _repository.RemoveAsync(doc.Key);
         }
    
    </code_context>
    
    <issue_to_address>
    Integration tests do not cover cancellation or error propagation.
    
    Please add integration tests for cancellation scenarios and exception propagation to ensure these cases are handled correctly.
    </issue_to_address>
    
    ### Comment 15
    <location> `Tests/CrispyWaffle.Tests/Cache/MemoryCacheRepositoryTests.cs:47` </location>
    <code_context>
    
         [Fact]
    -    public void RemoveShouldRemoveStoredValue()
    +    public async Task RemoveShouldRemoveStoredValue()
         {
             // Arrange
             var key = "test-key";
    -        _repository.Set("test-value", key);
    -        _repository.Remove(key);
    +        var Task1 = _repository.SetAsync("test-value", key).AsTask();
    +        var Task2 = _repository.RemoveAsync(key);
    
    +        await Task.WhenAll(Task1, Task2);
             // Act
    -        var exception = Assert.Throws<InvalidOperationException>(() =>
    -            _repository.Get<string>(key)
    +        var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => await _repository.GetAsync<string>(key)
             );
    
    </code_context>
    
    <issue_to_address>
    No test for MemoryCacheRepository GetAsync with subKey.
    
    Add tests for GetAsync with both key and subKey, including scenarios where the subKey is missing to verify exception handling.
    </issue_to_address>
    
    ### Comment 16
    <location> `Tests/CrispyWaffle.Tests/Cache/CouchDBCacheRepositoryTests.cs:54` </location>
    <code_context>
    
         [Fact]
    -    public void RemoveFromDatabaseShouldRemoveValue()
    +    public async Task RemoveFromDatabaseShouldRemoveValue()
         {
             // Arrange
             var key = "test-key";
    +        var value = "test-value";
    +
    +        // Act
    +        await _repository.SetAsync(value, key);
    
             // Act
    -        _repository.Remove(key);
    +        await _repository.RemoveAsync(key);
    +        (bool success, _)= await _repository.TryGetAsync<object>(key);
    
             // Assert
    +        success.Should().Be(false);
         }
     }
    </code_context>
    
    <issue_to_address>
    No test for CouchDBCacheRepository TryGetAsync with subKey.
    
    Add a test for TryGetAsync using both key and subKey, including a scenario where the subKey is missing, to verify correct handling.
    </issue_to_address>
    
    <suggested_fix>
    <<<<<<< SEARCH
    
        [Fact]
        public async Task SetToDatabaseShouldStoreValue()
        {
    =======
    
        [Fact]
        public async Task TryGetAsync_WithSubKey_ShouldReturnCorrectly()
        {
            // Arrange
            var key = "test-key";
            var subKey = "test-subkey";
            var value = "test-value";
            var missingSubKey = "missing-subkey";
    
            // Store value with subKey
            await _repository.SetAsync(value, key, subKey);
    
            // Act & Assert: Should retrieve successfully with correct subKey
            (bool found, object? retrieved) = await _repository.TryGetAsync<string>(key, subKey);
            found.Should().BeTrue();
            retrieved.Should().Be(value);
    
            // Act & Assert: Should not retrieve with missing subKey
            (bool foundMissing, object? retrievedMissing) = await _repository.TryGetAsync<string>(key, missingSubKey);
            foundMissing.Should().BeFalse();
            retrievedMissing.Should().BeNull();
        }
    
        [Fact]
        public async Task SetToDatabaseShouldStoreValue()
        {
    >>>>>>> REPLACE
    
    </suggested_fix>
    
    ### Comment 17
    <location> `Src/CrispyWaffle/Cache/CacheManager.cs:123` </location>
    <code_context>
    +    /// <param name="cancellationToken">Cancel.</param>
    +    /// <exception cref="OperationCanceledException">If cancelled.</exception>
    +    /// <returns>void.</returns>
    +    public static async ValueTask SetAsync<T>(T value, [Localizable(false)] string key, CancellationToken cancellationToken = default)
         {
    +        cancellationToken.ThrowIfCancellationRequested();
    </code_context>
    
    <issue_to_address>
    Consider refactoring the repeated repository iteration logic into two generic helper methods to greatly simplify and reduce duplication in the public API methods.
    
    Here’s one way to collapse all of these almost‐identical methods into two small helpersβ€”one for β€œrun on all repos in parallel” (e.g. SetAsync/RemoveAsync) and one for β€œquery until first success” (e.g. GetAsync/TryGetAsync).  Then each public API method becomes a one–liner.
    
    ```csharp
    // 1) helper for doing something in parallel on all repositories
    private static async ValueTask RunOnAllAsync(
        Func<ICacheRepository, CancellationToken, Task> action,
        string startMessage,
        string summaryMessage,
        CancellationToken cancellationToken = default
    )
    {
        cancellationToken.ThrowIfCancellationRequested();
        LogConsumer.Trace(startMessage, _repositories.Count);
    
        using var timeoutCts = new CancellationTokenSource(DefaultTimeout);
        using var linkedCts  = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
    
        var results = await Task.WhenAll(_repositories.Values.Select(async repo =>
        {
            try
            {
                await action(repo, linkedCts.Token);
                return true;
            }
            catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
            {
                LogConsumer.Info("Operation canceled by user");
                return false;
            }
            catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested)
            {
                LogConsumer.Error("Operation timed out");
                throw new TimeoutException("Cache operation timed out");
            }
            catch (Exception ex)
            {
                LogConsumer.Error("Cache error in {0}: {1}", repo.GetType().Name, ex.Message);
                return false;
            }
        }));
    
        LogConsumer.Info(summaryMessage,
                         results.Count(r => r),
                         _repositories.Count);
    }
    
    // 2) helper for querying each repo in turn until one succeeds
    private static async ValueTask<T> QueryFirstAsync<T>(
        Func<ICacheRepository, CancellationToken, Task<T>> query,
        string startMessage,
        string notFoundMessage,
        CancellationToken cancellationToken = default
    )
    {
        cancellationToken.ThrowIfCancellationRequested();
        LogConsumer.Trace(startMessage, _repositories.Count);
    
        using var timeoutCts = new CancellationTokenSource(DefaultTimeout);
        using var linkedCts  = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
    
        foreach (var repo in _repositories.Values)
        {
            try
            {
                var result = await query(repo, linkedCts.Token);
                LogConsumer.Info("Found in {0}", repo.GetType().Name);
                return result;
            }
            catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
            {
                LogConsumer.Info("Operation canceled by user");
                throw;
            }
            catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested)
            {
                LogConsumer.Error("Operation timed out");
                throw new TimeoutException("Cache operation timed out");
            }
            catch (Exception ex)
            {
                LogConsumer.Error("Cache error in {0}: {1}",
                                  repo.GetType().Name,
                                  ex.Message);
            }
        }
    
        throw new InvalidOperationException(notFoundMessage);
    }
    ```
    
    Now your public methods shrink to:
    
    ```csharp
    public static ValueTask SetAsync<T>(T value, string key, CancellationToken ct = default)
        => RunOnAllAsync(
             (repo, c) => repo.SetAsync(value, key, cancellationToken: c),
             "Adding {0} to {1} cache repositories",
             "Successfully set {0} in {1} out of {2} repositories",
             ct
           );
    
    public static ValueTask RemoveAsync(string key, CancellationToken ct = default)
        => RunOnAllAsync(
             (repo, c) => repo.RemoveAsync(key, cancellationToken: c),
             "Removing key {0} from {1} repositories",
             "Removed key {0} from all repositories",
             ct
           );
    
    public static ValueTask<T> GetAsync<T>(string key, CancellationToken ct = default)
        => QueryFirstAsync(
             (repo, c) => repo.GetAsync<T>(key, cancellationToken: c),
             "Getting {0} from {1} cache repositories",
             $"Unable to get the item with key {key}",
             ct
           );
    ```
    
    You can parameterize those lambdas to handle sub-keys and TTLs, and similarly replace all of your `SetToAsync`, `GetFromAsync`, `TryGetAsync`, etc.  This collapses hundreds of lines into one helper + one small invocation per overload.
    </issue_to_address>
    
    ### Comment 18
    <location> `Src/CrispyWaffle.Redis/Cache/RedisCacheRepository.cs:160` </location>
    <code_context>
    
         /// <inheritdoc />
    -    public void Set<T>(T value, string key, TimeSpan? ttl = null)
    +    public async ValueTask SetAsync<T>(T value, string key, TimeSpan? ttl = null, CancellationToken cancellationToken = default)
         {
             if (!typeof(CouchDBCacheDocument).IsAssignableFrom(typeof(T)))
    </code_context>
    
    <issue_to_address>
    Consider extracting common try/catch and cancellation logic into reusable helper methods to dramatically simplify each public API method into concise one-liners.
    
    Here’s one way to collapse almost all of the per‐method boilerplate into two small helpers and turn your 30–50 line methods into one‐ or two‐liners. All functionality (cancellation, exception‐suppression, TTL, exists‐checks, etc.) remains intact, but you no longer repeat the same try/catch/ThrowIfCancelled logic everywhere.
    
    ```csharp
    // inside RedisCacheRepository
    private async ValueTask ExecuteAsync(
        Func<CancellationToken, Task> op,
        CancellationToken ct)
    {
        ct.ThrowIfCancellationRequested();
        try
        {
            await op(ct).ConfigureAwait(false);
        }
        catch (OperationCanceledException)
        {
            throw;
        }
        catch (Exception e) when (!ShouldPropagateExceptions)
        {
            HandleException(e);
        }
    }
    
    // for return‐value methods
    private async ValueTask<T> ExecuteAsync<T>(
        Func<CancellationToken, Task<T>> op,
        CancellationToken ct,
        string errorMsg)
    {
        ct.ThrowIfCancellationRequested();
        try
        {
            return await op(ct).ConfigureAwait(false);
        }
        catch (OperationCanceledException)
        {
            throw;
        }
        catch (Exception e) when (!ShouldPropagateExceptions)
        {
            HandleException(e);
            throw new InvalidOperationException(errorMsg, e);
        }
    }
    ```
    
    Now each public API is a one‐liner:
    
    ```csharp
    public ValueTask SetAsync<T>(
        T value,
        string key,
        TimeSpan? ttl = null,
        CancellationToken ct = default)
        => ExecuteAsync(
           token => ttl.HasValue
             ? _cacheClient.Db0.AddAsync(key, value, ttl.Value)
             : _cacheClient.Db0.AddAsync(key, value),
           ct);
    
    public ValueTask<T> GetAsync<T>(
        string key,
        CancellationToken ct = default)
        => ExecuteAsync(async token =>
        {
            if (!await _cacheClient.Db0.ExistsAsync(key).ConfigureAwait(false))
                throw new InvalidOperationException($"Unable to get the item with key {key}");
            return await _cacheClient.Db0.GetAsync<T>(key, CommandFlags.PreferReplica)
                                         .ConfigureAwait(false);
        }, ct, $"Unable to get the item with key {key}");
    
    public ValueTask<(bool Success, T Value)> TryGetAsync<T>(
        string key,
        CancellationToken ct = default)
        => ExecuteAsync(async token =>
        {
            if (!await _cacheClient.Db0.ExistsAsync(key).ConfigureAwait(false))
                return (false, default(T));
            var val = await _cacheClient.Db0.GetAsync<T>(key, CommandFlags.PreferReplica)
                                            .ConfigureAwait(false);
            return (true, val);
        }, ct, null);
    ```
    
    You can apply the same pattern to hash‐sets, TTL, RemoveAsync, Clear, etc.  This removes all the duplicated try/catch, cancellation checks, `Wait()/.Result`, and huge XML comments from each method.
    </issue_to_address>
    
    ### Comment 19
    <location> `Src/CrispyWaffle.CouchDB/Cache/CouchDBCacheRepository.cs:147` </location>
    <code_context>
    +    /// <exception cref="OperationCanceledException">Thrown if the operation is cancelled.</exception>
         /// <exception cref="InvalidOperationException">Thrown in case the operation fails.</exception>
    -    public T GetSpecific<T>(string key)
    +    public async Task<T> GetSpecificAsync<T>(string key, CancellationToken cancellationToken = default)
             where T : CouchDBCacheDocument
         {
    </code_context>
    
    <issue_to_address>
    Consider introducing a single generic async executor method to centralize error handling, cancellation, and database resolution logic for all async entry-points.
    
    Here’s one way to dramatically DRY out almost all of your async entry‐points by introducing a single generic executor.  All of your methods then become one‐liners that pass to this executor instead of re‐implementing the same try/catch/ResolveDatabase/ThrowIfCancellationRequested logic everywhere.
    
    ```csharp
    // 1) Put this in your class as a private helper
    private async Task<TResult> ExecuteAsync<TDoc, TResult>(
        Func<CouchDatabase<TDoc>, CancellationToken, Task<TResult>> work,
        string operationDescription,
        CancellationToken ct = default
    )
        where TDoc : CouchDBCacheDocument
    {
        try
        {
            ct.ThrowIfCancellationRequested();
            var db = await ResolveDatabase<TDoc>().ConfigureAwait(false);
            return await work(db, ct).ConfigureAwait(false);
        }
        catch (OperationCanceledException)
        {
            LogConsumer.Warning($"Operation cancelled: {operationDescription}");
            throw;
        }
        catch (Exception ex)
        {
            if (ShouldPropagateExceptions) throw;
            LogConsumer.Handle(ex);
            throw new InvalidOperationException(operationDescription, ex);
        }
    }
    ```
    
    ```csharp
    // 2) Refactor GetSpecificAsync
    public Task<T> GetSpecificAsync<T>(string key, CancellationToken ct = default)
        where T : CouchDBCacheDocument
        => ExecuteAsync<T, T>(
            async (db, token) =>
            {
                var doc = await db.FindAsync(key).ConfigureAwait(false);
                if (doc != null && doc.ExpiresAt <= DateTime.UtcNow)
                {
                    await RemoveSpecificAsync<T>(key, token).ConfigureAwait(false);
                    return default;
                }
                return doc;
            },
            $"Unable to get item with key: {key}",
            ct
        );
    ```
    
    ```csharp
    // 3) Refactor RemoveSpecificAsync
    public Task RemoveSpecificAsync<T>(string key, CancellationToken ct = default)
        where T : CouchDBCacheDocument
        => ExecuteAsync<T, object>(
            async (db, token) =>
            {
                var doc = await db.FindAsync(key).ConfigureAwait(false);
                if (doc != null)
                {
                    await db.DeleteAsync(doc).ConfigureAwait(false);
                    LogConsumer.Info($"Removed document {key}");
                }
                else
                {
                    LogConsumer.Info($"Document {key} not found");
                }
                return null;
            },
            $"Failed removing document with key: {key}",
            ct
        );
    ```
    
    ```csharp
    // 4) Refactor SetSpecificAsync
    public Task SetSpecificAsync<T>(T value, string key, TimeSpan? ttl = null, CancellationToken ct = default)
        where T : CouchDBCacheDocument
        => ExecuteAsync<T, object>(
            async (db, token) =>
            {
                value.Key = key;
                if (ttl != null)
                {
                    value.TTL = ttl.Value;
                    value.ExpiresAt = DateTime.UtcNow.Add(ttl.Value);
                }
                await db.CreateAsync(value).ConfigureAwait(false);
                return null;
            },
            $"Failed setting document with key: {key}",
            ct
        );
    ```
    
    You can then delete all of the duplicated try/catch/cancellation/ResolveDatabase boilerplate in every method.  The sync counterparts can either call `.GetAwaiter().GetResult()` on these or be slim wrappers around them as well.
    </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.

    }
    catch (OperationCanceledException)
    {
    Console.WriteLine("Cache operation was cancelled");
    Copy link
    Copy Markdown

    Choose a reason for hiding this comment

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

    suggestion: Console.WriteLine for cancellation is inconsistent with logging strategy.

    Use LogConsumer.Info or LogConsumer.Warning for cancellation events to align with the project's logging approach.

    Suggested change
    Console.WriteLine("Cache operation was cancelled");
    LogConsumer.Info("Cache operation was cancelled");

    Comment on lines +149 to 153
    var successCount = results.Count(r => r);

    LogConsumer.Info("Successfully set {0} in {1} out of {2} repositories", key, successCount, _repositories.Count);
    }

    Copy link
    Copy Markdown

    Choose a reason for hiding this comment

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

    suggestion (bug_risk): Success count may be misleading if all repositories fail.

    Log a warning or error when successCount is zero or less than the total repository count to highlight partial or total failures.

    Suggested change
    var results = await Task.WhenAll(tasks);
    var successCount = results.Count(r => r);
    LogConsumer.Info("Successfully set {0} in {1} out of {2} repositories", key, successCount, _repositories.Count);
    }
    var results = await Task.WhenAll(tasks);
    var successCount = results.Count(r => r);
    if (successCount == 0)
    {
    LogConsumer.Error("Failed to set {0} in all {1} repositories", key, _repositories.Count);
    }
    else if (successCount < _repositories.Count)
    {
    LogConsumer.Warning("Successfully set {0} in only {1} out of {2} repositories", key, successCount, _repositories.Count);
    }
    else
    {
    LogConsumer.Info("Successfully set {0} in all {1} repositories", key, _repositories.Count);
    }
    }

    Comment on lines +464 to 465
    using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);

    Copy link
    Copy Markdown

    Choose a reason for hiding this comment

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

    suggestion: Timeout is hardcoded and not configurable.

    Allow the timeout value to be set by the caller or made configurable to better accommodate different use cases and environments.

    Comment on lines +504 to +506
    return result;
    }
    catch (InvalidOperationException)
    Copy link
    Copy Markdown

    Choose a reason for hiding this comment

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

    suggestion: Debug log message may be misleading for successful gets.

    The log statement should only be executed when the key is not found to avoid confusion.

    Suggested change
    LogConsumer.Debug("Key {0} not found in repository {1} ({2}ms)", key, repositoryName, repositoryStopwatch.ElapsedMilliseconds);
    return result;
    }
    if (result == null)
    {
    LogConsumer.Debug("Key {0} not found in repository {1} ({2}ms)", key, repositoryName, repositoryStopwatch.ElapsedMilliseconds);
    }
    return result;
    }

    {
    cancellationToken.ThrowIfCancellationRequested();

    if (_data.Count >= int.MaxValue)
    Copy link
    Copy Markdown

    Choose a reason for hiding this comment

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

    nitpick: Check for int.MaxValue is unnecessary for most practical scenarios.

    System memory will be exhausted before reaching int.MaxValue. Consider removing this check or using a more practical resource constraint.

    Comment on lines +47 to +56
    public async Task RemoveShouldRemoveStoredValue()
    {
    // Arrange
    var key = "test-key";
    _repository.Set("test-value", key);
    _repository.Remove(key);
    var Task1 = _repository.SetAsync("test-value", key).AsTask();
    var Task2 = _repository.RemoveAsync(key);

    await Task.WhenAll(Task1, Task2);
    // Act
    var exception = Assert.Throws<InvalidOperationException>(() =>
    _repository.Get<string>(key)
    var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => await _repository.GetAsync<string>(key)
    Copy link
    Copy Markdown

    Choose a reason for hiding this comment

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

    suggestion (testing): No test for MemoryCacheRepository GetAsync with subKey.

    Add tests for GetAsync with both key and subKey, including scenarios where the subKey is missing to verify exception handling.

    Comment on lines 26 to 29

    [Fact]
    public void SetToDatabaseShouldStoreValue()
    public async Task SetToDatabaseShouldStoreValue()
    {
    Copy link
    Copy Markdown

    Choose a reason for hiding this comment

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

    suggestion (testing): No test for CouchDBCacheRepository TryGetAsync with subKey.

    Add a test for TryGetAsync using both key and subKey, including a scenario where the subKey is missing, to verify correct handling.

    Suggested change
    [Fact]
    public void SetToDatabaseShouldStoreValue()
    public async Task SetToDatabaseShouldStoreValue()
    {
    [Fact]
    public async Task TryGetAsync_WithSubKey_ShouldReturnCorrectly()
    {
    // Arrange
    var key = "test-key";
    var subKey = "test-subkey";
    var value = "test-value";
    var missingSubKey = "missing-subkey";
    // Store value with subKey
    await _repository.SetAsync(value, key, subKey);
    // Act & Assert: Should retrieve successfully with correct subKey
    (bool found, object? retrieved) = await _repository.TryGetAsync<string>(key, subKey);
    found.Should().BeTrue();
    retrieved.Should().Be(value);
    // Act & Assert: Should not retrieve with missing subKey
    (bool foundMissing, object? retrievedMissing) = await _repository.TryGetAsync<string>(key, missingSubKey);
    foundMissing.Should().BeFalse();
    retrievedMissing.Should().BeNull();
    }
    [Fact]
    public async Task SetToDatabaseShouldStoreValue()
    {

    /// <exception cref="OperationCanceledException">If cancelled.</exception>
    /// <returns>void.</returns>
    public static async ValueTask SetAsync<T>(T value, [Localizable(false)] string key, CancellationToken cancellationToken = default)
    {
    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 (complexity): Consider refactoring the repeated repository iteration logic into two generic helper methods to greatly simplify and reduce duplication in the public API methods.

    Here’s one way to collapse all of these almost‐identical methods into two small helpersβ€”one for β€œrun on all repos in parallel” (e.g. SetAsync/RemoveAsync) and one for β€œquery until first success” (e.g. GetAsync/TryGetAsync). Then each public API method becomes a one–liner.

    // 1) helper for doing something in parallel on all repositories
    private static async ValueTask RunOnAllAsync(
        Func<ICacheRepository, CancellationToken, Task> action,
        string startMessage,
        string summaryMessage,
        CancellationToken cancellationToken = default
    )
    {
        cancellationToken.ThrowIfCancellationRequested();
        LogConsumer.Trace(startMessage, _repositories.Count);
    
        using var timeoutCts = new CancellationTokenSource(DefaultTimeout);
        using var linkedCts  = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
    
        var results = await Task.WhenAll(_repositories.Values.Select(async repo =>
        {
            try
            {
                await action(repo, linkedCts.Token);
                return true;
            }
            catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
            {
                LogConsumer.Info("Operation canceled by user");
                return false;
            }
            catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested)
            {
                LogConsumer.Error("Operation timed out");
                throw new TimeoutException("Cache operation timed out");
            }
            catch (Exception ex)
            {
                LogConsumer.Error("Cache error in {0}: {1}", repo.GetType().Name, ex.Message);
                return false;
            }
        }));
    
        LogConsumer.Info(summaryMessage,
                         results.Count(r => r),
                         _repositories.Count);
    }
    
    // 2) helper for querying each repo in turn until one succeeds
    private static async ValueTask<T> QueryFirstAsync<T>(
        Func<ICacheRepository, CancellationToken, Task<T>> query,
        string startMessage,
        string notFoundMessage,
        CancellationToken cancellationToken = default
    )
    {
        cancellationToken.ThrowIfCancellationRequested();
        LogConsumer.Trace(startMessage, _repositories.Count);
    
        using var timeoutCts = new CancellationTokenSource(DefaultTimeout);
        using var linkedCts  = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
    
        foreach (var repo in _repositories.Values)
        {
            try
            {
                var result = await query(repo, linkedCts.Token);
                LogConsumer.Info("Found in {0}", repo.GetType().Name);
                return result;
            }
            catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
            {
                LogConsumer.Info("Operation canceled by user");
                throw;
            }
            catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested)
            {
                LogConsumer.Error("Operation timed out");
                throw new TimeoutException("Cache operation timed out");
            }
            catch (Exception ex)
            {
                LogConsumer.Error("Cache error in {0}: {1}",
                                  repo.GetType().Name,
                                  ex.Message);
            }
        }
    
        throw new InvalidOperationException(notFoundMessage);
    }

    Now your public methods shrink to:

    public static ValueTask SetAsync<T>(T value, string key, CancellationToken ct = default)
        => RunOnAllAsync(
             (repo, c) => repo.SetAsync(value, key, cancellationToken: c),
             "Adding {0} to {1} cache repositories",
             "Successfully set {0} in {1} out of {2} repositories",
             ct
           );
    
    public static ValueTask RemoveAsync(string key, CancellationToken ct = default)
        => RunOnAllAsync(
             (repo, c) => repo.RemoveAsync(key, cancellationToken: c),
             "Removing key {0} from {1} repositories",
             "Removed key {0} from all repositories",
             ct
           );
    
    public static ValueTask<T> GetAsync<T>(string key, CancellationToken ct = default)
        => QueryFirstAsync(
             (repo, c) => repo.GetAsync<T>(key, cancellationToken: c),
             "Getting {0} from {1} cache repositories",
             $"Unable to get the item with key {key}",
             ct
           );

    You can parameterize those lambdas to handle sub-keys and TTLs, and similarly replace all of your SetToAsync, GetFromAsync, TryGetAsync, etc. This collapses hundreds of lines into one helper + one small invocation per overload.

    public void Set<T>(T value, string key, TimeSpan? ttl = null)
    /// <param name="cancellationToken">To cancel operation.</param>
    /// <returns>A valuetask representing the asynchronous operation.</returns>
    public async ValueTask SetAsync<T>(T value, string key, TimeSpan? ttl = null, CancellationToken cancellationToken = default)
    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 (complexity): Consider extracting common try/catch and cancellation logic into reusable helper methods to dramatically simplify each public API method into concise one-liners.

    Here’s one way to collapse almost all of the per‐method boilerplate into two small helpers and turn your 30–50 line methods into one‐ or two‐liners. All functionality (cancellation, exception‐suppression, TTL, exists‐checks, etc.) remains intact, but you no longer repeat the same try/catch/ThrowIfCancelled logic everywhere.

    // inside RedisCacheRepository
    private async ValueTask ExecuteAsync(
        Func<CancellationToken, Task> op,
        CancellationToken ct)
    {
        ct.ThrowIfCancellationRequested();
        try
        {
            await op(ct).ConfigureAwait(false);
        }
        catch (OperationCanceledException)
        {
            throw;
        }
        catch (Exception e) when (!ShouldPropagateExceptions)
        {
            HandleException(e);
        }
    }
    
    // for return‐value methods
    private async ValueTask<T> ExecuteAsync<T>(
        Func<CancellationToken, Task<T>> op,
        CancellationToken ct,
        string errorMsg)
    {
        ct.ThrowIfCancellationRequested();
        try
        {
            return await op(ct).ConfigureAwait(false);
        }
        catch (OperationCanceledException)
        {
            throw;
        }
        catch (Exception e) when (!ShouldPropagateExceptions)
        {
            HandleException(e);
            throw new InvalidOperationException(errorMsg, e);
        }
    }

    Now each public API is a one‐liner:

    public ValueTask SetAsync<T>(
        T value,
        string key,
        TimeSpan? ttl = null,
        CancellationToken ct = default)
        => ExecuteAsync(
           token => ttl.HasValue
             ? _cacheClient.Db0.AddAsync(key, value, ttl.Value)
             : _cacheClient.Db0.AddAsync(key, value),
           ct);
    
    public ValueTask<T> GetAsync<T>(
        string key,
        CancellationToken ct = default)
        => ExecuteAsync(async token =>
        {
            if (!await _cacheClient.Db0.ExistsAsync(key).ConfigureAwait(false))
                throw new InvalidOperationException($"Unable to get the item with key {key}");
            return await _cacheClient.Db0.GetAsync<T>(key, CommandFlags.PreferReplica)
                                         .ConfigureAwait(false);
        }, ct, $"Unable to get the item with key {key}");
    
    public ValueTask<(bool Success, T Value)> TryGetAsync<T>(
        string key,
        CancellationToken ct = default)
        => ExecuteAsync(async token =>
        {
            if (!await _cacheClient.Db0.ExistsAsync(key).ConfigureAwait(false))
                return (false, default(T));
            var val = await _cacheClient.Db0.GetAsync<T>(key, CommandFlags.PreferReplica)
                                            .ConfigureAwait(false);
            return (true, val);
        }, ct, null);

    You can apply the same pattern to hash‐sets, TTL, RemoveAsync, Clear, etc. This removes all the duplicated try/catch, cancellation checks, Wait()/.Result, and huge XML comments from each method.

    /// <exception cref="InvalidOperationException">Thrown in case the operation fails.</exception>
    public T GetSpecific<T>(string key)
    public async Task<T> GetSpecificAsync<T>(string key, CancellationToken cancellationToken = default)
    where T : CouchDBCacheDocument
    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 (complexity): Consider introducing a single generic async executor method to centralize error handling, cancellation, and database resolution logic for all async entry-points.

    Here’s one way to dramatically DRY out almost all of your async entry‐points by introducing a single generic executor. All of your methods then become one‐liners that pass to this executor instead of re‐implementing the same try/catch/ResolveDatabase/ThrowIfCancellationRequested logic everywhere.

    // 1) Put this in your class as a private helper
    private async Task<TResult> ExecuteAsync<TDoc, TResult>(
        Func<CouchDatabase<TDoc>, CancellationToken, Task<TResult>> work,
        string operationDescription,
        CancellationToken ct = default
    )
        where TDoc : CouchDBCacheDocument
    {
        try
        {
            ct.ThrowIfCancellationRequested();
            var db = await ResolveDatabase<TDoc>().ConfigureAwait(false);
            return await work(db, ct).ConfigureAwait(false);
        }
        catch (OperationCanceledException)
        {
            LogConsumer.Warning($"Operation cancelled: {operationDescription}");
            throw;
        }
        catch (Exception ex)
        {
            if (ShouldPropagateExceptions) throw;
            LogConsumer.Handle(ex);
            throw new InvalidOperationException(operationDescription, ex);
        }
    }
    // 2) Refactor GetSpecificAsync
    public Task<T> GetSpecificAsync<T>(string key, CancellationToken ct = default)
        where T : CouchDBCacheDocument
        => ExecuteAsync<T, T>(
            async (db, token) =>
            {
                var doc = await db.FindAsync(key).ConfigureAwait(false);
                if (doc != null && doc.ExpiresAt <= DateTime.UtcNow)
                {
                    await RemoveSpecificAsync<T>(key, token).ConfigureAwait(false);
                    return default;
                }
                return doc;
            },
            $"Unable to get item with key: {key}",
            ct
        );
    // 3) Refactor RemoveSpecificAsync
    public Task RemoveSpecificAsync<T>(string key, CancellationToken ct = default)
        where T : CouchDBCacheDocument
        => ExecuteAsync<T, object>(
            async (db, token) =>
            {
                var doc = await db.FindAsync(key).ConfigureAwait(false);
                if (doc != null)
                {
                    await db.DeleteAsync(doc).ConfigureAwait(false);
                    LogConsumer.Info($"Removed document {key}");
                }
                else
                {
                    LogConsumer.Info($"Document {key} not found");
                }
                return null;
            },
            $"Failed removing document with key: {key}",
            ct
        );
    // 4) Refactor SetSpecificAsync
    public Task SetSpecificAsync<T>(T value, string key, TimeSpan? ttl = null, CancellationToken ct = default)
        where T : CouchDBCacheDocument
        => ExecuteAsync<T, object>(
            async (db, token) =>
            {
                value.Key = key;
                if (ttl != null)
                {
                    value.TTL = ttl.Value;
                    value.ExpiresAt = DateTime.UtcNow.Add(ttl.Value);
                }
                await db.CreateAsync(value).ConfigureAwait(false);
                return null;
            },
            $"Failed setting document with key: {key}",
            ct
        );

    You can then delete all of the duplicated try/catch/cancellation/ResolveDatabase boilerplate in every method. The sync counterparts can either call .GetAwaiter().GetResult() on these or be slim wrappers around them as well.

    @penify-dev
    Copy link
    Copy Markdown
    Contributor

    penify-dev Bot commented Jul 29, 2025

    PR Code Suggestions ✨

    CategorySuggestionΒ  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Β  Score
    Best practice
    Change the return type of the TTLAsync method to better reflect its asynchronous nature

    Ensure that the TTLAsync method is properly marked as asynchronous by returning a
    ValueTask instead of a Task.

    Src/CrispyWaffle/Cache/MemoryCacheRepository.cs [244]

    -public Task<TimeSpan> TTLAsync(string key, CancellationToken cancellationToken = default)
    +public ValueTask<TimeSpan> TTLAsync(string key, CancellationToken cancellationToken = default)
     
    Suggestion importance[1-10]: 8

    Why: Changing the return type to ValueTask<TimeSpan> is a good practice for asynchronous methods that may complete synchronously, improving performance and clarity in the method's intent.

    8
    Consider changing the return type from ValueTask to Task for better clarity on method usage

    Ensure that the ValueTask return type is appropriate for the methods, as it is generally
    used for methods that are expected to complete quickly and may not require allocation of a
    Task. If the methods are expected to be long-running, consider using Task instead.

    Src/CrispyWaffle/Cache/ICacheRepository.cs [34]

    -ValueTask SetAsync<T>(T value, string key, TimeSpan? ttl = null, CancellationToken cancellationToken = default);
    +Task SetAsync<T>(T value, string key, TimeSpan? ttl = null, CancellationToken cancellationToken = default);
     
    Suggestion importance[1-10]: 7

    Why: While changing ValueTask to Task can improve clarity, it may not be necessary if the methods are indeed expected to complete quickly. This suggestion addresses a potential performance concern but is not critical.

    7
    Improve error logging before handling exceptions

    Ensure that the exception handling in the SendAsync method properly logs the exception
    before handling it, to maintain visibility into errors.

    Src/CrispyWaffle.Utils/Communications/SmtpMailer.cs [333-334]

    +TelemetryAnalytics.TrackMetric("SMTPError", e.Message);
     var result = await HandleExtension(e, cacheKey);
     
    Suggestion importance[1-10]: 7

    Why: While adding logging for the exception is a good practice, the suggestion does not address a critical issue and is more of a style improvement.

    7
    Enhancement
    Utilize the CancellationToken parameter in method implementations for better cancellation support

    Ensure that the CancellationToken parameter is utilized in the method implementations to
    allow for cancellation of the operations, enhancing responsiveness and resource
    management.

    Src/CrispyWaffle/Cache/ICacheRepository.cs [34]

    -ValueTask SetAsync<T>(T value, string key, TimeSpan? ttl = null, CancellationToken cancellationToken = default);
    +// Implementation should use cancellationToken to support cancellation
     
    Suggestion importance[1-10]: 8

    Why: Utilizing the CancellationToken is important for improving responsiveness and resource management, especially in asynchronous operations. This is a significant enhancement for the method's usability.

    8
    Enhance the method to support cancellation of asynchronous operations

    Consider using a cancellation token in the HandleExtension method to allow for
    cancellation of the asynchronous operation if needed.

    Src/CrispyWaffle.Utils/Communications/SmtpMailer.cs [375]

    -private static async Task<bool> HandleExtension(Exception e, string cacheKey)
    +private static async Task<bool> HandleExtension(Exception e, string cacheKey, CancellationToken cancellationToken)
     
    Suggestion importance[1-10]: 6

    Why: Adding a cancellation token is a useful enhancement, but it is not crucial for the current functionality of the method.

    6
    Test improvement
    Validate that the removed value is indeed no longer retrievable

    Ensure that the test for RemoveShouldRemoveStoredValue verifies that the value is indeed
    removed by checking the result after removal.

    Tests/CrispyWaffle.Tests/Cache/MemoryCacheRepositoryTests.cs [56-57]

    -var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => await _repository.GetAsync<string>(key);
    +var actualValue = await _repository.GetAsync<string>(key);
    +actualValue.Should().BeNull();
     
    Suggestion importance[1-10]: 8

    Why: This suggestion addresses a significant issue in the test by ensuring that the removal of the value is properly validated, which is essential for test accuracy.

    8
    Ensure the test sets a value before retrieval to validate functionality correctly

    In the GetFromDatabaseShouldReturnStoredValue test, ensure that the value is set before
    attempting to retrieve it to avoid false negatives.

    Tests/CrispyWaffle.Tests/Cache/CouchDBCacheRepositoryTests.cs [47]

    +await _repository.SetAsync("test-value", key);
     var actualValue = await _repository.GetAsync<string>(key);
     
    Suggestion importance[1-10]: 8

    Why: This suggestion is important as it ensures that the test accurately reflects the intended functionality by setting a value before retrieval, preventing false negatives.

    8
    Testing
    Add assertions to verify the behavior of RemoveAsync when the key does not exist

    Consider adding assertions to verify that the RemoveAsync method correctly handles cases
    where the key does not exist, ensuring robustness in the implementation.

    Tests/CrispyWaffle.IntegrationTests/Cache/CouchDBCacheRepositoryTests.cs [72]

    -await _repository.RemoveAsync(doc.Key);
    +var result = await _repository.RemoveAsync(doc.Key);
    +Assert.True(result); // Assuming RemoveAsync returns a boolean indicating success
     
    Suggestion importance[1-10]: 7

    Why: Adding assertions for edge cases like non-existent keys enhances the robustness of the tests, making it a valuable suggestion for improving test coverage and reliability.

    7
    Add handling for CancellationToken in tests to ensure proper behavior during cancellation

    Ensure that the tests are properly handling exceptions and edge cases, such as when the
    CancellationToken is triggered, to verify that the methods behave correctly under
    cancellation scenarios.

    Tests/CrispyWaffle.Tests/Cache/CacheManagerTests.cs [103]

    -Func<Task> act = async () => await CacheManager.GetAsync<dynamic>(key);
    +Func<Task> act = async () => await CacheManager.GetAsync<dynamic>(key, CancellationToken.None);
     
    Suggestion importance[1-10]: 6

    Why: Adding handling for CancellationToken in tests is a good practice to ensure that the methods behave correctly during cancellation scenarios, but it is a minor improvement in the context of overall test coverage.

    6
    Performance
    Optimize the return type for completed tasks in the remove methods

    Consider using ValueTask.CompletedTask instead of Task.CompletedTask for better
    performance in the RemoveAsync methods.

    Src/CrispyWaffle/Cache/MemoryCacheRepository.cs [213]

    -return Task.CompletedTask;
    +return ValueTask.CompletedTask;
     
    Suggestion importance[1-10]: 6

    Why: This suggestion is valid as it can improve performance by using ValueTask for completed tasks, but the impact may be minor in this context.

    6
    Possible issue
    Change the behavior of the GetAsync method to return a default value when the key is not found

    In the GetAsync method, consider returning a default value instead of throwing an
    exception when the key is not found, to improve usability.

    Src/CrispyWaffle/Cache/MemoryCacheRepository.cs [92-93]

    -throw new InvalidOperationException($"Unable to get the item with key {key}");
    +return default;
     
    Suggestion importance[1-10]: 4

    Why: While returning a default value can improve usability, it may hide errors in the code where the absence of a key is unexpected, making this a less favorable change.

    4
    Update the handling of existing keys in the cache during the set operation

    Ensure that the SetAsync method properly handles the case where the key already exists in
    the cache, potentially updating the value instead of just adding it.

    Src/CrispyWaffle/Cache/MemoryCacheRepository.cs [54]

    -_data.AddOrUpdate(key, value, (_, _) => value);
    +_data.AddOrUpdate(key, value, (_, oldValue) => value);
     
    Suggestion importance[1-10]: 3

    Why: The current implementation already uses _data.AddOrUpdate, which updates the value if the key exists. The suggestion does not provide a significant improvement.

    3

    Copy link
    Copy Markdown

    @korbit-ai korbit-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.

    Review by Korbit AI

    Korbit automatically attempts to detect when you fix issues in new commits.
    Category Issue Status
    Readability Implicit whitespace handling β–Ή view
    Performance Inefficient Document Lookup β–Ή view
    Performance Inefficient Document Counting β–Ή view
    Logging Misleading Cache Hit Log Message β–Ή view
    Error Handling Inconsistent logging mechanism β–Ή view
    Performance Unnecessary Error List Allocation β–Ή view
    Files scanned ​
    File Path Reviewed
    Src/CrispyWaffle/Cache/ICacheRepository.cs βœ…
    Src/CrispyWaffle.Utils/Communications/SmtpMailer.cs βœ…
    Src/CrispyWaffle/Cache/MemoryCacheRepository.cs βœ…
    Src/CrispyWaffle.Redis/Cache/RedisCacheRepository.cs βœ…
    Src/CrispyWaffle.CouchDB/Cache/CouchDBCacheRepository.cs βœ…
    Src/CrispyWaffle/Cache/CacheManager.cs βœ…

    Explore our documentation to understand the languages and file types we support and the files we ignore.

    Check out our docs on how you can make Korbit work best for you and your team.

    Loving Korbit!? Share us on LinkedIn Reddit and X

    Comment thread Src/CrispyWaffle.CouchDB/Cache/CouchDBCacheRepository.cs
    Comment thread Src/CrispyWaffle.CouchDB/Cache/CouchDBCacheRepository.cs
    Comment thread Src/CrispyWaffle.CouchDB/Cache/CouchDBCacheRepository.cs
    Comment thread Src/CrispyWaffle/Cache/CacheManager.cs
    Comment thread Src/CrispyWaffle/Cache/CacheManager.cs
    Comment thread Src/CrispyWaffle/Cache/CacheManager.cs
    Copy link
    Copy Markdown

    @coderabbitai coderabbitai Bot left a comment

    Choose a reason for hiding this comment

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

    Actionable comments posted: 19

    πŸ”­ Outside diff range comments (2)
    Tests/CrispyWaffle.Tests/Cache/CacheManagerTests.cs (1)

    147-162: Fix incorrect async test pattern

    The test method is not async but uses async assertions. This will cause the test to complete before the async operation finishes, potentially leading to false positives.

    -public void RemoveFromShouldThrowWhenRepositoryNotFound()
    +public async Task RemoveFromAsyncShouldThrowWhenRepositoryNotFound()
     {
         // Arrange
         var key = "testKey";
         CacheManager.AddRepository(_mockCacheRepository);
     
         // Act
         Func<Task> act = async () => await CacheManager.RemoveFrom<ICacheRepository>(key);
     
         // Assert
    -    act.Should()
    -        .ThrowAsync<InvalidOperationException>()
    +    await act.Should()
    +        .ThrowAsync<InvalidOperationException>()
             .WithMessage(
                 "The repository of type CrispyWaffle.Cache.ICacheRepository isn't available in the repositories providers list"
             );
     }
    Src/CrispyWaffle.CouchDB/Cache/CouchDBCacheRepository.cs (1)

    81-81: Replace Task.WaitAll with await Task.WhenAll

    Using Task.WaitAll in an async method blocks the thread and defeats the purpose of async/await. This should use await Task.WhenAll instead.

    -Task.WaitAll(tasks.ToArray());
    +await Task.WhenAll(tasks);
    🧹 Nitpick comments (4)
    Tests/CrispyWaffle.Tests/Cache/CacheManagerTests.cs (2)

    112-112: Update test method name to reflect async operation

    The test method name should be updated from SetToShouldThrowWhenRepositoryNotFound to SetToAsyncShouldThrowWhenRepositoryNotFound to accurately reflect that it's testing the async method.

    -public async Task SetToShouldThrowWhenRepositoryNotFound()
    +public async Task SetToAsyncShouldThrowWhenRepositoryNotFound()

    131-131: Update test method name to reflect async operation

    The test method name should be updated to TTLAsyncShouldReturnCorrectTTLFromRepositories to accurately reflect that it's testing the async method.

    -public async Task TTLShouldReturnCorrectTTLFromRepositories()
    +public async Task TTLAsyncShouldReturnCorrectTTLFromRepositories()
    Src/CrispyWaffle/Cache/MemoryCacheRepository.cs (2)

    87-97: Remove unnecessary async keyword

    The method doesn't use await so the async keyword is unnecessary. ValueTask can be created directly.

    -public async ValueTask<T> GetAsync<T>(string key, CancellationToken cancellationToken = default)
    +public ValueTask<T> GetAsync<T>(string key, CancellationToken cancellationToken = default)
     {
         cancellationToken.ThrowIfCancellationRequested();
     
         if (!_data.TryGetValue(key, out var value))
         {
             throw new InvalidOperationException($"Unable to get the item with key {key}");
         }
     
    -    return (T)value;
    +    return new ValueTask<T>((T)value);
     }

    208-211: Remove redundant ContainsKey check

    The ContainsKey check is redundant because TryRemove safely handles non-existent keys by returning false.

    -if (_data.ContainsKey(key))
    -{
    -    _data.TryRemove(key, out _);
    -}
    +_data.TryRemove(key, out _);
    πŸ“œ Review details

    Configuration used: CodeRabbit UI
    Review profile: CHILL
    Plan: Pro

    πŸ“₯ Commits

    Reviewing files that changed from the base of the PR and between f65b491 and 3af3711.

    πŸ“’ Files selected for processing (10)
    • Src/CrispyWaffle.CouchDB/Cache/CouchDBCacheRepository.cs (16 hunks)
    • Src/CrispyWaffle.Redis/Cache/RedisCacheRepository.cs (11 hunks)
    • Src/CrispyWaffle.Utils/Communications/SmtpMailer.cs (6 hunks)
    • Src/CrispyWaffle/Cache/CacheManager.cs (22 hunks)
    • Src/CrispyWaffle/Cache/ICacheRepository.cs (3 hunks)
    • Src/CrispyWaffle/Cache/MemoryCacheRepository.cs (4 hunks)
    • Tests/CrispyWaffle.IntegrationTests/Cache/CouchDBCacheRepositoryTests.cs (2 hunks)
    • Tests/CrispyWaffle.Tests/Cache/CacheManagerTests.cs (4 hunks)
    • Tests/CrispyWaffle.Tests/Cache/CouchDBCacheRepositoryTests.cs (2 hunks)
    • Tests/CrispyWaffle.Tests/Cache/MemoryCacheRepositoryTests.cs (2 hunks)
    🧰 Additional context used
    πŸͺ› GitHub Check: linter-check
    Src/CrispyWaffle.Utils/Communications/SmtpMailer.cs

    [failure] 350-350:
    File is not formatted correctly. Run 'dotnet csharpier .' to fix.

    Tests/CrispyWaffle.Tests/Cache/MemoryCacheRepositoryTests.cs

    [failure] 56-56:
    File is not formatted correctly. Run 'dotnet csharpier .' to fix.

    Tests/CrispyWaffle.Tests/Cache/CouchDBCacheRepositoryTests.cs

    [failure] 65-65:
    File is not formatted correctly. Run 'dotnet csharpier .' to fix.

    Tests/CrispyWaffle.Tests/Cache/CacheManagerTests.cs

    [failure] 55-55:
    File is not formatted correctly. Run 'dotnet csharpier .' to fix.

    Src/CrispyWaffle.Redis/Cache/RedisCacheRepository.cs

    [failure] 150-150:
    File is not formatted correctly. Run 'dotnet csharpier .' to fix.

    Src/CrispyWaffle/Cache/ICacheRepository.cs

    [failure] 34-34:
    File is not formatted correctly. Run 'dotnet csharpier .' to fix.

    Tests/CrispyWaffle.IntegrationTests/Cache/CouchDBCacheRepositoryTests.cs

    [failure] 45-45:
    File is not formatted correctly. Run 'dotnet csharpier .' to fix.

    Src/CrispyWaffle/Cache/MemoryCacheRepository.cs

    [failure] 45-45:
    File is not formatted correctly. Run 'dotnet csharpier .' to fix.

    Src/CrispyWaffle/Cache/CacheManager.cs

    [failure] 123-123:
    File is not formatted correctly. Run 'dotnet csharpier .' to fix.

    Src/CrispyWaffle.CouchDB/Cache/CouchDBCacheRepository.cs

    [failure] 124-124:
    File is not formatted correctly. Run 'dotnet csharpier .' to fix.

    ⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
    • GitHub Check: Analyze (csharp)
    • GitHub Check: Deep Source Coverage report
    • GitHub Check: Codacy Security Scan
    πŸ”‡ Additional comments (19)
    Src/CrispyWaffle.Utils/Communications/SmtpMailer.cs (4)

    11-11: Good addition of Threading namespace for cancellation support.

    The addition of System.Threading namespace is appropriate for the async cache operations that now support cancellation tokens.


    333-333: Proper async handling of exception extension.

    The HandleExtension method call is correctly awaited, maintaining the asynchronous flow.


    375-375: Proper async method signature conversion.

    The HandleExtension method signature is correctly updated to return Task<bool> for async operation.


    384-384: Correct async cache operation.

    The CacheManager.SetAsync call is properly awaited with appropriate timeout configuration.

    Tests/CrispyWaffle.Tests/Cache/MemoryCacheRepositoryTests.cs (3)

    2-2: Good addition of Task namespace for async tests.

    Adding System.Threading.Tasks is appropriate for the async test methods.


    16-29: LGTM - Proper async test conversion.

    The test method is correctly converted to async with proper await usage on repository operations.


    32-44: LGTM - Consistent async pattern.

    The test follows the same proper async pattern as the previous test.

    Tests/CrispyWaffle.Tests/Cache/CouchDBCacheRepositoryTests.cs (3)

    1-1: Good addition of Task namespace.

    Adding System.Threading.Tasks is appropriate for async test methods.


    28-38: LGTM - Proper async test conversion.

    The test method correctly uses async/await pattern with the repository operations.


    41-51: LGTM - Consistent async testing.

    The test follows proper async patterns and correctly awaits repository operations.

    Tests/CrispyWaffle.IntegrationTests/Cache/CouchDBCacheRepositoryTests.cs (4)

    27-38: LGTM - Proper async integration test.

    The integration test correctly uses async/await patterns for CouchDB repository operations.


    41-63: LGTM - Comprehensive async testing with specific operations.

    The test properly exercises both regular and specific async operations with correct await usage.


    94-108: Excellent concurrent execution pattern.

    The concurrent execution of multiple SetAsync operations using Task.WhenAll is a good testing approach for database operations.


    111-125: LGTM - Proper TTL testing with async operations.

    The TTL test correctly uses async operations for both setting and getting cached values with timeout verification.

    Src/CrispyWaffle/Cache/ICacheRepository.cs (4)

    2-3: Good namespace additions for async support.

    Adding System.Threading and System.Threading.Tasks is appropriate for the async interface methods.


    90-90: Excellent improvement: tuple return instead of out parameters.

    The change from out parameters to tuple return (bool Success, T Value) is a significant improvement that makes the API more modern and async-friendly.


    108-108: Proper async method signature for Remove operations.

    Using Task return type for RemoveAsync is appropriate since removal operations are typically always asynchronous.


    125-131: LGTM - Consistent async method signatures.

    Both TTLAsync and Clear methods have appropriate return types and cancellation support.

    Src/CrispyWaffle/Cache/CacheManager.cs (1)

    829-830: Fix incorrect return type documentation

    The XML documentation says the method returns <c>true</c> if the value was found; otherwise, <c>false</c>, but the actual method signature returns a tuple (bool Success, T Value).

    -/// <returns><c>true</c> if the value was found; otherwise, <c>false</c>.</returns>
    +/// <returns>A tuple containing Success (true if found) and Value (the cached item or default(T)).</returns>

    Likely an incorrect or invalid review comment.

    Comment thread Src/CrispyWaffle.CouchDB/Cache/CouchDBCacheRepository.cs
    Comment thread Src/CrispyWaffle.CouchDB/Cache/CouchDBCacheRepository.cs
    Comment thread Src/CrispyWaffle.CouchDB/Cache/CouchDBCacheRepository.cs
    Comment thread Src/CrispyWaffle.Redis/Cache/RedisCacheRepository.cs
    Comment thread Src/CrispyWaffle.Redis/Cache/RedisCacheRepository.cs
    Comment thread Tests/CrispyWaffle.Tests/Cache/CacheManagerTests.cs
    Comment thread Tests/CrispyWaffle.Tests/Cache/CacheManagerTests.cs
    Comment thread Tests/CrispyWaffle.Tests/Cache/CouchDBCacheRepositoryTests.cs
    Comment thread Tests/CrispyWaffle.Tests/Cache/MemoryCacheRepositoryTests.cs
    @AppVeyorBot
    Copy link
    Copy Markdown

    @gstraccini gstraccini Bot added the gitauto GitAuto label to trigger the app in a issue. label Aug 8, 2025
    @AppVeyorBot
    Copy link
    Copy Markdown

    @guibranco guibranco removed the gitauto GitAuto label to trigger the app in a issue. label Dec 4, 2025
    @gstraccini gstraccini Bot added the gitauto GitAuto label to trigger the app in a issue. label Dec 4, 2025
    @AppVeyorBot
    Copy link
    Copy Markdown

    @guibranco guibranco changed the title feat: update cachemanage and cachereposiotry implementations to async. [FEATURE] Update cachemanage and cachereposiotry implementations to async. Dec 17, 2025
    @guibranco guibranco changed the title [FEATURE] Update cachemanage and cachereposiotry implementations to async. [FEATURE] Update cachemanager and cacherepository implementations to async Jan 12, 2026
    @AppVeyorBot
    Copy link
    Copy Markdown

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

    Labels

    cache ♻️ code quality Code quality-related tasks or issues communications CouchDB CouchDB related issues/pull requests 🎲 database Database-related operations πŸ“ documentation Tasks related to writing or updating documentation enhancement New feature or request gitauto GitAuto label to trigger the app in a issue. hacktoberfest Participation in the Hacktoberfest event πŸ•” high effort A task that can be completed in a few days .NET Pull requests that update .net code performance Redis Review effort [1-5]: 4 πŸ§‘β€πŸ’» tech-debit Technical debt that needs to be addressed πŸ§ͺ tests Tasks related to testing ⏳ waiting response Waiting on a response from another party

    Projects

    None yet

    Development

    Successfully merging this pull request may close these issues.

    [FEATURE] Add async operations for CacheManager and ICacheRepository classes

    5 participants