Skip to content

Commit ce65da5

Browse files
committed
feat: implement cache management methods and enhance ExecuteCachedAsync functionality
- Added InvalidateCache, InvalidateCacheByPrefix, and ClearCache methods for better cache control. - Updated ExecuteCachedAsync to ensure only the first caller's callbacks are executed, improving deduplication. - Enhanced documentation to clarify cache invalidation strategies and callback behaviors. - Updated version to 2.0.5 and revised release notes to reflect new features and improvements.
1 parent e723d43 commit ce65da5

8 files changed

Lines changed: 825 additions & 28 deletions

File tree

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,23 @@ async Task LoadProduct(int productId, CancellationToken ct = default)
417417
| Single component, no deduplication needed | `ExecuteAsync` |
418418
| Need data without state updates | `LazyLoad` |
419419

420+
#### Cache Invalidation
421+
422+
Control cached entries when data changes:
423+
424+
```csharp
425+
// Remove specific cached entry
426+
executor.InvalidateCache($"product-{productId}");
427+
428+
// Remove all entries with prefix (e.g., after bulk operation)
429+
executor.InvalidateCacheByPrefix("product-");
430+
431+
// Clear all cached results (e.g., on user logout)
432+
executor.ClearCache();
433+
```
434+
435+
> **Note:** Only the first caller's callbacks (loading, success, error) are executed. Concurrent callers receive the same result but their callbacks are NOT invoked. This is intentional for deduplication.
436+
420437
---
421438

422439
## Optimistic Updates

docs-site/api-reference.html

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -876,6 +876,147 @@ <h4><code>DispatchAsync</code></h4>
876876
</div>
877877
</section>
878878

