Skip to content

feat: Distributed cache interceptor for queries (ICacheableQuery<TResponse>)#152

Open
Copilot wants to merge 5 commits intomainfrom
copilot/add-distributed-cache-interceptor
Open

feat: Distributed cache interceptor for queries (ICacheableQuery<TResponse>)#152
Copilot wants to merge 5 commits intomainfrom
copilot/add-distributed-cache-interceptor

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 27, 2026

Adds transparent IDistributedCache caching for queries via a pipeline interceptor, eliminating per-handler cache boilerplate. Caching is opt-in per query type via ICacheableQuery<TResponse>.

New contracts (NetEvolve.Pulse.Extensibility)

  • ICacheableQuery<TResponse> — extends IQuery<TResponse> with CacheKey and Expiry properties.
  • CacheExpirationMode — enum to distinguish between Absolute (default) and Sliding expiration.
  • QueryCachingOptions — options class with:
    • JsonSerializerOptions — for custom cache serialization (defaults to JsonSerializerOptions.Default)
    • ExpirationMode — absolute vs. sliding expiry (defaults to Absolute)
    • DefaultExpiry — fallback TimeSpan? applied when ICacheableQuery<TResponse>.Expiry returns null (defaults to null, meaning no explicit expiration)

New interceptor (NetEvolve.Pulse)

  • DistributedCacheQueryInterceptor<TQuery, TResponse> — registered as IRequestInterceptor<,> (scoped). Behavior:
    • Non-ICacheableQuery queries pass through unchanged.
    • Cache hit → deserializes and returns; handler is skipped.
    • Cache miss → invokes handler, serializes response via System.Text.Json, stores with optional expiry.
    • IDistributedCache not registered → falls through silently.
    • Serialization uses the JsonSerializerOptions from QueryCachingOptions.
    • Effective expiry is resolved as ICacheableQuery<TResponse>.Expiry ?? QueryCachingOptions.DefaultExpiry; applied as absolute or sliding based on ExpirationMode.

Registration

  • AddQueryCaching(Action<QueryCachingOptions>? configure = null) added to IMediatorConfigurator and implemented in MediatorConfigurator. Options are registered via the standard Options pattern.
services.AddDistributedMemoryCache();
services.AddPulse(c => c.AddQueryCaching(options =>
{
    options.ExpirationMode = CacheExpirationMode.Sliding;
    options.DefaultExpiry = TimeSpan.FromMinutes(10); // fallback when query Expiry is null
    options.JsonSerializerOptions = new JsonSerializerOptions
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    };
}));

public record GetProductQuery(Guid Id) : ICacheableQuery<ProductDto>
{
    public string? CorrelationId { get; set; }
    public string CacheKey => $"product:{Id}";
    public TimeSpan? Expiry => TimeSpan.FromMinutes(5); // overrides DefaultExpiry when set
}

Test coverage

  • Unit tests: cache hit, cache miss, expiry, null expiry, no-cache fallthrough, non-cacheable pass-through, expired entry refresh, sliding expiration mode, custom JsonSerializerOptions, DefaultExpiry applied when query expiry is null, query expiry overrides DefaultExpiry.
  • Integration tests: end-to-end with MemoryDistributedCache verifying second-invocation cache hit, non-cacheable query bypass, no-cache fallthrough, sliding expiration, custom JSON options, and DefaultExpiry fallback.

Breaking change (interface)

Existing IMediatorConfigurator stub implementations in tests (Polly, SqlServer, EntityFramework) updated to satisfy the updated AddQueryCaching(Action<QueryCachingOptions>? configure = null) signature.


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI linked an issue Mar 27, 2026 that may be closed by this pull request
9 tasks
Copilot AI changed the title [WIP] Implement caching interceptor for queries with distributed cache feat: Distributed cache interceptor for queries (ICacheableQuery<TResponse>) Mar 28, 2026
Copilot AI requested a review from samtrion March 28, 2026 00:15
@samtrion samtrion force-pushed the copilot/add-distributed-cache-interceptor branch from 6c64782 to bd52217 Compare March 28, 2026 00:16
@samtrion
Copy link
Copy Markdown
Contributor

@copilot Please update the README files for each project and the solution readme

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 28, 2026

@copilot Please update the README files for each project and the solution readme

Done in commit 16dcb19. Updated the three READMEs directly affected by the new feature:

  • Root README.md — added distributed query caching to the solution features list
  • src/NetEvolve.Pulse/README.md — added distributed query caching to the features list and a new "Distributed Query Caching" configuration section with a behavior table
  • src/NetEvolve.Pulse.Extensibility/README.md — added ICacheableQuery<TResponse> to the features list and a new "Cacheable Queries" usage section with a full code example

The provider packages (Polly, EntityFramework, SqlServer, Dapr) were left unchanged as query caching is unrelated to their functionality.

@samtrion
Copy link
Copy Markdown
Contributor

@copilot Please solve open comments

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 28, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.48%. Comparing base (19680ce) to head (294fa7c).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #152      +/-   ##
==========================================
+ Coverage   86.12%   86.48%   +0.36%     
==========================================
  Files          42       44       +2     
  Lines        1312     1347      +35     
  Branches      123      130       +7     
==========================================
+ Hits         1130     1165      +35     
  Misses        145      145              
  Partials       37       37              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 28, 2026

@copilot Please solve open comments

Addressed in commit 6f7e513:

  • Added CacheExpirationMode enum (Absolute / Sliding) in NetEvolve.Pulse.Extensibility
  • Added QueryCachingOptions class in NetEvolve.Pulse.Extensibility with JsonSerializerOptions (for custom serialization) and ExpirationMode (to choose between absolute and sliding expiration)
  • Updated AddQueryCaching(Action<QueryCachingOptions>? configure = null) on IMediatorConfigurator to accept optional configuration
  • Updated DistributedCacheQueryInterceptor to inject IOptions<QueryCachingOptions> and use the configured options for both serialization and expiry mode
  • Extended unit and integration tests to cover both new options

Copilot AI requested a review from samtrion March 28, 2026 01:07
@samtrion samtrion force-pushed the copilot/add-distributed-cache-interceptor branch from 6f7e513 to ed9f327 Compare March 28, 2026 07:58
Extract logic for setting DistributedCacheEntryOptions into a new private GetCacheEntryOptions method. This improves readability and maintainability by centralizing expiration mode handling and reducing code duplication.
Copilot AI requested a review from samtrion March 28, 2026 08:21
@samtrion samtrion marked this pull request as ready for review March 28, 2026 08:28
@samtrion samtrion requested a review from a team as a code owner March 28, 2026 08:28
return this;
}

/// <inheritdoc />
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Implemented IMediatorConfigurator.AddQueryCaching(Action?) — adding this required interface method breaks existing implementers. Add a default interface implementation or keep the interface unchanged.

Details

✨ AI Reasoning
​The concrete MediatorConfigurator type now includes a new public AddQueryCaching method to match the updated interface. While adding the implementation itself is fine, the underlying interface addition forces all external implementers of IMediatorConfigurator to add the same method, breaking source compatibility.

🔧 How do I fix it?
Support both old and new parameter names during transition periods. Add new optional parameters with defaults. Keep existing response fields while adding new ones. Focus on backwards compatibility.

Reply @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Distributed cache interceptor for queries (ICacheableQuery)

2 participants