Skip to content

Commit 1853dae

Browse files
committed
Document the expiration differences
1 parent 6263889 commit 1853dae

File tree

2 files changed

+68
-2
lines changed

2 files changed

+68
-2
lines changed

docs/guide/caching.md

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,13 @@ Different methods handle the `expiresIn` parameter slightly differently. The tab
7171
| `ReplaceAsync` | No TTL (removes existing) | Sets TTL | Removes key | `false` |
7272
| `ReplaceIfEqualAsync` | No TTL (removes existing) | Sets TTL | Removes key | `false` |
7373
| `IncrementAsync` | **Preserves existing TTL** | Sets/updates TTL | Removes key | `0` |
74-
| `SetIfHigherAsync` | No TTL (removes existing) | Sets TTL | Removes key | `0` |
75-
| `SetIfLowerAsync` | No TTL (removes existing) | Sets TTL | Removes key | `0` |
74+
| `SetIfHigherAsync` | No TTL (removes existing)* | Sets TTL* | Removes key | `0` |
75+
| `SetIfLowerAsync` | No TTL (removes existing)* | Sets TTL* | Removes key | `0` |
7676
| `ListAddAsync` | No TTL | Sets TTL | Removes key | `0` |
7777
| `ListRemoveAsync` | Preserves existing TTL | Sets TTL | Removes key | `0` |
7878

79+
\* **Conditional operations**: `SetIfHigherAsync` and `SetIfLowerAsync` only update TTL when the condition is met. If the value is not higher/lower, the entire operation is a no-op (including expiration).
80+
7981
::: tip Key Difference: IncrementAsync
8082
`IncrementAsync` is unique: passing `null` **preserves** any existing TTL rather than removing it. This is intentional for use cases like rate limiting, where you want to increment a counter without resetting its expiration window.
8183

@@ -91,6 +93,27 @@ await cache.IncrementAsync("rate:user:123", 1, TimeSpan.FromHours(1));
9193
```
9294
:::
9395

96+
::: info Integer vs Floating-Point Increments
97+
`IncrementAsync` supports both integer (`long`) and floating-point (`double`) amounts. Both overloads work correctly with expiration:
98+
99+
```csharp
100+
// Integer increments
101+
await cache.IncrementAsync("counter", 1L, TimeSpan.FromHours(1)); // long overload
102+
await cache.IncrementAsync("counter", 5L, TimeSpan.FromHours(1));
103+
104+
// Floating-point increments
105+
await cache.IncrementAsync("score", 1.5, TimeSpan.FromHours(1)); // double overload
106+
await cache.IncrementAsync("score", 2.25, TimeSpan.FromHours(1)); // Total: 3.75
107+
108+
// Mixed increments work correctly
109+
await cache.IncrementAsync("mixed", 1, TimeSpan.FromHours(1)); // 1
110+
await cache.IncrementAsync("mixed", 1.5, TimeSpan.FromHours(1)); // 2.5
111+
await cache.IncrementAsync("mixed", 2, TimeSpan.FromHours(1)); // 4.5
112+
```
113+
114+
For Redis implementations, integer amounts (including `2.0` where the fractional part is zero) use the more efficient `INCRBY` command, while fractional amounts use `INCRBYFLOAT`.
115+
:::
116+
94117
### Detailed Examples
95118

96119
```csharp
@@ -351,6 +374,43 @@ await cache.SetIfHigherAsync("max-concurrent-users", currentUsers);
351374
await cache.SetIfLowerAsync("fastest-response-ms", responseTime);
352375
```
353376

377+
#### SetIfHigher/SetIfLower Return Values
378+
379+
These methods return the **difference** between the new and old values, not the new value itself:
380+
381+
```csharp
382+
// Key doesn't exist - returns the value itself (difference from 0)
383+
double diff = await cache.SetIfHigherAsync("max-users", 100); // Returns 100
384+
385+
// Value is higher - returns the delta
386+
diff = await cache.SetIfHigherAsync("max-users", 150); // Returns 50 (150 - 100)
387+
388+
// Value is NOT higher - returns 0 (no change)
389+
diff = await cache.SetIfHigherAsync("max-users", 120); // Returns 0
390+
391+
// To get the actual current value after the operation:
392+
var currentMax = (await cache.GetAsync<double>("max-users")).Value; // 150
393+
```
394+
395+
::: warning Conditional Expiration Behavior
396+
`SetIfHigherAsync` and `SetIfLowerAsync` only update the expiration **when the condition is met**. If the value is not higher/lower, the operation is a complete no-op—including the expiration.
397+
398+
```csharp
399+
// Set with 1-hour TTL
400+
await cache.SetIfHigherAsync("max-users", 100, TimeSpan.FromHours(1));
401+
402+
// Try to set lower value with 2-hour TTL
403+
await cache.SetIfHigherAsync("max-users", 50, TimeSpan.FromHours(2));
404+
// TTL is STILL 1 hour! The condition failed, so nothing changed.
405+
406+
// Set higher value with 2-hour TTL
407+
await cache.SetIfHigherAsync("max-users", 200, TimeSpan.FromHours(2));
408+
// TTL is now 2 hours (condition was met)
409+
```
410+
411+
This is intentional—the semantic is "set IF higher/lower", so a failed condition means the entire operation is skipped.
412+
:::
413+
354414
### List Operations
355415

356416
Foundatio lists support **per-value expiration**, where each item in the list can have its own independent TTL. This is different from standard cache keys where expiration applies to the entire key.

src/Foundatio.TestHarness/Caching/CacheClientTestsBase.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -896,6 +896,12 @@ public virtual async Task IncrementAsync_WithFloatingPointDecimals_PreservesDeci
896896
// Verify the values with tolerance for floating-point precision
897897
var storedValue = (await cache.GetAsync<double>("small-decimal")).Value;
898898
Assert.True(Math.Abs(storedValue - 0.003) < 0.0001, $"Expected ~0.003 but got {storedValue}");
899+
900+
// Mixed type: increment an integer-initialized key with a fractional amount
901+
await cache.SetAsync("mixed-type", 10L);
902+
result = await cache.IncrementAsync("mixed-type", 2.5);
903+
Assert.Equal(12.5, result);
904+
Assert.Equal(12.5, (await cache.GetAsync<double>("mixed-type")).Value);
899905
}
900906
}
901907

0 commit comments

Comments
 (0)