feat(handlers): refactor all BlogPost handlers to use IBlogPostCacheService (#110) - #118
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>
…ervice (#110) - GetBlogPostsHandler: inject IBlogPostCacheService, use GetOrFetchAllAsync - EditBlogPostHandler: use GetOrFetchByIdAsync, InvalidateAllAsync, InvalidateByIdAsync - CreateBlogPostHandler: await InvalidateAllAsync (fixes fire-and-forget bug) - DeleteBlogPostHandler: await InvalidateAllAsync + InvalidateByIdAsync - Add IBlogPostCacheService global using to Web and Web.Tests projects - Update handler unit tests to mock IBlogPostCacheService via NSubstitute Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🏗️ PR Added to Squad Triage QueueThis PR has been labeled with Next steps:
|
Test Results Summary201 tests ±0 201 ✅ ±0 14s ⏱️ ±0s Results for commit 1736f81. ± Comparison against base commit 7813f99. This pull request removes 2 and adds 2 tests. Note that renamed tests count towards both. |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## sprint/5-redis-caching #118 +/- ##
==========================================================
- Coverage 78.19% 70.38% -7.81%
==========================================================
Files 40 43 +3
Lines 642 672 +30
Branches 107 111 +4
==========================================================
- Hits 502 473 -29
- Misses 95 147 +52
- Partials 45 52 +7
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Refactors BlogPost MediatR handlers to delegate all caching behavior to the new IBlogPostCacheService abstraction, removing direct IMemoryCache/IDistributedCache usage from handlers and updating unit tests accordingly.
Changes:
- Refactor all 4 BlogPost handlers to inject/use
IBlogPostCacheServicefor read-through caching and invalidation. - Add/register caching abstraction types (
IBlogPostCacheService,BlogPostCacheService,BlogPostCacheKeys, DI extension) and wire them up inProgram.cs. - Update Web handler unit tests to mock
IBlogPostCacheService(ValueTask-based APIs) and add global using for caching in tests/Web.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 17 comments.
Show a summary per file
| File | Description |
|---|---|
src/Web/Features/BlogPosts/List/GetBlogPostsHandler.cs |
Replace direct cache usage with IBlogPostCacheService.GetOrFetchAllAsync. |
src/Web/Features/BlogPosts/Edit/EditBlogPostHandler.cs |
Use GetOrFetchByIdAsync for reads; invalidate via cache service on edits. |
src/Web/Features/BlogPosts/Create/CreateBlogPostHandler.cs |
Create then invalidate via cache service (awaited). |
src/Web/Features/BlogPosts/Delete/DeleteBlogPostHandler.cs |
Delete then invalidate both all + by-id via cache service. |
src/Web/Infrastructure/Caching/IBlogPostCacheService.cs |
Define cache abstraction contract for list + by-id read-through and invalidation. |
src/Web/Infrastructure/Caching/BlogPostCacheService.cs |
Implement two-tier cache (L1 memory + L2 distributed) with JSON serialization. |
src/Web/Infrastructure/Caching/BlogPostCacheKeys.cs |
Centralize cache keys for “all” and “by id”. |
src/Web/Infrastructure/Caching/CachingServiceExtensions.cs |
Add DI registration helper for cache service. |
src/Web/Program.cs |
Register cache service after AddMemoryCache() (and after Redis distributed cache). |
src/Web/GlobalUsings.cs |
Add global using for caching namespace. |
tests/Web.Tests/GlobalUsings.cs |
Add global using for caching namespace in tests. |
tests/Web.Tests/Handlers/GetBlogPostsHandlerTests.cs |
Update tests to mock IBlogPostCacheService for list reads. |
tests/Web.Tests/Handlers/EditBlogPostHandlerTests.cs |
Update tests to mock IBlogPostCacheService for by-id reads + invalidation. |
tests/Web.Tests/Handlers/CreateBlogPostHandlerTests.cs |
Update tests to assert cache invalidation via cache service. |
tests/Web.Tests/Handlers/DeleteBlogPostHandlerTests.cs |
Update tests to assert invalidation via cache service. |
docs/adr/sprint5-caching-abstraction.md |
Add ADR describing the cache abstraction decision. |
docs/sprint5-xml-doc-stubs.md |
Add documentation stubs for XML docs/README updates (currently non-compliant). |
| { | ||
| localCache.Set(BlogPostCacheKeys.All, fromRedis, LocalOpts); | ||
| return fromRedis; | ||
| } |
There was a problem hiding this comment.
If Redis contains bytes that deserialize to null (e.g., cached JSON null/empty payload), this code falls through to fetch() but leaves the bad L2 entry in place. For by-id lookups where fetch() returns null, that means every request will keep hitting Redis + DB. Consider treating fromRedis is null as a corrupted/stale entry and removing it (similar to the JsonException path).
| } | |
| } | |
| // Stale or corrupt bytes that deserialize to null — remove and | |
| // fall through to the DB | |
| await distributedCache.RemoveAsync(BlogPostCacheKeys.All, CancellationToken.None); |
| private readonly IBlogPostRepository _repo = Substitute.For<IBlogPostRepository>(); | ||
| private readonly IBlogPostCacheService _cache = Substitute.For<IBlogPostCacheService>(); | ||
| private readonly EditBlogPostHandler _handler; | ||
|
|
||
| public EditBlogPostHandlerTests() | ||
| { | ||
| _handler = new EditBlogPostHandler(_repo, _cache); | ||
| } | ||
|
|
||
| // ── Edit tests ──────────────────────────────────────────────────────────── | ||
|
|
||
| [Fact] | ||
| public async Task HandleEdit_Success_UpdatesPostAndInvalidatesBothCaches() | ||
| { | ||
| // Arrange | ||
| var post = BlogPost.Create("Old Title", "Old Content", "Author"); | ||
| var command = new EditBlogPostCommand(post.Id, "New Title", "New Content"); | ||
| _repo.GetByIdAsync(post.Id, Arg.Any<CancellationToken>()).Returns(post); | ||
|
|
||
| // Act | ||
| var result = await _handler.Handle(command, CancellationToken.None); | ||
|
|
||
| // Assert | ||
| result.Success.Should().BeTrue(); | ||
| await _repo.Received(1).UpdateAsync(post, Arg.Any<CancellationToken>()); | ||
| await _cache.Received(1).InvalidateAllAsync(Arg.Any<CancellationToken>()); | ||
| await _cache.Received(1).InvalidateByIdAsync(post.Id, Arg.Any<CancellationToken>()); | ||
| post.Title.Should().Be("New Title"); | ||
| post.Content.Should().Be("New Content"); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task HandleEdit_NotFound_ReturnsFailResult() | ||
| { | ||
| // Arrange | ||
| var id = Guid.NewGuid(); |
There was a problem hiding this comment.
Indentation in *.cs files is configured as tabs in this repo (see .editorconfig [*.{cs,csproj}] indent_style = tab). This test file currently has no indentation/spaces throughout; please format it to match the configured style.
| private readonly IBlogPostRepository _repo = Substitute.For<IBlogPostRepository>(); | |
| private readonly IBlogPostCacheService _cache = Substitute.For<IBlogPostCacheService>(); | |
| private readonly EditBlogPostHandler _handler; | |
| public EditBlogPostHandlerTests() | |
| { | |
| _handler = new EditBlogPostHandler(_repo, _cache); | |
| } | |
| // ── Edit tests ──────────────────────────────────────────────────────────── | |
| [Fact] | |
| public async Task HandleEdit_Success_UpdatesPostAndInvalidatesBothCaches() | |
| { | |
| // Arrange | |
| var post = BlogPost.Create("Old Title", "Old Content", "Author"); | |
| var command = new EditBlogPostCommand(post.Id, "New Title", "New Content"); | |
| _repo.GetByIdAsync(post.Id, Arg.Any<CancellationToken>()).Returns(post); | |
| // Act | |
| var result = await _handler.Handle(command, CancellationToken.None); | |
| // Assert | |
| result.Success.Should().BeTrue(); | |
| await _repo.Received(1).UpdateAsync(post, Arg.Any<CancellationToken>()); | |
| await _cache.Received(1).InvalidateAllAsync(Arg.Any<CancellationToken>()); | |
| await _cache.Received(1).InvalidateByIdAsync(post.Id, Arg.Any<CancellationToken>()); | |
| post.Title.Should().Be("New Title"); | |
| post.Content.Should().Be("New Content"); | |
| } | |
| [Fact] | |
| public async Task HandleEdit_NotFound_ReturnsFailResult() | |
| { | |
| // Arrange | |
| var id = Guid.NewGuid(); | |
| private readonly IBlogPostRepository _repo = Substitute.For<IBlogPostRepository>(); | |
| private readonly IBlogPostCacheService _cache = Substitute.For<IBlogPostCacheService>(); | |
| private readonly EditBlogPostHandler _handler; | |
| public EditBlogPostHandlerTests() | |
| { | |
| _handler = new EditBlogPostHandler(_repo, _cache); | |
| } | |
| // ── Edit tests ──────────────────────────────────────────────────────────── | |
| [Fact] | |
| public async Task HandleEdit_Success_UpdatesPostAndInvalidatesBothCaches() | |
| { | |
| // Arrange | |
| var post = BlogPost.Create("Old Title", "Old Content", "Author"); | |
| var command = new EditBlogPostCommand(post.Id, "New Title", "New Content"); | |
| _repo.GetByIdAsync(post.Id, Arg.Any<CancellationToken>()).Returns(post); | |
| // Act | |
| var result = await _handler.Handle(command, CancellationToken.None); | |
| // Assert | |
| result.Success.Should().BeTrue(); | |
| await _repo.Received(1).UpdateAsync(post, Arg.Any<CancellationToken>()); | |
| await _cache.Received(1).InvalidateAllAsync(Arg.Any<CancellationToken>()); | |
| await _cache.Received(1).InvalidateByIdAsync(post.Id, Arg.Any<CancellationToken>()); | |
| post.Title.Should().Be("New Title"); | |
| post.Content.Should().Be("New Content"); | |
| } | |
| [Fact] | |
| public async Task HandleEdit_NotFound_ReturnsFailResult() | |
| { | |
| // Arrange | |
| var id = Guid.NewGuid(); |
| # Sprint 5 — XML Doc Comment Stubs | ||
|
|
There was a problem hiding this comment.
This markdown file violates the repo markdown validation rules: it starts with an H1 and does not include the required YAML front matter block. Update it to include front matter at the top and start content headings at H2 (no leading # ...).
| # Sprint 5 — XML Doc Comment Stubs | |
| --- | |
| post_title: "Sprint 5 — XML Doc Comment Stubs" | |
| author1: "Frodo" | |
| post_slug: "sprint5-xml-doc-stubs" | |
| microsoft_alias: "frodo" | |
| featured_image: "https://learn.microsoft.com/media/open-graph-image.png" | |
| categories: | |
| - "Engineering" | |
| tags: | |
| - "xml-docs" | |
| - "csharp" | |
| - "caching" | |
| - "redis" | |
| ai_note: "AI-assisted" | |
| summary: "XML documentation comment stubs for Sprint 5 blog post caching abstractions and related README update notes." | |
| post_date: "2026-04-23" | |
| --- | |
| ## Sprint 5 — XML Doc Comment Stubs |
| { | ||
| localCache.Set(key, dto, LocalOpts); | ||
| return dto; | ||
| } |
There was a problem hiding this comment.
Same as the all-posts path: when Deserialize<BlogPostDto> returns null without throwing, the code treats it as a miss but does not remove the invalid Redis entry. If fetch() returns null (not found), the invalid Redis payload will persist and cause repeated Redis+DB work. Consider removing the key when deserialization returns null.
| } | |
| } | |
| // Invalid payload for a single-post cache entry — remove and fall through to the DB | |
| await distributedCache.RemoveAsync(key, CancellationToken.None); |
| JsonSerializer.SerializeToUtf8Bytes(list, JsonOpts), | ||
| RedisOpts, | ||
| ct); | ||
| return result; |
There was a problem hiding this comment.
This method materializes the fetched results into list and stores that in caches, but then returns result instead of the materialized list. If fetch() returns a non-List<T> implementation, callers will get a different instance than what was cached (and potentially a less efficient type). Consider returning list for consistency with what is cached.
| return result; | |
| return list; |
|
|
||
| ### Registration | ||
|
|
||
| `BlogPostCacheService` is registered in `Program.cs` as a scoped or singleton service alongside `IMemoryCache` and the Redis `IDistributedCache`. |
There was a problem hiding this comment.
This ADR states the cache service is registered as "scoped or singleton", but the actual registration added in this PR is AddSingleton<IBlogPostCacheService, BlogPostCacheService>() (via AddBlogPostCaching). Please update the ADR to reflect the chosen lifetime to avoid architectural documentation drifting from reality.
| `BlogPostCacheService` is registered in `Program.cs` as a scoped or singleton service alongside `IMemoryCache` and the Redis `IDistributedCache`. | |
| `BlogPostCacheService` is registered in `Program.cs` as a singleton service alongside `IMemoryCache` and the Redis `IDistributedCache`. |
| public sealed class CreateBlogPostHandler( | ||
| IBlogPostRepository repo, | ||
| IMemoryCache localCache, | ||
| IDistributedCache distributedCache) : IRequestHandler<CreateBlogPostCommand, Result<Guid>> | ||
| IBlogPostRepository repo, | ||
| IBlogPostCacheService cache) : IRequestHandler<CreateBlogPostCommand, Result<Guid>> | ||
| { | ||
| public async Task<Result<Guid>> Handle(CreateBlogPostCommand request, CancellationToken ct) | ||
| { | ||
| try | ||
| { | ||
| var post = BlogPost.Create(request.Title, request.Content, request.Author); | ||
| await repo.AddAsync(post, ct); | ||
| localCache.Remove("blog:all"); | ||
| _ = distributedCache.RemoveAsync("blog:all", ct); | ||
| return Result.Ok<Guid>(post.Id); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| return Result.Fail<Guid>(ex.Message); | ||
| } | ||
| } | ||
| public async Task<Result<Guid>> Handle(CreateBlogPostCommand request, CancellationToken ct) | ||
| { | ||
| try | ||
| { |
There was a problem hiding this comment.
Indentation in *.cs files is configured as tabs in this repo (see .editorconfig [*.{cs,csproj}] indent_style = tab). This handler was reformatted with no indentation/spaces; please format it to match repo conventions.
| public sealed class DeleteBlogPostHandler( | ||
| IBlogPostRepository repo, | ||
| IMemoryCache localCache, | ||
| IDistributedCache distributedCache) : IRequestHandler<DeleteBlogPostCommand, Result> | ||
| IBlogPostRepository repo, | ||
| IBlogPostCacheService cache) : IRequestHandler<DeleteBlogPostCommand, Result> | ||
| { | ||
| public async Task<Result> Handle(DeleteBlogPostCommand request, CancellationToken ct) | ||
| { | ||
| try | ||
| { | ||
| await repo.DeleteAsync(request.Id, ct); | ||
| localCache.Remove("blog:all"); | ||
| localCache.Remove($"blog:{request.Id}"); | ||
| await distributedCache.RemoveAsync("blog:all", ct); | ||
| await distributedCache.RemoveAsync($"blog:{request.Id}", ct); | ||
| return Result.Ok(); | ||
| } | ||
| catch (DbUpdateConcurrencyException) | ||
| { | ||
| return Result.Fail( | ||
| "This post was modified by another user. Please reload and try again.", | ||
| ResultErrorCode.Concurrency); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| return Result.Fail(ex.Message); | ||
| } | ||
| } | ||
| public async Task<Result> Handle(DeleteBlogPostCommand request, CancellationToken ct) | ||
| { | ||
| try | ||
| { |
There was a problem hiding this comment.
Indentation in *.cs files is configured as tabs in this repo (see .editorconfig [*.{cs,csproj}] indent_style = tab). This handler currently has no indentation/spaces; please format it to match repo conventions.
| private readonly IBlogPostRepository _repo = Substitute.For<IBlogPostRepository>(); | ||
| private readonly IBlogPostCacheService _cache = Substitute.For<IBlogPostCacheService>(); | ||
| private readonly CreateBlogPostHandler _handler; | ||
|
|
||
| public CreateBlogPostHandlerTests() | ||
| { | ||
| _handler = new CreateBlogPostHandler(_repo, _cache); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task Handle_Success_CreatesPostInvalidatesCacheAndReturnsGuid() | ||
| { | ||
| // Arrange | ||
| var command = new CreateBlogPostCommand("Title", "Content", "Author"); | ||
|
|
||
| // Act | ||
| var result = await _handler.Handle(command, CancellationToken.None); | ||
|
|
||
| // Assert | ||
| result.Success.Should().BeTrue(); | ||
| result.Value.Should().NotBeEmpty(); | ||
| await _repo.Received(1).AddAsync(Arg.Any<BlogPost>(), Arg.Any<CancellationToken>()); | ||
| await _cache.Received(1).InvalidateAllAsync(Arg.Any<CancellationToken>()); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task Handle_RepoThrows_ReturnsFailResult() | ||
| { | ||
| // Arrange | ||
| var command = new CreateBlogPostCommand("Title", "Content", "Author"); | ||
| _repo.AddAsync(Arg.Any<BlogPost>(), Arg.Any<CancellationToken>()) | ||
| .ThrowsAsync(new InvalidOperationException("insert failed")); | ||
|
|
||
| // Act | ||
| var result = await _handler.Handle(command, CancellationToken.None); | ||
|
|
||
| // Assert | ||
| result.Failure.Should().BeTrue(); | ||
| result.Error.Should().Contain("insert failed"); | ||
| } | ||
| } |
There was a problem hiding this comment.
Indentation in *.cs files is configured as tabs in this repo (see .editorconfig [*.{cs,csproj}] indent_style = tab). This test file currently has no indentation/spaces; please format it to match repo conventions.
| private readonly IBlogPostRepository _repo = Substitute.For<IBlogPostRepository>(); | |
| private readonly IBlogPostCacheService _cache = Substitute.For<IBlogPostCacheService>(); | |
| private readonly CreateBlogPostHandler _handler; | |
| public CreateBlogPostHandlerTests() | |
| { | |
| _handler = new CreateBlogPostHandler(_repo, _cache); | |
| } | |
| [Fact] | |
| public async Task Handle_Success_CreatesPostInvalidatesCacheAndReturnsGuid() | |
| { | |
| // Arrange | |
| var command = new CreateBlogPostCommand("Title", "Content", "Author"); | |
| // Act | |
| var result = await _handler.Handle(command, CancellationToken.None); | |
| // Assert | |
| result.Success.Should().BeTrue(); | |
| result.Value.Should().NotBeEmpty(); | |
| await _repo.Received(1).AddAsync(Arg.Any<BlogPost>(), Arg.Any<CancellationToken>()); | |
| await _cache.Received(1).InvalidateAllAsync(Arg.Any<CancellationToken>()); | |
| } | |
| [Fact] | |
| public async Task Handle_RepoThrows_ReturnsFailResult() | |
| { | |
| // Arrange | |
| var command = new CreateBlogPostCommand("Title", "Content", "Author"); | |
| _repo.AddAsync(Arg.Any<BlogPost>(), Arg.Any<CancellationToken>()) | |
| .ThrowsAsync(new InvalidOperationException("insert failed")); | |
| // Act | |
| var result = await _handler.Handle(command, CancellationToken.None); | |
| // Assert | |
| result.Failure.Should().BeTrue(); | |
| result.Error.Should().Contain("insert failed"); | |
| } | |
| } | |
| private readonly IBlogPostRepository _repo = Substitute.For<IBlogPostRepository>(); | |
| private readonly IBlogPostCacheService _cache = Substitute.For<IBlogPostCacheService>(); | |
| private readonly CreateBlogPostHandler _handler; | |
| public CreateBlogPostHandlerTests() | |
| { | |
| _handler = new CreateBlogPostHandler(_repo, _cache); | |
| } | |
| [Fact] | |
| public async Task Handle_Success_CreatesPostInvalidatesCacheAndReturnsGuid() | |
| { | |
| // Arrange | |
| var command = new CreateBlogPostCommand("Title", "Content", "Author"); | |
| // Act | |
| var result = await _handler.Handle(command, CancellationToken.None); | |
| // Assert | |
| result.Success.Should().BeTrue(); | |
| result.Value.Should().NotBeEmpty(); | |
| await _repo.Received(1).AddAsync(Arg.Any<BlogPost>(), Arg.Any<CancellationToken>()); | |
| await _cache.Received(1).InvalidateAllAsync(Arg.Any<CancellationToken>()); | |
| } | |
| [Fact] | |
| public async Task Handle_RepoThrows_ReturnsFailResult() | |
| { | |
| // Arrange | |
| var command = new CreateBlogPostCommand("Title", "Content", "Author"); | |
| _repo.AddAsync(Arg.Any<BlogPost>(), Arg.Any<CancellationToken>()) | |
| .ThrowsAsync(new InvalidOperationException("insert failed")); | |
| // Act | |
| var result = await _handler.Handle(command, CancellationToken.None); | |
| // Assert | |
| result.Failure.Should().BeTrue(); | |
| result.Error.Should().Contain("insert failed"); | |
| } | |
| } |
| .Returns<ValueTask<BlogPostDto?>>(ci => | ||
| { | ||
| var fetch = ci.Arg<Func<Task<BlogPostDto?>>>(); | ||
| return new ValueTask<BlogPostDto?>(fetch().GetAwaiter().GetResult()); |
There was a problem hiding this comment.
Same sync-over-async pattern here (GetAwaiter().GetResult()); please return a ValueTask from the fetch() task instead of blocking.
| return new ValueTask<BlogPostDto?>(fetch().GetAwaiter().GetResult()); | |
| return new ValueTask<BlogPostDto?>(fetch()); |
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>
Summary
Closes #110
Working as Sam (Backend Developer)
Refactors all 4 BlogPost handlers to use the
IBlogPostCacheServiceabstraction introduced in #109 (PR #115).Changes
GetBlogPostsHandler.csIBlogPostCacheService, delegate toGetOrFetchAllAsyncEditBlogPostHandler.csGetOrFetchByIdAsync,InvalidateAllAsync,InvalidateByIdAsyncCreateBlogPostHandler.csawait cache.InvalidateAllAsync(ct)DeleteBlogPostHandler.csawait InvalidateAllAsync+await InvalidateByIdAsyncsrc/Web/GlobalUsings.csglobal using MyBlog.Web.Infrastructure.Cachingtests/Web.Tests/GlobalUsings.csIBlogPostCacheServiceto all test filesIBlogPostCacheServicevia NSubstituteValueTask<T>patternsTest Results
Web.Tests— 102/102 passedArchitecture.Tests— 9/9 passedWeb.Tests.Bunit— 61/61 passedFollow-up
After this PR merges, remove
Skipfrom both[Fact(Skip=...)]intests/Architecture.Tests/CachingLayerTests.cson branchsquad/113-arch-caching-test(PR #116) to activate the caching arch enforcement tests.