feat(caching): extract IBlogPostCacheService abstraction and centralize cache keys (#109) - #115
Conversation
…print5) - Add docs/adr/sprint5-caching-abstraction.md (ADR for IBlogPostCacheService) - Add docs/sprint5-xml-doc-stubs.md (XML doc stubs for Sam to apply) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ze cache keys (#109) - Add BlogPostCacheKeys with All constant and ById method - Add IBlogPostCacheService interface (L1+L2 two-tier cache abstraction) - Add BlogPostCacheService implementation (L1=1min, L2=5min, JsonException handling) - Add CachingServiceExtensions.AddBlogPostCaching() DI registration - Register AddBlogPostCaching() in Program.cs after AddMemoryCache() Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🏗️ PR Added to Squad Triage QueueThis PR has been labeled with Next steps:
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## sprint/5-redis-caching #115 +/- ##
==========================================================
- Coverage 78.19% 71.30% -6.89%
==========================================================
Files 40 43 +3
Lines 642 704 +62
Branches 107 115 +8
==========================================================
Hits 502 502
- Misses 95 149 +54
- Partials 45 53 +8
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Adds a dedicated two-tier caching abstraction for blog post reads/writes and introduces centralized cache key definitions, wiring the new service into the Web DI container to enable upcoming handler refactors.
Changes:
- Introduces
IBlogPostCacheServiceandBlogPostCacheService(L1IMemoryCache+ L2IDistributedCache) plusAddBlogPostCaching()DI registration. - Centralizes cache key strings in
BlogPostCacheKeys. - Adds Sprint 5 documentation (ADR + XML doc stub draft).
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Web/Program.cs | Registers the new blog post cache service in DI. |
| src/Web/Infrastructure/Caching/IBlogPostCacheService.cs | Defines the two-tier cache abstraction API. |
| src/Web/Infrastructure/Caching/BlogPostCacheService.cs | Implements L1/L2 get-or-fetch logic and invalidation. |
| src/Web/Infrastructure/Caching/BlogPostCacheKeys.cs | Adds centralized cache key constants/helpers. |
| src/Web/Infrastructure/Caching/CachingServiceExtensions.cs | Provides AddBlogPostCaching() registration extension. |
| docs/sprint5-xml-doc-stubs.md | Adds draft XML doc stubs and planned README updates. |
| docs/adr/sprint5-caching-abstraction.md | Documents the caching abstraction decision. |
| JsonSerializer.SerializeToUtf8Bytes(list, JsonOpts), | ||
| RedisOpts, | ||
| ct); | ||
| return result; |
| public async ValueTask<IReadOnlyList<BlogPostDto>> GetOrFetchAllAsync( | ||
| Func<Task<IReadOnlyList<BlogPostDto>>> fetch, | ||
| CancellationToken ct = default) | ||
| { | ||
| // L1 hit (synchronous — no heap allocation) |
| # Sprint 5 — XML Doc Comment Stubs | ||
|
|
| /// Retrieves all blog posts from the cache, checking L1 (in-memory) before L2 (Redis). | ||
| /// Returns <see langword="null"/> when no cached entry exists. | ||
| /// </summary> | ||
| /// <param name="ct">A <see cref="CancellationToken"/> to observe while waiting for the task to complete.</param> | ||
| /// <returns> | ||
| /// A <see cref="Task{TResult}"/> that resolves to a read-only list of <see cref="BlogPostDto"/> | ||
| /// instances, or <see langword="null"/> if the entry is not present in either cache tier. | ||
| /// </returns> | ||
| Task<IReadOnlyList<BlogPostDto>?> GetAllAsync(CancellationToken ct); | ||
|
|
||
| /// <summary> | ||
| /// Stores all blog posts in both cache tiers (L1 in-memory and L2 Redis). | ||
| /// </summary> | ||
| /// <param name="posts">The list of blog post DTOs to cache.</param> | ||
| /// <param name="ct">A <see cref="CancellationToken"/> to observe while waiting for the task to complete.</param> | ||
| /// <returns>A <see cref="Task"/> that represents the asynchronous set operation.</returns> | ||
| Task SetAllAsync(IReadOnlyList<BlogPostDto> posts, CancellationToken ct); | ||
|
|
||
| /// <summary> | ||
| /// Retrieves a single blog post by its unique identifier from the cache, | ||
| /// checking L1 (in-memory) before L2 (Redis). | ||
| /// Returns <see langword="null"/> when no cached entry exists for the given ID. | ||
| /// </summary> | ||
| /// <param name="id">The unique identifier of the blog post to retrieve.</param> | ||
| /// <param name="ct">A <see cref="CancellationToken"/> to observe while waiting for the task to complete.</param> | ||
| /// <returns> | ||
| /// A <see cref="Task{TResult}"/> that resolves to the cached <see cref="BlogPostDto"/>, | ||
| /// or <see langword="null"/> if no entry is found. | ||
| /// </returns> | ||
| Task<BlogPostDto?> GetByIdAsync(Guid id, CancellationToken ct); | ||
|
|
||
| /// <summary> | ||
| /// Stores a single blog post in both cache tiers (L1 in-memory and L2 Redis), | ||
| /// keyed by its unique identifier. | ||
| /// </summary> | ||
| /// <param name="id">The unique identifier used as the cache key.</param> | ||
| /// <param name="post">The blog post DTO to cache.</param> | ||
| /// <param name="ct">A <see cref="CancellationToken"/> to observe while waiting for the task to complete.</param> | ||
| /// <returns>A <see cref="Task"/> that represents the asynchronous set operation.</returns> | ||
| Task SetByIdAsync(Guid id, BlogPostDto post, CancellationToken ct); | ||
|
|
||
| /// <summary> |
| /// back-fill L1 on a hit. Write operations populate both tiers simultaneously. | ||
| /// Invalidation removes from both tiers. | ||
| /// </remarks> | ||
| public sealed class BlogPostCacheService : IBlogPostCacheService |
|
|
||
| ### Registration | ||
|
|
||
| `BlogPostCacheService` is registered in `Program.cs` as a scoped or singleton service alongside `IMemoryCache` and the Redis `IDistributedCache`. |
| /// <summary>Key for the list of all blog posts.</summary> | ||
| public const string All = "blog:all"; | ||
|
|
||
| /// <summary>Key for a single blog post identified by <paramref name="id"/>.</summary> | ||
| public static string ById(Guid id) => $"blog:{id}"; |
Sam's #115 and #118 have merged, so handlers now use IBlogPostCacheService. Both arch tests pass green: - Features_Should_Not_Reference_IDistributedCache_Directly - Features_Should_Not_Reference_IMemoryCache_Directly Closes #113 Working as Gimli (Tester) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… slnx Sam's #115 (IBlogPostCacheService) and #118 (handler refactor) have merged. Changes: - Add Unit.Tests to MyBlog.slnx (safe now that IBlogPostCacheService exists) - Fix 3 NSubstitute mock setups: async lambda → ValueTask<T>(task) ctor (NSubstitute cannot convert async lambdas to ValueTask<T> return type) All 16 unit tests pass green across 5 handler test classes. Closes #111 Working as Gimli (Tester) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… slnx Sam's #115 (IBlogPostCacheService) and #118 (handler refactor) have merged. Changes: - Add Unit.Tests to MyBlog.slnx (safe now that IBlogPostCacheService exists) - Fix 3 NSubstitute mock setups: async lambda → ValueTask<T>(task) ctor (NSubstitute cannot convert async lambdas to ValueTask<T> return type) All 16 unit tests pass green across 5 handler test classes. Closes #111 Working as Gimli (Tester) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Closes #109
Working as Sam (Backend Developer)
What
Introduces a two-tier cache abstraction (
IBlogPostCacheService) and centralizes all cache key strings inBlogPostCacheKeys. This is the foundation required before handlers can be refactored in #110.Changes
src/Web/Infrastructure/Caching/BlogPostCacheKeys.cs"blog:all"and$"blog:{id}"— eliminates magic strings in 4 handlerssrc/Web/Infrastructure/Caching/IBlogPostCacheService.csGetOrFetchAllAsync,GetOrFetchByIdAsync,InvalidateAllAsync,InvalidateByIdAsyncsrc/Web/Infrastructure/Caching/BlogPostCacheService.csinternal sealedimplementation — L1 (IMemoryCache, 1 min) + L2 (IDistributedCache/Redis, 5 min),JsonExceptioncatch-and-remove on bad L2 bytes,CancellationToken.Noneon removalssrc/Web/Infrastructure/Caching/CachingServiceExtensions.csAddBlogPostCaching()DI extension, registers as Singletonsrc/Web/Program.csbuilder.Services.AddBlogPostCaching()afterAddMemoryCache()Design Decisions
GetOrFetchpattern over split Get/Set — if the interface only exposes Get/Set/Invalidate, handlers would still orchestrate the L1→miss→L2→miss→DB flow themselves, defeating the abstraction.GetOrFetchlets the service own the entire read path.CancellationToken.Nonefor invalidations — a cancelled request CT must not leave Redis stale indefinitely; removals always complete.ValueTaskon reads — L1 (IMemoryCache.TryGetValue) is synchronous;ValueTaskavoids aTaskheap allocation on the hot L1-hit path.IMemoryCacheandIDistributedCache(Redis) are both Singleton in ASP.NET Core; no captive dependency issue.Related
IBlogPostCacheServiceinstead ofIMemoryCache+IDistributedCache