879+
<!-- IAsyncActionExecutor -->
880+
<section class="section">
881+
<h2 id="async-action-executor">IAsyncActionExecutor&lt;TState&gt;</h2>
882+
<p>Service interface for executing async actions with automatic loading, success, and error state management. Provides caching and deduplication capabilities for optimized data fetching.</p>
883+
884+
<div class="code-block">
885+
<div class="code-block-title">Registration</div>
886+
<pre><code class="language-csharp">builder.Services.AddAsyncActionExecutor&lt;MyState&gt;();
887+
// Or use AddStoreWithUtilities which includes this
888+
builder.Services.AddStoreWithUtilities(...);</code></pre>
889+
</div>
890+
891+
<div class="api-section">
892+
<h4><code>ExecuteAsync&lt;TResult&gt;</code></h4>
893+
<p>Executes an async action with automatic loading, success, and error state handling.</p>
894+
<div class="code-block">
895+
<pre><code class="language-csharp">await ExecuteAsync(
896+
async () => await userService.GetUserAsync(userId),
897+
loading: s => s with { IsLoading = true },
898+
success: (s, user) => s with { User = user, IsLoading = false },
899+
error: (s, ex) => s with { Error = ex.Message, IsLoading = false }
900+
);</code></pre>
901+
</div>
902+
</div>
903+
904+
<div class="api-section">
905+
<h4><code>ExecuteCachedAsync&lt;TResult&gt;</code></h4>
906+
<p>Executes an async action with full deduplication of both fetch and state updates. When multiple components call this with the same key, only 2 state updates occur instead of N×2.</p>
907+
<div class="code-block">
908+
<pre><code class="language-csharp">var product = await ExecuteCachedAsync(
909+
$"product-{productId}",
910+
async () => await productService.GetProductAsync(productId),
911+
loading: s => s with { Product = s.Product.ToLoading() },
912+
success: (s, p) => s with { Product = AsyncData.Success(p) },
913+
error: (s, ex) => s with { Product = AsyncData.Failure(ex.Message) },
914+
cacheFor: TimeSpan.FromMinutes(5)
915+
);</code></pre>
916+
</div>
917+
<p><strong>Important:</strong> Only the first caller's callbacks (loading, success, error) are executed. Concurrent callers waiting for the same cache key receive the result but their callbacks are NOT invoked.</p>
918+
</div>
919+
920+
<h3>Cache Management Methods</h3>
921+
922+
<div class="api-section">
923+
<h4><code>InvalidateCache(string cacheKey)</code></h4>
924+
<p>Removes a specific cached result by key. Use this when you know the exact cache key to invalidate.</p>
925+
<div class="code-block">
926+
<pre><code class="language-csharp">// After updating a product, invalidate its cache
927+
await productService.UpdateProductAsync(product);
928+
asyncExecutor.InvalidateCache($"product-{productId}");</code></pre>
929+
</div>
930+
<p><strong>Behavior:</strong></p>
931+
<ul>
932+
<li>Only removes cached results, does not affect in-flight operations</li>
933+
<li>Next call with this key will execute the async action again</li>
934+
<li>Safe to call even if the key doesn't exist</li>
935+
</ul>
936+
</div>
937+
938+
<div class="api-section">
939+
<h4><code>InvalidateCacheByPrefix(string prefix)</code></h4>
940+
<p>Removes all cached results with keys starting with the specified prefix. Useful for invalidating related cache entries.</p>
941+
<div class="code-block">
942+
<pre><code class="language-csharp">// After adding a new product, invalidate all product caches
943+
await productService.CreateProductAsync(newProduct);
944+
asyncExecutor.InvalidateCacheByPrefix("product-");
945+
946+
// Invalidates: "product-1", "product-2", "product-list", etc.</code></pre>
947+
</div>
948+
<p><strong>Common use cases:</strong></p>
949+
<ul>
950+
<li>Invalidate all items in a category after bulk updates</li>
951+
<li>Clear user-specific caches after logout</li>
952+
<li>Reset related data after configuration changes</li>
953+
</ul>
954+
<p><strong>Example patterns:</strong></p>
955+
<div class="code-block">
956+
<pre><code class="language-csharp">// Invalidate all product-related caches
957+
asyncExecutor.InvalidateCacheByPrefix("product-");
958+
959+
// Invalidate all user data for a specific user
960+
asyncExecutor.InvalidateCacheByPrefix($"user-{userId}-");
961+
962+
// Invalidate all caches for a specific tenant
963+
asyncExecutor.InvalidateCacheByPrefix($"tenant-{tenantId}-");</code></pre>
964+
</div>
965+
</div>
966+
967+
<div class="api-section">
968+
<h4><code>ClearCache()</code></h4>
969+
<p>Clears all cached results. Use this sparingly, typically only for global state resets.</p>
970+
<div class="code-block">
971+
<pre><code class="language-csharp">// Clear all caches on logout
972+
await authService.LogoutAsync();
973+
asyncExecutor.ClearCache();</code></pre>
974+
</div>
975+
<p><strong>When to use:</strong></p>
976+
<ul>
977+
<li>User logout - clear all cached user data</li>
978+
<li>Switching accounts or tenants</li>
979+
<li>Major configuration changes requiring full data refresh</li>
980+
<li>Testing scenarios requiring clean state</li>
981+
</ul>
982+
<p><strong>Warning:</strong> This is a broad operation. Consider using <code>InvalidateCacheByPrefix</code> for more targeted invalidation.</p>
983+
</div>
984+
985+
<div class="api-section">
986+
<h4>Cache Management Best Practices</h4>
987+
<table class="table">
988+
<thead>
989+
<tr>
990+
<th>Scenario</th>
991+
<th>Recommended Method</th>
992+
</tr>
993+
</thead>
994+
<tbody>
995+
<tr>
996+
<td>Single item updated</td>
997+
<td><code>InvalidateCache("item-{id}")</code></td>
998+
</tr>
999+
<tr>
1000+
<td>Multiple related items changed</td>
1001+
<td><code>InvalidateCacheByPrefix("category-")</code></td>
1002+
</tr>
1003+
<tr>
1004+
<td>User logout</td>
1005+
<td><code>ClearCache()</code></td>
1006+
</tr>
1007+
<tr>
1008+
<td>List item added/removed</td>
1009+
<td><code>InvalidateCacheByPrefix("list-")</code></td>
1010+
</tr>
1011+
<tr>
1012+
<td>Settings changed</td>
1013+
<td><code>InvalidateCacheByPrefix("settings-")</code></td>
1014+
</tr>
1015+
</tbody>
1016+
</table>
1017+
</div>
1018+
</section>
1019+
8791020
<!-- Complete Configuration Example -->
8801021
<section class="section">
8811022
<h2 id="complete-example">Complete Configuration Example</h2>
@@ -983,6 +1124,18 @@ <h2 id="quick-reference">Quick Reference</h2>
9831124
<td><code>IStateValidator&lt;T&gt;</code></td>
9841125
<td>Validate state integrity</td>
9851126
</tr>
1127+
<tr>
1128+
<td><code>InvalidateCache()</code></td>
1129+
<td>Remove specific cached result</td>
1130+
</tr>
1131+
<tr>
1132+
<td><code>InvalidateCacheByPrefix()</code></td>
1133+
<td>Remove cached results by prefix</td>
1134+
</tr>
1135+
<tr>
1136+
<td><code>ClearCache()</code></td>
1137+
<td>Clear all cached results</td>
1138+
</tr>
9861139
</tbody>
9871140
</table>
9881141
</section>

docs-site/async-helpers/execute-cached-async.html

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,11 @@ <h2>The Solution: ExecuteCachedAsync</h2>
167167
<li>All callers receive the same result</li>
168168
</ul>
169169
</div>
170+
171+
<div class="alert alert-warning">
172+
<div class="alert-title">Important: First Caller's Callbacks Only</div>
173+
<p>Only the first caller's <code>loading</code>, <code>success</code>, and <code>error</code> callbacks are executed. Concurrent callers waiting for the same cache key receive the result but their callbacks are NOT invoked. This ensures exactly 2 state updates regardless of concurrent caller count. Ensure all callers use consistent callbacks, or handle state updates separately after receiving the cached result.</p>
174+
</div>
170175
</section>
171176

172177
<!-- API Reference -->
@@ -233,6 +238,31 @@ <h2>API Reference</h2>
233238
</tr>
234239
</tbody>
235240
</table>
241+
242+
<h3>Cache Invalidation Methods</h3>
243+
244+
<table class="table">
245+
<thead>
246+
<tr>
247+
<th>Method</th>
248+
<th>Description</th>
249+
</tr>
250+
</thead>
251+
<tbody>
252+
<tr>
253+
<td><code>InvalidateCache(string cacheKey)</code></td>
254+
<td>Removes a specific cache entry by key. Use after edit/delete operations to force fresh data on next call.</td>
255+
</tr>
256+
<tr>
257+
<td><code>InvalidateCacheByPrefix(string prefix)</code></td>
258+
<td>Removes all cache entries matching the prefix. Example: <code>InvalidateCacheByPrefix("product-")</code> clears all product caches.</td>
259+
</tr>
260+
<tr>
261+
<td><code>ClearCache()</code></td>
262+
<td>Removes all cache entries. Use on logout or when global state reset is needed.</td>
263+
</tr>
264+
</tbody>
265+
</table>
236266
</section>
237267

238268
<!-- When to Use -->
@@ -386,6 +416,76 @@ <h2>Cancellation Support</h2>
386416
</div>
387417
</section>
388418

419+
<!-- Cache Invalidation -->
420+
<section class="section">
421+
<h2>Cache Invalidation</h2>
422+
<p>Control when cached data should be refetched by manually invalidating cache entries:</p>
423+
424+
<div class="code-block">
425+
<div class="code-block-title">Invalidate Specific Entry</div>
426+
<pre><code class="language-csharp">// After editing a product
427+
await ProductApi.UpdateAsync(product);
428+
429+
// Force fresh data on next ExecuteCachedAsync call
430+
InvalidateCache($"product-{product.Id}");
431+
432+
// Next component that calls ExecuteCachedAsync will fetch fresh data
433+
await ExecuteCachedAsync(
434+
$"product-{product.Id}",
435+
async () => await ProductApi.GetAsync(product.Id),
436+
loading: s => s with { Product = s.Product.ToLoading() },
437+
success: (s, p) => s with { Product = AsyncData.Success(p) }
438+
);</code></pre>
439+
</div>
440+
441+
<div class="code-block">
442+
<div class="code-block-title">Invalidate by Prefix</div>
443+
<pre><code class="language-csharp">// After bulk operations or when category changes
444+
await ProductApi.BulkUpdateAsync(products);
445+
446+
// Clear all product-related caches
447+
InvalidateCacheByPrefix("product-");
448+
449+
// Or clear category-specific caches
450+
InvalidateCacheByPrefix($"category-{categoryId}-products");</code></pre>
451+
</div>
452+
453+
<div class="code-block">
454+
<div class="code-block-title">Clear All Cache</div>
455+
<pre><code class="language-csharp">// On logout - clear all cached data
456+
public async Task LogoutAsync()
457+
{
458+
await AuthApi.LogoutAsync();
459+
460+
// Clear all cached data
461+
ClearCache();
462+
463+
// Reset state
464+
await UpdateAsync(_ => AppState.Initial);
465+
466+
NavigationManager.NavigateTo("/login");
467+
}
468+
469+
// On global state reset
470+
public async Task ResetApplicationAsync()
471+
{
472+
ClearCache();
473+
await UpdateAsync(_ => AppState.Initial);
474+
}</code></pre>
475+
</div>
476+
477+
<div class="alert alert-info">
478+
<div class="alert-title">When to Invalidate Cache</div>
479+
<ul>
480+
<li><strong>After mutations</strong>: Invalidate specific entries after create/update/delete operations</li>
481+
<li><strong>Bulk operations</strong>: Use prefix invalidation for related entries (e.g., all products in a category)</li>
482+
<li><strong>Authentication changes</strong>: Clear all cache on login/logout to prevent data leakage</li>
483+
<li><strong>Stale data prevention</strong>: Invalidate when external events indicate cached data is outdated</li>
484+
<li><strong>User-triggered refresh</strong>: Provide manual refresh buttons that invalidate and refetch</li>
485+
</ul>
486+
</div>
487+
</section>
488+
389489
<!-- Key Behaviors -->
390490
<section class="section">
391491
<h2>Key Behaviors</h2>
@@ -423,8 +523,8 @@ <h3>Cancellation Support</h3>
423523

424524
<div class="feature-card">
425525
<div class="feature-icon">🗑️</div>
426-
<h3>Disposable</h3>
427-
<p><code>AsyncActionExecutor</code> implements <code>IDisposable</code> for proper resource cleanup.</p>
526+
<h3>Cache Invalidation</h3>
527+
<p>Manually invalidate cache entries by key, prefix, or clear all entries for precise cache control.</p>
428528
</div>
429529
</div>
430530
</section>

0 commit comments

Comments
 (0)