From e32eb6ee5bf5b11f6719c3680fa41f1eab70167d Mon Sep 17 00:00:00 2001 From: Calin Popa Date: Mon, 31 Aug 2026 15:34:43 -0700 Subject: [PATCH 1/9] feat(cache): add TryAddAsync conditional add (Redis When.NotExists) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a create-if-absent member to ICache and ICache (plus a blocking TryAdd on the typed surface). On Redis-backed caches it maps to StackExchange.Redis When.NotExists — SET key value EX .. NX — a single atomic round-trip with the TTL applied by the same command, so exactly one caller across all nodes wins a given key and a won key is never briefly immortal between the write and a follow-up EXPIRE. Intended for at-most-once semantics keyed by something: idempotency keys, dedup markers whose TTL is the dedup window, electing which replica runs a job. Previously the only NX primitive in the library was IDistributedLock, which is a lease rather than a value store, and GetOrAddAsync's check-then-write is not a substitute — the gap between probe and write is exactly what NX removes. Contract decisions: - false is fail-closed and deliberately ambiguous: the key already existed, or the write could not be completed (disconnected, threw, or a null/default value the cache cannot represent). A caller treating true as "I own this key" is never wrongly told it won. Same conflation IDistributedLock.TryAcquireAsync already documents. - Never deletes. Where SetAsync removes the key when handed a null with CacheNullValues off, TryAddAsync reports false and leaves it untouched. - A win is never downgraded. On MultilayerCache the L2 arbitrates and the L1 write plus invalidation broadcast are best-effort *after* the win — reporting a loss there would strand the entry with no owner until its TTL. - L1 never arbitrates while an L2 exists, since a key absent locally may be present in the shared store. With the L2 disconnected the call returns false rather than granting a local-only claim every node would also get (SetAsync degrades to a local write there). The memory-only provider has no L2, so the local tier arbitrates and exclusion narrows to in-process, serialized by Lock.LocalLockEnabled. Ships as default interface methods so existing implementations keep compiling, per the convention established for the 1.3.0 ICache additions. The default body throws NotSupportedException rather than emulating the operation with a probe followed by a write, which would not be atomic and would silently void the only guarantee the method makes. No multi-key overload: Redis has no atomic multi-key NX, and all-or-nothing versus per-key semantics would be a guess. No hash-surface member: NX there is per-field (HSETNX) and a different shape. Also corrects interfaces.md, which described the IHashCache.SetAsync(.., HashCacheEntryOptions, ..) overload as offering "conditional set, individual field TTL". It offers neither — HashCacheSetOption selects write scope, and there is no per-field TTL. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 39 ++ docs/concepts.md | 14 + docs/index.md | 1 + docs/recipes/conditional-add.md | 116 ++++++ docs/reference/interfaces.md | 34 +- src/UiPath.Caching.Abstractions/CacheOfT.cs | 9 + .../ConditionalAdd.cs | 19 + src/UiPath.Caching.Abstractions/ICache.cs | 58 +++ .../ICacheOfT.Sync.cs | 12 + src/UiPath.Caching.Abstractions/ICacheOfT.cs | 23 ++ src/UiPath.Caching.Abstractions/NullCache.cs | 15 + .../PublicAPI.Unshipped.txt | 27 +- src/UiPath.Caching/MultilayerCache.cs | 128 +++++++ src/UiPath.Caching/MultilayerCacheBase.cs | 12 + src/UiPath.Caching/Redis/RedisCache.cs | 71 ++++ .../CacheOfTTryAddTests.cs | 91 +++++ .../Fakes/DictionaryCache.cs | 20 ++ .../MultilayerCacheTryAddTests.cs | 338 ++++++++++++++++++ .../Redis/RedisCacheTryAddTests.cs | 245 +++++++++++++ 19 files changed, 1265 insertions(+), 7 deletions(-) create mode 100644 docs/recipes/conditional-add.md create mode 100644 src/UiPath.Caching.Abstractions/ConditionalAdd.cs create mode 100644 tests/UiPath.Caching.Tests/CacheOfTTryAddTests.cs create mode 100644 tests/UiPath.Caching.Tests/MultilayerCacheTryAddTests.cs create mode 100644 tests/UiPath.Caching.Tests/Redis/RedisCacheTryAddTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 735b10b..66561e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,34 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) `AddDistributedCache` enables it on the private provider it builds, because a silently dropped refresh shortens a session and fire-and-forget cannot report one. Honored by both `RedisCache` and `RedisHashCache`. +- **Conditional add: `ICache.TryAddAsync` / `ICache.TryAddAsync` (+ blocking `ICache.TryAdd`).** + Writes a key only if it does not already exist, returning `true` only to the caller that created it. + On Redis-backed caches this is StackExchange.Redis `When.NotExists` — `SET key value EX … NX` — a + single atomic round-trip with the TTL applied by the same command, so exactly one caller across all + nodes wins a given key and a won key is never briefly immortal. Intended for at-most-once semantics + keyed by something: idempotency keys, dedup markers whose TTL is the dedup window, electing which + replica runs a job. `false` deliberately conflates "the key already existed" with "the write could + not be completed" (disconnected, threw, or a `null`/`default` value the cache cannot represent) — + fail-closed, so a caller treating `true` as "I own this key" is never wrongly told it won; the same + conflation `IDistributedLock.TryAcquireAsync` already documents. Unlike `SetAsync`, it never + deletes: where `SetAsync` removes the key when handed a `null` with `CacheNullValues` off, + `TryAddAsync` reports `false` and leaves the key untouched. Per tier: `RedisCache` issues the `NX` + write; `MultilayerCache` lets the L2 arbitrate and populates L1 (plus an invalidation broadcast) + only after a win, best-effort — a broadcast or L1 failure never downgrades a real win to `false`, + since that would strand the entry with no owner; with the L2 disconnected it returns `false` rather + than granting a local-only claim every node would also be granted (`SetAsync` degrades to a local + write there); the memory-only provider has no L2 to arbitrate, so the local tier does and exclusion + narrows to in-process, serialized by `Lock.LocalLockEnabled` (on by default). `NullCache` returns + `true`, consistent with its `SetAsync` — caching is off, so nothing is excluded. Added to both + interfaces as **default interface methods**, so existing implementations keep compiling; the default + body throws `NotSupportedException` rather than emulating the operation with a probe followed by a + write, which would not be atomic and would silently void the guarantee. A hand-written `ICache` / + `ICache` implementation must override it before callers can use it. No multi-key overload: Redis + has no atomic multi-key `NX`, and all-or-nothing versus per-key semantics would be a guess. This is + a cache primitive, not a lock — no ownership token, no early release, and a later + `SetAsync`/`RemoveAsync` ignores the claim; for a fencing token and explicit release use + `IDistributedLock`. No conditional-add member was added to the hash surface, where `NX` is per-field + (`HSETNX`) and a different shape. ### Changed @@ -77,6 +105,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) onto the builder object instead. Every `CacheOptions` value supplied through that entry point was silently ignored; `Enabled`, `AppShortName`, `KeyCasing` and the rest now take effect, which changes behavior for anyone who was unknowingly running on the defaults. +### Documentation + +- New recipe [`conditional-add.md`](docs/recipes/conditional-add.md) — claiming a key exactly once with + `TryAddAsync`, why check-then-set is not equivalent, and when to reach for `IDistributedLock` + instead. `interfaces.md` documents the member on both cache surfaces with a per-provider table of + who arbitrates and how far exclusion reaches; `concepts.md` places it next to the two lock + abstractions, whose goal is reducing redundant work rather than a decision the caller can branch on. +- Corrected `interfaces.md`, which described the `IHashCache.SetAsync(…, HashCacheEntryOptions, …)` + overload as offering "conditional set, individual field TTL". `HashCacheEntryOptions` carries + neither: `HashCacheSetOption` selects write *scope* (`HashReplace` merges fields, `KeyReplace` drops + the key first), and there is no per-field TTL. ## [1.3.0] - 2026-08-19 diff --git a/docs/concepts.md b/docs/concepts.md index eda0d8b..6ffcb1e 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -245,6 +245,20 @@ register `RedisDistributedLock` (the in-memory provider passes custom wiring that skips `AddInMemoryRedis()` should add `.AddRedisDistributedLock()` explicitly on the builder. +`ICache.TryAddAsync` sits alongside these but answers a different question. The +locks exist to reduce redundant work — the degradation above is deliberate, and +neither lock guarantees exactly-once generator execution. `TryAddAsync` instead +returns a decision the caller can branch on: it writes the key only if it is +absent (Redis `SET … NX`) and returns `true` only to the caller that created it, +so it is the right primitive for at-most-once semantics keyed by something (a +dedup marker, an idempotency key, electing which node runs a job). It is not a +lock: the claim expires on its own TTL, carries no ownership token, and any later +`SetAsync`/`RemoveAsync` on the key ignores it. When you need a fencing token and +an explicit release, use `IDistributedLock`; when you need a durable +"first one here wins" marker, use `TryAddAsync`. See +[the interfaces reference](reference/interfaces.md#icache) for the per-provider +behavior, including why it fails closed when the L2 is disconnected. + When both locks are enabled, `GetOrAddAsync` takes the local lock first (cheap, in-process) and then — under a double-checked read to avoid redundant generator runs after waiting — attempts the distributed lock. A failure to acquire the diff --git a/docs/index.md b/docs/index.md index eac1d4e..6219ba1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -32,6 +32,7 @@ UiPath's internal multilayer caching library. L1 in-memory + L2 Redis, cross-nod - [provider-fallback.md](recipes/provider-fallback.md) — InMemoryRedis when Redis is on, InMemory otherwise. - [factory-extension-methods.md](recipes/factory-extension-methods.md) — typed `ICache` via `ICacheFactory` extension methods. - [batch-get-or-add.md](recipes/batch-get-or-add.md) — one source round trip for N cache misses via multi-key `GetOrAddAsync`. +- [conditional-add.md](recipes/conditional-add.md) — claim a key exactly once with `TryAddAsync` (Redis `SET … NX`). - [app-version-prefix.md](recipes/app-version-prefix.md) — auto-invalidate on deploy via assembly-version key prefix. - [mediatr-pipeline-behavior.md](recipes/mediatr-pipeline-behavior.md) — generic per-request cache as a MediatR behavior. - [hash-cache-with-metadata.md](recipes/hash-cache-with-metadata.md) — payload + freshness metadata side-by-side. diff --git a/docs/recipes/conditional-add.md b/docs/recipes/conditional-add.md new file mode 100644 index 0000000..68334a8 --- /dev/null +++ b/docs/recipes/conditional-add.md @@ -0,0 +1,116 @@ +# Claim a key exactly once with `TryAddAsync` + +**What:** Use `TryAddAsync` when you need "only one caller may proceed", keyed by something. It writes +the key only if it is absent and returns `true` only to the caller that created it. On a Redis-backed +cache that is StackExchange.Redis `When.NotExists` — `SET key value EX … NX` — one atomic round-trip, +so exactly one caller across every node wins. + +**When to use:** +- **Idempotency keys.** A webhook or payment callback that may be delivered twice: the first delivery + claims the key and does the work, the redelivery sees `false` and returns the cached outcome. +- **At-most-once side effects.** "Send this alert / this welcome email once per user per day." +- **Electing a worker.** Which of N replicas runs a periodic job this tick. +- **Dedup markers** where the marker's TTL *is* the dedup window. + +**When not to use:** if you need a lock you can release early, or a fencing token that proves you +still hold it, use [`IDistributedLock`](../reference/interfaces.md#idistributedlock) instead. See +[Notes](#notes). + +## Code + +```csharp +using UiPath.Caching; + +public class WebhookHandler(ICache cache, IPaymentService payments) +{ + // The TTL is the dedup window: retries inside 24h are dropped, and the key + // cleans itself up afterwards without a sweeper. + private static readonly TimeSpan DedupWindow = TimeSpan.FromHours(24); + + public async Task HandleAsync(string eventId, CancellationToken token) + { + var claimed = await cache.TryAddAsync( + (CacheKey)$"webhook:{eventId}", + DateTimeOffset.UtcNow, + DedupWindow, + token: token); + + if (!claimed) + { + // Either a concurrent/duplicate delivery already claimed it, or the cache + // was unreachable. Both mean "do not run the side effect" — see Notes. + return HandlingResult.AlreadyHandled; + } + + await payments.CaptureAsync(eventId, token); + return HandlingResult.Handled; + } +} +``` + +The typed surface is the same shape without the `policy` parameter: + +```csharp +public class JobElection(ICache cache) +{ + public Task TryClaimTickAsync(string jobName, DateTimeOffset tick, CancellationToken token) => + cache.TryAddAsync( + (CacheKey)$"{jobName}:{tick:yyyyMMddHHmm}", + Environment.MachineName, + TimeSpan.FromMinutes(5), + token) + .AsTask(); +} +``` + +## Why not check-then-set + +The obvious hand-rolled version is not equivalent: + +```csharp +// BROKEN: two callers can both observe "absent" before either writes. +if (!await cache.ContainsAsync(key, token)) +{ + await cache.SetAsync(key, DateTimeOffset.UtcNow, DedupWindow, token: token); + await payments.CaptureAsync(eventId, token); // runs twice under concurrency +} +``` + +The gap between the probe and the write is the whole problem, and no amount of narrowing closes it — +that is exactly the gap `NX` removes by making the decision part of the write. This is also why +`TryAddAsync` ships as a default interface method whose body **throws** `NotSupportedException` +rather than falling back to the code above: a silent non-atomic emulation would void the only +guarantee the method makes. + +## Notes + +- **`false` does not mean "the key existed".** It means "you did not create it" — the key already + existed, *or* the write could not be completed (store disconnected, write threw, or the value was a + `null`/`default` the cache cannot represent). This is fail-closed on purpose: nobody is ever wrongly + told they won. Design the `false` branch so that "skip the side effect" is the safe outcome. If you + need to tell an outage from a lost race, check `IConnectionState.IsConnected` before reacting, or + use `IDistributedLock.TryAcquireAsync`. +- **It never deletes.** `SetAsync` handed a `null` with `CacheNullValues` off *removes* the key; + `TryAddAsync` returns `false` and leaves it alone. With `CacheNullValues` on, a `null` claims the key + through the cached-null sentinel — so prefer a meaningful value (a timestamp, the machine name) that + makes the claim diagnosable in `redis-cli`. +- **The TTL is applied by the same command,** so a won key is never briefly immortal between the write + and a follow-up `EXPIRE`. Give every claim a TTL you are willing to wait out: there is no release, + so a crashed winner blocks the key until it expires. That is the main reason to prefer + `IDistributedLock` for long critical sections. +- **It is not a lock.** No ownership token, no early release, and a later `SetAsync`/`RemoveAsync` on + the key silently overwrites the claim. If a bug elsewhere writes that key, exclusion is gone with no + error. +- **In-memory-only caches exclude in-process only.** With the `InMemory` provider there is no shared + store to arbitrate, so the local tier does — serialized by `Lock.LocalLockEnabled`, on by default. + Two processes both win. `InMemoryRedis` and `Redis` are cross-node correct. +- **On `InMemoryRedis` the L2 decides and L1 is populated only after a win.** When the L2 is + disconnected the call returns `false` rather than granting a local claim every node would also be + granted — unlike `SetAsync`, which degrades to a local-only write there. +- **There is no multi-key overload.** Redis has no atomic multi-key `NX`, and choosing all-or-nothing + versus per-key semantics on your behalf would be a guess. Claim keys one at a time, or take a single + claim on a key that stands for the whole batch. + +**See also:** [`ICache`](../reference/interfaces.md#icache), +[`IDistributedLock`](../reference/interfaces.md#idistributedlock), +[Concepts — locking](../concepts.md) diff --git a/docs/reference/interfaces.md b/docs/reference/interfaces.md index c26379f..a299489 100644 --- a/docs/reference/interfaces.md +++ b/docs/reference/interfaces.md @@ -53,6 +53,12 @@ public partial interface ICache ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset? expiration = null, CancellationToken token = default); + ValueTask TryAddAsync(CacheKey cacheKey, T? value, CancellationToken token = default); + + ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan? expiration, CancellationToken token = default); + + ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CancellationToken token = default); + ValueTask RefreshAsync(CacheKey cacheKey, CancellationToken token = default); ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration, CancellationToken token = default); @@ -71,6 +77,8 @@ public partial interface ICache The multi-key `GetOrAddAsync` overloads pair each key with an opaque caller state (`TState`) — a database id, a request object, whatever identifies the entry in the caller's own vocabulary. The generator is invoked at most once, with only the states of the entries that missed, and results come back keyed by state. These three overloads are **abstract** on `ICache` — unlike the equivalent members on `ICache`, there is no default body, because `ICache` has no `GetCacheEntriesAsync` and so cannot distinguish a genuine miss from a cached `null` inside a default implementation. A hand-written `ICache` implementation (a test fake, typically) must add all three. There is no key-only convenience overload on the async surface: a caller whose keys are their own identity pairs each key with itself (`TState = CacheKey`). The blocking `GetOrAdd` facade above is the one place that accepts `CacheKey[]` directly and does that pairing for you. +`TryAddAsync` is the conditional-add (create-if-absent) member: it writes only when the key does not already exist, and returns `true` only to the caller that created it. On a Redis-backed cache it maps to StackExchange.Redis `When.NotExists` (`SET key value EX … NX`) — a single atomic round-trip, so exactly one caller across all nodes wins a given key. It is the primitive to reach for when you need at-most-once semantics keyed by something: a dedup marker, an idempotency key, a "who runs this job" election. See [`ICache.TryAddAsync`](#icache) for the full contract, including what a `false` return does and does not tell you. + > **Typical vs. power-user surface:** This is the standard typed surface. If you need to vary the value type or key per call rather than per cache instance, use [`ICache`](#icache) instead. `ICache` and `ICache` are different shapes for different problems — neither is strictly more capable. **Use this when:** @@ -140,6 +148,12 @@ public partial interface ICache : IDisposable ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default); + ValueTask TryAddAsync(CacheKey cacheKey, T? value, CachePolicy? policy = null, CancellationToken token = default); + + ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default); + + ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default); + ValueTask RefreshAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default); ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default); @@ -158,6 +172,24 @@ public partial interface ICache : IDisposable The multi-key `GetOrAddAsync` overloads shown above pair each key with an opaque caller state (`TState`) and are **default interface methods** — each forwards to the shared `BatchGetOrAdd.RunAsync` machinery, so existing `ICache` implementations keep compiling without adding them. The generator is invoked at most once, only with the states of the entries that missed every cache layer, never with keys; results come back keyed by state, one entry per distinct requested state in first-occurrence order. Cache operations de-duplicate by `CacheKey`; results de-duplicate by state — when two states share a key the generator is asked once and the value is reported under both. Their parameter shape matches the single-key `GetOrAddAsync` exactly — `expiration`, `policy` and `token` all optional. `ICache.Compat.cs` carries no token-positional forwarder for them: that file is pre-`CachePolicy` back-compat sugar, and this API predates nothing. There is no key-only convenience overload; a caller whose keys are their own identity pairs each key with itself (`TState = CacheKey`). +`TryAddAsync` writes only if the key is absent — StackExchange.Redis `When.NotExists` (`SET … NX`) on Redis-backed caches, in one atomic command with the TTL applied by the same write. Three points decide whether it fits your problem: + +- **`false` is deliberately ambiguous.** It means "you did not create this key" — either it already existed, or the write could not be completed (backing store disconnected, write threw, or the value was a `null`/`default` that the cache has no way to represent). This is fail-closed by design: a caller treating `true` as "I own this key" is never wrongly told it won. If you must distinguish a lost race from an outage, check [`IConnectionState.IsConnected`](#idistributedlock) before reacting to `false`, or use [`IDistributedLock.TryAcquireAsync`](#idistributedlock). +- **It never deletes.** Where `SetAsync` removes the key when handed a `null` and `CacheNullValues` is off, `TryAddAsync` returns `false` and leaves the key untouched. With `CacheNullValues` on, a `null` claims the key via the cached-null sentinel. +- **It is a cache primitive, not a lock.** The entry expires on its own TTL, there is no ownership token, and any later `SetAsync`/`RemoveAsync` on the key ignores the claim. For mutual exclusion with a fencing token and explicit release, use [`IDistributedLock`](#idistributedlock). + +Tier behavior follows from the same rule — L1 can never arbitrate while an L2 exists, because a key absent locally may well be present in the shared store: + +| Provider | Who decides | Scope of exclusion | +| --- | --- | --- | +| `Redis` | Redis (`SET … NX`) | Cross-node | +| `InMemoryRedis` | L2 Redis; L1 populated only after a win, best-effort, plus an invalidation broadcast | Cross-node | +| `InMemoryRedis`, L2 disconnected | Nobody — returns `false` | None (fails closed rather than granting a claim every node would also get) | +| `InMemory` | The local tier, serialized by `Lock.LocalLockEnabled` (on by default) | In-process only | +| `NullCache` | Nobody — returns `true` for every caller | None | + +The three overloads are **default interface methods**, so existing `ICache` implementations keep compiling. The default body throws `NotSupportedException`: a probe followed by a write is not atomic, and silently substituting one would void the only guarantee the method makes. All in-box implementations override it; a hand-written implementation must too before callers can use it. There is no multi-key overload — Redis has no atomic multi-key `NX`, and "all-or-nothing" versus "per-key" would be a coin flip on the caller's behalf. + > **Typical vs. power-user surface:** `ICache` is the power-user surface. For most application code where the value type is fixed and you want automatic policy resolution, prefer [`ICache`](#icachet) — it is simpler and less error-prone. **Use this when:** @@ -228,7 +260,7 @@ public partial interface IHashCache } ``` -`IHashCache` is the typed hash-cache surface. Each cache key maps to a dictionary of named fields rather than a single value — the backing store is a Redis hash (or an in-memory equivalent). `GetItemAsync` retrieves a single field by name; `GetAsync` retrieves all fields or a subset. `SetAsync` accepts a `HashCacheEntryOptions` overload for fine-grained per-write control (e.g. conditional set, individual field TTL). Metadata (`GetMetadataAsync` / `SetMetadataAsync`) provides a side-channel string dictionary attached to the same key, useful for audit or versioning data. Sync overloads (`Get`, `GetItem`, `GetOrAdd`, `Set`, `Refresh`, `Remove`, `Contains`, etc.) are provided as blocking default interface methods. +`IHashCache` is the typed hash-cache surface. Each cache key maps to a dictionary of named fields rather than a single value — the backing store is a Redis hash (or an in-memory equivalent). `GetItemAsync` retrieves a single field by name; `GetAsync` retrieves all fields or a subset. `SetAsync` accepts a `HashCacheEntryOptions` overload for per-write control of expiration, metadata, and write scope — `HashCacheSetOption.HashReplace` merges the given fields into the existing hash, `KeyReplace` drops the key first so the written fields are the whole hash. Note that this is write *scope*, not a precondition: the hash surface has no conditional-add member, and `TryAddAsync` exists only on [`ICache`](#icache) / [`ICache`](#icachet). Metadata (`GetMetadataAsync` / `SetMetadataAsync`) provides a side-channel string dictionary attached to the same key, useful for audit or versioning data. Sync overloads (`Get`, `GetItem`, `GetOrAdd`, `Set`, `Refresh`, `Remove`, `Contains`, etc.) are provided as blocking default interface methods. > **Typical vs. power-user surface:** `IHashCache` is the standard typed hash surface. If you need to vary the value type per call, use [`IHashCache`](#ihashcache) instead. The two surfaces are different shapes for different problems. diff --git a/src/UiPath.Caching.Abstractions/CacheOfT.cs b/src/UiPath.Caching.Abstractions/CacheOfT.cs index 6c920b6..384685d 100644 --- a/src/UiPath.Caching.Abstractions/CacheOfT.cs +++ b/src/UiPath.Caching.Abstractions/CacheOfT.cs @@ -95,6 +95,15 @@ public ValueTask SetAsync(CacheKey cacheKey, T? value, TimeSpan? expiratio public ValueTask SetAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CancellationToken token = default) => _cache.SetAsync(GetCacheKey(cacheKey), value, expiration, Policy, token); + public ValueTask TryAddAsync(CacheKey cacheKey, T? value, CancellationToken token = default) => + _cache.TryAddAsync(GetCacheKey(cacheKey), value, policy: Policy, token: token); + + public ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan? expiration, CancellationToken token = default) => + _cache.TryAddAsync(GetCacheKey(cacheKey), value, expiration, Policy, token); + + public ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CancellationToken token = default) => + _cache.TryAddAsync(GetCacheKey(cacheKey), value, expiration, Policy, token); + public ValueTask SetAsync(KeyValuePair[] keyValues, CancellationToken token = default) => _cache.SetAsync(GetKeyValuePairs(keyValues), policy: Policy, token: token); diff --git a/src/UiPath.Caching.Abstractions/ConditionalAdd.cs b/src/UiPath.Caching.Abstractions/ConditionalAdd.cs new file mode 100644 index 0000000..5532400 --- /dev/null +++ b/src/UiPath.Caching.Abstractions/ConditionalAdd.cs @@ -0,0 +1,19 @@ +namespace UiPath.Caching; + +/// +/// Shared plumbing for the conditional-add (TryAddAsync) surface. +/// +internal static class ConditionalAdd +{ + /// + /// Built for the TryAddAsync default interface bodies. The members ship as default + /// interface methods so hand-written / + /// implementations keep compiling, but there is no correct generic fallback: a probe followed + /// by a write is not atomic, and silently substituting one would break the single guarantee the + /// method exists to make. Throwing keeps the gap visible instead. + /// + public static NotSupportedException NotSupported(string cacheName, string implementationType) => + new($"Cache '{cacheName}' ({implementationType}) does not implement conditional add. " + + $"TryAddAsync needs an atomic create-if-absent primitive from the backing store " + + $"(Redis SET NX); it has no safe emulation, so this implementation must override it."); +} diff --git a/src/UiPath.Caching.Abstractions/ICache.cs b/src/UiPath.Caching.Abstractions/ICache.cs index 09f61d6..8860413 100644 --- a/src/UiPath.Caching.Abstractions/ICache.cs +++ b/src/UiPath.Caching.Abstractions/ICache.cs @@ -46,6 +46,64 @@ public partial interface ICache : IDisposable ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default); + /// + /// Conditional add: writes only if does not + /// already exist. On Redis-backed caches this is StackExchange.Redis + /// When.NotExists (SET key value EX … NX) — a single atomic round-trip, so exactly + /// one caller across all nodes can win a given key. + /// + /// + /// true when the key did not exist and this call created it. false when the key + /// already existed or the write could not be completed — the backing store was + /// disconnected, the write threw, or the value could not be represented (see below). The two are + /// deliberately conflated, fail-closed: a caller that treats true as "I own this key" is + /// never wrongly told it won. Callers that must distinguish infrastructure failure from a lost + /// race should use IDistributedLock.TryAcquireAsync, or check + /// IConnectionState.IsConnected before deciding how to react to false. + /// + /// + /// + /// Unlike SetAsync, this never deletes: a null/default + /// is written as the cached-null sentinel when the provider's + /// CacheNullValues is on, and otherwise returns false without touching the key, + /// where SetAsync would remove it. + /// + /// + /// On a multilayer (L1+L2) cache the distributed tier arbitrates the race and the local tier is + /// populated only after a win; when the L2 is disconnected the call returns false rather + /// than granting a local-only claim that a second node would also be granted. On a memory-only + /// provider there is no L2 to arbitrate, so exclusion is in-process only and depends on + /// Lock.LocalLockEnabled (on by default for the InMemory provider). + /// + /// + /// This is a cache primitive, not a lock: the entry expires on its own TTL, there is no + /// ownership token, and any SetAsync or RemoveAsync on the same key ignores it. + /// For mutual exclusion with a fencing token and explicit release, use + /// IDistributedLock. + /// + /// + /// Ships as a default interface method so existing implementations keep + /// compiling; the default body throws , because a probe + /// followed by a write would not be atomic and would quietly void the guarantee above. All + /// in-box implementations override it. + /// + /// + ValueTask TryAddAsync(CacheKey cacheKey, T? value, CachePolicy? policy = null, CancellationToken token = default) + => throw ConditionalAdd.NotSupported(Name, GetType().FullName ?? nameof(ICache)); + + /// + /// + /// Lifetime of the entry if it is created. Falls back to CachePolicy.DistributedExpiration + /// and then the cache's default expiration when null. Applied in the same atomic command as the + /// conditional write, so a won key is never left without a TTL. + /// + ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + => throw ConditionalAdd.NotSupported(Name, GetType().FullName ?? nameof(ICache)); + + /// + ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + => throw ConditionalAdd.NotSupported(Name, GetType().FullName ?? nameof(ICache)); + ValueTask RefreshAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default); ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default); diff --git a/src/UiPath.Caching.Abstractions/ICacheOfT.Sync.cs b/src/UiPath.Caching.Abstractions/ICacheOfT.Sync.cs index 1063bfb..24c1aa1 100644 --- a/src/UiPath.Caching.Abstractions/ICacheOfT.Sync.cs +++ b/src/UiPath.Caching.Abstractions/ICacheOfT.Sync.cs @@ -84,6 +84,18 @@ bool Set(KeyValuePair[] keyValues, TimeSpan? expiration = null, Ca bool Set(KeyValuePair[] keyValues, DateTimeOffset? expiration = null, CancellationToken token = default) => SetAsync(keyValues, expiration, token).AsTask().GetAwaiter().GetResult(); + [ExcludeFromCodeCoverage] + bool TryAdd(CacheKey cacheKey, T? value, CancellationToken token = default) + => TryAddAsync(cacheKey, value, token).AsTask().GetAwaiter().GetResult(); + + [ExcludeFromCodeCoverage] + bool TryAdd(CacheKey cacheKey, T? value, TimeSpan? expiration, CancellationToken token = default) + => TryAddAsync(cacheKey, value, expiration, token).AsTask().GetAwaiter().GetResult(); + + [ExcludeFromCodeCoverage] + bool TryAdd(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CancellationToken token = default) + => TryAddAsync(cacheKey, value, expiration, token).AsTask().GetAwaiter().GetResult(); + [ExcludeFromCodeCoverage] bool Refresh(CacheKey cacheKey, CancellationToken token = default) => RefreshAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); diff --git a/src/UiPath.Caching.Abstractions/ICacheOfT.cs b/src/UiPath.Caching.Abstractions/ICacheOfT.cs index 47d4939..537d026 100644 --- a/src/UiPath.Caching.Abstractions/ICacheOfT.cs +++ b/src/UiPath.Caching.Abstractions/ICacheOfT.cs @@ -35,6 +35,29 @@ public partial interface ICache ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset? expiration = null, CancellationToken token = default); + /// + /// Conditional add: writes only if does not + /// already exist. Typed façade over + /// — + /// see that member for the full contract, including what false means and why it is + /// fail-closed. + /// + /// + /// true only when the key did not exist and this call created it; false when it + /// already existed or the write could not be completed. + /// + ValueTask TryAddAsync(CacheKey cacheKey, T? value, CancellationToken token = default) + => throw ConditionalAdd.NotSupported(Name, GetType().FullName ?? nameof(ICache)); + + /// + /// Lifetime of the entry if it is created. + ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan? expiration, CancellationToken token = default) + => throw ConditionalAdd.NotSupported(Name, GetType().FullName ?? nameof(ICache)); + + /// + ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CancellationToken token = default) + => throw ConditionalAdd.NotSupported(Name, GetType().FullName ?? nameof(ICache)); + ValueTask RefreshAsync(CacheKey cacheKey, CancellationToken token = default); ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration, CancellationToken token = default); diff --git a/src/UiPath.Caching.Abstractions/NullCache.cs b/src/UiPath.Caching.Abstractions/NullCache.cs index 5da0af1..5e2858a 100644 --- a/src/UiPath.Caching.Abstractions/NullCache.cs +++ b/src/UiPath.Caching.Abstractions/NullCache.cs @@ -76,6 +76,21 @@ public ValueTask ContainsAsync(CacheKey cacheKey, CancellationToken tok public ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => ReturnTrueAsync(); + /// + /// Always true. Consistent with SetAsync on this type: the null store accepts every + /// write and retains none, so no key can pre-exist and no caller can lose the race. Note the + /// consequence — NullCache provides no mutual exclusion whatsoever, so every caller is + /// told it won. That is the same "caching is off, carry on" degradation the rest of this type + /// applies, and the alternative (always false) would starve every caller instead. + /// + public ValueTask TryAddAsync(CacheKey cacheKey, T? value, CachePolicy? policy = null, CancellationToken token = default) => ReturnTrueAsync(); + + /// + public ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => ReturnTrueAsync(); + + /// + public ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => ReturnTrueAsync(); + public ValueTask TimeToLiveAsync(CacheKey cacheKey, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); diff --git a/src/UiPath.Caching.Abstractions/PublicAPI.Unshipped.txt b/src/UiPath.Caching.Abstractions/PublicAPI.Unshipped.txt index e4688ba..2a2d0b8 100644 --- a/src/UiPath.Caching.Abstractions/PublicAPI.Unshipped.txt +++ b/src/UiPath.Caching.Abstractions/PublicAPI.Unshipped.txt @@ -1,21 +1,36 @@ #nullable enable +UiPath.Caching.Cache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.Cache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.Cache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.CacheKey.CacheKey(string? name, UiPath.Caching.CacheKeyCasing casing) -> void UiPath.Caching.CacheKey.Casing.get -> UiPath.Caching.CacheKeyCasing UiPath.Caching.CacheKey.WithName(string? name) -> UiPath.Caching.CacheKey UiPath.Caching.CacheKeyCasing UiPath.Caching.CacheKeyCasing.Insensitive = 0 -> UiPath.Caching.CacheKeyCasing +UiPath.Caching.CacheKeyCasing.Sensitive = 1 -> UiPath.Caching.CacheKeyCasing UiPath.Caching.CacheKeyComparer UiPath.Caching.CacheKeyComparer.CacheKeyComparer() -> void -static UiPath.Caching.CacheKeyComparer.Insensitive.get -> UiPath.Caching.CacheKeyComparer! -static UiPath.Caching.CacheKeyComparer.Sensitive.get -> UiPath.Caching.CacheKeyComparer! -UiPath.Caching.CacheKeyCasing.Sensitive = 1 -> UiPath.Caching.CacheKeyCasing UiPath.Caching.CacheOptions.KeyCasing.get -> UiPath.Caching.CacheKeyCasing UiPath.Caching.CacheOptions.KeyCasing.set -> void +UiPath.Caching.ICache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.TryAdd(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +UiPath.Caching.ICache.TryAdd(UiPath.Caching.CacheKey cacheKey, T? value, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +UiPath.Caching.ICache.TryAdd(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +UiPath.Caching.ICache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullCache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullCache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullCache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.SystemJsonByteSerializerProxy -UiPath.Caching.SystemJsonByteSerializerProxy.SystemJsonByteSerializerProxy(System.Text.Json.JsonSerializerOptions? options = null) -> void -UiPath.Caching.SystemJsonByteSerializerProxy.Serialize(object? value) -> byte[]? UiPath.Caching.SystemJsonByteSerializerProxy.Deserialize(byte[]? value) -> T? -UiPath.Caching.SystemJsonByteSerializerProxy.TryDeserialize(string? value, out T? result) -> bool +UiPath.Caching.SystemJsonByteSerializerProxy.Serialize(object? value) -> byte[]? +UiPath.Caching.SystemJsonByteSerializerProxy.SystemJsonByteSerializerProxy(System.Text.Json.JsonSerializerOptions? options = null) -> void UiPath.Caching.SystemJsonByteSerializerProxy.TryDeserialize(object? value, out T? result) -> bool +UiPath.Caching.SystemJsonByteSerializerProxy.TryDeserialize(string? value, out T? result) -> bool static UiPath.Caching.CacheKey.DefaultCasing.get -> UiPath.Caching.CacheKeyCasing static UiPath.Caching.CacheKey.DefaultCasing.set -> void +static UiPath.Caching.CacheKeyComparer.Insensitive.get -> UiPath.Caching.CacheKeyComparer! +static UiPath.Caching.CacheKeyComparer.Sensitive.get -> UiPath.Caching.CacheKeyComparer! diff --git a/src/UiPath.Caching/MultilayerCache.cs b/src/UiPath.Caching/MultilayerCache.cs index 07a737d..55dfd7d 100644 --- a/src/UiPath.Caching/MultilayerCache.cs +++ b/src/UiPath.Caching/MultilayerCache.cs @@ -8,6 +8,9 @@ internal sealed partial class MultilayerCache : MultilayerCacheBase, ICache private readonly ICache _innerCache; private readonly CacheEntryBuilder _entryBuilder; private readonly LocalMemorySetter _localMemorySetter; + // The memory-only provider passes NullCache as the L2: it accepts every write and keeps + // nothing, so it cannot arbitrate a conditional add and the local tier has to instead. + private readonly bool _innerCacheNeverStores; public MultilayerCache( string cacheName, @@ -27,6 +30,7 @@ public MultilayerCache( : base(cacheName, innerCache, memoryCacheFactory, topicFactory, cacheEventFactory, telemetryProvider, multiLayerCacheOptions, memoryCacheOptions, cacheOptions, localLock, distributedLock, policyFactory, logger) { _innerCache = innerCache; + _innerCacheNeverStores = innerCache is NullCache; var cacheKeyStrategy = _multiLayerCacheOptions.CacheKeyStrategy ?? new DefaultCacheKeyStrategy(); var topicKeyStrategy = _multiLayerCacheOptions.TopicKeyStrategy ?? new DefaultTopicKeyStrategy(cacheOptions.Separator); _entryBuilder = new CacheEntryBuilder(cacheKeyStrategy, topicKeyStrategy, _clock); @@ -657,6 +661,115 @@ public async ValueTask SetAsync(CacheKey cacheKey, T? value, DateTimeOf } + public ValueTask TryAddAsync(CacheKey cacheKey, T? value, CachePolicy? policy = null, CancellationToken token = default) + { + policy ??= _defaultPolicy; + return TryAddAsync(cacheKey, value, ResolveWriteDuration(policy), policy, token); + } + + public ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + { + policy ??= _defaultPolicy; + return TryAddAsync(cacheKey, value, _clock.ToDateTimeOffset(ResolveWriteDuration(policy, expiration)), policy, token); + } + + /// + /// The L2 arbitrates the race and the L1 is only populated after a win — the reverse of + /// SetAsync, which writes both tiers unconditionally. L1 cannot arbitrate: a key missing + /// locally may well exist in the shared store, so a local probe would hand the same win to every + /// node. When there is no L2 at all (memory-only provider) the local tier becomes the arbiter and + /// exclusion narrows to this process; when the L2 exists but is disconnected the call fails + /// closed instead of granting a local-only claim. + /// + public async ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + { + NotCacheableException.ThrowIfNotCacheable(); + policy ??= _defaultPolicy; + expiration ??= _clock.ToDateTimeOffset(ResolveWriteDuration(policy)); + var options = _entryBuilder.BuildEntryOptions(cacheKey, expiration, token); + + if (value is null && !_multiLayerCacheOptions.CacheNullValues) + { + // SetAsync removes the key in this case. A conditional add must not delete, and it has + // nothing to claim the key with either, so it reports the no-op. + LogTryAddSkippedUnrepresentableValue(options.CacheKey); + return false; + } + + if (_innerCacheNeverStores) + { + return await LocalTryAddAsync(options, value, policy).ConfigureAwait(false); + } + + if (GetInnerCacheDisconnected()) + { + LogTryAddInnerDisconnected(options.CacheKey); + return false; + } + + bool added; + try + { + added = await _innerCache.TryAddAsync(options.CacheKey, value, options.Expiration, policy, options.Token).ConfigureAwait(false); + } + catch (Exception ex) + { + LogInnerCacheTryAddError(ex, options.CacheKey); + return false; + } + + if (!added) + { + return false; + } + + // The key is claimed in the shared store, so this caller has won and must be told so + // regardless of what the local tier does next — reporting a loss here would strand the entry + // with no owner until its TTL expires. Broadcast (to drop other nodes' stale L1 copies) and + // the local write are therefore best-effort, and ordered after the win rather than before it + // as in SetAsync: invalidating peers for a write that never happened is pure waste. + try + { + await _eventPublisher.CacheSetAsync(options).ConfigureAwait(false); + MemorySet(options, value, policy.LocalExpiration ?? _multiLayerCacheOptions.LocalMaxExpiration); + } + catch (Exception ex) + { + LogTryAddLocalPropagationFailed(ex, options.CacheKey); + } + + return true; + } + + /// + /// Conditional add against the in-memory tier, for the memory-only provider where it is the only + /// store. exposes no create-if-absent primitive, so the local lock is + /// what makes the probe-then-write atomic; without it two in-process callers can both win, which + /// is logged rather than silently tolerated. + /// + private async ValueTask LocalTryAddAsync(CacheEntryOptions options, T? value, CachePolicy policy) + { + var localLock = await AcquireLocalLockAsync(options.CacheKey, policy.Lock, options.Token).ConfigureAwait(false); + try + { + if (localLock is null) + { + LogTryAddUnserialized(options.CacheKey); + } + + if (_memoryCache.TryGetValue(options.CacheKey, out _)) + { + return false; + } + + return MemorySet(options, value, policy.LocalExpiration ?? _multiLayerCacheOptions.LocalMaxExpiration); + } + finally + { + localLock?.Dispose(); + } + } + public ValueTask SetAsync(KeyValuePair[] keyValues, CachePolicy? policy = null, CancellationToken token = default) { policy ??= _defaultPolicy; @@ -1124,6 +1237,21 @@ private readonly struct CacheEntryValue(CacheEntryOptions cacheEntry, T? valu [LoggerMessage(Level = LogLevel.Debug, Message = "Batch cache missed. generating {Count} keys for {CacheKey}")] private partial void LogBatchCacheMissed(CacheKey cacheKey, int count); + [LoggerMessage(Level = LogLevel.Debug, Message = "TryAdd skipped for {CacheKey}: a null value cannot be represented unless CacheNullValues is on.")] + private partial void LogTryAddSkippedUnrepresentableValue(CacheKey cacheKey); + + [LoggerMessage(Level = LogLevel.Debug, Message = "TryAdd for {CacheKey} reported not-added: the inner cache is disconnected, so no cross-node claim can be made.")] + private partial void LogTryAddInnerDisconnected(CacheKey cacheKey); + + [LoggerMessage(Level = LogLevel.Warning, Message = "Inner cache TryAdd for cacheKey {CacheKey}")] + private partial void LogInnerCacheTryAddError(Exception ex, CacheKey cacheKey); + + [LoggerMessage(Level = LogLevel.Warning, Message = "TryAdd won cacheKey {CacheKey} in the inner cache but could not broadcast or populate the local tier. The add still stands.")] + private partial void LogTryAddLocalPropagationFailed(Exception ex, CacheKey cacheKey); + + [LoggerMessage(Level = LogLevel.Warning, Message = "TryAdd for {CacheKey} ran without the local lock, so concurrent in-process callers may both be told they added the key. Enable Lock.LocalLockEnabled for exclusion on a memory-only cache.")] + private partial void LogTryAddUnserialized(CacheKey cacheKey); + [LoggerMessage(Level = LogLevel.Debug, Message = "Replacing cached key {CacheKey}")] private partial void LogReplacingCachedKey(CacheKey cacheKey); diff --git a/src/UiPath.Caching/MultilayerCacheBase.cs b/src/UiPath.Caching/MultilayerCacheBase.cs index d5bc077..746a4d4 100644 --- a/src/UiPath.Caching/MultilayerCacheBase.cs +++ b/src/UiPath.Caching/MultilayerCacheBase.cs @@ -193,6 +193,18 @@ private static TimeSpan NonNegativeOrFallback(TimeSpan? value, TimeSpan fallback return ApplyJitter(resolved, policy.JitterMaxDuration, _clock.UtcNow); } + /// + /// Acquires only the local lock for , resolving enablement and timeout + /// the same way does. Returns null when the local lock is + /// disabled by policy or options, or when the acquire timed out — callers that need the lock for + /// correctness (rather than as a de-duplication optimization) must handle that case explicitly + /// rather than assume exclusion. + /// + private protected ValueTask AcquireLocalLockAsync(CacheKey cacheKey, LockProfile? policyLock, CancellationToken token) => + (policyLock?.LocalLockEnabled ?? _localLockEnabled) + ? TryAcquireLocalLockAsync(cacheKey, PositiveOrFallback(policyLock?.LocalLockTimeout, _localLockTimeout), token) + : new ValueTask(default(IDisposable)); + private async ValueTask TryAcquireLocalLockAsync(CacheKey cacheKey, TimeSpan localLockTimeout, CancellationToken token) { var lockKey = _localLockKeyPrefix + cacheKey.Name; diff --git a/src/UiPath.Caching/Redis/RedisCache.cs b/src/UiPath.Caching/Redis/RedisCache.cs index f42bbcb..e4e8d8e 100644 --- a/src/UiPath.Caching/Redis/RedisCache.cs +++ b/src/UiPath.Caching/Redis/RedisCache.cs @@ -237,6 +237,23 @@ public ValueTask SetAsync(KeyValuePair[] keyValues, DateT return SetInternalAsync(keyValues, Clock.ToTimeSpan(effective), token); } + public ValueTask TryAddAsync(CacheKey cacheKey, T? value, CachePolicy? policy = null, CancellationToken token = default) => + TryAddAsync(cacheKey, value, expiration: (TimeSpan?)null, policy, token); + + public ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + { + NotCacheableException.ThrowIfNotCacheable(); + var effective = ResolveExpiration(expiration, policy); + return TryAddInternalAsync(ToRedisKey(cacheKey, token), value, Clock.ToTimeSpan(effective), token); + } + + public ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + { + NotCacheableException.ThrowIfNotCacheable(); + var effective = ResolveExpiration(expiration, policy); + return TryAddInternalAsync(ToRedisKey(cacheKey, token), value, Clock.ToTimeSpan(effective), token); + } + public async ValueTask ContainsAsync(CacheKey cacheKey, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); @@ -459,6 +476,57 @@ private async ValueTask SetInternalAsync(RedisKey redisKey, T? value, T return ret; } + /// + /// SET key value EX … NX in one round-trip: Redis itself decides the race, so no probe + /// precedes the write. Returns false for every non-win — key already present, not + /// connected, write threw, or a default value that this cache has no way to represent. + /// + private async ValueTask TryAddInternalAsync(RedisKey redisKey, T? value, TimeSpan expiration, CancellationToken token) + { + bool ret = default; + token.ThrowIfCancellationRequested(); + + // Fail closed. Handing out a win we could not write would let a second caller win the same key. + if (!IsConnected) + { + return false; + } + + var operation = StartOperation(nameof(TryAddAsync)); + try + { + var isNull = IsDefault(value); + if (isNull && (!_cacheNullValues || expiration <= TimeSpan.Zero)) + { + // A conditional add must never delete, which is what SetAsync does with a default + // value here. With no sentinel available there is nothing to claim the key with. + LogTryAddSkippedUnrepresentableValue(redisKey); + } + else + { + var serialized = isNull ? RedisValue.EmptyString : _serializer.Serialize(value); + + ret = await _write.ExecuteAsync(async token => + { + token.ThrowIfCancellationRequested(); + return await Database.StringSetAsync(redisKey, serialized, expiration, When.NotExists, CommandFlags.DemandMaster).ConfigureAwait(false); + }, default, token).ConfigureAwait(false); + } + operation.Stop(); + } + catch (Exception ex) + { + operation.Stop(); + LogRedisCacheException(ex); + } + finally + { + operation.Track(ret); + } + + return ret; + } + private async ValueTask SetInternalAsync(KeyValuePair[] keyValues, TimeSpan expiration, CancellationToken token) { bool ret = default; @@ -881,6 +949,9 @@ private void AuditKeySize(RedisKey key, RedisValue value) [LoggerMessage(Level = LogLevel.Trace, Message = "Refreshing key {RedisKey} at expiration {Expiration}")] private partial void LogRefreshingKey(RedisKey redisKey, DateTimeOffset? expiration); + [LoggerMessage(Level = LogLevel.Debug, Message = "TryAdd skipped for {RedisKey}: a default value cannot be represented unless CacheNullValues is on with a positive expiration.")] + private partial void LogTryAddSkippedUnrepresentableValue(RedisKey redisKey); + [LoggerMessage(Level = LogLevel.Warning, Message = "RedisCache exception.")] private partial void LogRedisCacheException(Exception ex); diff --git a/tests/UiPath.Caching.Tests/CacheOfTTryAddTests.cs b/tests/UiPath.Caching.Tests/CacheOfTTryAddTests.cs new file mode 100644 index 0000000..e98f84b --- /dev/null +++ b/tests/UiPath.Caching.Tests/CacheOfTTryAddTests.cs @@ -0,0 +1,91 @@ +using UiPath.Caching.Tests.Fakes; + +namespace UiPath.Caching.Tests; + +/// +/// The typed façade. owns two things the untyped surface does not: the key +/// strategy that namespaces keys per type, and the policy snapshot taken at construction. Both must +/// apply to a conditional add exactly as they do to SetAsync, or a caller electing a winner +/// under Cache<A> would collide with one under Cache<B>. +/// +public class CacheOfTTryAddTests(ITestContextAccessor testContextAccessor) +{ + private CancellationToken Ct => testContextAccessor.Current.CancellationToken; + + [Fact] + public async Task TryAdd_forwards_to_the_inner_cache_and_reports_the_win() + { + var inner = new DictionaryCache(); + var sut = new Cache(inner); + + (await sut.TryAddAsync("k", "first", Ct)).Should().BeTrue(); + (await sut.TryAddAsync("k", "second", Ct)).Should().BeFalse(); + inner.TryAddCalls.Should().Be(2); + } + + [Fact] + public async Task TryAdd_does_not_overwrite_the_winner() + { + var inner = new DictionaryCache(); + var sut = new Cache(inner); + + await sut.TryAddAsync("k", "first", Ct); + await sut.TryAddAsync("k", "second", Ct); + + (await sut.GetAsync("k", Ct)).Should().Be("first"); + } + + [Fact] + public async Task TryAdd_applies_the_type_key_strategy() + { + var inner = new DictionaryCache(); + var keyStrategy = Substitute.For(); + keyStrategy.GetCacheKey(Arg.Any()).Returns((CacheKey)"namespaced:k"); + var sut = new Cache(inner, keyStrategy); + + await sut.TryAddAsync("k", "v", Ct); + + inner.Contains("namespaced:k").Should().BeTrue(); + inner.Contains("k").Should().BeFalse("an unnamespaced claim could collide with another type's key"); + } + + [Fact] + public async Task TryAdd_passes_the_constructed_policy_through() + { + var policy = new CachePolicy { DistributedExpiration = TimeSpan.FromMinutes(11) }; + var inner = Substitute.For(); + inner.TryAddAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(true); + var sut = new Cache(inner, cacheKeyStrategy: null, policy); + + await sut.TryAddAsync("k", "v", Ct); + + await inner.Received(1).TryAddAsync(Arg.Any(), "v", policy, Arg.Any()); + } + + [Fact] + public async Task TryAdd_forwards_both_expiration_shapes_with_the_policy() + { + var policy = new CachePolicy { DistributedExpiration = TimeSpan.FromMinutes(11) }; + var inner = Substitute.For(); + var sut = new Cache(inner, cacheKeyStrategy: null, policy); + var ttl = TimeSpan.FromMinutes(3); + var absolute = DateTimeOffset.UtcNow.AddMinutes(3); + + await sut.TryAddAsync("k", "v", ttl, Ct); + await sut.TryAddAsync("k", "v", absolute, Ct); + + await inner.Received(1).TryAddAsync(Arg.Any(), "v", ttl, policy, Arg.Any()); + await inner.Received(1).TryAddAsync(Arg.Any(), "v", absolute, policy, Arg.Any()); + } + + [Fact] + public void TryAdd_blocking_forwarder_matches_the_async_result() + { + var inner = new DictionaryCache(); + ICache sut = new Cache(inner); + + sut.TryAdd("k", "first", Ct).Should().BeTrue(); + sut.TryAdd("k", "second", Ct).Should().BeFalse(); + } +} diff --git a/tests/UiPath.Caching.Tests/Fakes/DictionaryCache.cs b/tests/UiPath.Caching.Tests/Fakes/DictionaryCache.cs index 24c37b7..5e3d70a 100644 --- a/tests/UiPath.Caching.Tests/Fakes/DictionaryCache.cs +++ b/tests/UiPath.Caching.Tests/Fakes/DictionaryCache.cs @@ -13,6 +13,8 @@ internal sealed class DictionaryCache : ICache public int SetCalls { get; private set; } + public int TryAddCalls { get; private set; } + public List SetKeySets { get; } = []; public void Seed(CacheKey key, T? value) => _store[key] = value; @@ -86,6 +88,24 @@ public ValueTask SetAsync(KeyValuePair[] keyValues, TimeS public ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => SetAsync(keyValues, policy, token); + // A real conditional add: the dictionary itself decides, so this fake can stand in for a store + // that supports NX. + public ValueTask TryAddAsync(CacheKey cacheKey, T? value, CachePolicy? policy = null, CancellationToken token = default) + { + TryAddCalls++; + if (value is null && !CacheNullValues) + { + return ValueTask.FromResult(false); + } + return ValueTask.FromResult(_store.TryAdd(cacheKey, value)); + } + + public ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => + TryAddAsync(cacheKey, value, policy, token); + + public ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => + TryAddAsync(cacheKey, value, policy, token); + public ValueTask RemoveAsync(CacheKey cacheKey, CancellationToken token = default) => ValueTask.FromResult(_store.Remove(cacheKey)); diff --git a/tests/UiPath.Caching.Tests/MultilayerCacheTryAddTests.cs b/tests/UiPath.Caching.Tests/MultilayerCacheTryAddTests.cs new file mode 100644 index 0000000..d30329b --- /dev/null +++ b/tests/UiPath.Caching.Tests/MultilayerCacheTryAddTests.cs @@ -0,0 +1,338 @@ +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Internal; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using NSubstitute.ExceptionExtensions; +using UiPath.Caching; +using UiPath.Caching.Config; +using UiPath.Caching.Locking; +using UiPath.Caching.Telemetry; +using UiPath.Caching.Tests.Broadcast; + +namespace UiPath.Caching.Tests; + +/// +/// Conditional add across the two tiers. The invariant: L1 never arbitrates while an L2 exists — a +/// key missing locally may still exist in the shared store, so a local probe would hand the same win +/// to every node. +/// +public class MultilayerCacheTryAddTests(ITestContextAccessor testContextAccessor) : IAsyncLifetime +{ + private readonly IFixture _fixture = AutoFixtureCreator.NSubstitute(); + + private ICache _innerCache = default!; + private IChangeTokenFactory _changeTokenFactory = default!; + private ITopicFactory _topicFactory = default!; + private ITopicProviderWithConnectionState _topicProvider = default!; + private ITopic _topic = default!; + private IMemoryCache _memoryCache = default!; + private IMemoryCacheFactory _memoryCacheFactory = default!; + private InMemoryRedisCacheOptions _options = default!; + private TopicKey _topicKey = default!; + private CacheKey _cacheKey = default!; + private ILogger _logger = default!; + + private MultilayerCache? _sut; + + private MultilayerCache Sut => _sut ??= _fixture.Create(); + + private CancellationToken Ct => testContextAccessor.Current.CancellationToken; + + [Fact] + public async Task TryAdd_delegates_the_decision_to_the_inner_cache() + { + var value = _fixture.Create(); + _innerCache.TryAddAsync(_cacheKey, value, Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(true); + _topic.PublishAsync(Arg.Any(), Arg.Any()).Returns(_ => true); + + var added = await Sut.TryAddAsync(_cacheKey, value, policy: null, token: Ct); + + added.Should().BeTrue(); + await _innerCache.Received(1).TryAddAsync(_cacheKey, value, Arg.Any(), Arg.Any(), Arg.Any()); + _memoryCache.Received(1).CreateEntry(_cacheKey); + } + + [Fact] + public async Task TryAdd_never_probes_the_local_tier_to_decide() + { + // A local hit means nothing here: the key may be absent locally and present in the shared + // store, so only the L2 can say who won. + _memoryCache.TryGetValue(_cacheKey, out Arg.Any()) + .Returns(x => + { + x[1] = new TestCacheEntry { Value = _fixture.Create() }; + return true; + }); + _innerCache.TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(true); + _topic.PublishAsync(Arg.Any(), Arg.Any()).Returns(_ => true); + + var added = await Sut.TryAddAsync(_cacheKey, _fixture.Create(), policy: null, token: Ct); + + added.Should().BeTrue("the inner cache said the key was free, and it is the only authority"); + await _innerCache.Received(1).TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task TryAdd_leaves_both_tiers_untouched_when_the_inner_cache_reports_a_loss() + { + _innerCache.TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(false); + + var added = await Sut.TryAddAsync(_cacheKey, _fixture.Create(), policy: null, token: Ct); + + added.Should().BeFalse(); + _memoryCache.DidNotReceive().CreateEntry(_cacheKey); + // Invalidating peers for a write that never happened is pure waste. + await _topic.DidNotReceive().PublishAsync(Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task TryAdd_broadcasts_after_a_win_so_peers_drop_stale_local_copies() + { + _innerCache.TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(true); + _topic.PublishAsync(Arg.Any(), Arg.Any()).Returns(_ => true); + + await Sut.TryAddAsync(_cacheKey, _fixture.Create(), policy: null, token: Ct); + + await _topic.Received(1).PublishAsync(Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task TryAdd_still_reports_the_win_when_the_broadcast_fails() + { + _innerCache.TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(true); + _topic.PublishAsync(Arg.Any(), Arg.Any()) + .ThrowsAsync(new InvalidOperationException("broadcast down")); + + var added = await Sut.TryAddAsync(_cacheKey, _fixture.Create(), policy: null, token: Ct); + + added.Should().BeTrue("the key is claimed in the shared store; denying the win would strand it with no owner"); + } + + [Fact] + public async Task TryAdd_fails_closed_when_the_inner_cache_throws() + { + _innerCache.TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new InvalidOperationException("redis down")); + + var added = await Sut.TryAddAsync(_cacheKey, _fixture.Create(), policy: null, token: Ct); + + added.Should().BeFalse(); + _memoryCache.DidNotReceive().CreateEntry(_cacheKey); + } + + [Fact] + public async Task TryAdd_fails_closed_when_the_inner_cache_is_disconnected() + { + _options.UseLocalOnlyWhenDisconnected = true; + _options.ConnectionMonitorEnabled = true; + _topicProvider.IsConnected.Returns(false); + _sut = null; + + var added = await Sut.TryAddAsync(_cacheKey, _fixture.Create(), policy: null, token: Ct); + + added.Should().BeFalse("a local-only claim would be granted to every node independently"); + // SetAsync degrades to a local write here; a conditional add must not. + _memoryCache.DidNotReceive().CreateEntry(_cacheKey); + await _innerCache.DidNotReceive().TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task TryAdd_of_a_null_value_never_deletes_and_never_reaches_the_inner_cache() + { + _options.CacheNullValues = false; + _sut = null; + + var added = await Sut.TryAddAsync(_cacheKey, default(string), policy: null, token: Ct); + + added.Should().BeFalse(); + // SetAsync removes the key in this case; a conditional add must not. + _memoryCache.DidNotReceive().Remove(_cacheKey); + await _innerCache.DidNotReceive().RemoveAsync(_cacheKey, Arg.Any()); + await _innerCache.DidNotReceive().TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task TryAdd_of_a_null_value_reaches_the_inner_cache_when_CacheNullValues_is_on() + { + _options.CacheNullValues = true; + _sut = null; + _innerCache.TryAddAsync(_cacheKey, default, Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(true); + _topic.PublishAsync(Arg.Any(), Arg.Any()).Returns(_ => true); + + var added = await Sut.TryAddAsync(_cacheKey, default(string), policy: null, token: Ct); + + added.Should().BeTrue(); + await _innerCache.Received(1).TryAddAsync(_cacheKey, default, Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task TryAdd_forwards_the_caller_expiration_to_the_inner_cache() + { + var ttl = TimeSpan.FromMinutes(7); + _innerCache.TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(true); + _topic.PublishAsync(Arg.Any(), Arg.Any()).Returns(_ => true); + + await Sut.TryAddAsync(_cacheKey, _fixture.Create(), ttl, token: Ct); + + await _innerCache.Received(1).TryAddAsync( + _cacheKey, + Arg.Any(), + Arg.Is(e => e.HasValue && e.Value > DateTimeOffset.UtcNow), + Arg.Any(), + Arg.Any()); + } + + [Fact] + public async Task TryAdd_rejects_a_null_key() + { + string? nullKey = null; + var act = async () => await Sut.TryAddAsync(nullKey!, _fixture.Create(), policy: null, token: Ct); + + await act.Should().ThrowAsync(); + } + + public ValueTask InitializeAsync() + { + _cacheKey = _fixture.Create(); + _topicKey = _fixture.Create(); + + _changeTokenFactory = _fixture.Freeze(); + _memoryCache = _fixture.Freeze(); + _innerCache = _fixture.Freeze(); + _logger = _fixture.Freeze(); + _logger.IsEnabled(Arg.Any()).Returns(true); + _options = new() + { + DefaultExpiration = TimeSpan.FromMinutes(10), + EntryFactory = new TestCacheEntryFactory(), + }; + + var cacheKeyStrategy = _fixture.Create(); + var topicKeyStrategy = _fixture.Create(); + cacheKeyStrategy.GetCacheKey(_cacheKey).Returns(_cacheKey); + topicKeyStrategy.GetTopicKey().Returns(_topicKey); + _topicFactory = _fixture.Freeze(); + _topicProvider = _fixture.Freeze(); + _topic = _fixture.Freeze>(); + _topicFactory.Get(Arg.Any()).Returns(_topicProvider); + _topicProvider.Create(_topicKey).Returns(_topic); + _topicProvider.Create(Arg.Any()).Returns(_topic); + _memoryCacheFactory = _fixture.Freeze(); + _memoryCacheFactory.Get(Arg.Any()).Returns(_ => _memoryCache); + _fixture.Inject(_options); + _fixture.Inject(_options); + _fixture.Inject>(new CacheClearEventFormatterProxy()); + var cacheEventFactory = _fixture.Freeze(); + cacheEventFactory.Create(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(c => new TestCacheEvent + { + Id = c.ArgAt(3), + Data = c.Arg(), + Type = c.ArgAt(1), + }); + return ValueTask.CompletedTask; + } + + public ValueTask DisposeAsync() + { + _sut?.Dispose(); + return ValueTask.CompletedTask; + } + + public interface ITopicProviderWithConnectionState : ITopicProvider, IConnectionState + { + } +} + +/// +/// The memory-only provider: a real over , so +/// the local tier is the storage and the arbiter. Exclusion here is in-process only, which +/// is the honest ceiling for a cache with no shared store — these tests pin that it is at least +/// correct within the process. +/// +public class InMemoryCacheTryAddTests +{ + private static CancellationToken Ct => TestContext.Current.CancellationToken; + + private static MultilayerCache CreateSut(InMemoryCacheOptions? options = null) + { + options ??= new InMemoryCacheOptions(); + var cacheOptions = new CacheOptions { AppShortName = "test" }; + return new MultilayerCache( + KnownCacheProviderNames.InMemory, + NullCache.Instance, + new MemoryCacheFactory(null, NullLoggerFactory.Instance), + NullChangeTokenFactory.Instance, + NullTopicFactory.Instance, + NullCacheEventFactory.Instance, + NullTelemetryProvider.Instance, + options, + options, + cacheOptions, + localLock: new AsyncKeyedLocalLock(Options.Create(cacheOptions)), + distributedLock: NullDistributedLock.Instance, + policyFactory: NullCachePolicyFactory.Instance, + logger: NullLogger.Instance); + } + + [Fact] + public async Task First_caller_adds_and_the_second_loses() + { + using var sut = CreateSut(); + + (await sut.TryAddAsync("k", "first", policy: null, token: Ct)).Should().BeTrue(); + (await sut.TryAddAsync("k", "second", policy: null, token: Ct)).Should().BeFalse(); + } + + [Fact] + public async Task A_lost_add_does_not_overwrite_the_winner_value() + { + using var sut = CreateSut(); + + await sut.TryAddAsync("k", "first", policy: null, token: Ct); + await sut.TryAddAsync("k", "second", policy: null, token: Ct); + + (await sut.GetAsync("k", policy: null, token: Ct)).Should().Be("first"); + } + + [Fact] + public async Task The_key_is_claimable_again_once_removed() + { + using var sut = CreateSut(); + + await sut.TryAddAsync("k", "first", policy: null, token: Ct); + await sut.RemoveAsync("k", Ct); + + (await sut.TryAddAsync("k", "second", policy: null, token: Ct)).Should().BeTrue(); + (await sut.GetAsync("k", policy: null, token: Ct)).Should().Be("second"); + } + + [Fact] + public async Task TryAdd_does_not_claim_a_key_an_unconditional_set_already_wrote() + { + using var sut = CreateSut(); + + await sut.SetAsync("k", "written", policy: null, token: Ct); + + (await sut.TryAddAsync("k", "claimed", policy: null, token: Ct)).Should().BeFalse(); + } + + [Fact] + public async Task Exactly_one_of_many_concurrent_callers_wins() + { + using var sut = CreateSut(); + const int callers = 32; + + var results = await Task.WhenAll(Enumerable.Range(0, callers).Select(i => + Task.Run(async () => await sut.TryAddAsync("k", $"caller-{i}", policy: null, token: Ct), Ct))); + + results.Count(won => won).Should().Be(1, "the local lock is what makes probe-then-write atomic"); + } +} diff --git a/tests/UiPath.Caching.Tests/Redis/RedisCacheTryAddTests.cs b/tests/UiPath.Caching.Tests/Redis/RedisCacheTryAddTests.cs new file mode 100644 index 0000000..8c4ddff --- /dev/null +++ b/tests/UiPath.Caching.Tests/Redis/RedisCacheTryAddTests.cs @@ -0,0 +1,245 @@ +using Microsoft.Extensions.Internal; +using NSubstitute.ExceptionExtensions; +using StackExchange.Redis; +using UiPath.Caching; +using UiPath.Caching.Policies; +using UiPath.Caching.Telemetry; +using UiPath.Caching.Tests.Telemetry; + +namespace UiPath.Caching.Tests.Redis; + +/// +/// Conditional add (TryAddAsync) on the Redis tier. The contract under test is narrow: one +/// SET … NX command, the Redis reply is the answer, and every non-win — lost race, +/// disconnected, thrown, unrepresentable value — reports false without a second round-trip. +/// +public class RedisCacheTryAddTests(ITestContextAccessor testContextAccessor) : IAsyncLifetime +{ + private readonly IFixture _fixture = AutoFixtureCreator.NSubstitute(); + private ISystemClock _clock = default!; + private RedisCacheOptions _cacheOptions = default!; + private IDatabase _database = default!; + private ISerializerProxy _serializer = default!; + private readonly DateTimeOffset _now = DateTimeOffset.UtcNow; + private CacheKey _cacheKey = default!; + private RedisKey _redisKey = default!; + private IRedisConnector _connector = default!; + private bool _isConnected = true; + private readonly RecordingTelemetryProvider _telemetry = new(); + private RedisCache? _sut; + + private RedisCache Sut => _sut ??= _fixture.Create(); + + [Fact] + public async Task TryAdd_issues_a_single_NX_write_and_reports_the_win() + { + var value = _fixture.Create(); + _database.StringSetAsync(_redisKey, Arg.Any(), Arg.Any(), When.NotExists, CommandFlags.DemandMaster) + .Returns(true); + + var added = await Sut.TryAddAsync(_cacheKey, value, policy: null, token: testContextAccessor.Current.CancellationToken); + + added.Should().BeTrue(); + await _database.Received(1).StringSetAsync(_redisKey, Arg.Any(), Arg.Any(), When.NotExists, CommandFlags.DemandMaster); + // The point of NX is that the decision costs one command; a probe would reintroduce the race. + await _database.DidNotReceive().StringGetAsync(Arg.Any(), Arg.Any()); + await _database.DidNotReceive().KeyExistsAsync(Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task TryAdd_never_writes_unconditionally() + { + _database.StringSetAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(true); + + await Sut.TryAddAsync(_cacheKey, _fixture.Create(), policy: null, token: testContextAccessor.Current.CancellationToken); + + await _database.DidNotReceive().StringSetAsync(Arg.Any(), Arg.Any(), Arg.Any(), When.Always, Arg.Any()); + await _database.DidNotReceive().StringSetAsync(Arg.Any(), Arg.Any(), Arg.Any(), When.Exists, Arg.Any()); + } + + [Fact] + public async Task TryAdd_reports_not_added_when_the_key_already_exists() + { + _database.StringSetAsync(_redisKey, Arg.Any(), Arg.Any(), When.NotExists, CommandFlags.DemandMaster) + .Returns(false); + + var added = await Sut.TryAddAsync(_cacheKey, _fixture.Create(), policy: null, token: testContextAccessor.Current.CancellationToken); + + added.Should().BeFalse(); + } + + [Fact] + public async Task TryAdd_writes_a_payload_a_reader_can_deserialize() + { + var value = _fixture.Create(); + RedisValue captured = default; + _database.StringSetAsync(_redisKey, Arg.Do(v => captured = v), Arg.Any(), When.NotExists, Arg.Any()) + .Returns(true); + + await Sut.TryAddAsync(_cacheKey, value, policy: null, token: testContextAccessor.Current.CancellationToken); + + _serializer.Deserialize(captured).Should().Be(value); + } + + [Fact] + public async Task TryAdd_fails_closed_when_disconnected() + { + _isConnected = false; + _database.StringSetAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(true); + + var added = await Sut.TryAddAsync(_cacheKey, _fixture.Create(), policy: null, token: testContextAccessor.Current.CancellationToken); + + added.Should().BeFalse("a win that was never written would let a second caller win the same key"); + await _database.DidNotReceive().StringSetAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task TryAdd_fails_closed_when_the_write_throws() + { + _database.StringSetAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new RedisException("test")); + + var added = await Sut.TryAddAsync(_cacheKey, _fixture.Create(), policy: null, token: testContextAccessor.Current.CancellationToken); + + added.Should().BeFalse(); + } + + [Fact] + public async Task TryAdd_applies_the_caller_expiration_in_the_same_command() + { + var ttl = TimeSpan.FromMinutes(3); + _database.StringSetAsync(_redisKey, Arg.Any(), Arg.Any(), When.NotExists, CommandFlags.DemandMaster) + .Returns(true); + + await Sut.TryAddAsync(_cacheKey, _fixture.Create(), ttl, token: testContextAccessor.Current.CancellationToken); + + // One command, so a won key is never briefly immortal between the write and a follow-up EXPIRE. + await _database.Received(1).StringSetAsync(_redisKey, Arg.Any(), ttl, When.NotExists, CommandFlags.DemandMaster); + await _database.DidNotReceive().KeyExpireAsync(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task TryAdd_falls_back_to_the_policy_expiration() + { + var policyTtl = TimeSpan.FromMinutes(7); + _database.StringSetAsync(_redisKey, Arg.Any(), Arg.Any(), When.NotExists, CommandFlags.DemandMaster) + .Returns(true); + + await Sut.TryAddAsync(_cacheKey, _fixture.Create(), policy: new CachePolicy { DistributedExpiration = policyTtl }, token: testContextAccessor.Current.CancellationToken); + + await _database.Received(1).StringSetAsync(_redisKey, Arg.Any(), policyTtl, When.NotExists, CommandFlags.DemandMaster); + } + + [Fact] + public async Task TryAdd_accepts_an_absolute_expiration() + { + var absolute = _now.AddMinutes(4); + _database.StringSetAsync(_redisKey, Arg.Any(), Arg.Any(), When.NotExists, CommandFlags.DemandMaster) + .Returns(true); + + await Sut.TryAddAsync(_cacheKey, _fixture.Create(), absolute, token: testContextAccessor.Current.CancellationToken); + + await _database.Received(1).StringSetAsync(_redisKey, Arg.Any(), TimeSpan.FromMinutes(4), When.NotExists, CommandFlags.DemandMaster); + } + + [Fact] + public async Task TryAdd_of_a_default_value_never_deletes_the_key() + { + _cacheOptions.CacheNullValues = false; + + var added = await Sut.TryAddAsync(_cacheKey, null, TimeSpan.FromMinutes(1), token: testContextAccessor.Current.CancellationToken); + + added.Should().BeFalse("with no cached-null sentinel available there is nothing to claim the key with"); + // SetAsync removes the key in this case; a conditional add must not. + await _database.DidNotReceive().KeyDeleteAsync(Arg.Any(), Arg.Any()); + await _database.DidNotReceive().StringSetAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task TryAdd_of_a_default_value_claims_the_key_with_the_sentinel_when_CacheNullValues_is_on() + { + _cacheOptions.CacheNullValues = true; + _sut = null; + RedisValue captured = _fixture.Create(); + _database.StringSetAsync(_redisKey, Arg.Do(v => captured = v), Arg.Any(), When.NotExists, CommandFlags.DemandMaster) + .Returns(true); + + var added = await Sut.TryAddAsync(_cacheKey, null, TimeSpan.FromMinutes(1), token: testContextAccessor.Current.CancellationToken); + + added.Should().BeTrue(); + captured.Length().Should().Be(0, "the empty string is the cached-null sentinel on the wire"); + } + + [Theory] + [InlineData(true, "Caching.Stats.Hits.Redis.TryAddAsync.String")] + [InlineData(false, "Caching.Stats.Misses.Redis.TryAddAsync.String")] + public async Task TryAdd_reports_the_outcome_under_its_own_metric_scope(bool redisAdded, string expectedMetric) + { + _database.StringSetAsync(_redisKey, Arg.Any(), Arg.Any(), When.NotExists, CommandFlags.DemandMaster) + .Returns(redisAdded); + + await Sut.TryAddAsync(_cacheKey, _fixture.Create(), policy: null, token: testContextAccessor.Current.CancellationToken); + + // Conditional adds must be attributable on their own, not folded into the SetAsync scope, + // so a lost-race rate is observable separately from a write-failure rate. + _telemetry.Metrics.Should().ContainSingle(m => m.Name == expectedMetric); + } + + [Fact] + public async Task TryAdd_honors_a_cancelled_token_before_touching_redis() + { + using var cts = new CancellationTokenSource(); + await cts.CancelAsync(); + + var act = async () => await Sut.TryAddAsync(_cacheKey, _fixture.Create(), policy: null, token: cts.Token); + + await act.Should().ThrowAsync(); + await _database.DidNotReceive().StringSetAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + } + + public ValueTask InitializeAsync() + { + const string prefix = "test"; + _cacheKey = _fixture.Create(); + _redisKey = string.Join(':', prefix, RedisTypePrefixes.String, _cacheKey).ToLowerInvariant(); + _clock = _fixture.Freeze(); + _clock.UtcNow.Returns(_ => _now); + var resiliencePipelineProvider = _fixture.Freeze(); + var noOpExecutor = new EmptyResiliencePipeline(); + resiliencePipelineProvider.Get(ResiliencePipelineNames.Read).Returns(noOpExecutor); + resiliencePipelineProvider.Get(ResiliencePipelineNames.Write).Returns(noOpExecutor); + var cacheKeyStrategy = _fixture.Create(); + cacheKeyStrategy.GetCacheKey(_cacheKey).Returns(_cacheKey); + var redisKeyStrategyFactory = _fixture.Create(); + var redisKeyStrategy = _fixture.Create(); + redisKeyStrategy.GetRedisKey(_cacheKey).Returns(_redisKey); + redisKeyStrategyFactory.Create(Arg.Any(), Arg.Any()).Returns(redisKeyStrategy); + _cacheOptions = new RedisCacheOptions + { + Clock = _clock, + CacheKeyStrategy = cacheKeyStrategy, + RedisKeyStrategyFactory = redisKeyStrategyFactory, + }; + + _database = _fixture.Freeze(); + _serializer = new SystemJsonSerializerProxy(); + _fixture.Inject(_serializer); + _fixture.Inject(Options.Create(_cacheOptions)); + _fixture.Inject(_cacheOptions); + _fixture.Inject(_telemetry); + _fixture.Inject(new CacheOptions { AppShortName = "test", ConnectionMonitorEnabled = true }); + _connector = _fixture.Freeze(); + _connector.Database.Returns(_ => _database); + _connector.Version.Returns(_ => new Version(6, 0)); + _connector.IsConnected.Returns(_ => _isConnected); + return ValueTask.CompletedTask; + } + + public ValueTask DisposeAsync() + { + _sut?.Dispose(); + return ValueTask.CompletedTask; + } +} From 63794f7d44484e03fa43b26405fc713423bf1050 Mon Sep 17 00:00:00 2001 From: Cosmin Staicu Date: Wed, 2 Sep 2026 16:38:43 +0300 Subject: [PATCH 2/9] fix(cache): close the fail-open paths in TryAddAsync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-up on the conditional add. The contract the member exists to make is "a caller told true owns this key"; several paths could break it. MultilayerCache, memory-only tier: AcquireLocalLockAsync answered null both when Lock.LocalLockEnabled was off and when the acquire timed out, and the probe-then-write ran unserialized anyway — 11 of 32 concurrent callers were told they added the key with the lock disabled, 20 of 32 with a contended acquire. The local lock is the whole guarantee here rather than a single-flight optimization, so it is now taken regardless of Lock.LocalLockEnabled, and a caller that cannot acquire it within Lock.LocalLockTimeout is told it lost. NullCache.TryAddAsync returns false. It is the one member where the type does not degrade to "caching is off, carry on": it cannot complete the write, which is what a fail-closed false means, and true there hands every caller a claim of exclusive ownership. It is also reached by accident, being what ICacheFactory.CreateCache resolves to when the requested provider is absent or has Enabled=false, so the old true turned at-most-once into at-least-once with no error. NullSetCache.AddAsync — SADD, the same question — already answered false; this aligns the two. Local arbitration is restricted to the InMemory provider. NullCache is the L2 both for the memory-only provider by design and for any provider whose real L2 was absent or disabled, since CacheFactory falls back to it — so an InMemoryRedis configured with DefaultCache=Redis and no Redis provider was routed through LocalTryAddAsync and handed every process its own winner, under a provider name that promises cross-node exclusion. It now reaches the L2 and takes NullCache's fail-closed false, and the construction-time warning covers this composition as well as a nested multilayer L2. A nested in-process arbiter fails closed instead of being delegated to. An InMemoryRedis whose DefaultCache is InMemory resolves that provider's multilayer cache as its L2, and delegating let its local arbiter grant a win per process under a provider name that promises cross-node exclusion; a construction-time warning did not change that, so the add path now reports false. MemorySet reporting success is no longer taken as retention on the local path. A size-limited IMemoryCache declines an entry it cannot fit without throwing, and MemoryCacheSetter still returns true, so the key was probed after the write. The L2 gate no longer runs through GetInnerCacheDisconnected, whose state aggregates the broadcast transport as well as the inner cache: with UseLocalOnlyWhenDisconnected on, a dead topic stopped an otherwise healthy Redis from arbitrating, in a method that already treats broadcast as best-effort after a win. The L2 is asked instead and fails closed on its own — RedisCache checks its connection before issuing NX — so a disconnected L2 still yields false rather than a local-only claim. The test that covered this disconnected only the topic provider, which is exactly the case that should now succeed; it is split into that expectation and one on the L2's own answer. A publish that reports false rather than throwing is logged too. CacheSetAsync signals an ordinary failure — a disconnected topic among them — with a false return, which both broadcast sites discarded, so a win could leave peers on stale L1 data without the propagation warning the code promises. The local lock serializes conditional adds against each other only: SetAsync and RemoveAsync take no lock, so a set landing between the probe and the write is overwritten by the claim, which still reports true. IMemoryCache has no create-if-absent primitive to close that with, and locking every local mutation is a change to a hot path well outside this member, so the limit is documented on the method, in the recipe and in the interfaces.md exclusion column instead. Redis has no such gap, NX being atomic against a concurrent SET. On the L2-win path the invalidation broadcast and the L1 write are now separate best-effort steps: sharing one try/catch meant a dead topic also cost the winning node its local copy, though they are described as independent. The local path publishes no broadcast. It runs only on the InMemory provider, and ChangeTokenFactory accepts only CacheRemoved and CacheRefreshed there, so peers ignore CacheSet — deliberately, since each node's memory is the store rather than a copy of a shared one, and a peer's write says nothing about this node's entry. A non-positive effective local retention reports false on the InMemory provider, where it is the only retention: MemoryCacheSetter writes an entry IMemoryCache evicts on arrival and still returns true, so every later caller would win too. CachePolicyFactoryValidator catches the options-level value, but a per-call CachePolicy is not validated at all, which is the path that reaches this. An expiration that is not in the future now reports false on both tiers. IMemoryCache evicts such an entry on the way in, so MemorySet reported success while retaining nothing and the next caller won too; Redis rejected the negative PX and answered false. A win on the local tier now publishes the invalidation broadcast, as SetAsync does — without it a broadcast-enabled memory provider leaves peers serving a stale copy of a key this node believes it just claimed. An inner cache's NotSupportedException is no longer swallowed into false. That exception is the ICache default body saying the store has no atomic create-if-absent primitive; reported as false it is indistinguishable from permanent contention, so no caller ever wins and the guarded work silently never runs. A cancellation raised while the write is in flight now propagates rather than being reported as false, which would assert the key belongs to someone else — a fact the cancelled call never established. Both tiers do this, so InMemoryRedis and Redis agree. The write itself stays on the shared Write resilience pipeline: retries fire on exceptions only, and re-issuing SET .. NX is harmless — an attempt whose reply was lost is refused by the key it just wrote and reports the same false the exception would have, while an attempt that never reached Redis is recovered as the true it should have been. Contrast SPOP behind ISetCache.PopAsync, where a retry pops a second item and loses the first, which is why RedisSetCacheOptions.ResilienceKeyName exists. MultilayerCache warns at construction when it resolved another multilayer cache as its distributed tier, which arbitrates in-process only under a provider name that suggests otherwise. Reaching that state takes a deliberate misconfiguration — with the default DefaultCache the same composition fails loudly on Lazy re-entrancy instead — so it stays a warning next to the existing innerCache is NullCache test rather than earning a capability member on ICache. The docs no longer offer IConnectionState as a way to tell an outage from a lost race, because it is not one: a serialization or command failure returns false with the connection snapshot still healthy, and IDistributedLock.TryAcquireAsync conflates backend-unavailable with already-held in the same way. The ambiguity is documented as unrecoverable — design the false branch so that not proceeding is safe — and interfaces.md describes IConnectionState as the cache-health signal it actually is. For the same reason the recipe's worked example is now a daily digest rather than a payment capture: a claim marker records that someone started, never that anyone finished, so an at-least-once operation needs a recorded outcome and no branching on false substitutes for one. The XML docs on the conditional-add members are cut to a couple of lines each, pointing at docs/recipes/conditional-add.md for the contract. Every other member of ICache carries no doc comment at all, and the reference docs had drifted from these ones twice already. ICache.Compat.cs gains the three token-positional TryAddAsync forwarders, so cache.TryAddAsync(key, value, ttl, ct) compiles like the SetAsync it is written next to — the only public API this commit adds. RunUnderLocksAsync and AcquireLocalLockAsync now resolve the local-lock policy through one ResolveLocalLock helper instead of two copies of the same expressions. Tests: 21 added, pinning each of the above — including the two paths where the contract actually broke (the lock-disabled and lock-timeout local paths), the NotSupportedException surfacing, and cancellation crossing both tiers. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FkpKLown2fG6juC2DDiQgS Signed-off-by: Cosmin Staicu --- CHANGELOG.md | 55 +++- docs/concepts.md | 2 +- docs/recipes/conditional-add.md | 80 ++++-- docs/reference/interfaces.md | 44 +++- src/UiPath.Caching.Abstractions/CacheOfT.cs | 1 + .../CacheOptions.cs | 1 + .../ConditionalAdd.cs | 8 +- .../ICache.Compat.cs | 12 + src/UiPath.Caching.Abstractions/ICache.cs | 50 +--- src/UiPath.Caching.Abstractions/ICacheOfT.cs | 13 +- src/UiPath.Caching.Abstractions/NullCache.cs | 22 +- .../Policies/ResiliencePipelineNames.cs | 1 + .../PublicAPI.Unshipped.txt | 3 + src/UiPath.Caching/MultilayerCache.cs | 123 ++++++--- src/UiPath.Caching/MultilayerCacheBase.cs | 29 +- src/UiPath.Caching/PublicAPI.Unshipped.txt | 18 +- src/UiPath.Caching/Redis/RedisCache.cs | 24 +- .../MultilayerCacheTryAddTests.cs | 249 +++++++++++++++++- .../NullCacheConditionalAddTests.cs | 42 +++ .../Redis/RedisCacheTryAddTests.cs | 80 +++++- 20 files changed, 689 insertions(+), 168 deletions(-) create mode 100644 tests/UiPath.Caching.Tests/NullCacheConditionalAddTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 66561e2..9a16d38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,18 +65,39 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) only after a win, best-effort — a broadcast or L1 failure never downgrades a real win to `false`, since that would strand the entry with no owner; with the L2 disconnected it returns `false` rather than granting a local-only claim every node would also be granted (`SetAsync` degrades to a local - write there); the memory-only provider has no L2 to arbitrate, so the local tier does and exclusion - narrows to in-process, serialized by `Lock.LocalLockEnabled` (on by default). `NullCache` returns - `true`, consistent with its `SetAsync` — caching is off, so nothing is excluded. Added to both - interfaces as **default interface methods**, so existing implementations keep compiling; the default - body throws `NotSupportedException` rather than emulating the operation with a probe followed by a - write, which would not be atomic and would silently void the guarantee. A hand-written `ICache` / - `ICache` implementation must override it before callers can use it. No multi-key overload: Redis - has no atomic multi-key `NX`, and all-or-nothing versus per-key semantics would be a guess. This is - a cache primitive, not a lock — no ownership token, no early release, and a later - `SetAsync`/`RemoveAsync` ignores the claim; for a fencing token and explicit release use - `IDistributedLock`. No conditional-add member was added to the hash surface, where `NX` is per-field - (`HSETNX`) and a different shape. + write there); the `InMemory` provider has no L2 to arbitrate, so the local tier does and exclusion + narrows to in-process. There the local lock is the whole guarantee — it makes the probe-then-write + atomic — so a conditional add takes it regardless of `Lock.LocalLockEnabled` (which trades + single-flight for throughput on `GetOrAddAsync` and must not be able to hand one key to two + callers), and a caller that cannot acquire it within `Lock.LocalLockTimeout` is told it lost rather + than proceeding unserialized. It publishes no invalidation broadcast: `ChangeTokenFactory` accepts + only `CacheRemoved` and `CacheRefreshed` for that provider, so peers ignore `CacheSet` — + deliberately, since each node's memory is the store rather than a copy of a shared one. + An expiration that is not in the future reports `false` on every tier: the entry would be evicted on + arrival, so a `true` would be handed to every later caller as well. `NullCache` returns `false` — + the one member where it does not degrade to "caching is off, carry on", because it cannot complete + the write (which is exactly what a fail-closed `false` means) and because `true` there would hand + every caller a claim of exclusive ownership; it is reached by accident, being what + `ICacheFactory.CreateCache` resolves to when the requested provider is absent or has + `Enabled=false`. Added to both interfaces as **default interface methods**, so + existing implementations keep compiling; the default body throws `NotSupportedException` rather than + emulating the operation with a probe followed by a write, which would not be atomic and would + silently void the guarantee. A hand-written `ICache` / `ICache` implementation must override it + before callers can use it — and `MultilayerCache` lets that `NotSupportedException` surface from an + inner cache rather than reporting it as `false`, which would be indistinguishable from permanent + contention. `ICache.Compat.cs` carries the token-positional forwarders + (`TryAddAsync(key, value, token)` and the `TimeSpan?`/`DateTimeOffset?` pairs), matching `SetAsync`. + No multi-key overload: Redis has no atomic multi-key `NX`, and all-or-nothing versus per-key + semantics would be a guess. This is a cache primitive, not a lock — no ownership token, no early + release, and a later `SetAsync`/`RemoveAsync` ignores the claim; for a fencing token and explicit + release use `IDistributedLock`. No conditional-add member was added to the hash surface, where `NX` + is per-field (`HSETNX`) and a different shape. +- A cancellation raised while the `SET … NX` write is in flight now propagates instead of being + reported as `false`, which would have claimed the key belongs to someone else — a fact the cancelled + call never established. The write itself stays on the shared `Write` resilience pipeline: retries + fire on exceptions only, and re-issuing `NX` is harmless, since an attempt whose reply was lost is + refused by the key it just wrote and reports the same `false` the exception would have, while an + attempt that never reached Redis is recovered as the `true` it should have been. ### Changed @@ -112,6 +133,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) instead. `interfaces.md` documents the member on both cache surfaces with a per-provider table of who arbitrates and how far exclusion reaches; `concepts.md` places it next to the two lock abstractions, whose goal is reducing redundant work rather than a decision the caller can branch on. + The recipe's worked example is a daily digest rather than a payment capture, and says why: a claim + marker records that someone *started*, never that anyone finished, so an operation that must + eventually happen needs a recorded outcome instead. Both documents also state plainly that the + ambiguity in `false` is not recoverable — a serialization or command failure returns `false` with + the connection snapshot still healthy — rather than pointing at `IConnectionState` as an escape + hatch, and both explain why the `NX` write is safe to retry where `SPOP` is not. +- New `IConnectionState` section in `interfaces.md`, a public type that was documented nowhere (and + that the `TryAddAsync` guidance linked to through an anchor resolving to `IDistributedLock`). It is + described as what it is — a cache-health snapshot for probes, metrics and backoff — not as a way to + explain a negative result. - Corrected `interfaces.md`, which described the `IHashCache.SetAsync(…, HashCacheEntryOptions, …)` overload as offering "conditional set, individual field TTL". `HashCacheEntryOptions` carries neither: `HashCacheSetOption` selects write *scope* (`HashReplace` merges fields, `KeyReplace` drops diff --git a/docs/concepts.md b/docs/concepts.md index 6ffcb1e..13fb468 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -254,7 +254,7 @@ so it is the right primitive for at-most-once semantics keyed by something (a dedup marker, an idempotency key, electing which node runs a job). It is not a lock: the claim expires on its own TTL, carries no ownership token, and any later `SetAsync`/`RemoveAsync` on the key ignores it. When you need a fencing token and -an explicit release, use `IDistributedLock`; when you need a durable +an explicit release, use `IDistributedLock`; when you need an atomic, expiring "first one here wins" marker, use `TryAddAsync`. See [the interfaces reference](reference/interfaces.md#icache) for the per-provider behavior, including why it fails closed when the L2 is disconnected. diff --git a/docs/recipes/conditional-add.md b/docs/recipes/conditional-add.md index 68334a8..ef56df8 100644 --- a/docs/recipes/conditional-add.md +++ b/docs/recipes/conditional-add.md @@ -6,11 +6,16 @@ cache that is StackExchange.Redis `When.NotExists` — `SET key value EX … NX` so exactly one caller across every node wins. **When to use:** -- **Idempotency keys.** A webhook or payment callback that may be delivered twice: the first delivery - claims the key and does the work, the redelivery sees `false` and returns the cached outcome. -- **At-most-once side effects.** "Send this alert / this welcome email once per user per day." +- **At-most-once side effects,** where skipping is the safe failure. "Send this alert / this welcome + email once per user per day." - **Electing a worker.** Which of N replicas runs a periodic job this tick. - **Dedup markers** where the marker's TTL *is* the dedup window. +- **Suppressing duplicate work,** as an optimization ahead of an operation that is idempotent anyway. + +**When not to use:** as the sole guard on an operation that must eventually happen. The claim marker +records that someone started, not that anyone finished, so a winner that dies mid-flight leaves the +work undone until the TTL lapses — and `false` cannot tell you which of "already claimed" or "write +failed" you are looking at. Those cases need a recorded outcome, not a claim. **When not to use:** if you need a lock you can release early, or a fencing token that proves you still hold it, use [`IDistributedLock`](../reference/interfaces.md#idistributedlock) instead. See @@ -21,33 +26,42 @@ still hold it, use [`IDistributedLock`](../reference/interfaces.md#idistributedl ```csharp using UiPath.Caching; -public class WebhookHandler(ICache cache, IPaymentService payments) +public class DailyDigest(ICache cache, IMailer mailer) { - // The TTL is the dedup window: retries inside 24h are dropped, and the key - // cleans itself up afterwards without a sweeper. - private static readonly TimeSpan DedupWindow = TimeSpan.FromHours(24); + // The key is the dedup unit: one send per user per UTC calendar day. The TTL only + // cleans the marker up afterwards — it is not a rolling 24-hour window, so two + // triggers minutes apart either side of midnight use different keys and both send. + private static readonly TimeSpan MarkerTtl = TimeSpan.FromHours(24); - public async Task HandleAsync(string eventId, CancellationToken token) + public async Task SendOnceAsync(string userId, CancellationToken token) { var claimed = await cache.TryAddAsync( - (CacheKey)$"webhook:{eventId}", + (CacheKey)$"digest:{userId}:{DateTime.UtcNow:yyyyMMdd}", DateTimeOffset.UtcNow, - DedupWindow, + MarkerTtl, token: token); if (!claimed) { - // Either a concurrent/duplicate delivery already claimed it, or the cache - // was unreachable. Both mean "do not run the side effect" — see Notes. - return HandlingResult.AlreadyHandled; + // Someone else claimed it, or the write could not be completed. Both mean + // "do not send" — which is the whole reason this side effect fits the + // primitive: skipping is the safe failure, so the ambiguity costs nothing. + return; } - await payments.CaptureAsync(eventId, token); - return HandlingResult.Handled; + await mailer.SendDigestAsync(userId, token); } } ``` +`TryAddAsync` fits here because **not** sending is the safe failure. Note what it is *not* doing: +nothing recovers the missed digest if the winner crashes between the claim and the send, and nothing +tells a lost race apart from a failed write. If your side effect must eventually happen — capturing a +payment, say — a claim marker is the wrong shape and no amount of branching on `false` fixes it: the +marker says "someone is handling this", never "this was handled". Record the *outcome* instead +(`Pending` → `Captured` on a durable store, or an idempotency key the downstream provider itself +honors) and let redelivery retry until the outcome is written. + The typed surface is the same shape without the `policy` parameter: ```csharp @@ -71,7 +85,7 @@ The obvious hand-rolled version is not equivalent: // BROKEN: two callers can both observe "absent" before either writes. if (!await cache.ContainsAsync(key, token)) { - await cache.SetAsync(key, DateTimeOffset.UtcNow, DedupWindow, token: token); + await cache.SetAsync(key, DateTimeOffset.UtcNow, MarkerTtl, token: token); await payments.CaptureAsync(eventId, token); // runs twice under concurrency } ``` @@ -87,9 +101,26 @@ guarantee the method makes. - **`false` does not mean "the key existed".** It means "you did not create it" — the key already existed, *or* the write could not be completed (store disconnected, write threw, or the value was a `null`/`default` the cache cannot represent). This is fail-closed on purpose: nobody is ever wrongly - told they won. Design the `false` branch so that "skip the side effect" is the safe outcome. If you - need to tell an outage from a lost race, check `IConnectionState.IsConnected` before reacting, or - use `IDistributedLock.TryAcquireAsync`. + told they won. Design the `false` branch so that "skip the side effect" is the safe outcome — + nothing recovers the distinction. `IConnectionState.IsConnected` does not: a serialization or + command failure returns `false` with the connection snapshot still healthy, and the snapshot can + change either side of the call anyway. `IDistributedLock.TryAcquireAsync` does not either; it + documents backend-unavailable and already-held as the same `null`. If the two readings need + different handling, you need a primitive with a richer result than a `bool`. +- **A non-positive TTL claims nothing.** An expiration that is not in the future returns `false` on + every tier rather than a win, because the entry would be evicted on arrival and the next caller + would be told it won too. +- **Caching switched off means nobody wins.** `NullCache.TryAddAsync` returns `false` — it cannot + complete the write — and it is what `ICacheFactory.CreateCache` falls back to when the requested + provider is missing or has `Enabled=false`. That is fail-closed rather than silently at-least-once, + but it does mean the guarded work never runs, so assert the provider you expect at startup if that + matters: `if (cacheFactory.CreateCache(KnownCacheProviderNames.Redis) is NullCache) throw …`. +- **The `NX` write is retryable,** so it stays on the shared `Write` resilience pipeline. Retries + fire on exceptions only, and the ambiguous case costs nothing: if the write lands but its reply is + lost, the retry is refused by the key it just wrote and reports `false` — the same answer the + un-retried exception would have produced — while a first attempt that never reached Redis is + recovered as the `true` it should have been. (`SPOP` in `UiPath.Caching.Queue` is the opposite: a + retry there pops a second item and loses the first, which is why it has its own opt-in pipeline.) - **It never deletes.** `SetAsync` handed a `null` with `CacheNullValues` off *removes* the key; `TryAddAsync` returns `false` and leaves it alone. With `CacheNullValues` on, a `null` claims the key through the cached-null sentinel — so prefer a meaningful value (a timestamp, the machine name) that @@ -100,10 +131,15 @@ guarantee the method makes. `IDistributedLock` for long critical sections. - **It is not a lock.** No ownership token, no early release, and a later `SetAsync`/`RemoveAsync` on the key silently overwrites the claim. If a bug elsewhere writes that key, exclusion is gone with no - error. + error. On the `InMemory` provider the reverse also holds: the local lock serializes conditional adds + against each other, but `SetAsync` takes no lock, so a set landing between the probe and the write + is overwritten by the claim, which still reports `true`. Redis has no such gap — `NX` is atomic + against a concurrent `SET`. - **In-memory-only caches exclude in-process only.** With the `InMemory` provider there is no shared - store to arbitrate, so the local tier does — serialized by `Lock.LocalLockEnabled`, on by default. - Two processes both win. `InMemoryRedis` and `Redis` are cross-node correct. + store to arbitrate, so the local tier does — serialized by the local lock, which a conditional add + takes whatever `Lock.LocalLockEnabled` says, because here it *is* the guarantee rather than a + single-flight optimization. A caller that cannot acquire it within `Lock.LocalLockTimeout` is told + it lost. Two processes still both win. `InMemoryRedis` and `Redis` are cross-node correct. - **On `InMemoryRedis` the L2 decides and L1 is populated only after a win.** When the L2 is disconnected the call returns `false` rather than granting a local claim every node would also be granted — unlike `SetAsync`, which degrades to a local-only write there. diff --git a/docs/reference/interfaces.md b/docs/reference/interfaces.md index a299489..fbd585a 100644 --- a/docs/reference/interfaces.md +++ b/docs/reference/interfaces.md @@ -172,11 +172,12 @@ public partial interface ICache : IDisposable The multi-key `GetOrAddAsync` overloads shown above pair each key with an opaque caller state (`TState`) and are **default interface methods** — each forwards to the shared `BatchGetOrAdd.RunAsync` machinery, so existing `ICache` implementations keep compiling without adding them. The generator is invoked at most once, only with the states of the entries that missed every cache layer, never with keys; results come back keyed by state, one entry per distinct requested state in first-occurrence order. Cache operations de-duplicate by `CacheKey`; results de-duplicate by state — when two states share a key the generator is asked once and the value is reported under both. Their parameter shape matches the single-key `GetOrAddAsync` exactly — `expiration`, `policy` and `token` all optional. `ICache.Compat.cs` carries no token-positional forwarder for them: that file is pre-`CachePolicy` back-compat sugar, and this API predates nothing. There is no key-only convenience overload; a caller whose keys are their own identity pairs each key with itself (`TState = CacheKey`). -`TryAddAsync` writes only if the key is absent — StackExchange.Redis `When.NotExists` (`SET … NX`) on Redis-backed caches, in one atomic command with the TTL applied by the same write. Three points decide whether it fits your problem: +`TryAddAsync` writes only if the key is absent — StackExchange.Redis `When.NotExists` (`SET … NX`) on Redis-backed caches, in one atomic command with the TTL applied by the same write. Four points decide whether it fits your problem: -- **`false` is deliberately ambiguous.** It means "you did not create this key" — either it already existed, or the write could not be completed (backing store disconnected, write threw, or the value was a `null`/`default` that the cache has no way to represent). This is fail-closed by design: a caller treating `true` as "I own this key" is never wrongly told it won. If you must distinguish a lost race from an outage, check [`IConnectionState.IsConnected`](#idistributedlock) before reacting to `false`, or use [`IDistributedLock.TryAcquireAsync`](#idistributedlock). +- **`false` is deliberately ambiguous.** It means "you did not create this key" — either it already existed, or the write could not be completed (backing store disconnected, write threw, or the value was a `null`/`default` that the cache has no way to represent). This is fail-closed by design: a caller treating `true` as "I own this key" is never wrongly told it won. The ambiguity is not recoverable: a serialization or command failure also returns `false`, with [`IConnectionState.IsConnected`](#iconnectionstate) still reporting healthy, and [`IDistributedLock.TryAcquireAsync`](#idistributedlock) conflates backend-unavailable with already-held in the same way. Design the `false` branch so that not proceeding is safe; if the two readings must be handled differently, the caller needs a primitive with a richer result than a `bool`. - **It never deletes.** Where `SetAsync` removes the key when handed a `null` and `CacheNullValues` is off, `TryAddAsync` returns `false` and leaves the key untouched. With `CacheNullValues` on, a `null` claims the key via the cached-null sentinel. - **It is a cache primitive, not a lock.** The entry expires on its own TTL, there is no ownership token, and any later `SetAsync`/`RemoveAsync` on the key ignores the claim. For mutual exclusion with a fencing token and explicit release, use [`IDistributedLock`](#idistributedlock). +- **An expiration that is not in the future claims nothing** and reports `false` on every tier. The entry would be evicted on arrival, so answering `true` would hand the same key to every later caller as well. Tier behavior follows from the same rule — L1 can never arbitrate while an L2 exists, because a key absent locally may well be present in the shared store: @@ -185,8 +186,20 @@ Tier behavior follows from the same rule — L1 can never arbitrate while an L2 | `Redis` | Redis (`SET … NX`) | Cross-node | | `InMemoryRedis` | L2 Redis; L1 populated only after a win, best-effort, plus an invalidation broadcast | Cross-node | | `InMemoryRedis`, L2 disconnected | Nobody — returns `false` | None (fails closed rather than granting a claim every node would also get) | -| `InMemory` | The local tier, serialized by `Lock.LocalLockEnabled` (on by default) | In-process only | -| `NullCache` | Nobody — returns `true` for every caller | None | +| `InMemory` | The local tier, serialized by the local lock — taken regardless of `Lock.LocalLockEnabled`, since here it *is* the guarantee; a caller that cannot acquire it within `Lock.LocalLockTimeout` is told it lost | In-process only, and against other conditional adds only: `SetAsync` takes no lock, so a set interleaved between the probe and the write is overwritten by the claim | +| `NullCache` | Nobody — returns `false` for every caller | None | +| Any other provider whose L2 resolved to `NullCache` | Nobody — returns `false`; local arbitration is reserved for the `InMemory` provider, so a misconfigured `InMemoryRedis` fails closed rather than granting one winner per process | None | + +`NullCache` is what `ICacheFactory.CreateCache` falls back to when the requested provider is absent or has `Enabled=false`, so its `TryAddAsync` returns `false` rather than following the "caching is off, carry on" degradation the rest of that type applies: it cannot complete the write, which is exactly what a fail-closed `false` means, and `true` there would be a claim of exclusive ownership handed to every caller at once. `NullSetCache.AddAsync` in `UiPath.Caching.Queue` — `SADD`, the same question — answers the same way. Code that must not silently land there can reject the configuration at startup: + +```csharp +if (cacheFactory.CreateCache(KnownCacheProviderNames.Redis) is NullCache) +{ + throw new InvalidOperationException("Webhook dedup needs the Redis provider registered and Enabled."); +} +``` + +**Retries are safe here,** unlike the destructive reads in `UiPath.Caching.Queue`, so the `NX` write stays on the shared `Write` pipeline. That pipeline retries on exceptions only — a `false` reply is never retried — and in the one ambiguous case the retry changes nothing: if attempt 1 creates the key and its reply is lost, the retried attempt is refused by that key and reports `false`, which is exactly what the un-retried exception would have reported. Where the first attempt never reached Redis, the retry recovers the correct `true` instead. Contrast `SPOP` behind `ISetCache.PopAsync`, where a retry pops a *second* item and loses the first — which is why `RedisSetCacheOptions.ResilienceKeyName` exists and defaults to no pipeline. The three overloads are **default interface methods**, so existing `ICache` implementations keep compiling. The default body throws `NotSupportedException`: a probe followed by a write is not atomic, and silently substituting one would void the only guarantee the method makes. All in-box implementations override it; a hand-written implementation must too before callers can use it. There is no multi-key overload — Redis has no atomic multi-key `NX`, and "all-or-nothing" versus "per-key" would be a coin flip on the caller's behalf. @@ -632,6 +645,29 @@ public interface IDistributedLock --- +## Connection state + +### `IConnectionState` + +```csharp +public interface IConnectionState +{ + event EventHandler? OnConnectionFailed; + + event EventHandler? OnConnectionRestored; + + event EventHandler? OnReconnected; + + bool IsConnected { get; } +} +``` + +A non-blocking snapshot of whether the backing store is reachable, plus the transitions as events. `IsConnected` never blocks and never throws. Implemented by `RedisCacheBase`, so `Redis`-provider caches expose it; the multilayer caches and `NullCache` do not, and neither does `Cache`, which holds its underlying `ICache` privately. + +What it is *not*: a way to explain a negative result. It is a cached snapshot refreshed on connection events and a timer, it says nothing about whether any particular command succeeded, and it is `true` both where there is nothing to disconnect from and where `ConnectionMonitorEnabled` is off. A `false` from `SetAsync` or [`TryAddAsync`](#icache) can perfectly well coincide with `IsConnected == true` — a serialization failure or a rejected command does that — so reading it afterwards does not recover why the call failed. + +**Use this when:** you are reporting or reacting to cache *health* — a readiness probe, a metric, a log line, or backing off writes while a tier is known down. Subscribe to `OnConnectionFailed` / `OnConnectionRestored` for the transitions rather than polling. + ## Telemetry seam ### `ICachingTelemetryProvider` diff --git a/src/UiPath.Caching.Abstractions/CacheOfT.cs b/src/UiPath.Caching.Abstractions/CacheOfT.cs index 384685d..bbb79b1 100644 --- a/src/UiPath.Caching.Abstractions/CacheOfT.cs +++ b/src/UiPath.Caching.Abstractions/CacheOfT.cs @@ -104,6 +104,7 @@ public ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan? expira public ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CancellationToken token = default) => _cache.TryAddAsync(GetCacheKey(cacheKey), value, expiration, Policy, token); + public ValueTask SetAsync(KeyValuePair[] keyValues, CancellationToken token = default) => _cache.SetAsync(GetKeyValuePairs(keyValues), policy: Policy, token: token); diff --git a/src/UiPath.Caching.Abstractions/CacheOptions.cs b/src/UiPath.Caching.Abstractions/CacheOptions.cs index da1fb64..e397a16 100644 --- a/src/UiPath.Caching.Abstractions/CacheOptions.cs +++ b/src/UiPath.Caching.Abstractions/CacheOptions.cs @@ -35,6 +35,7 @@ public class CacheOptions public bool ConnectionMonitorEnabled { get; set; } + /// /// Size of the reusable semaphore pool inside the default local lock implementation. /// This is an allocation hint, not a hard concurrency cap — when the pool is exhausted the diff --git a/src/UiPath.Caching.Abstractions/ConditionalAdd.cs b/src/UiPath.Caching.Abstractions/ConditionalAdd.cs index 5532400..867366f 100644 --- a/src/UiPath.Caching.Abstractions/ConditionalAdd.cs +++ b/src/UiPath.Caching.Abstractions/ConditionalAdd.cs @@ -6,11 +6,9 @@ namespace UiPath.Caching; internal static class ConditionalAdd { /// - /// Built for the TryAddAsync default interface bodies. The members ship as default - /// interface methods so hand-written / - /// implementations keep compiling, but there is no correct generic fallback: a probe followed - /// by a write is not atomic, and silently substituting one would break the single guarantee the - /// method exists to make. Throwing keeps the gap visible instead. + /// The TryAddAsync default interface bodies throw this: the members ship as default + /// methods so existing implementations keep compiling, and there is no atomic generic fallback + /// to substitute. /// public static NotSupportedException NotSupported(string cacheName, string implementationType) => new($"Cache '{cacheName}' ({implementationType}) does not implement conditional add. " + diff --git a/src/UiPath.Caching.Abstractions/ICache.Compat.cs b/src/UiPath.Caching.Abstractions/ICache.Compat.cs index d8f1a5e..588b5f3 100644 --- a/src/UiPath.Caching.Abstractions/ICache.Compat.cs +++ b/src/UiPath.Caching.Abstractions/ICache.Compat.cs @@ -57,6 +57,18 @@ ValueTask SetAsync(KeyValuePair[] keyValues, TimeSpan? ex ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset? expiration, CancellationToken token = default) => SetAsync(keyValues, expiration, null, token); + [ExcludeFromCodeCoverage] + ValueTask TryAddAsync(CacheKey cacheKey, T? value, CancellationToken token = default) + => TryAddAsync(cacheKey, value, (CachePolicy?)null, token); + + [ExcludeFromCodeCoverage] + ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan? expiration, CancellationToken token = default) + => TryAddAsync(cacheKey, value, expiration, null, token); + + [ExcludeFromCodeCoverage] + ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CancellationToken token = default) + => TryAddAsync(cacheKey, value, expiration, null, token); + [ExcludeFromCodeCoverage] ValueTask RefreshAsync(CacheKey cacheKey, CancellationToken token = default) => RefreshAsync(cacheKey, (CachePolicy?)null, token); diff --git a/src/UiPath.Caching.Abstractions/ICache.cs b/src/UiPath.Caching.Abstractions/ICache.cs index 8860413..6e73ca6 100644 --- a/src/UiPath.Caching.Abstractions/ICache.cs +++ b/src/UiPath.Caching.Abstractions/ICache.cs @@ -47,55 +47,23 @@ public partial interface ICache : IDisposable ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default); /// - /// Conditional add: writes only if does not - /// already exist. On Redis-backed caches this is StackExchange.Redis - /// When.NotExists (SET key value EX … NX) — a single atomic round-trip, so exactly - /// one caller across all nodes can win a given key. + /// Conditional add: writes only if is + /// absent. Redis SET … NX, one atomic round-trip, so one caller wins the key. /// /// - /// true when the key did not exist and this call created it. false when the key - /// already existed or the write could not be completed — the backing store was - /// disconnected, the write threw, or the value could not be represented (see below). The two are - /// deliberately conflated, fail-closed: a caller that treats true as "I own this key" is - /// never wrongly told it won. Callers that must distinguish infrastructure failure from a lost - /// race should use IDistributedLock.TryAcquireAsync, or check - /// IConnectionState.IsConnected before deciding how to react to false. + /// true only if this call created the key. false means "you did not create it" — + /// it existed, or the write could not be completed — and the two are deliberately conflated, + /// fail-closed. Never deletes. Not a lock: no ownership token, no release. See + /// docs/recipes/conditional-add.md for the full contract and the per-provider table. /// - /// - /// - /// Unlike SetAsync, this never deletes: a null/default - /// is written as the cached-null sentinel when the provider's - /// CacheNullValues is on, and otherwise returns false without touching the key, - /// where SetAsync would remove it. - /// - /// - /// On a multilayer (L1+L2) cache the distributed tier arbitrates the race and the local tier is - /// populated only after a win; when the L2 is disconnected the call returns false rather - /// than granting a local-only claim that a second node would also be granted. On a memory-only - /// provider there is no L2 to arbitrate, so exclusion is in-process only and depends on - /// Lock.LocalLockEnabled (on by default for the InMemory provider). - /// - /// - /// This is a cache primitive, not a lock: the entry expires on its own TTL, there is no - /// ownership token, and any SetAsync or RemoveAsync on the same key ignores it. - /// For mutual exclusion with a fencing token and explicit release, use - /// IDistributedLock. - /// - /// - /// Ships as a default interface method so existing implementations keep - /// compiling; the default body throws , because a probe - /// followed by a write would not be atomic and would quietly void the guarantee above. All - /// in-box implementations override it. - /// - /// ValueTask TryAddAsync(CacheKey cacheKey, T? value, CachePolicy? policy = null, CancellationToken token = default) => throw ConditionalAdd.NotSupported(Name, GetType().FullName ?? nameof(ICache)); + /// /// - /// Lifetime of the entry if it is created. Falls back to CachePolicy.DistributedExpiration - /// and then the cache's default expiration when null. Applied in the same atomic command as the - /// conditional write, so a won key is never left without a TTL. + /// Lifetime of the entry if it is created, applied by the same atomic command. Falls back to + /// CachePolicy.DistributedExpiration then the cache default. Not in the future: no-op. /// ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => throw ConditionalAdd.NotSupported(Name, GetType().FullName ?? nameof(ICache)); diff --git a/src/UiPath.Caching.Abstractions/ICacheOfT.cs b/src/UiPath.Caching.Abstractions/ICacheOfT.cs index 537d026..02e26fc 100644 --- a/src/UiPath.Caching.Abstractions/ICacheOfT.cs +++ b/src/UiPath.Caching.Abstractions/ICacheOfT.cs @@ -36,19 +36,14 @@ public partial interface ICache ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset? expiration = null, CancellationToken token = default); /// - /// Conditional add: writes only if does not - /// already exist. Typed façade over - /// — - /// see that member for the full contract, including what false means and why it is - /// fail-closed. + /// Typed façade over + /// ; + /// see that member for the contract. /// - /// - /// true only when the key did not exist and this call created it; false when it - /// already existed or the write could not be completed. - /// ValueTask TryAddAsync(CacheKey cacheKey, T? value, CancellationToken token = default) => throw ConditionalAdd.NotSupported(Name, GetType().FullName ?? nameof(ICache)); + /// /// Lifetime of the entry if it is created. ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan? expiration, CancellationToken token = default) diff --git a/src/UiPath.Caching.Abstractions/NullCache.cs b/src/UiPath.Caching.Abstractions/NullCache.cs index 5e2858a..d7a6cb2 100644 --- a/src/UiPath.Caching.Abstractions/NullCache.cs +++ b/src/UiPath.Caching.Abstractions/NullCache.cs @@ -77,19 +77,19 @@ public ValueTask ContainsAsync(CacheKey cacheKey, CancellationToken tok public ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => ReturnTrueAsync(); /// - /// Always true. Consistent with SetAsync on this type: the null store accepts every - /// write and retains none, so no key can pre-exist and no caller can lose the race. Note the - /// consequence — NullCache provides no mutual exclusion whatsoever, so every caller is - /// told it won. That is the same "caching is off, carry on" degradation the rest of this type - /// applies, and the alternative (always false) would starve every caller instead. + /// Always false: retaining nothing, this store cannot arbitrate a conditional add, and + /// true would hand every caller exclusive ownership of the key. The one member here that + /// does not degrade to "caching is off, carry on", because this type is reached by accident — + /// ICacheFactory.CreateCache resolves to it for an absent or disabled provider. /// - public ValueTask TryAddAsync(CacheKey cacheKey, T? value, CachePolicy? policy = null, CancellationToken token = default) => ReturnTrueAsync(); + public ValueTask TryAddAsync(CacheKey cacheKey, T? value, CachePolicy? policy = null, CancellationToken token = default) => ReturnFalseAsync(); /// - public ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => ReturnTrueAsync(); + public ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => ReturnFalseAsync(); /// - public ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => ReturnTrueAsync(); + public ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => ReturnFalseAsync(); + public ValueTask TimeToLiveAsync(CacheKey cacheKey, CancellationToken token = default) { @@ -103,6 +103,12 @@ private static ValueTask ReturnTrueAsync() return ValueTask.FromResult(true); } + private static ValueTask ReturnFalseAsync() + { + NotCacheableException.ThrowIfNotCacheable(); + return ValueTask.FromResult(false); + } + private static async ValueTask ReturnGeneratorAsync(Func> generator, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); diff --git a/src/UiPath.Caching.Abstractions/Policies/ResiliencePipelineNames.cs b/src/UiPath.Caching.Abstractions/Policies/ResiliencePipelineNames.cs index 8e4ffd3..8742275 100644 --- a/src/UiPath.Caching.Abstractions/Policies/ResiliencePipelineNames.cs +++ b/src/UiPath.Caching.Abstractions/Policies/ResiliencePipelineNames.cs @@ -5,4 +5,5 @@ public static class ResiliencePipelineNames public const string Read = "read"; public const string Write = "write"; + } diff --git a/src/UiPath.Caching.Abstractions/PublicAPI.Unshipped.txt b/src/UiPath.Caching.Abstractions/PublicAPI.Unshipped.txt index 2a2d0b8..12654ad 100644 --- a/src/UiPath.Caching.Abstractions/PublicAPI.Unshipped.txt +++ b/src/UiPath.Caching.Abstractions/PublicAPI.Unshipped.txt @@ -13,7 +13,10 @@ UiPath.Caching.CacheKeyComparer.CacheKeyComparer() -> void UiPath.Caching.CacheOptions.KeyCasing.get -> UiPath.Caching.CacheKeyCasing UiPath.Caching.CacheOptions.KeyCasing.set -> void UiPath.Caching.ICache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.TryAdd(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool UiPath.Caching.ICache.TryAdd(UiPath.Caching.CacheKey cacheKey, T? value, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool diff --git a/src/UiPath.Caching/MultilayerCache.cs b/src/UiPath.Caching/MultilayerCache.cs index 55dfd7d..d2d8776 100644 --- a/src/UiPath.Caching/MultilayerCache.cs +++ b/src/UiPath.Caching/MultilayerCache.cs @@ -8,9 +8,8 @@ internal sealed partial class MultilayerCache : MultilayerCacheBase, ICache private readonly ICache _innerCache; private readonly CacheEntryBuilder _entryBuilder; private readonly LocalMemorySetter _localMemorySetter; - // The memory-only provider passes NullCache as the L2: it accepts every write and keeps - // nothing, so it cannot arbitrate a conditional add and the local tier has to instead. - private readonly bool _innerCacheNeverStores; + private readonly bool _localTierArbitrates; + private readonly bool _innerCacheArbitratesInProcessOnly; public MultilayerCache( string cacheName, @@ -30,7 +29,16 @@ public MultilayerCache( : base(cacheName, innerCache, memoryCacheFactory, topicFactory, cacheEventFactory, telemetryProvider, multiLayerCacheOptions, memoryCacheOptions, cacheOptions, localLock, distributedLock, policyFactory, logger) { _innerCache = innerCache; - _innerCacheNeverStores = innerCache is NullCache; + // NullCache as the L2 is the memory-only provider by design, but it is also what + // CacheFactory falls back to when a distributed provider is absent or disabled. Only the + // former may arbitrate locally; the latter must reach the L2 and get its fail-closed false, + // or an InMemoryRedis cache would hand every process its own winner. + _localTierArbitrates = innerCache is NullCache && cacheName == KnownCacheProviderNames.InMemory; + _innerCacheArbitratesInProcessOnly = innerCache is MultilayerCache; + if (!_localTierArbitrates && innerCache is NullCache or MultilayerCache) + { + LogInnerCacheCannotArbitrateAcrossNodes(cacheName, innerCache.Name); + } var cacheKeyStrategy = _multiLayerCacheOptions.CacheKeyStrategy ?? new DefaultCacheKeyStrategy(); var topicKeyStrategy = _multiLayerCacheOptions.TopicKeyStrategy ?? new DefaultTopicKeyStrategy(cacheOptions.Separator); _entryBuilder = new CacheEntryBuilder(cacheKeyStrategy, topicKeyStrategy, _clock); @@ -674,12 +682,11 @@ public ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan? exp } /// - /// The L2 arbitrates the race and the L1 is only populated after a win — the reverse of - /// SetAsync, which writes both tiers unconditionally. L1 cannot arbitrate: a key missing - /// locally may well exist in the shared store, so a local probe would hand the same win to every - /// node. When there is no L2 at all (memory-only provider) the local tier becomes the arbiter and - /// exclusion narrows to this process; when the L2 exists but is disconnected the call fails - /// closed instead of granting a local-only claim. + /// The L2 arbitrates and the L1 is populated only after a win — the reverse of SetAsync. + /// L1 cannot arbitrate: a key absent locally may exist in the shared store, so a local probe + /// would hand the same win to every node. A disconnected L2 answers for itself (RedisCache + /// checks its connection first) rather than being gated on GetInnerCacheDisconnected, + /// whose state also covers the broadcast transport — a dead topic must not stop a healthy Redis. /// public async ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) { @@ -696,14 +703,22 @@ public async ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTim return false; } - if (_innerCacheNeverStores) + if (options.Expiration <= _clock.UtcNow) + { + LogTryAddSkippedExpiredEntry(options.CacheKey, options.Expiration); + return false; + } + + if (_localTierArbitrates) { return await LocalTryAddAsync(options, value, policy).ConfigureAwait(false); } - if (GetInnerCacheDisconnected()) + if (_innerCacheArbitratesInProcessOnly) { - LogTryAddInnerDisconnected(options.CacheKey); + // Delegating would let the nested cache's local arbiter grant a win per process under a + // provider name that promises cross-node exclusion, so this composition fails closed. + LogTryAddInnerCacheNotDistributed(options.CacheKey); return false; } @@ -712,7 +727,8 @@ public async ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTim { added = await _innerCache.TryAddAsync(options.CacheKey, value, options.Expiration, policy, options.Token).ConfigureAwait(false); } - catch (Exception ex) + catch (Exception ex) when (ex is not NotSupportedException + && !(ex is OperationCanceledException && options.Token.IsCancellationRequested)) { LogInnerCacheTryAddError(ex, options.CacheKey); return false; @@ -730,7 +746,18 @@ public async ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTim // as in SetAsync: invalidating peers for a write that never happened is pure waste. try { - await _eventPublisher.CacheSetAsync(options).ConfigureAwait(false); + if (!await _eventPublisher.CacheSetAsync(options).ConfigureAwait(false)) + { + LogTryAddBroadcastNotPublished(options.CacheKey); + } + } + catch (Exception ex) + { + LogTryAddLocalPropagationFailed(ex, options.CacheKey); + } + + try + { MemorySet(options, value, policy.LocalExpiration ?? _multiLayerCacheOptions.LocalMaxExpiration); } catch (Exception ex) @@ -742,31 +769,53 @@ public async ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTim } /// - /// Conditional add against the in-memory tier, for the memory-only provider where it is the only - /// store. exposes no create-if-absent primitive, so the local lock is - /// what makes the probe-then-write atomic; without it two in-process callers can both win, which - /// is logged rather than silently tolerated. + /// The InMemory provider, where the local tier is the only store. + /// has no create-if-absent primitive, so the local lock is what makes probe-then-write atomic: + /// taken whatever Lock.LocalLockEnabled says, and a caller that cannot get it is told it + /// lost rather than run unserialized. It serializes conditional adds against each other only — + /// SetAsync and RemoveAsync take no lock, so a set landing between the probe and + /// the write is overwritten by the claim, which still reports true. Redis has no such gap, + /// NX being atomic against a concurrent SET; closing it here would mean locking + /// every local mutation. /// private async ValueTask LocalTryAddAsync(CacheEntryOptions options, T? value, CachePolicy policy) { + var localMaxExpiration = policy.LocalExpiration ?? _multiLayerCacheOptions.LocalMaxExpiration; + if (localMaxExpiration is { } max && max <= TimeSpan.Zero) + { + // Here the local retention is the only retention, so a non-positive one is evicted on + // arrival and every later caller would win too. Options validation does not cover this, + // and a per-call CachePolicy bypasses it regardless. + LogTryAddSkippedNonPositiveLocalRetention(options.CacheKey, max); + return false; + } + var localLock = await AcquireLocalLockAsync(options.CacheKey, policy.Lock, options.Token).ConfigureAwait(false); - try + if (localLock is null) { - if (localLock is null) - { - LogTryAddUnserialized(options.CacheKey); - } + LogTryAddLocalLockUnavailable(options.CacheKey); + return false; + } + try + { if (_memoryCache.TryGetValue(options.CacheKey, out _)) { return false; } - return MemorySet(options, value, policy.LocalExpiration ?? _multiLayerCacheOptions.LocalMaxExpiration); + // No invalidation broadcast: ChangeTokenFactory accepts only CacheRemoved and + // CacheRefreshed for this provider, so peers ignore CacheSet — deliberately, since here + // each node's memory is the store rather than a copy of a shared one, and a peer's write + // says nothing about this node's entry. + // A size-limited IMemoryCache drops an entry it cannot fit without throwing, and + // MemorySet still reports success, so the claim is only real if the key is retained. + return MemorySet(options, value, localMaxExpiration) + && _memoryCache.TryGetValue(options.CacheKey, out _); } finally { - localLock?.Dispose(); + localLock.Dispose(); } } @@ -1240,17 +1289,29 @@ private readonly struct CacheEntryValue(CacheEntryOptions cacheEntry, T? valu [LoggerMessage(Level = LogLevel.Debug, Message = "TryAdd skipped for {CacheKey}: a null value cannot be represented unless CacheNullValues is on.")] private partial void LogTryAddSkippedUnrepresentableValue(CacheKey cacheKey); - [LoggerMessage(Level = LogLevel.Debug, Message = "TryAdd for {CacheKey} reported not-added: the inner cache is disconnected, so no cross-node claim can be made.")] - private partial void LogTryAddInnerDisconnected(CacheKey cacheKey); - [LoggerMessage(Level = LogLevel.Warning, Message = "Inner cache TryAdd for cacheKey {CacheKey}")] private partial void LogInnerCacheTryAddError(Exception ex, CacheKey cacheKey); [LoggerMessage(Level = LogLevel.Warning, Message = "TryAdd won cacheKey {CacheKey} in the inner cache but could not broadcast or populate the local tier. The add still stands.")] private partial void LogTryAddLocalPropagationFailed(Exception ex, CacheKey cacheKey); - [LoggerMessage(Level = LogLevel.Warning, Message = "TryAdd for {CacheKey} ran without the local lock, so concurrent in-process callers may both be told they added the key. Enable Lock.LocalLockEnabled for exclusion on a memory-only cache.")] - private partial void LogTryAddUnserialized(CacheKey cacheKey); + [LoggerMessage(Level = LogLevel.Warning, Message = "TryAdd for {CacheKey} reported not-added: the local lock could not be acquired within Lock.LocalLockTimeout, and without it two in-process callers could both be told they added the key. Raise Lock.LocalLockTimeout if this key is contended.")] + private partial void LogTryAddLocalLockUnavailable(CacheKey cacheKey); + + [LoggerMessage(Level = LogLevel.Debug, Message = "TryAdd skipped for {CacheKey}: the requested expiration {Expiration} is not in the future, so the entry would retain nothing.")] + private partial void LogTryAddSkippedExpiredEntry(CacheKey cacheKey, DateTimeOffset expiration); + + [LoggerMessage(Level = LogLevel.Debug, Message = "TryAdd skipped for {CacheKey}: the effective local retention {LocalMaxExpiration} is not positive, and on this provider it is the only retention.")] + private partial void LogTryAddSkippedNonPositiveLocalRetention(CacheKey cacheKey, TimeSpan localMaxExpiration); + + [LoggerMessage(Level = LogLevel.Warning, Message = "TryAdd won cacheKey {CacheKey} but the invalidation broadcast reported not-published. The add still stands; peers may serve a stale copy until it expires.")] + private partial void LogTryAddBroadcastNotPublished(CacheKey cacheKey); + + [LoggerMessage(Level = LogLevel.Warning, Message = "Cache {CacheName} resolved {InnerCacheName} as its distributed tier, which cannot arbitrate a conditional add across nodes, so TryAddAsync on {CacheName} does not exclude other nodes. Check that the intended distributed provider is registered and Enabled.")] + private partial void LogInnerCacheCannotArbitrateAcrossNodes(string cacheName, string innerCacheName); + + [LoggerMessage(Level = LogLevel.Debug, Message = "TryAdd for {CacheKey} reported not-added: the distributed tier arbitrates in-process only, so no cross-node claim can be made.")] + private partial void LogTryAddInnerCacheNotDistributed(CacheKey cacheKey); [LoggerMessage(Level = LogLevel.Debug, Message = "Replacing cached key {CacheKey}")] private partial void LogReplacingCachedKey(CacheKey cacheKey); diff --git a/src/UiPath.Caching/MultilayerCacheBase.cs b/src/UiPath.Caching/MultilayerCacheBase.cs index 746a4d4..742ec38 100644 --- a/src/UiPath.Caching/MultilayerCacheBase.cs +++ b/src/UiPath.Caching/MultilayerCacheBase.cs @@ -100,10 +100,9 @@ protected async ValueTask RunUnderLocksAsync( CancellationToken token, LockProfile? policyLock = null) { - var localLockEnabled = policyLock?.LocalLockEnabled ?? _localLockEnabled; + var (localLockEnabled, localLockTimeout) = ResolveLocalLock(policyLock); var distributedLockEnabled = policyLock?.DistributedLockEnabled ?? _distributedLockEnabled; // Per-call LockProfile bypasses options validators; mirror LockSettingsValidator's accepted ranges and fall back when out-of-range. - var localLockTimeout = PositiveOrFallback(policyLock?.LocalLockTimeout, _localLockTimeout); var distributedLockTimeout = NonNegativeOrFallback(policyLock?.DistributedLockTimeout, _distributedLockTimeout); var distributedLockExpiry = PositiveOrFallback(policyLock?.DistributedLockExpiry, _distributedLockExpiry); @@ -194,16 +193,26 @@ private static TimeSpan NonNegativeOrFallback(TimeSpan? value, TimeSpan fallback } /// - /// Acquires only the local lock for , resolving enablement and timeout - /// the same way does. Returns null when the local lock is - /// disabled by policy or options, or when the acquire timed out — callers that need the lock for - /// correctness (rather than as a de-duplication optimization) must handle that case explicitly - /// rather than assume exclusion. + /// Acquires only the local lock for , for the callers to which it is a + /// correctness requirement rather than a de-duplication optimization — the conditional add on a + /// memory-only cache, where the lock is the only thing making probe-then-write atomic. It is + /// therefore taken regardless of Lock.LocalLockEnabled, which exists to trade single-flight + /// for throughput on GetOrAddAsync and says nothing about exclusion this tier cannot fake. + /// Only the timeout is honored, resolved the same way resolves it; + /// null comes back when the acquire timed out, and the caller must fail closed rather than + /// proceed unserialized. /// private protected ValueTask AcquireLocalLockAsync(CacheKey cacheKey, LockProfile? policyLock, CancellationToken token) => - (policyLock?.LocalLockEnabled ?? _localLockEnabled) - ? TryAcquireLocalLockAsync(cacheKey, PositiveOrFallback(policyLock?.LocalLockTimeout, _localLockTimeout), token) - : new ValueTask(default(IDisposable)); + TryAcquireLocalLockAsync(cacheKey, ResolveLocalLock(policyLock).Timeout, token); + + /// + /// Single place resolving the local-lock policy: a per-call wins over the + /// options-derived defaults, and because a per-call profile bypasses the options validators the + /// timeout mirrors LockSettingsValidator's accepted range and falls back when out of range. + /// + private (bool Enabled, TimeSpan Timeout) ResolveLocalLock(LockProfile? policyLock) => + (policyLock?.LocalLockEnabled ?? _localLockEnabled, + PositiveOrFallback(policyLock?.LocalLockTimeout, _localLockTimeout)); private async ValueTask TryAcquireLocalLockAsync(CacheKey cacheKey, TimeSpan localLockTimeout, CancellationToken token) { diff --git a/src/UiPath.Caching/PublicAPI.Unshipped.txt b/src/UiPath.Caching/PublicAPI.Unshipped.txt index 1c5428d..00628a2 100644 --- a/src/UiPath.Caching/PublicAPI.Unshipped.txt +++ b/src/UiPath.Caching/PublicAPI.Unshipped.txt @@ -1,20 +1,20 @@ #nullable enable -const UiPath.Caching.Config.DistributedCacheCollectionExtensions.DistributedCacheServiceKey = "UiPath.Caching.Distributed" -> string! -static UiPath.Caching.Config.DistributedCacheCollectionExtensions.AddDistributedCache(this UiPath.Caching.Config.ICachingBuilder! builder, string! providerName, System.Action? configure = null) -> UiPath.Caching.Config.ICachingBuilder! UiPath.Caching.Config.DistributedCacheCollectionExtensions UiPath.Caching.Distributed.UiPathDistributedCacheOptions -UiPath.Caching.Distributed.UiPathDistributedCacheOptions.PolicyName.get -> string? -UiPath.Caching.Distributed.UiPathDistributedCacheOptions.PolicyName.set -> void -UiPath.Caching.Distributed.UiPathDistributedCacheOptions.UiPathDistributedCacheOptions() -> void -UiPath.Caching.Distributed.UiPathDistributedCacheOptions.DefaultEntryExpiration.get -> System.TimeSpan? -UiPath.Caching.Distributed.UiPathDistributedCacheOptions.DefaultEntryExpiration.set -> void UiPath.Caching.Distributed.UiPathDistributedCacheOptions.AllowUnboundedEntries.get -> bool UiPath.Caching.Distributed.UiPathDistributedCacheOptions.AllowUnboundedEntries.set -> void -UiPath.Caching.Redis.RedisCacheOptions.AwaitRefresh.get -> bool -UiPath.Caching.Redis.RedisCacheOptions.AwaitRefresh.set -> void UiPath.Caching.Distributed.UiPathDistributedCacheOptions.CacheKeyStrategy.get -> UiPath.Caching.ICacheKeyStrategy? UiPath.Caching.Distributed.UiPathDistributedCacheOptions.CacheKeyStrategy.set -> void +UiPath.Caching.Distributed.UiPathDistributedCacheOptions.DefaultEntryExpiration.get -> System.TimeSpan? +UiPath.Caching.Distributed.UiPathDistributedCacheOptions.DefaultEntryExpiration.set -> void +UiPath.Caching.Distributed.UiPathDistributedCacheOptions.PolicyName.get -> string? +UiPath.Caching.Distributed.UiPathDistributedCacheOptions.PolicyName.set -> void UiPath.Caching.Distributed.UiPathDistributedCacheOptions.RedisKeyDifferentiator.get -> string? UiPath.Caching.Distributed.UiPathDistributedCacheOptions.RedisKeyDifferentiator.set -> void UiPath.Caching.Distributed.UiPathDistributedCacheOptions.RedisKeyStrategyFactory.get -> UiPath.Caching.Redis.IRedisKeyStrategyFactory? UiPath.Caching.Distributed.UiPathDistributedCacheOptions.RedisKeyStrategyFactory.set -> void +UiPath.Caching.Distributed.UiPathDistributedCacheOptions.UiPathDistributedCacheOptions() -> void +UiPath.Caching.Redis.RedisCacheOptions.AwaitRefresh.get -> bool +UiPath.Caching.Redis.RedisCacheOptions.AwaitRefresh.set -> void +const UiPath.Caching.Config.DistributedCacheCollectionExtensions.DistributedCacheServiceKey = "UiPath.Caching.Distributed" -> string! +static UiPath.Caching.Config.DistributedCacheCollectionExtensions.AddDistributedCache(this UiPath.Caching.Config.ICachingBuilder! builder, string! providerName, System.Action? configure = null) -> UiPath.Caching.Config.ICachingBuilder! diff --git a/src/UiPath.Caching/Redis/RedisCache.cs b/src/UiPath.Caching/Redis/RedisCache.cs index e4e8d8e..f8808cc 100644 --- a/src/UiPath.Caching/Redis/RedisCache.cs +++ b/src/UiPath.Caching/Redis/RedisCache.cs @@ -46,6 +46,7 @@ public RedisCache( public string Name => KnownCacheProviderNames.Redis; + public ValueTask GetAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); @@ -477,9 +478,10 @@ private async ValueTask SetInternalAsync(RedisKey redisKey, T? value, T } /// - /// SET key value EX … NX in one round-trip: Redis itself decides the race, so no probe - /// precedes the write. Returns false for every non-win — key already present, not - /// connected, write threw, or a default value that this cache has no way to represent. + /// SET key value EX … NX in one round-trip: Redis decides the race, so no probe precedes + /// the write. false for every non-win — present, disconnected, threw, or unrepresentable. + /// Safe on the retrying write pipeline: a retry after a lost reply is refused by the key it just + /// wrote and reports the same false the exception would have. /// private async ValueTask TryAddInternalAsync(RedisKey redisKey, T? value, TimeSpan expiration, CancellationToken token) { @@ -496,7 +498,11 @@ private async ValueTask TryAddInternalAsync(RedisKey redisKey, T? value try { var isNull = IsDefault(value); - if (isNull && (!_cacheNullValues || expiration <= TimeSpan.Zero)) + if (expiration <= TimeSpan.Zero) + { + LogTryAddSkippedExpiredEntry(redisKey, expiration); + } + else if (isNull && !_cacheNullValues) { // A conditional add must never delete, which is what SetAsync does with a default // value here. With no sentinel available there is nothing to claim the key with. @@ -514,6 +520,11 @@ private async ValueTask TryAddInternalAsync(RedisKey redisKey, T? value } operation.Stop(); } + catch (OperationCanceledException) when (token.IsCancellationRequested) + { + operation.Stop(); + throw; + } catch (Exception ex) { operation.Stop(); @@ -949,9 +960,12 @@ private void AuditKeySize(RedisKey key, RedisValue value) [LoggerMessage(Level = LogLevel.Trace, Message = "Refreshing key {RedisKey} at expiration {Expiration}")] private partial void LogRefreshingKey(RedisKey redisKey, DateTimeOffset? expiration); - [LoggerMessage(Level = LogLevel.Debug, Message = "TryAdd skipped for {RedisKey}: a default value cannot be represented unless CacheNullValues is on with a positive expiration.")] + [LoggerMessage(Level = LogLevel.Debug, Message = "TryAdd skipped for {RedisKey}: a default value cannot be represented unless CacheNullValues is on.")] private partial void LogTryAddSkippedUnrepresentableValue(RedisKey redisKey); + [LoggerMessage(Level = LogLevel.Debug, Message = "TryAdd skipped for {RedisKey}: the requested expiration {Expiration} is not positive, so the entry would retain nothing.")] + private partial void LogTryAddSkippedExpiredEntry(RedisKey redisKey, TimeSpan expiration); + [LoggerMessage(Level = LogLevel.Warning, Message = "RedisCache exception.")] private partial void LogRedisCacheException(Exception ex); diff --git a/tests/UiPath.Caching.Tests/MultilayerCacheTryAddTests.cs b/tests/UiPath.Caching.Tests/MultilayerCacheTryAddTests.cs index d30329b..7c427d4 100644 --- a/tests/UiPath.Caching.Tests/MultilayerCacheTryAddTests.cs +++ b/tests/UiPath.Caching.Tests/MultilayerCacheTryAddTests.cs @@ -84,7 +84,6 @@ public async Task TryAdd_leaves_both_tiers_untouched_when_the_inner_cache_report added.Should().BeFalse(); _memoryCache.DidNotReceive().CreateEntry(_cacheKey); - // Invalidating peers for a write that never happened is pure waste. await _topic.DidNotReceive().PublishAsync(Arg.Any(), Arg.Any()); } @@ -126,19 +125,121 @@ public async Task TryAdd_fails_closed_when_the_inner_cache_throws() } [Fact] - public async Task TryAdd_fails_closed_when_the_inner_cache_is_disconnected() + public async Task TryAdd_surfaces_an_inner_cache_that_cannot_arbitrate_at_all() + { + _innerCache.TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new NotSupportedException("no NX here")); + + var act = async () => await Sut.TryAddAsync(_cacheKey, _fixture.Create(), policy: null, token: Ct); + + await act.Should().ThrowAsync(); + _memoryCache.DidNotReceive().CreateEntry(_cacheKey); + } + + [Fact] + public async Task TryAdd_claims_nothing_for_an_expiration_that_has_already_passed() + { + var added = await Sut.TryAddAsync(_cacheKey, _fixture.Create(), DateTimeOffset.UtcNow.AddMinutes(-5), token: Ct); + + added.Should().BeFalse(); + await _innerCache.DidNotReceive().TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + _memoryCache.DidNotReceive().CreateEntry(_cacheKey); + } + + [Fact] + public async Task TryAdd_claims_nothing_for_a_zero_expiration() + { + var added = await Sut.TryAddAsync(_cacheKey, _fixture.Create(), TimeSpan.Zero, token: Ct); + + added.Should().BeFalse(); + await _innerCache.DidNotReceive().TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task A_broadcast_that_reports_not_published_still_stands_and_is_logged() + { + // CacheSetAsync signals an ordinary publish failure with false rather than throwing, so the + // catch alone would let it pass unreported. + _innerCache.TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(true); + _topic.PublishAsync(Arg.Any(), Arg.Any()).Returns(_ => false); + + var added = await Sut.TryAddAsync(_cacheKey, _fixture.Create(), policy: null, token: Ct); + + added.Should().BeTrue(); + _logger.Received().Log( + LogLevel.Warning, + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any>()); + } + + [Fact] + public async Task A_failed_broadcast_still_populates_the_local_tier() + { + // The broadcast and the L1 write are independent best-effort steps after the win; a dead + // topic must not cost the winning node its local copy. + _innerCache.TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(true); + _topic.PublishAsync(Arg.Any(), Arg.Any()) + .ThrowsAsync(new InvalidOperationException("broadcast down")); + + var added = await Sut.TryAddAsync(_cacheKey, _fixture.Create(), policy: null, token: Ct); + + added.Should().BeTrue(); + _memoryCache.Received(1).CreateEntry(_cacheKey); + } + + [Fact] + public async Task TryAdd_fails_closed_when_the_inner_cache_cannot_claim_the_key() { _options.UseLocalOnlyWhenDisconnected = true; _options.ConnectionMonitorEnabled = true; - _topicProvider.IsConnected.Returns(false); + _innerCache.TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(false); _sut = null; var added = await Sut.TryAddAsync(_cacheKey, _fixture.Create(), policy: null, token: Ct); added.Should().BeFalse("a local-only claim would be granted to every node independently"); - // SetAsync degrades to a local write here; a conditional add must not. _memoryCache.DidNotReceive().CreateEntry(_cacheKey); - await _innerCache.DidNotReceive().TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task A_disconnected_broadcast_transport_does_not_stop_a_healthy_L2_from_arbitrating() + { + _options.UseLocalOnlyWhenDisconnected = true; + _options.ConnectionMonitorEnabled = true; + _topicProvider.IsConnected.Returns(false); + _innerCache.TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(true); + _sut = null; + + var added = await Sut.TryAddAsync(_cacheKey, _fixture.Create(), policy: null, token: Ct); + + added.Should().BeTrue("the aggregate connection state covers the topic too, and broadcast is best-effort after a win"); + await _innerCache.Received(1).TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task TryAdd_surfaces_a_cancellation_raised_by_the_inner_cache() + { + // Cancelling before the call would throw in BuildEntryOptions and never reach the catch + // filter, so the inner cache cancels mid-flight instead. + using var cts = new CancellationTokenSource(); + _innerCache.TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns>(_ => + { + cts.Cancel(); + throw new OperationCanceledException(cts.Token); + }); + + var act = async () => await Sut.TryAddAsync(_cacheKey, _fixture.Create(), policy: null, token: cts.Token); + + await act.Should().ThrowAsync( + "reporting false would say someone else owns the key, which InMemoryRedis must not claim any more than Redis does"); + await _innerCache.Received(1).TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); } [Fact] @@ -189,6 +290,41 @@ await _innerCache.Received(1).TryAddAsync( Arg.Any()); } + [Fact] + public async Task An_inner_cache_that_arbitrates_in_process_only_fails_closed() + { + // InMemoryRedis with DefaultCache=InMemory resolves the InMemory multilayer cache as its L2; + // delegating would let that cache's local arbiter grant a win per process. + var inner = CreateInMemorySut(); + _fixture.Inject(inner); + using var sut = _fixture.Create(); + + var added = await sut.TryAddAsync(_cacheKey, _fixture.Create(), policy: null, token: Ct); + + added.Should().BeFalse("a per-process win under a cross-node provider name is worse than no win"); + } + + private static MultilayerCache CreateInMemorySut() + { + var options = new InMemoryCacheOptions(); + var cacheOptions = new CacheOptions { AppShortName = "test" }; + return new MultilayerCache( + KnownCacheProviderNames.InMemory, + NullCache.Instance, + new MemoryCacheFactory(null, NullLoggerFactory.Instance), + NullChangeTokenFactory.Instance, + NullTopicFactory.Instance, + NullCacheEventFactory.Instance, + NullTelemetryProvider.Instance, + options, + options, + cacheOptions, + localLock: new AsyncKeyedLocalLock(Options.Create(cacheOptions)), + distributedLock: NullDistributedLock.Instance, + policyFactory: NullCachePolicyFactory.Instance, + logger: NullLogger.Instance); + } + [Fact] public async Task TryAdd_rejects_a_null_key() { @@ -261,12 +397,15 @@ public class InMemoryCacheTryAddTests { private static CancellationToken Ct => TestContext.Current.CancellationToken; - private static MultilayerCache CreateSut(InMemoryCacheOptions? options = null) + private static MultilayerCache CreateSut( + InMemoryCacheOptions? options = null, + ILocalLock? localLock = null, + string cacheName = KnownCacheProviderNames.InMemory) { options ??= new InMemoryCacheOptions(); var cacheOptions = new CacheOptions { AppShortName = "test" }; return new MultilayerCache( - KnownCacheProviderNames.InMemory, + cacheName, NullCache.Instance, new MemoryCacheFactory(null, NullLoggerFactory.Instance), NullChangeTokenFactory.Instance, @@ -276,7 +415,7 @@ private static MultilayerCache CreateSut(InMemoryCacheOptions? options = null) options, options, cacheOptions, - localLock: new AsyncKeyedLocalLock(Options.Create(cacheOptions)), + localLock: localLock ?? new AsyncKeyedLocalLock(Options.Create(cacheOptions)), distributedLock: NullDistributedLock.Instance, policyFactory: NullCachePolicyFactory.Instance, logger: NullLogger.Instance); @@ -335,4 +474,98 @@ public async Task Exactly_one_of_many_concurrent_callers_wins() results.Count(won => won).Should().Be(1, "the local lock is what makes probe-then-write atomic"); } + + [Fact] + public async Task A_distributed_provider_that_resolved_a_null_L2_fails_closed_instead_of_arbitrating_locally() + { + // NullCache is the L2 both for the memory-only provider (by design) and for an InMemoryRedis + // whose Redis provider is absent or disabled — CacheFactory falls back to it. Arbitrating + // locally in the second case would hand every process its own winner under a provider name + // that promises cross-node exclusion, so only the InMemory provider may do it. + using var sut = CreateSut(cacheName: KnownCacheProviderNames.InMemoryRedis); + + (await sut.TryAddAsync("k", "first", policy: null, token: Ct)).Should().BeFalse(); + (await sut.TryAddAsync("k", "second", policy: null, token: Ct)).Should().BeFalse(); + } + + [Fact] + public async Task Exactly_one_caller_still_wins_with_the_local_lock_disabled() + { + using var sut = CreateSut(new InMemoryCacheOptions { LocalLockEnabled = false }); + const int callers = 32; + + var results = await Task.WhenAll(Enumerable.Range(0, callers).Select(i => + Task.Run(async () => await sut.TryAddAsync("k", $"caller-{i}", policy: null, token: Ct), Ct))); + + results.Count(won => won).Should().Be(1, "an unserialized probe-then-write would hand the same win to several callers"); + } + + [Fact] + public async Task A_caller_that_cannot_take_the_local_lock_is_told_it_lost() + { + var options = new InMemoryCacheOptions { LocalLockTimeout = TimeSpan.FromMilliseconds(50) }; + using var sut = CreateSut(options, localLock: new NeverGrantingLocalLock()); + + var added = await sut.TryAddAsync("k", "first", policy: null, token: Ct); + + added.Should().BeFalse(); + (await sut.GetAsync("k", policy: null, token: Ct)).Should().BeNull("a loss must not write anything either"); + } + + [Fact] + public async Task A_size_limited_memory_cache_that_drops_the_entry_claims_nothing() + { + // IMemoryCache silently declines an entry it cannot fit, and MemoryCacheSetter still reports + // success, so without the retention check every caller would be told it won. + using var sut = CreateSut(new InMemoryCacheOptions { SizeLimit = 1, SizeProvider = new OversizedEntryProvider() }); + + (await sut.TryAddAsync("k", "first", policy: null, token: Ct)).Should().BeFalse(); + (await sut.TryAddAsync("k", "second", policy: null, token: Ct)).Should().BeFalse(); + } + + private sealed class OversizedEntryProvider : ICacheEntrySizeProvider + { + public long GetSize(ICacheEntry entry) => long.MaxValue; + } + + [Theory] + [InlineData(0)] + [InlineData(-1)] + public async Task A_non_positive_local_retention_claims_nothing(int minutes) + { + // On this provider the local retention is the only retention, so an entry evicted on arrival + // would leave the key free for every later caller. Options-level values are caught by + // CachePolicyFactoryValidator; a per-call policy is not validated at all, which is the path + // that reaches here. + using var sut = CreateSut(); + var policy = new CachePolicy { LocalExpiration = TimeSpan.FromMinutes(minutes) }; + + (await sut.TryAddAsync("k", "first", policy, token: Ct)).Should().BeFalse(); + (await sut.TryAddAsync("k", "second", policy, token: Ct)).Should().BeFalse(); + (await sut.GetAsync("k", policy: null, token: Ct)).Should().BeNull(); + } + + [Fact] + public async Task An_expiration_that_has_already_passed_claims_nothing() + { + using var sut = CreateSut(); + var past = DateTimeOffset.UtcNow.AddMinutes(-5); + + (await sut.TryAddAsync("k", "first", past, token: Ct)).Should().BeFalse(); + (await sut.TryAddAsync("k", "second", past, token: Ct)).Should().BeFalse(); + (await sut.GetAsync("k", policy: null, token: Ct)).Should().BeNull(); + } + + /// + /// Stands in for a local lock held by someone else for longer than the acquire budget: the wait is + /// abandoned by the linked timeout, which is the only way AcquireLocalLockAsync answers null. + /// + private sealed class NeverGrantingLocalLock : ILocalLock + { + public async ValueTask AcquireAsync(string key, CancellationToken token) + { + await Task.Delay(Timeout.Infinite, token).ConfigureAwait(false); + throw new InvalidOperationException("unreachable: the delay above only ever cancels"); + } + } } diff --git a/tests/UiPath.Caching.Tests/NullCacheConditionalAddTests.cs b/tests/UiPath.Caching.Tests/NullCacheConditionalAddTests.cs new file mode 100644 index 0000000..17af5fe --- /dev/null +++ b/tests/UiPath.Caching.Tests/NullCacheConditionalAddTests.cs @@ -0,0 +1,42 @@ +using UiPath.Caching; + +namespace UiPath.Caching.Tests; + +public class NullCacheConditionalAddTests +{ + private static CancellationToken Ct => TestContext.Current.CancellationToken; + + [Fact] + public async Task TryAdd_reports_not_added_for_every_caller() + { + var sut = NullCache.Instance; + + (await sut.TryAddAsync("k", "first", policy: null, token: Ct)).Should().BeFalse(); + (await sut.TryAddAsync("k", "second", policy: null, token: Ct)).Should().BeFalse(); + } + + [Theory] + [InlineData(null)] + [InlineData(5)] + public async Task TryAdd_reports_not_added_whatever_the_expiration(int? minutes) + { + TimeSpan? ttl = minutes is { } m ? TimeSpan.FromMinutes(m) : null; + + (await NullCache.Instance.TryAddAsync("k", "v", ttl, token: Ct)).Should().BeFalse(); + (await NullCache.Instance.TryAddAsync("k", "v", ttl.HasValue ? DateTimeOffset.UtcNow.Add(ttl.Value) : null, token: Ct)).Should().BeFalse(); + } + + [Fact] + public async Task SetAsync_still_reports_success() + { + (await NullCache.Instance.SetAsync("k", "v", policy: null, token: Ct)).Should().BeTrue(); + } + + [Fact] + public void An_uncacheable_type_still_throws() + { + var act = () => NullCache.Instance.TryAddAsync("k", 1, policy: null, token: Ct); + + act.Should().Throw(); + } +} diff --git a/tests/UiPath.Caching.Tests/Redis/RedisCacheTryAddTests.cs b/tests/UiPath.Caching.Tests/Redis/RedisCacheTryAddTests.cs index 8c4ddff..efb24b5 100644 --- a/tests/UiPath.Caching.Tests/Redis/RedisCacheTryAddTests.cs +++ b/tests/UiPath.Caching.Tests/Redis/RedisCacheTryAddTests.cs @@ -24,6 +24,7 @@ public class RedisCacheTryAddTests(ITestContextAccessor testContextAccessor) : I private CacheKey _cacheKey = default!; private RedisKey _redisKey = default!; private IRedisConnector _connector = default!; + private IResiliencePipelineProvider _pipelineProvider = default!; private bool _isConnected = true; private readonly RecordingTelemetryProvider _telemetry = new(); private RedisCache? _sut; @@ -199,6 +200,62 @@ public async Task TryAdd_honors_a_cancelled_token_before_touching_redis() await _database.DidNotReceive().StringSetAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); } + [Fact] + public async Task TryAdd_runs_the_NX_write_through_the_shared_write_pipeline() + { + var write = new CountingResiliencePipeline(); + _pipelineProvider.Get(ResiliencePipelineNames.Write).Returns(write); + _database.StringSetAsync(_redisKey, Arg.Any(), Arg.Any(), When.NotExists, CommandFlags.DemandMaster) + .Returns(true); + + var added = await Sut.TryAddAsync(_cacheKey, _fixture.Create(), policy: null, token: testContextAccessor.Current.CancellationToken); + + added.Should().BeTrue(); + write.Executions.Should().Be(1); + } + + [Fact] + public async Task TryAdd_surfaces_a_cancellation_raised_while_the_write_is_in_flight() + { + using var cts = new CancellationTokenSource(); + _database.StringSetAsync(_redisKey, Arg.Any(), Arg.Any(), When.NotExists, CommandFlags.DemandMaster) + .Returns>(_ => + { + cts.Cancel(); + throw new OperationCanceledException(cts.Token); + }); + + var act = async () => await Sut.TryAddAsync(_cacheKey, _fixture.Create(), policy: null, token: cts.Token); + + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task A_redis_failure_that_is_not_a_cancellation_still_reports_not_added() + { + _database.StringSetAsync(_redisKey, Arg.Any(), Arg.Any(), When.NotExists, CommandFlags.DemandMaster) + .ThrowsAsync(new InvalidOperationException("redis down")); + + var added = await Sut.TryAddAsync(_cacheKey, _fixture.Create(), policy: null, token: testContextAccessor.Current.CancellationToken); + + added.Should().BeFalse(); + } + + [Theory] + [InlineData(0)] + [InlineData(-5)] + public async Task TryAdd_claims_nothing_for_a_non_positive_expiration(int minutes) + { + var added = await Sut.TryAddAsync( + _cacheKey, + _fixture.Create(), + TimeSpan.FromMinutes(minutes), + token: testContextAccessor.Current.CancellationToken); + + added.Should().BeFalse(); + await _database.DidNotReceive().StringSetAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + } + public ValueTask InitializeAsync() { const string prefix = "test"; @@ -206,10 +263,10 @@ public ValueTask InitializeAsync() _redisKey = string.Join(':', prefix, RedisTypePrefixes.String, _cacheKey).ToLowerInvariant(); _clock = _fixture.Freeze(); _clock.UtcNow.Returns(_ => _now); - var resiliencePipelineProvider = _fixture.Freeze(); + _pipelineProvider = _fixture.Freeze(); var noOpExecutor = new EmptyResiliencePipeline(); - resiliencePipelineProvider.Get(ResiliencePipelineNames.Read).Returns(noOpExecutor); - resiliencePipelineProvider.Get(ResiliencePipelineNames.Write).Returns(noOpExecutor); + _pipelineProvider.Get(ResiliencePipelineNames.Read).Returns(noOpExecutor); + _pipelineProvider.Get(ResiliencePipelineNames.Write).Returns(noOpExecutor); var cacheKeyStrategy = _fixture.Create(); cacheKeyStrategy.GetCacheKey(_cacheKey).Returns(_cacheKey); var redisKeyStrategyFactory = _fixture.Create(); @@ -242,4 +299,21 @@ public ValueTask DisposeAsync() _sut?.Dispose(); return ValueTask.CompletedTask; } + + /// + /// Executes the callback once and counts it, so a test can say which named pipeline a command was + /// routed through without reaching into Polly. + /// + private sealed class CountingResiliencePipeline : IResiliencePipeline + { + private int _executions; + + public int Executions => Volatile.Read(ref _executions); + + public ValueTask ExecuteAsync(Func> callback, TResult defaultValue, CancellationToken cancellationToken = default) + { + Interlocked.Increment(ref _executions); + return callback(cancellationToken); + } + } } From 1526da729105cac5fd0407f62236ba6680b4f396 Mon Sep 17 00:00:00 2001 From: Cosmin Staicu Date: Thu, 3 Sep 2026 00:28:30 +0300 Subject: [PATCH 3/9] test: clear the Sonar findings on the TryAdd test files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four findings Sonar raised on this PR's changed lines. All three rules sit at their default Info severity, so they surface in the IDE and in Sonar's import but never as build warnings — which is why the build has been clean throughout. CA1859 on RedisCacheTryAddTests._serializer needs care: narrowing the field to SystemJsonSerializerProxy as the rule suggests silently breaks the fixture, because AutoFixture's Inject binds on the argument's *static* type. With a bare Inject the concrete type gets registered, RedisCache resolves a substitute for ISerializerProxy instead of the real serializer, and TryAdd_writes_a_payload_a_reader_can_deserialize fails with "JsonException: 'v' is an invalid start of a value" — the payload was written raw rather than as JSON. Verified by applying the naive form first. The registration is now pinned with an explicit type argument, with a comment saying why. CA1816 on both DisposeAsync hooks: xunit v3's IAsyncLifetime derives from IAsyncDisposable, so the rule fires on what is really a runner-invoked lifecycle hook. Neither class has a finalizer, so the call is a no-op, but it is a one-liner and keeps the file clean. CA2012 on the NSubstitute arrange: suppressed with a pragma and a justification. NSubstitute intercepts the call and Returns only uses the ValueTask as its receiver — it is never awaited, so there is no single-consumption hazard. Scope note: these rules fire 57 times across the repo (42 CA1816, all in tests; 13 CA1859, 2 of them in src; 2 CA2012), and this commit clears the 4 that Sonar attributed to this PR, leaving 53. The remaining CA1816 and CA2012 hits are the same two false-positive patterns — xunit lifecycle hooks and NSubstitute arranges — so silencing them for the test project in .editorconfig would be a better fix than 40 more SuppressFinalize calls; the 2 src CA1859 hits (MemorySetCache/RedisSetCache Deserialize returning IReadOnlyCollection where List would do) are legitimate and worth their own change. Verified: Release build clean (16 warnings, all pre-existing CS0618), 1503/1503 on net8.0 and net10.0, and the 4 findings confirmed gone by temporarily raising the three rules to warning (57 -> 53). Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_015RLSnsYiZsqeaHHYYybXW1 Signed-off-by: Cosmin Staicu --- tests/UiPath.Caching.Tests/MultilayerCacheTryAddTests.cs | 5 +++++ tests/UiPath.Caching.Tests/Redis/RedisCacheTryAddTests.cs | 8 ++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/UiPath.Caching.Tests/MultilayerCacheTryAddTests.cs b/tests/UiPath.Caching.Tests/MultilayerCacheTryAddTests.cs index 7c427d4..29b4605 100644 --- a/tests/UiPath.Caching.Tests/MultilayerCacheTryAddTests.cs +++ b/tests/UiPath.Caching.Tests/MultilayerCacheTryAddTests.cs @@ -228,12 +228,16 @@ public async Task TryAdd_surfaces_a_cancellation_raised_by_the_inner_cache() // Cancelling before the call would throw in BuildEntryOptions and never reach the catch // filter, so the inner cache cancels mid-flight instead. using var cts = new CancellationTokenSource(); + // CA2012: NSubstitute intercepts the call and Returns only uses the ValueTask as its + // receiver — it is never awaited, so there is no single-consumption hazard here. +#pragma warning disable CA2012 _innerCache.TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns>(_ => { cts.Cancel(); throw new OperationCanceledException(cts.Token); }); +#pragma warning restore CA2012 var act = async () => await Sut.TryAddAsync(_cacheKey, _fixture.Create(), policy: null, token: cts.Token); @@ -379,6 +383,7 @@ public ValueTask InitializeAsync() public ValueTask DisposeAsync() { _sut?.Dispose(); + GC.SuppressFinalize(this); return ValueTask.CompletedTask; } diff --git a/tests/UiPath.Caching.Tests/Redis/RedisCacheTryAddTests.cs b/tests/UiPath.Caching.Tests/Redis/RedisCacheTryAddTests.cs index efb24b5..012edeb 100644 --- a/tests/UiPath.Caching.Tests/Redis/RedisCacheTryAddTests.cs +++ b/tests/UiPath.Caching.Tests/Redis/RedisCacheTryAddTests.cs @@ -19,7 +19,7 @@ public class RedisCacheTryAddTests(ITestContextAccessor testContextAccessor) : I private ISystemClock _clock = default!; private RedisCacheOptions _cacheOptions = default!; private IDatabase _database = default!; - private ISerializerProxy _serializer = default!; + private SystemJsonSerializerProxy _serializer = default!; private readonly DateTimeOffset _now = DateTimeOffset.UtcNow; private CacheKey _cacheKey = default!; private RedisKey _redisKey = default!; @@ -282,7 +282,10 @@ public ValueTask InitializeAsync() _database = _fixture.Freeze(); _serializer = new SystemJsonSerializerProxy(); - _fixture.Inject(_serializer); + // Registered under the interface explicitly: Inject binds on the argument's static type, so + // with _serializer narrowed for CA1859 a bare Inject would register the concrete type and + // leave RedisCache resolving a substitute instead of this real serializer. + _fixture.Inject>(_serializer); _fixture.Inject(Options.Create(_cacheOptions)); _fixture.Inject(_cacheOptions); _fixture.Inject(_telemetry); @@ -297,6 +300,7 @@ public ValueTask InitializeAsync() public ValueTask DisposeAsync() { _sut?.Dispose(); + GC.SuppressFinalize(this); return ValueTask.CompletedTask; } From 453e8ed0bc21710b915a7a0cc737a26d09b4d231 Mon Sep 17 00:00:00 2001 From: Cosmin Staicu Date: Wed, 2 Sep 2026 21:15:02 +0300 Subject: [PATCH 4/9] test: de-flake the FactoryTimeout and circuit-breaker timing tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two unrelated non-determinisms, both surfaced by running the Release suite under parallel load on net10.0. ResiliencePipelineFactoryTest.Pipeline_works_as_expected checked that the circuit closes using a fixed 250ms + 4x100ms budget against a DurationOfBreak of 500ms — 150ms of slack, measured from wherever the preceding exception loop happened to finish. Under load the breaker is still open on the fourth probe. Replaced with the polling shape already used by ConnectionStateMonitorTests.WaitUntilAsync: 20ms polls under a 30s ceiling, so a slow agent costs latency rather than a failure. The guard CTS goes from 5s to 30s for the same reason — it exists only to stop a hang. The four tests asserting ThrowAsync passed the ambient xunit token as the *caller* token. FactoryTimeout.RunAsync only converts cancellation to TimeoutException while that token is uncancelled: catch (OperationCanceledException) when (linkedCts.IsCancellationRequested && !token.IsCancellationRequested) so the assertion depended on the runner not cancelling it, and a raw TaskCanceledException escapes when it does. Each now uses a CancellationTokenSource it owns, matching GetOrAdd_FactoryTimeout_does_not_swallow_caller_cancellation next door. The 50ms FactoryTimeout is what bounds these calls, so dropping the ambient token cannot hang them; the batch test keeps it on its Task.WhenAny guard. All four are fixed, not just the one observed failing — the Multilayer, Multilayer hash and both RedisCacheTests cases share the same defect. Verified with three consecutive full Release runs, 1498/1498 on net8.0 and net10.0, two of them at 3m35s-3m59s against a 1m25s baseline (i.e. heavier load than the runs that originally flaked). Before: 2 failures in 5 runs. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_015RLSnsYiZsqeaHHYYybXW1 Signed-off-by: Cosmin Staicu --- ...ltilayerCachePerNameFactoryTimeoutTests.cs | 7 +++++- ...ayerHashCachePerNameFactoryTimeoutTests.cs | 7 +++++- .../Redis/RedisCacheTests.cs | 15 ++++++++++-- .../ResiliencePipelineFactoryTests.cs | 24 +++++++++++-------- 4 files changed, 39 insertions(+), 14 deletions(-) diff --git a/tests/UiPath.Caching.Tests/MultilayerCachePerNameFactoryTimeoutTests.cs b/tests/UiPath.Caching.Tests/MultilayerCachePerNameFactoryTimeoutTests.cs index b3c8ff2..b421fe3 100644 --- a/tests/UiPath.Caching.Tests/MultilayerCachePerNameFactoryTimeoutTests.cs +++ b/tests/UiPath.Caching.Tests/MultilayerCachePerNameFactoryTimeoutTests.cs @@ -42,6 +42,11 @@ public async Task GetOrAdd_with_null_FactoryTimeout_runs_generator_to_completion [Fact] public async Task GetOrAdd_with_FactoryTimeout_throws_TimeoutException_when_generator_exceeds_budget() { + // FactoryTimeout only surfaces as TimeoutException while the caller's own token is + // uncancelled — see the catch filter in FactoryTimeout.RunAsync. Pass a token this test + // owns rather than the ambient one the runner may cancel under load; the 50ms budget is + // what bounds the call, so nothing here can hang. + using var caller = new CancellationTokenSource(); var policy = new CachePolicy { FactoryTimeout = TimeSpan.FromMilliseconds(50) }; Func> generator = async ct => { @@ -49,7 +54,7 @@ public async Task GetOrAdd_with_FactoryTimeout_throws_TimeoutException_when_gene return "v"; }; - var act = async () => await Sut.GetOrAddAsync(_cacheKey, generator, policy, testContextAccessor.Current.CancellationToken); + var act = async () => await Sut.GetOrAddAsync(_cacheKey, generator, policy, caller.Token); await act.Should().ThrowAsync(); } diff --git a/tests/UiPath.Caching.Tests/MultilayerHashCachePerNameFactoryTimeoutTests.cs b/tests/UiPath.Caching.Tests/MultilayerHashCachePerNameFactoryTimeoutTests.cs index 7ca6aa0..88d4f3a 100644 --- a/tests/UiPath.Caching.Tests/MultilayerHashCachePerNameFactoryTimeoutTests.cs +++ b/tests/UiPath.Caching.Tests/MultilayerHashCachePerNameFactoryTimeoutTests.cs @@ -42,6 +42,11 @@ public async Task GetOrAdd_with_null_FactoryTimeout_runs_generator_to_completion [Fact] public async Task GetOrAdd_with_FactoryTimeout_throws_TimeoutException_when_generator_exceeds_budget() { + // FactoryTimeout only surfaces as TimeoutException while the caller's own token is + // uncancelled — see the catch filter in FactoryTimeout.RunAsync. Pass a token this test + // owns rather than the ambient one the runner may cancel under load; the 50ms budget is + // what bounds the call, so nothing here can hang. + using var caller = new CancellationTokenSource(); var policy = new CachePolicy { FactoryTimeout = TimeSpan.FromMilliseconds(50) }; Func>> generator = async ct => { @@ -49,7 +54,7 @@ public async Task GetOrAdd_with_FactoryTimeout_throws_TimeoutException_when_gene return new Dictionary { ["f"] = "v" }; }; - var act = async () => await Sut.GetOrAddAsync(_cacheKey, generator, policy, testContextAccessor.Current.CancellationToken); + var act = async () => await Sut.GetOrAddAsync(_cacheKey, generator, policy, caller.Token); await act.Should().ThrowAsync(); } diff --git a/tests/UiPath.Caching.Tests/Redis/RedisCacheTests.cs b/tests/UiPath.Caching.Tests/Redis/RedisCacheTests.cs index bdacd9c..0c02fe1 100644 --- a/tests/UiPath.Caching.Tests/Redis/RedisCacheTests.cs +++ b/tests/UiPath.Caching.Tests/Redis/RedisCacheTests.cs @@ -755,6 +755,11 @@ await _database.Received(1).StringSetAsync( [Fact] public async Task GetOrAdd_policy_FactoryTimeout_cancels_slow_generator() { + // FactoryTimeout only surfaces as TimeoutException while the caller's own token is + // uncancelled — see the catch filter in FactoryTimeout.RunAsync. Pass a token this test + // owns rather than the ambient one the runner may cancel under load; the 50ms budget is + // what bounds the call, so nothing here can hang. + using var caller = new CancellationTokenSource(); var policy = new CachePolicy { FactoryTimeout = TimeSpan.FromMilliseconds(50) }; Func> generator = async ct => { @@ -762,7 +767,7 @@ public async Task GetOrAdd_policy_FactoryTimeout_cancels_slow_generator() return "never"; }; - var act = async () => await Sut.GetOrAddAsync(_cacheKey, generator, policy: policy, token: testContextAccessor.Current.CancellationToken); + var act = async () => await Sut.GetOrAddAsync(_cacheKey, generator, policy: policy, token: caller.Token); await act.Should().ThrowAsync(); } @@ -771,6 +776,12 @@ public async Task GetOrAdd_policy_FactoryTimeout_cancels_slow_generator() public async Task Batch_GetOrAdd_policy_FactoryTimeout_cancels_slow_generator() { var token = testContextAccessor.Current.CancellationToken; + // FactoryTimeout only surfaces as TimeoutException while the caller's own token is + // uncancelled — see the catch filter in FactoryTimeout.RunAsync. Pass a token this test + // owns rather than the ambient one the runner may cancel under load; the 50ms budget is + // what bounds the call, so nothing here can hang. The ambient token still guards the race + // below. + using var caller = new CancellationTokenSource(); var policy = new CachePolicy { FactoryTimeout = TimeSpan.FromMilliseconds(50) }; var entries = new KeyValuePair[] { new(_cacheKey, "one"), new(_multiKey, "two") }; static async Task[]> Generator(string[] _, CancellationToken ct) @@ -779,7 +790,7 @@ public async Task Batch_GetOrAdd_policy_FactoryTimeout_cancels_slow_generator() return []; } - var call = ((ICache)Sut).GetOrAddAsync(entries, Generator, policy, token).AsTask(); + var call = ((ICache)Sut).GetOrAddAsync(entries, Generator, policy, caller.Token).AsTask(); var finished = await Task.WhenAny(call, Task.Delay(TimeSpan.FromSeconds(10), token)); finished.Should().BeSameAs(call, "the batch generator must be bounded by CachePolicy.FactoryTimeout"); diff --git a/tests/UiPath.Caching.Tests/ResiliencePipelineFactoryTests.cs b/tests/UiPath.Caching.Tests/ResiliencePipelineFactoryTests.cs index 938fb89..c547a69 100644 --- a/tests/UiPath.Caching.Tests/ResiliencePipelineFactoryTests.cs +++ b/tests/UiPath.Caching.Tests/ResiliencePipelineFactoryTests.cs @@ -135,7 +135,7 @@ public async Task Pipeline_works_as_expected() var resiliencePipelineFactory = _fixture.Create(); var pipeline = resiliencePipelineFactory.Create("read", false); using var guard = CancellationTokenSource.CreateLinkedTokenSource(testContextAccessor.Current.CancellationToken); - guard.CancelAfter(TimeSpan.FromSeconds(5)); + guard.CancelAfter(TimeSpan.FromSeconds(30)); var act = async () => await pipeline.ExecuteAsync(timeoutFunc, guard.Token); await act.Should().ThrowAsync(); @@ -156,20 +156,24 @@ public async Task Pipeline_works_as_expected() } } actual.Should().BeFalse(); - actual = null; - await Task.Delay(250, testContextAccessor.Current.CancellationToken); - for (int i = 0; i < 4; i++) + + // The breaker reopens after DurationOfBreak, half-opens, then closes on the first + // successful probe. Poll for that instead of assuming it lands inside a fixed 250ms + 4x100ms + // budget: that leaves only 150ms of slack over a 500ms DurationOfBreak, and under parallel + // load it does not land, which is what made this test flaky. + var closed = false; + var sw = System.Diagnostics.Stopwatch.StartNew(); + while (sw.Elapsed < TimeSpan.FromSeconds(30)) { - await Task.Delay(100, testContextAccessor.Current.CancellationToken); - //we are in a circuit breaker state => no exception is thrown, returning default value - actual = await pipeline.ExecuteAsync(successFunc, testContextAccessor.Current.CancellationToken); - if(actual == true) + // While the breaker is open the fallback returns default(bool) rather than throwing. + closed = await pipeline.ExecuteAsync(successFunc, testContextAccessor.Current.CancellationToken); + if (closed) { - // circuit breaker is closed break; } + await Task.Delay(20, testContextAccessor.Current.CancellationToken); } - actual.Should().BeTrue(); + closed.Should().BeTrue("the breaker must close once DurationOfBreak has elapsed"); logMessages.Should().NotBeEmpty(); logMessages.Should().Contain(log => log.Contains("Execution timed out after")); From 6285a198dfe80915b0a0eb2b9efff961cfc941ef Mon Sep 17 00:00:00 2001 From: Cosmin Staicu Date: Wed, 2 Sep 2026 21:15:20 +0300 Subject: [PATCH 5/9] refactor(cache)!: move the pre-CachePolicy overloads to extension methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ICache.Compat.cs, IHashCache.Compat.cs and ISetCache.Compat.cs carried 42 convenience overloads as default interface methods — GetAsync(key, token), SetAsync(key, value, expiration, token), TryAddAsync(key, value, token), AddAsync(key, item, token), PopAsync(key, token) and so on — each forwarding to the policy-bearing member with policy: null. They are now extension methods on CacheExtensions, HashCacheExtensions and SetCacheExtensions, in the same UiPath.Caching namespace, so no call site needed an edit. The interfaces shrink to just the policy-bearing members. An implementation now writes one member per operation instead of one plus an inherited forwarder it could accidentally override, and no implementation in the repo had declared the forwarders, so nothing had to be rewritten. CachePolicy? policy also becomes *required* on every member that takes one, along with the expiration / setOption parameters that precede it (C# forbids an optional parameter before a required one). This is what makes the extensions load-bearing rather than decorative: instance members always beat extension members in overload resolution, so while the interfaces still declared policy = null an applicable interface overload existed for every short call and the extensions were unreachable. With policy required there is exactly one way to spell each call, and an implementation no longer gets to declare its own default for "no policy". Implementations drop the defaults too — MultilayerCache, RedisCache, their hash counterparts, MultilayerSetCache, RedisSetCache, NullCache, NullHashCache, NullSetCache — so behavior is identical whether the call goes through the interface or the concrete type. The policy ??= DefaultPolicy bodies are untouched, so passing null still resolves the default exactly as before. The typed ICache / IHashCache / ISetCache facades are unchanged; they never had a policy parameter. Binary-breaking for external implementors: 127 entries leave PublicAPI.Shipped.txt across the two packages. Verified: Debug and Release builds clean (only the 8 pre-existing CS0618 warnings), 1498/1498 tests on net8.0 and net10.0. Every existing call site compiled unchanged — the only build errors at any point were RS0016/RS0017 baseline bookkeeping. Compile-probed all 23 short call shapes to confirm they bind to the extensions, and forced GenerateDocumentationFile on to confirm the inheritdoc crefs in the three new files resolve (doc generation is off in this repo, so bad crefs fail silently). Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_015RLSnsYiZsqeaHHYYybXW1 Signed-off-by: Cosmin Staicu --- CHANGELOG.md | 27 +++- docs/recipes/batch-get-or-add.md | 7 +- docs/reference/interfaces.md | 91 ++++++++------ .../CacheExtensions.cs | 71 +++++++++++ .../HashCacheExtensions.cs | 59 +++++++++ .../ICache.Compat.cs | 83 ------------ src/UiPath.Caching.Abstractions/ICache.cs | 48 +++---- .../IHashCache.Compat.cs | 71 ----------- src/UiPath.Caching.Abstractions/IHashCache.cs | 34 ++--- src/UiPath.Caching.Abstractions/NullCache.cs | 38 +++--- .../NullHashCache.cs | 32 ++--- .../PublicAPI.Shipped.txt | 99 --------------- .../PublicAPI.Unshipped.txt | 119 ++++++++++++++++-- src/UiPath.Caching.Queue/ISetCache.Compat.cs | 35 ------ src/UiPath.Caching.Queue/ISetCache.cs | 16 +-- .../MultilayerSetCache.cs | 14 +-- src/UiPath.Caching.Queue/NullSetCache.cs | 14 +-- .../PublicAPI.Shipped.txt | 28 ----- .../PublicAPI.Unshipped.txt | 29 +++++ src/UiPath.Caching.Queue/RedisSetCache.cs | 14 +-- .../SetCacheExtensions.cs | 39 ++++++ src/UiPath.Caching/MultilayerCache.cs | 44 +++---- src/UiPath.Caching/MultilayerHashCache.cs | 32 ++--- src/UiPath.Caching/Redis/RedisCache.cs | 44 +++---- src/UiPath.Caching/Redis/RedisHashCache.cs | 32 ++--- .../Fakes/DictionaryCache.cs | 38 +++--- 26 files changed, 591 insertions(+), 567 deletions(-) create mode 100644 src/UiPath.Caching.Abstractions/CacheExtensions.cs create mode 100644 src/UiPath.Caching.Abstractions/HashCacheExtensions.cs delete mode 100644 src/UiPath.Caching.Abstractions/ICache.Compat.cs delete mode 100644 src/UiPath.Caching.Abstractions/IHashCache.Compat.cs delete mode 100644 src/UiPath.Caching.Queue/ISetCache.Compat.cs create mode 100644 src/UiPath.Caching.Queue/SetCacheExtensions.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a16d38..16f188f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,7 +85,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) silently void the guarantee. A hand-written `ICache` / `ICache` implementation must override it before callers can use it — and `MultilayerCache` lets that `NotSupportedException` surface from an inner cache rather than reporting it as `false`, which would be indistinguishable from permanent - contention. `ICache.Compat.cs` carries the token-positional forwarders + contention. `CacheExtensions` carries the token-positional overloads (`TryAddAsync(key, value, token)` and the `TimeSpan?`/`DateTimeOffset?` pairs), matching `SetAsync`. No multi-key overload: Redis has no atomic multi-key `NX`, and all-or-nothing versus per-key semantics would be a guess. This is a cache primitive, not a lock — no ownership token, no early @@ -101,6 +101,31 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ### Changed +- **BREAKING:** `ICache.Compat.cs`, `IHashCache.Compat.cs` and `ISetCache.Compat.cs` are gone. The + pre-`CachePolicy` convenience overloads they carried as default interface methods — + `GetAsync(key, token)`, `SetAsync(key, value, expiration, token)`, + `TryAddAsync(key, value, token)`, `AddAsync(key, item, token)`, `PopAsync(key, token)` and + the rest — now live as extension methods on the new `CacheExtensions`, `HashCacheExtensions` and + `SetCacheExtensions` static classes in the same `UiPath.Caching` namespace (`SetCacheExtensions` + ships in `UiPath.Caching.Queue`, alongside `ISetCache`). Call sites are unchanged and need no edit; + the interfaces shrink to just the policy-bearing members, so an implementation now has one member to + write per operation instead of one plus an inherited forwarder it could accidentally override. +- **BREAKING:** `CachePolicy? policy` is now a **required** parameter on every `ICache`, + `IHashCache` and `ISetCache` member that takes one, along with the `expiration` / `setOption` + parameters that precede it — the `= null` defaults are removed. `CacheExtensions` / + `HashCacheExtensions` / `SetCacheExtensions` supply the short forms, so + `cache.GetAsync(key, token)` and `cache.SetAsync(key, value, expiration)` still + compile; what no longer compiles is an interface call that relied on the defaults to skip the policy + slot positionally. This is also what makes the extensions load-bearing rather than decorative: while + the interface still declared `policy = null`, an applicable interface overload existed for every + short call and instance members always beat extension members, so `cache.GetAsync(key, token)` + kept binding to the interface and the extension was never reached. With `policy` required, no + interface overload is applicable to the short forms and there is exactly one way to spell "no + policy". Implementations (`MultilayerCache`, `RedisCache`, their hash + counterparts, `NullCache`, `NullHashCache`, `MultilayerSetCache`, `RedisSetCache`, `NullSetCache`) + drop the defaults too, so behavior is identical whether the call goes through the interface or the + concrete type. The typed `ICache` / `IHashCache` / `ISetCache` façades are unchanged — + they never had a `policy` parameter, since they resolve one at construction. - **BREAKING:** `CacheKey.Equals` and `GetHashCode` are now ordinal rather than `InvariantCultureIgnoreCase`. Insensitive keys are still lowercased at construction, so the stored key and every comparison between insensitive keys are unchanged; what changes is that equality is diff --git a/docs/recipes/batch-get-or-add.md b/docs/recipes/batch-get-or-add.md index 54daccf..0c8b762 100644 --- a/docs/recipes/batch-get-or-add.md +++ b/docs/recipes/batch-get-or-add.md @@ -87,9 +87,10 @@ takes `CacheKey[]` directly and does this pairing for you — a caller reaching The generator signature is `Func[]>>` — it receives the states of the entries that missed and returns a pair per state it could resolve. -Parameter order on the call is `entries, generator, [expiration], policy, token`, and every parameter -after the generator is optional — the same shape as the single-key `GetOrAddAsync`. On `ICache` -there is no `policy` parameter at all, since `ICache` resolves one at construction. +Parameter order on the call is `entries, generator, [expiration], policy, token`, and only `token` is +optional — the same shape as the single-key `GetOrAddAsync`. Callers that do not want to pass a +`policy` use the `CacheExtensions` overloads instead. On `ICache` there is no `policy` parameter at +all, since `ICache` resolves one at construction. - **Return a pair for every state you can resolve.** A state you omit comes back as `default(T)` (i.e. `null` for a reference type) and is **not** cached, so the next call retries the source. That is diff --git a/docs/reference/interfaces.md b/docs/reference/interfaces.md index fbd585a..6ffc433 100644 --- a/docs/reference/interfaces.md +++ b/docs/reference/interfaces.md @@ -102,23 +102,23 @@ The multi-key `GetOrAddAsync` overloads pair each key with an opaque cal **Namespace:** `UiPath.Caching` ```csharp -public partial interface ICache : IDisposable +public interface ICache : IDisposable { string Name { get; } - ValueTask GetAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default); + ValueTask GetAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default); - ValueTask[]> GetAsync(CacheKey[] cacheKeys, CachePolicy? policy = null, CancellationToken token = default); + ValueTask[]> GetAsync(CacheKey[] cacheKeys, CachePolicy? policy, CancellationToken token = default); - ValueTask> GetCacheEntryAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default); + ValueTask> GetCacheEntryAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default); - ValueTask>[]> GetCacheEntriesAsync(CacheKey[] cacheKeys, CachePolicy? policy = null, CancellationToken token = default); + ValueTask>[]> GetCacheEntriesAsync(CacheKey[] cacheKeys, CachePolicy? policy, CancellationToken token = default); - ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, CachePolicy? policy = null, CancellationToken token = default); + ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, CachePolicy? policy, CancellationToken token = default); - ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default); + ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default); + ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default); ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, CachePolicy? policy, CancellationToken token = default) where TState : notnull @@ -136,29 +136,29 @@ public partial interface ICache : IDisposable ValueTask RemoveAsync(CacheKey[] cacheKey, CancellationToken token = default); - ValueTask SetAsync(CacheKey cacheKey, T? value, CachePolicy? policy = null, CancellationToken token = default); + ValueTask SetAsync(CacheKey cacheKey, T? value, CachePolicy? policy, CancellationToken token = default); - ValueTask SetAsync(CacheKey cacheKey, T? value, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default); + ValueTask SetAsync(CacheKey cacheKey, T? value, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask SetAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default); + ValueTask SetAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask SetAsync(KeyValuePair[] keyValues, CachePolicy? policy = null, CancellationToken token = default); + ValueTask SetAsync(KeyValuePair[] keyValues, CachePolicy? policy, CancellationToken token = default); - ValueTask SetAsync(KeyValuePair[] keyValues, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default); + ValueTask SetAsync(KeyValuePair[] keyValues, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default); + ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask TryAddAsync(CacheKey cacheKey, T? value, CachePolicy? policy = null, CancellationToken token = default); + ValueTask TryAddAsync(CacheKey cacheKey, T? value, CachePolicy? policy, CancellationToken token = default); - ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default); + ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default); + ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask RefreshAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default); + ValueTask RefreshAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default); - ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default); + ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default); + ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default); ValueTask ContainsAsync(CacheKey cacheKey, CancellationToken token = default); @@ -170,7 +170,20 @@ public partial interface ICache : IDisposable `ICache` is the dynamic-key, dynamic-type cache surface. Unlike `ICache`, the value type is specified as a generic type argument on each method call rather than fixed at cache-creation time, and a `CachePolicy` can be supplied per call rather than resolved by `typeof(T).FullName`. It also exposes `GetCacheEntryAsync` for callers that need cache-entry metadata (hit/miss status, expiration) in addition to the value. `ICache` implements `IDisposable`, but instances returned by `ICacheFactory.CreateCache(...)` are provider-owned (typically singletons resolved through a `Lazy<>`); their lifetime is managed by the provider and the DI container, so callers should not dispose them per use. -The multi-key `GetOrAddAsync` overloads shown above pair each key with an opaque caller state (`TState`) and are **default interface methods** — each forwards to the shared `BatchGetOrAdd.RunAsync` machinery, so existing `ICache` implementations keep compiling without adding them. The generator is invoked at most once, only with the states of the entries that missed every cache layer, never with keys; results come back keyed by state, one entry per distinct requested state in first-occurrence order. Cache operations de-duplicate by `CacheKey`; results de-duplicate by state — when two states share a key the generator is asked once and the value is reported under both. Their parameter shape matches the single-key `GetOrAddAsync` exactly — `expiration`, `policy` and `token` all optional. `ICache.Compat.cs` carries no token-positional forwarder for them: that file is pre-`CachePolicy` back-compat sugar, and this API predates nothing. There is no key-only convenience overload; a caller whose keys are their own identity pairs each key with itself (`TState = CacheKey`). +Every policy-bearing member takes `policy` as a **required** parameter — there is no `= null` default on the interface. Call sites that do not want a per-call policy use the `CacheExtensions` overloads instead, which omit `policy` (and, where the interface pairs the two, `expiration`) and forward with `policy: null`: + +```csharp +// Interface: policy is explicit. +await cache.GetAsync(key, policy, token); + +// CacheExtensions: the same call without a policy. +await cache.GetAsync(key, token); +await cache.SetAsync(key, order, TimeSpan.FromMinutes(5), token); +``` + +Two reasons the interface is the strict surface. An implementation cannot silently disagree about what "no policy" means, because it never gets to declare a default. And the extensions only work if the interface is strict: instance members always beat extension members in overload resolution, so while the interface declared `policy = null` an applicable interface overload existed for every short call and the extension was never reached. With `policy` required there is exactly one way to spell each call. The extensions are pure forwarders with no behavior of their own, which is why they carry `[ExcludeFromCodeCoverage]` — the policy-bearing implementations are what the tests exercise. + +The multi-key `GetOrAddAsync` overloads shown above pair each key with an opaque caller state (`TState`) and are **default interface methods** — each forwards to the shared `BatchGetOrAdd.RunAsync` machinery, so existing `ICache` implementations keep compiling without adding them. The generator is invoked at most once, only with the states of the entries that missed every cache layer, never with keys; results come back keyed by state, one entry per distinct requested state in first-occurrence order. Cache operations de-duplicate by `CacheKey`; results de-duplicate by state — when two states share a key the generator is asked once and the value is reported under both. Their parameter shape matches the single-key `GetOrAddAsync` exactly — `expiration` and `policy` required, `token` optional. `CacheExtensions` carries no token-positional forwarder for them: those extensions are pre-`CachePolicy` back-compat sugar, and this API predates nothing. There is no key-only convenience overload; a caller whose keys are their own identity pairs each key with itself (`TState = CacheKey`). `TryAddAsync` writes only if the key is absent — StackExchange.Redis `When.NotExists` (`SET … NX`) on Redis-backed caches, in one atomic command with the TTL applied by the same write. Four points decide whether it fits your problem: @@ -298,41 +311,41 @@ public partial interface IHashCache **Namespace:** `UiPath.Caching` ```csharp -public partial interface IHashCache : IDisposable +public interface IHashCache : IDisposable { string Name { get; } - ValueTask GetItemAsync(CacheKey cacheKey, string field, CachePolicy? policy = null, CancellationToken token = default); + ValueTask GetItemAsync(CacheKey cacheKey, string field, CachePolicy? policy, CancellationToken token = default); - ValueTask> GetAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default); + ValueTask> GetAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default); - ValueTask> GetAsync(CacheKey cacheKey, string[] fields, CachePolicy? policy = null, CancellationToken token = default); + ValueTask> GetAsync(CacheKey cacheKey, string[] fields, CachePolicy? policy, CancellationToken token = default); - ValueTask>> GetCacheEntryAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default); + ValueTask>> GetCacheEntryAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default); - ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, CachePolicy? policy = null, CancellationToken token = default); + ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, CachePolicy? policy, CancellationToken token = default); - ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default); + ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default); + ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration = null, HashCacheSetOption? setOption = null, CachePolicy? policy = null, CancellationToken token = default); + ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration, HashCacheSetOption? setOption, CachePolicy? policy, CancellationToken token = default); - ValueTask SetAsync(CacheKey cacheKey, IDictionary values, CachePolicy? policy = null, CancellationToken token = default); + ValueTask SetAsync(CacheKey cacheKey, IDictionary values, CachePolicy? policy, CancellationToken token = default); - ValueTask SetAsync(CacheKey cacheKey, IDictionary values, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default); + ValueTask SetAsync(CacheKey cacheKey, IDictionary values, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask SetAsync(CacheKey cacheKey, IDictionary values, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default); + ValueTask SetAsync(CacheKey cacheKey, IDictionary values, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask SetAsync(CacheKey cacheKey, IDictionary values, HashCacheEntryOptions options, CachePolicy? policy = null, CancellationToken token = default); + ValueTask SetAsync(CacheKey cacheKey, IDictionary values, HashCacheEntryOptions options, CachePolicy? policy, CancellationToken token = default); - ValueTask RefreshAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default); + ValueTask RefreshAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default); - ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default); + ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default); + ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask RefreshAsync(CacheKey cacheKey, HashCacheEntryOptions options, CachePolicy? policy = null, CancellationToken token = default); + ValueTask RefreshAsync(CacheKey cacheKey, HashCacheEntryOptions options, CachePolicy? policy, CancellationToken token = default); ValueTask RemoveAsync(CacheKey cacheKey, CancellationToken token = default); @@ -350,6 +363,8 @@ public partial interface IHashCache : IDisposable `IHashCache` is the dynamic-type hash-cache surface. Like [`ICache`](#icache), the value type is specified per method call as a generic type argument, and a `CachePolicy` may be supplied at call time. It extends hash semantics with `GetCacheEntryAsync` for cache-entry metadata inspection. The extra `GetOrAddAsync` overload that accepts `HashCacheSetOption` enables conditional-set semantics (e.g. set-if-not-exists) at the call site. `IHashCache` implements `IDisposable`, but instances returned by `ICacheFactory.CreateHashCache(...)` are provider-owned (typically singletons resolved through a `Lazy<>`); their lifetime is managed by the provider and the DI container, so callers should not dispose them per use. +As on [`ICache`](#icache), `policy` is a **required** parameter on every policy-bearing member; `HashCacheExtensions` supplies the no-policy overloads and forwards with `policy: null`. + > **Typical vs. power-user surface:** `IHashCache` is the power-user hash surface. For most application code with a fixed value type, prefer [`IHashCache`](#ihashcachet) for compile-time safety and automatic policy resolution. The two surfaces are different shapes for different problems. **Use this when:** diff --git a/src/UiPath.Caching.Abstractions/CacheExtensions.cs b/src/UiPath.Caching.Abstractions/CacheExtensions.cs new file mode 100644 index 0000000..e2195b3 --- /dev/null +++ b/src/UiPath.Caching.Abstractions/CacheExtensions.cs @@ -0,0 +1,71 @@ +namespace UiPath.Caching; + +/// +/// Source-compatibility overloads for pre-CachePolicy call sites. Each forwards to the +/// policy-bearing member with policy: null. +/// +// Excluded from coverage — forwarders with no behavior of their own; the policy-bearing impls +// are what tests exercise. +[ExcludeFromCodeCoverage] +public static class CacheExtensions +{ + public static ValueTask GetAsync(this ICache cache, CacheKey cacheKey, CancellationToken token = default) + => cache.GetAsync(cacheKey, null, token); + + public static ValueTask[]> GetAsync(this ICache cache, CacheKey[] cacheKeys, CancellationToken token = default) + => cache.GetAsync(cacheKeys, null, token); + + public static ValueTask> GetCacheEntryAsync(this ICache cache, CacheKey cacheKey, CancellationToken token = default) + => cache.GetCacheEntryAsync(cacheKey, null, token); + + public static ValueTask>[]> GetCacheEntriesAsync(this ICache cache, CacheKey[] cacheKeys, CancellationToken token = default) + => cache.GetCacheEntriesAsync(cacheKeys, null, token); + + public static ValueTask GetOrAddAsync(this ICache cache, CacheKey cacheKey, Func> generator, CancellationToken token = default) + => cache.GetOrAddAsync(cacheKey, generator, (CachePolicy?)null, token); + + public static ValueTask GetOrAddAsync(this ICache cache, CacheKey cacheKey, Func> generator, TimeSpan? expiration, CancellationToken token = default) + => cache.GetOrAddAsync(cacheKey, generator, expiration, null, token); + + public static ValueTask GetOrAddAsync(this ICache cache, CacheKey cacheKey, Func> generator, DateTimeOffset? expiration, CancellationToken token = default) + => cache.GetOrAddAsync(cacheKey, generator, expiration, null, token); + + public static ValueTask SetAsync(this ICache cache, CacheKey cacheKey, T? value, CancellationToken token = default) + => cache.SetAsync(cacheKey, value, (CachePolicy?)null, token); + + public static ValueTask SetAsync(this ICache cache, CacheKey cacheKey, T? value, TimeSpan? expiration, CancellationToken token = default) + => cache.SetAsync(cacheKey, value, expiration, null, token); + + public static ValueTask SetAsync(this ICache cache, CacheKey cacheKey, T? value, DateTimeOffset? expiration, CancellationToken token = default) + => cache.SetAsync(cacheKey, value, expiration, null, token); + + public static ValueTask SetAsync(this ICache cache, KeyValuePair[] keyValues, CancellationToken token = default) + => cache.SetAsync(keyValues, (CachePolicy?)null, token); + + public static ValueTask SetAsync(this ICache cache, KeyValuePair[] keyValues, TimeSpan? expiration, CancellationToken token = default) + => cache.SetAsync(keyValues, expiration, null, token); + + public static ValueTask SetAsync(this ICache cache, KeyValuePair[] keyValues, DateTimeOffset? expiration, CancellationToken token = default) + => cache.SetAsync(keyValues, expiration, null, token); + + /// + public static ValueTask TryAddAsync(this ICache cache, CacheKey cacheKey, T? value, CancellationToken token = default) + => cache.TryAddAsync(cacheKey, value, (CachePolicy?)null, token); + + /// + public static ValueTask TryAddAsync(this ICache cache, CacheKey cacheKey, T? value, TimeSpan? expiration, CancellationToken token = default) + => cache.TryAddAsync(cacheKey, value, expiration, null, token); + + /// + public static ValueTask TryAddAsync(this ICache cache, CacheKey cacheKey, T? value, DateTimeOffset? expiration, CancellationToken token = default) + => cache.TryAddAsync(cacheKey, value, expiration, null, token); + + public static ValueTask RefreshAsync(this ICache cache, CacheKey cacheKey, CancellationToken token = default) + => cache.RefreshAsync(cacheKey, (CachePolicy?)null, token); + + public static ValueTask RefreshAsync(this ICache cache, CacheKey cacheKey, TimeSpan? expiration, CancellationToken token = default) + => cache.RefreshAsync(cacheKey, expiration, null, token); + + public static ValueTask RefreshAsync(this ICache cache, CacheKey cacheKey, DateTimeOffset? expiration, CancellationToken token = default) + => cache.RefreshAsync(cacheKey, expiration, null, token); +} diff --git a/src/UiPath.Caching.Abstractions/HashCacheExtensions.cs b/src/UiPath.Caching.Abstractions/HashCacheExtensions.cs new file mode 100644 index 0000000..cdd4c07 --- /dev/null +++ b/src/UiPath.Caching.Abstractions/HashCacheExtensions.cs @@ -0,0 +1,59 @@ +namespace UiPath.Caching; + +/// +/// Source-compatibility overloads for pre-CachePolicy call sites. Each forwards to the +/// policy-bearing member with policy: null. +/// +// Excluded from coverage — forwarders with no behavior of their own; the policy-bearing impls +// are what tests exercise. +[ExcludeFromCodeCoverage] +public static class HashCacheExtensions +{ + public static ValueTask GetItemAsync(this IHashCache cache, CacheKey cacheKey, string field, CancellationToken token = default) + => cache.GetItemAsync(cacheKey, field, null, token); + + public static ValueTask> GetAsync(this IHashCache cache, CacheKey cacheKey, CancellationToken token = default) + => cache.GetAsync(cacheKey, (CachePolicy?)null, token); + + public static ValueTask> GetAsync(this IHashCache cache, CacheKey cacheKey, string[] fields, CancellationToken token = default) + => cache.GetAsync(cacheKey, fields, null, token); + + public static ValueTask>> GetCacheEntryAsync(this IHashCache cache, CacheKey cacheKey, CancellationToken token = default) + => cache.GetCacheEntryAsync(cacheKey, null, token); + + public static ValueTask> GetOrAddAsync(this IHashCache cache, CacheKey cacheKey, Func>> generator, CancellationToken token = default) + => cache.GetOrAddAsync(cacheKey, generator, (CachePolicy?)null, token); + + public static ValueTask> GetOrAddAsync(this IHashCache cache, CacheKey cacheKey, Func>> generator, TimeSpan? expiration, CancellationToken token = default) + => cache.GetOrAddAsync(cacheKey, generator, expiration, null, token); + + public static ValueTask> GetOrAddAsync(this IHashCache cache, CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration, CancellationToken token = default) + => cache.GetOrAddAsync(cacheKey, generator, expiration, (CachePolicy?)null, token); + + public static ValueTask> GetOrAddAsync(this IHashCache cache, CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration, HashCacheSetOption? setOption, CancellationToken token = default) + => cache.GetOrAddAsync(cacheKey, generator, expiration, setOption, null, token); + + public static ValueTask SetAsync(this IHashCache cache, CacheKey cacheKey, IDictionary values, CancellationToken token = default) + => cache.SetAsync(cacheKey, values, (CachePolicy?)null, token); + + public static ValueTask SetAsync(this IHashCache cache, CacheKey cacheKey, IDictionary values, TimeSpan? expiration, CancellationToken token = default) + => cache.SetAsync(cacheKey, values, expiration, null, token); + + public static ValueTask SetAsync(this IHashCache cache, CacheKey cacheKey, IDictionary values, DateTimeOffset? expiration, CancellationToken token = default) + => cache.SetAsync(cacheKey, values, expiration, null, token); + + public static ValueTask SetAsync(this IHashCache cache, CacheKey cacheKey, IDictionary values, HashCacheEntryOptions options, CancellationToken token = default) + => cache.SetAsync(cacheKey, values, options, null, token); + + public static ValueTask RefreshAsync(this IHashCache cache, CacheKey cacheKey, CancellationToken token = default) + => cache.RefreshAsync(cacheKey, (CachePolicy?)null, token); + + public static ValueTask RefreshAsync(this IHashCache cache, CacheKey cacheKey, TimeSpan? expiration, CancellationToken token = default) + => cache.RefreshAsync(cacheKey, expiration, null, token); + + public static ValueTask RefreshAsync(this IHashCache cache, CacheKey cacheKey, DateTimeOffset? expiration, CancellationToken token = default) + => cache.RefreshAsync(cacheKey, expiration, null, token); + + public static ValueTask RefreshAsync(this IHashCache cache, CacheKey cacheKey, HashCacheEntryOptions options, CancellationToken token = default) + => cache.RefreshAsync(cacheKey, options, null, token); +} diff --git a/src/UiPath.Caching.Abstractions/ICache.Compat.cs b/src/UiPath.Caching.Abstractions/ICache.Compat.cs deleted file mode 100644 index 588b5f3..0000000 --- a/src/UiPath.Caching.Abstractions/ICache.Compat.cs +++ /dev/null @@ -1,83 +0,0 @@ -namespace UiPath.Caching; - -// Source-compatibility default interface methods for pre-CachePolicy call sites. Each forwards -// to the policy-bearing overload with policy=null. Excluded from coverage — these are forwarders -// with no behavior of their own; the policy-bearing impls are what tests exercise. -public partial interface ICache -{ - [ExcludeFromCodeCoverage] - ValueTask GetAsync(CacheKey cacheKey, CancellationToken token = default) - => GetAsync(cacheKey, null, token); - - [ExcludeFromCodeCoverage] - ValueTask[]> GetAsync(CacheKey[] cacheKeys, CancellationToken token = default) - => GetAsync(cacheKeys, null, token); - - [ExcludeFromCodeCoverage] - ValueTask> GetCacheEntryAsync(CacheKey cacheKey, CancellationToken token = default) - => GetCacheEntryAsync(cacheKey, null, token); - - [ExcludeFromCodeCoverage] - ValueTask>[]> GetCacheEntriesAsync(CacheKey[] cacheKeys, CancellationToken token = default) - => GetCacheEntriesAsync(cacheKeys, null, token); - - [ExcludeFromCodeCoverage] - ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, CancellationToken token = default) - => GetOrAddAsync(cacheKey, generator, (CachePolicy?)null, token); - - [ExcludeFromCodeCoverage] - ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, TimeSpan? expiration, CancellationToken token = default) - => GetOrAddAsync(cacheKey, generator, expiration, null, token); - - [ExcludeFromCodeCoverage] - ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, DateTimeOffset? expiration, CancellationToken token = default) - => GetOrAddAsync(cacheKey, generator, expiration, null, token); - - [ExcludeFromCodeCoverage] - ValueTask SetAsync(CacheKey cacheKey, T? value, CancellationToken token = default) - => SetAsync(cacheKey, value, (CachePolicy?)null, token); - - [ExcludeFromCodeCoverage] - ValueTask SetAsync(CacheKey cacheKey, T? value, TimeSpan? expiration, CancellationToken token = default) - => SetAsync(cacheKey, value, expiration, null, token); - - [ExcludeFromCodeCoverage] - ValueTask SetAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CancellationToken token = default) - => SetAsync(cacheKey, value, expiration, null, token); - - [ExcludeFromCodeCoverage] - ValueTask SetAsync(KeyValuePair[] keyValues, CancellationToken token = default) - => SetAsync(keyValues, (CachePolicy?)null, token); - - [ExcludeFromCodeCoverage] - ValueTask SetAsync(KeyValuePair[] keyValues, TimeSpan? expiration, CancellationToken token = default) - => SetAsync(keyValues, expiration, null, token); - - [ExcludeFromCodeCoverage] - ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset? expiration, CancellationToken token = default) - => SetAsync(keyValues, expiration, null, token); - - [ExcludeFromCodeCoverage] - ValueTask TryAddAsync(CacheKey cacheKey, T? value, CancellationToken token = default) - => TryAddAsync(cacheKey, value, (CachePolicy?)null, token); - - [ExcludeFromCodeCoverage] - ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan? expiration, CancellationToken token = default) - => TryAddAsync(cacheKey, value, expiration, null, token); - - [ExcludeFromCodeCoverage] - ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CancellationToken token = default) - => TryAddAsync(cacheKey, value, expiration, null, token); - - [ExcludeFromCodeCoverage] - ValueTask RefreshAsync(CacheKey cacheKey, CancellationToken token = default) - => RefreshAsync(cacheKey, (CachePolicy?)null, token); - - [ExcludeFromCodeCoverage] - ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration, CancellationToken token = default) - => RefreshAsync(cacheKey, expiration, null, token); - - [ExcludeFromCodeCoverage] - ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration, CancellationToken token = default) - => RefreshAsync(cacheKey, expiration, null, token); -} diff --git a/src/UiPath.Caching.Abstractions/ICache.cs b/src/UiPath.Caching.Abstractions/ICache.cs index 6e73ca6..ff71b90 100644 --- a/src/UiPath.Caching.Abstractions/ICache.cs +++ b/src/UiPath.Caching.Abstractions/ICache.cs @@ -1,32 +1,32 @@ -namespace UiPath.Caching; +namespace UiPath.Caching; -public partial interface ICache : IDisposable +public interface ICache : IDisposable { string Name { get; } - ValueTask GetAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default); + ValueTask GetAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default); - ValueTask[]> GetAsync(CacheKey[] cacheKeys, CachePolicy? policy = null, CancellationToken token = default); + ValueTask[]> GetAsync(CacheKey[] cacheKeys, CachePolicy? policy, CancellationToken token = default); - ValueTask> GetCacheEntryAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default); + ValueTask> GetCacheEntryAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default); - ValueTask>[]> GetCacheEntriesAsync(CacheKey[] cacheKeys, CachePolicy? policy = null, CancellationToken token = default); + ValueTask>[]> GetCacheEntriesAsync(CacheKey[] cacheKeys, CachePolicy? policy, CancellationToken token = default); - ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, CachePolicy? policy = null, CancellationToken token = default); + ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, CachePolicy? policy, CancellationToken token = default); - ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default); + ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default); + ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, CachePolicy? policy = null, CancellationToken token = default) + ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, CachePolicy? policy, CancellationToken token = default) where TState : notnull => BatchGetOrAdd.RunAsync(this, entries, generator, (pairs, t) => SetAsync(pairs, policy, t), policy, token); - ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) where TState : notnull => BatchGetOrAdd.RunAsync(this, entries, generator, (pairs, t) => SetAsync(pairs, expiration, policy, t), policy, token); - ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) where TState : notnull => BatchGetOrAdd.RunAsync(this, entries, generator, (pairs, t) => SetAsync(pairs, expiration, policy, t), policy, token); @@ -34,17 +34,17 @@ public partial interface ICache : IDisposable ValueTask RemoveAsync(CacheKey[] cacheKey, CancellationToken token = default); - ValueTask SetAsync(CacheKey cacheKey, T? value, CachePolicy? policy = null, CancellationToken token = default); + ValueTask SetAsync(CacheKey cacheKey, T? value, CachePolicy? policy, CancellationToken token = default); - ValueTask SetAsync(CacheKey cacheKey, T? value, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default); + ValueTask SetAsync(CacheKey cacheKey, T? value, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask SetAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default); + ValueTask SetAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask SetAsync(KeyValuePair[] keyValues, CachePolicy? policy = null, CancellationToken token = default); + ValueTask SetAsync(KeyValuePair[] keyValues, CachePolicy? policy, CancellationToken token = default); - ValueTask SetAsync(KeyValuePair[] keyValues, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default); + ValueTask SetAsync(KeyValuePair[] keyValues, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default); + ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default); /// /// Conditional add: writes only if is @@ -56,7 +56,7 @@ public partial interface ICache : IDisposable /// fail-closed. Never deletes. Not a lock: no ownership token, no release. See /// docs/recipes/conditional-add.md for the full contract and the per-provider table. /// - ValueTask TryAddAsync(CacheKey cacheKey, T? value, CachePolicy? policy = null, CancellationToken token = default) + ValueTask TryAddAsync(CacheKey cacheKey, T? value, CachePolicy? policy, CancellationToken token = default) => throw ConditionalAdd.NotSupported(Name, GetType().FullName ?? nameof(ICache)); @@ -65,18 +65,18 @@ ValueTask TryAddAsync(CacheKey cacheKey, T? value, CachePolicy? policy /// Lifetime of the entry if it is created, applied by the same atomic command. Falls back to /// CachePolicy.DistributedExpiration then the cache default. Not in the future: no-op. /// - ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) => throw ConditionalAdd.NotSupported(Name, GetType().FullName ?? nameof(ICache)); /// - ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) => throw ConditionalAdd.NotSupported(Name, GetType().FullName ?? nameof(ICache)); - ValueTask RefreshAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default); + ValueTask RefreshAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default); - ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default); + ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default); + ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default); ValueTask ContainsAsync(CacheKey cacheKey, CancellationToken token = default); diff --git a/src/UiPath.Caching.Abstractions/IHashCache.Compat.cs b/src/UiPath.Caching.Abstractions/IHashCache.Compat.cs deleted file mode 100644 index 3a8285c..0000000 --- a/src/UiPath.Caching.Abstractions/IHashCache.Compat.cs +++ /dev/null @@ -1,71 +0,0 @@ -namespace UiPath.Caching; - -// Source-compatibility default interface methods for pre-CachePolicy call sites. Each forwards -// to the policy-bearing overload with policy=null. Excluded from coverage — these are forwarders -// with no behavior of their own; the policy-bearing impls are what tests exercise. -public partial interface IHashCache -{ - [ExcludeFromCodeCoverage] - ValueTask GetItemAsync(CacheKey cacheKey, string field, CancellationToken token = default) - => GetItemAsync(cacheKey, field, null, token); - - [ExcludeFromCodeCoverage] - ValueTask> GetAsync(CacheKey cacheKey, CancellationToken token = default) - => GetAsync(cacheKey, (CachePolicy?)null, token); - - [ExcludeFromCodeCoverage] - ValueTask> GetAsync(CacheKey cacheKey, string[] fields, CancellationToken token = default) - => GetAsync(cacheKey, fields, null, token); - - [ExcludeFromCodeCoverage] - ValueTask>> GetCacheEntryAsync(CacheKey cacheKey, CancellationToken token = default) - => GetCacheEntryAsync(cacheKey, null, token); - - [ExcludeFromCodeCoverage] - ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, CancellationToken token = default) - => GetOrAddAsync(cacheKey, generator, (CachePolicy?)null, token); - - [ExcludeFromCodeCoverage] - ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, TimeSpan? expiration, CancellationToken token = default) - => GetOrAddAsync(cacheKey, generator, expiration, null, token); - - [ExcludeFromCodeCoverage] - ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration, CancellationToken token = default) - => GetOrAddAsync(cacheKey, generator, expiration, (CachePolicy?)null, token); - - [ExcludeFromCodeCoverage] - ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration, HashCacheSetOption? setOption, CancellationToken token = default) - => GetOrAddAsync(cacheKey, generator, expiration, setOption, null, token); - - [ExcludeFromCodeCoverage] - ValueTask SetAsync(CacheKey cacheKey, IDictionary values, CancellationToken token = default) - => SetAsync(cacheKey, values, (CachePolicy?)null, token); - - [ExcludeFromCodeCoverage] - ValueTask SetAsync(CacheKey cacheKey, IDictionary values, TimeSpan? expiration, CancellationToken token = default) - => SetAsync(cacheKey, values, expiration, null, token); - - [ExcludeFromCodeCoverage] - ValueTask SetAsync(CacheKey cacheKey, IDictionary values, DateTimeOffset? expiration, CancellationToken token = default) - => SetAsync(cacheKey, values, expiration, null, token); - - [ExcludeFromCodeCoverage] - ValueTask SetAsync(CacheKey cacheKey, IDictionary values, HashCacheEntryOptions options, CancellationToken token = default) - => SetAsync(cacheKey, values, options, null, token); - - [ExcludeFromCodeCoverage] - ValueTask RefreshAsync(CacheKey cacheKey, CancellationToken token = default) - => RefreshAsync(cacheKey, (CachePolicy?)null, token); - - [ExcludeFromCodeCoverage] - ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration, CancellationToken token = default) - => RefreshAsync(cacheKey, expiration, null, token); - - [ExcludeFromCodeCoverage] - ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration, CancellationToken token = default) - => RefreshAsync(cacheKey, expiration, null, token); - - [ExcludeFromCodeCoverage] - ValueTask RefreshAsync(CacheKey cacheKey, HashCacheEntryOptions options, CancellationToken token = default) - => RefreshAsync(cacheKey, options, null, token); -} diff --git a/src/UiPath.Caching.Abstractions/IHashCache.cs b/src/UiPath.Caching.Abstractions/IHashCache.cs index 13506fa..3819040 100644 --- a/src/UiPath.Caching.Abstractions/IHashCache.cs +++ b/src/UiPath.Caching.Abstractions/IHashCache.cs @@ -1,40 +1,40 @@ namespace UiPath.Caching; -public partial interface IHashCache : IDisposable +public interface IHashCache : IDisposable { string Name { get; } - ValueTask GetItemAsync(CacheKey cacheKey, string field, CachePolicy? policy = null, CancellationToken token = default); + ValueTask GetItemAsync(CacheKey cacheKey, string field, CachePolicy? policy, CancellationToken token = default); - ValueTask> GetAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default); + ValueTask> GetAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default); - ValueTask> GetAsync(CacheKey cacheKey, string[] fields, CachePolicy? policy = null, CancellationToken token = default); + ValueTask> GetAsync(CacheKey cacheKey, string[] fields, CachePolicy? policy, CancellationToken token = default); - ValueTask>> GetCacheEntryAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default); + ValueTask>> GetCacheEntryAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default); - ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, CachePolicy? policy = null, CancellationToken token = default); + ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, CachePolicy? policy, CancellationToken token = default); - ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default); + ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default); + ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration = null, HashCacheSetOption? setOption = null, CachePolicy? policy = null, CancellationToken token = default); + ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration, HashCacheSetOption? setOption, CachePolicy? policy, CancellationToken token = default); - ValueTask SetAsync(CacheKey cacheKey, IDictionary values, CachePolicy? policy = null, CancellationToken token = default); + ValueTask SetAsync(CacheKey cacheKey, IDictionary values, CachePolicy? policy, CancellationToken token = default); - ValueTask SetAsync(CacheKey cacheKey, IDictionary values, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default); + ValueTask SetAsync(CacheKey cacheKey, IDictionary values, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask SetAsync(CacheKey cacheKey, IDictionary values, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default); + ValueTask SetAsync(CacheKey cacheKey, IDictionary values, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask SetAsync(CacheKey cacheKey, IDictionary values, HashCacheEntryOptions options, CachePolicy? policy = null, CancellationToken token = default); + ValueTask SetAsync(CacheKey cacheKey, IDictionary values, HashCacheEntryOptions options, CachePolicy? policy, CancellationToken token = default); - ValueTask RefreshAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default); + ValueTask RefreshAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default); - ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default); + ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default); + ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask RefreshAsync(CacheKey cacheKey, HashCacheEntryOptions options, CachePolicy? policy = null, CancellationToken token = default); + ValueTask RefreshAsync(CacheKey cacheKey, HashCacheEntryOptions options, CachePolicy? policy, CancellationToken token = default); ValueTask RemoveAsync(CacheKey cacheKey, CancellationToken token = default); diff --git a/src/UiPath.Caching.Abstractions/NullCache.cs b/src/UiPath.Caching.Abstractions/NullCache.cs index d7a6cb2..f518028 100644 --- a/src/UiPath.Caching.Abstractions/NullCache.cs +++ b/src/UiPath.Caching.Abstractions/NullCache.cs @@ -19,25 +19,25 @@ public ValueTask ContainsAsync(CacheKey cacheKey, CancellationToken tok return ValueTask.FromResult(default(DateTimeOffset?)); } - public ValueTask GetAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask GetAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); return ValueTask.FromResult(default(T?)); } - public ValueTask[]> GetAsync(CacheKey[] cacheKeys, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask[]> GetAsync(CacheKey[] cacheKeys, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); return ValueTask.FromResult(cacheKeys.Select(k => new KeyValuePair(k, default(T?))).ToArray()); } - public ValueTask> GetCacheEntryAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask> GetCacheEntryAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); return ValueTask.FromResult(NullCacheEntry.Instance); } - public ValueTask>[]> GetCacheEntriesAsync(CacheKey[] cacheKeys, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask>[]> GetCacheEntriesAsync(CacheKey[] cacheKeys, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); return ValueTask.FromResult(cacheKeys @@ -45,36 +45,36 @@ public ValueTask ContainsAsync(CacheKey cacheKey, CancellationToken tok .ToArray()); } - public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, CachePolicy? policy = null, CancellationToken token = default) => + public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, CachePolicy? policy, CancellationToken token = default) => ReturnGeneratorAsync(generator, token); - public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => + public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) => ReturnGeneratorAsync(generator, token); - public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => + public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) => ReturnGeneratorAsync(generator, token); - public ValueTask RefreshAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default) => ReturnTrueAsync(); + public ValueTask RefreshAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); - public ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => ReturnTrueAsync(); + public ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); - public ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => ReturnTrueAsync(); + public ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); public ValueTask RemoveAsync(CacheKey cacheKey, CancellationToken token = default) => ReturnTrueAsync(); public ValueTask RemoveAsync(CacheKey[] cacheKey, CancellationToken token = default) => ReturnTrueAsync(); - public ValueTask SetAsync(CacheKey cacheKey, T? value, CachePolicy? policy = null, CancellationToken token = default) => ReturnTrueAsync(); + public ValueTask SetAsync(CacheKey cacheKey, T? value, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); - public ValueTask SetAsync(CacheKey cacheKey, T? value, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => ReturnTrueAsync(); + public ValueTask SetAsync(CacheKey cacheKey, T? value, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); - public ValueTask SetAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => ReturnTrueAsync(); + public ValueTask SetAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); - public ValueTask SetAsync(KeyValuePair[] keyValues, CachePolicy? policy = null, CancellationToken token = default) => ReturnTrueAsync(); + public ValueTask SetAsync(KeyValuePair[] keyValues, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); - public ValueTask SetAsync(KeyValuePair[] keyValues, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => ReturnTrueAsync(); + public ValueTask SetAsync(KeyValuePair[] keyValues, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); - public ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => ReturnTrueAsync(); + public ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); /// /// Always false: retaining nothing, this store cannot arbitrate a conditional add, and @@ -82,13 +82,13 @@ public ValueTask ContainsAsync(CacheKey cacheKey, CancellationToken tok /// does not degrade to "caching is off, carry on", because this type is reached by accident — /// ICacheFactory.CreateCache resolves to it for an absent or disabled provider. /// - public ValueTask TryAddAsync(CacheKey cacheKey, T? value, CachePolicy? policy = null, CancellationToken token = default) => ReturnFalseAsync(); + public ValueTask TryAddAsync(CacheKey cacheKey, T? value, CachePolicy? policy, CancellationToken token = default) => ReturnFalseAsync(); /// - public ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => ReturnFalseAsync(); + public ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) => ReturnFalseAsync(); /// - public ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => ReturnFalseAsync(); + public ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) => ReturnFalseAsync(); public ValueTask TimeToLiveAsync(CacheKey cacheKey, CancellationToken token = default) diff --git a/src/UiPath.Caching.Abstractions/NullHashCache.cs b/src/UiPath.Caching.Abstractions/NullHashCache.cs index cc83053..e15a9e0 100644 --- a/src/UiPath.Caching.Abstractions/NullHashCache.cs +++ b/src/UiPath.Caching.Abstractions/NullHashCache.cs @@ -21,19 +21,19 @@ public ValueTask ContainsAsync(CacheKey cacheKey, CancellationToken tok return ValueTask.FromResult(default(DateTimeOffset?)); } - public ValueTask> GetAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask> GetAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); return ValueTask.FromResult((IDictionary)ImmutableDictionary.Empty); } - public ValueTask> GetAsync(CacheKey cacheKey, string[] fields, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask> GetAsync(CacheKey cacheKey, string[] fields, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); return ValueTask.FromResult((IDictionary)ImmutableDictionary.Empty); } - public ValueTask>> GetCacheEntryAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask>> GetCacheEntryAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); return ValueTask.FromResult(NullCacheEntry>.Instance); @@ -45,41 +45,41 @@ public ValueTask ContainsAsync(CacheKey cacheKey, CancellationToken tok return ValueTask.FromResult?>(default); } - public ValueTask GetItemAsync(CacheKey cacheKey, string field, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask GetItemAsync(CacheKey cacheKey, string field, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); return ValueTask.FromResult(default(T?)); } - public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, CachePolicy? policy = null, CancellationToken token = default) => + public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, CachePolicy? policy, CancellationToken token = default) => ReturnGeneratorAsync(generator, token); - public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => + public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) => ReturnGeneratorAsync(generator, token); - public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => + public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) => ReturnGeneratorAsync(generator, token); - public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration = null, HashCacheSetOption? setOption = null, CachePolicy? policy = null, CancellationToken token = default) => + public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration, HashCacheSetOption? setOption, CachePolicy? policy, CancellationToken token = default) => ReturnGeneratorAsync(generator, token); - public ValueTask RefreshAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default) => ReturnTrueAsync(); + public ValueTask RefreshAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); - public ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => ReturnTrueAsync(); + public ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); - public ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => ReturnTrueAsync(); + public ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); - public ValueTask RefreshAsync(CacheKey cacheKey, HashCacheEntryOptions options, CachePolicy? policy = null, CancellationToken token = default) => ReturnTrueAsync(); + public ValueTask RefreshAsync(CacheKey cacheKey, HashCacheEntryOptions options, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); public ValueTask RemoveAsync(CacheKey cacheKey, CancellationToken token = default) => ReturnTrueAsync(); - public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, CachePolicy? policy = null, CancellationToken token = default) => ReturnTrueAsync(); + public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); - public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => ReturnTrueAsync(); + public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); - public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => ReturnTrueAsync(); + public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); - public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, HashCacheEntryOptions options, CachePolicy? policy = null, CancellationToken token = default) => ReturnTrueAsync(); + public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, HashCacheEntryOptions options, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); public ValueTask SetMetadataAsync(CacheKey cacheKey, IDictionary metadata, CancellationToken token = default) => ReturnTrueAsync(); diff --git a/src/UiPath.Caching.Abstractions/PublicAPI.Shipped.txt b/src/UiPath.Caching.Abstractions/PublicAPI.Shipped.txt index 9703a2f..074b775 100644 --- a/src/UiPath.Caching.Abstractions/PublicAPI.Shipped.txt +++ b/src/UiPath.Caching.Abstractions/PublicAPI.Shipped.txt @@ -234,44 +234,9 @@ UiPath.Caching.HashCacheSetOption.KeyReplace = 1 -> UiPath.Caching.HashCacheSetO UiPath.Caching.ICache UiPath.Caching.ICache.ContainsAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.ExpireTimeAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.GetAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.GetAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.GetAsync(UiPath.Caching.CacheKey[]! cacheKeys, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask[]!> -UiPath.Caching.ICache.GetAsync(UiPath.Caching.CacheKey[]! cacheKeys, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask[]!> -UiPath.Caching.ICache.GetCacheEntriesAsync(UiPath.Caching.CacheKey[]! cacheKeys, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!>[]!> -UiPath.Caching.ICache.GetCacheEntriesAsync(UiPath.Caching.CacheKey[]! cacheKeys, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!>[]!> -UiPath.Caching.ICache.GetCacheEntryAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> -UiPath.Caching.ICache.GetCacheEntryAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> -UiPath.Caching.ICache.GetOrAddAsync(System.Collections.Generic.KeyValuePair[]! entries, System.Func[]!>!>! generator, System.DateTimeOffset? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask[]!> -UiPath.Caching.ICache.GetOrAddAsync(System.Collections.Generic.KeyValuePair[]! entries, System.Func[]!>!>! generator, System.TimeSpan? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask[]!> -UiPath.Caching.ICache.GetOrAddAsync(System.Collections.Generic.KeyValuePair[]! entries, System.Func[]!>!>! generator, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask[]!> -UiPath.Caching.ICache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.DateTimeOffset? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.TimeSpan? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.Name.get -> string! -UiPath.Caching.ICache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.TimeSpan? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.RemoveAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.RemoveAsync(UiPath.Caching.CacheKey[]! cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.SetAsync(System.Collections.Generic.KeyValuePair[]! keyValues, System.DateTimeOffset? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.SetAsync(System.Collections.Generic.KeyValuePair[]! keyValues, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.SetAsync(System.Collections.Generic.KeyValuePair[]! keyValues, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.SetAsync(System.Collections.Generic.KeyValuePair[]! keyValues, System.TimeSpan? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.SetAsync(System.Collections.Generic.KeyValuePair[]! keyValues, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.SetAsync(System.Collections.Generic.KeyValuePair[]! keyValues, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.SetAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.SetAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.SetAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.SetAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.SetAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.SetAsync(UiPath.Caching.CacheKey cacheKey, T? value, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.TimeToLiveAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache UiPath.Caching.ICache.Contains(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool @@ -359,41 +324,9 @@ UiPath.Caching.IConnectionState.OnReconnected -> System.EventHandler? UiPath.Caching.IHashCache UiPath.Caching.IHashCache.ContainsAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.IHashCache.ExpireTimeAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.IHashCache.GetAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> -UiPath.Caching.IHashCache.GetAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> -UiPath.Caching.IHashCache.GetAsync(UiPath.Caching.CacheKey cacheKey, string![]! fields, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> -UiPath.Caching.IHashCache.GetAsync(UiPath.Caching.CacheKey cacheKey, string![]! fields, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> -UiPath.Caching.IHashCache.GetCacheEntryAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!>!> -UiPath.Caching.IHashCache.GetCacheEntryAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!>!> -UiPath.Caching.IHashCache.GetItemAsync(UiPath.Caching.CacheKey cacheKey, string! field, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.IHashCache.GetItemAsync(UiPath.Caching.CacheKey cacheKey, string! field, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.IHashCache.GetMetadataAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask?> -UiPath.Caching.IHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.DateTimeOffset? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> -UiPath.Caching.IHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.DateTimeOffset? expiration = null, UiPath.Caching.HashCacheSetOption? setOption = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> -UiPath.Caching.IHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> -UiPath.Caching.IHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.DateTimeOffset? expiration, UiPath.Caching.HashCacheSetOption? setOption, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> -UiPath.Caching.IHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> -UiPath.Caching.IHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.TimeSpan? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> -UiPath.Caching.IHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> -UiPath.Caching.IHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> UiPath.Caching.IHashCache.Name.get -> string! -UiPath.Caching.IHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.IHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.IHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.IHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.TimeSpan? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.IHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.IHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.IHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.HashCacheEntryOptions options, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.IHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.HashCacheEntryOptions options, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.IHashCache.RemoveAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.IHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.DateTimeOffset? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.IHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.IHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.IHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.TimeSpan? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.IHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.IHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.IHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, UiPath.Caching.HashCacheEntryOptions options, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.IHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, UiPath.Caching.HashCacheEntryOptions options, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.IHashCache.SetMetadataAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! metadata, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.IHashCache.TimeToLiveAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.IHashCache @@ -474,26 +407,10 @@ UiPath.Caching.NullCache UiPath.Caching.NullCache.ContainsAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.NullCache.Dispose() -> void UiPath.Caching.NullCache.ExpireTimeAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullCache.GetAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullCache.GetAsync(UiPath.Caching.CacheKey[]! cacheKeys, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask[]!> -UiPath.Caching.NullCache.GetCacheEntriesAsync(UiPath.Caching.CacheKey[]! cacheKeys, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!>[]!> -UiPath.Caching.NullCache.GetCacheEntryAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> -UiPath.Caching.NullCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.DateTimeOffset? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.TimeSpan? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.NullCache.Name.get -> string! UiPath.Caching.NullCache.NullCache() -> void -UiPath.Caching.NullCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.TimeSpan? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.NullCache.RemoveAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.NullCache.RemoveAsync(UiPath.Caching.CacheKey[]! cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullCache.SetAsync(System.Collections.Generic.KeyValuePair[]! keyValues, System.DateTimeOffset? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullCache.SetAsync(System.Collections.Generic.KeyValuePair[]! keyValues, System.TimeSpan? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullCache.SetAsync(System.Collections.Generic.KeyValuePair[]! keyValues, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullCache.SetAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullCache.SetAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullCache.SetAsync(UiPath.Caching.CacheKey cacheKey, T? value, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.NullCache.TimeToLiveAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.NullCacheFactory UiPath.Caching.NullCacheFactory.AddProvider(UiPath.Caching.ICacheProvider! provider) -> void @@ -506,26 +423,10 @@ UiPath.Caching.NullHashCache UiPath.Caching.NullHashCache.ContainsAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.NullHashCache.Dispose() -> void UiPath.Caching.NullHashCache.ExpireTimeAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullHashCache.GetAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> -UiPath.Caching.NullHashCache.GetAsync(UiPath.Caching.CacheKey cacheKey, string![]! fields, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> -UiPath.Caching.NullHashCache.GetCacheEntryAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!>!> -UiPath.Caching.NullHashCache.GetItemAsync(UiPath.Caching.CacheKey cacheKey, string! field, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.NullHashCache.GetMetadataAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask?> -UiPath.Caching.NullHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.DateTimeOffset? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> -UiPath.Caching.NullHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.DateTimeOffset? expiration = null, UiPath.Caching.HashCacheSetOption? setOption = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> -UiPath.Caching.NullHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.TimeSpan? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> -UiPath.Caching.NullHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> UiPath.Caching.NullHashCache.Name.get -> string! UiPath.Caching.NullHashCache.NullHashCache() -> void -UiPath.Caching.NullHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.TimeSpan? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.HashCacheEntryOptions options, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.NullHashCache.RemoveAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.DateTimeOffset? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.TimeSpan? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, UiPath.Caching.HashCacheEntryOptions options, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.NullHashCache.SetMetadataAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! metadata, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.NullHashCache.TimeToLiveAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.Policies.EmptyResiliencePipeline diff --git a/src/UiPath.Caching.Abstractions/PublicAPI.Unshipped.txt b/src/UiPath.Caching.Abstractions/PublicAPI.Unshipped.txt index 12654ad..e7609d2 100644 --- a/src/UiPath.Caching.Abstractions/PublicAPI.Unshipped.txt +++ b/src/UiPath.Caching.Abstractions/PublicAPI.Unshipped.txt @@ -2,6 +2,7 @@ UiPath.Caching.Cache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.Cache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.Cache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.CacheExtensions UiPath.Caching.CacheKey.CacheKey(string? name, UiPath.Caching.CacheKeyCasing casing) -> void UiPath.Caching.CacheKey.Casing.get -> UiPath.Caching.CacheKeyCasing UiPath.Caching.CacheKey.WithName(string? name) -> UiPath.Caching.CacheKey @@ -12,28 +13,128 @@ UiPath.Caching.CacheKeyComparer UiPath.Caching.CacheKeyComparer.CacheKeyComparer() -> void UiPath.Caching.CacheOptions.KeyCasing.get -> UiPath.Caching.CacheKeyCasing UiPath.Caching.CacheOptions.KeyCasing.set -> void -UiPath.Caching.ICache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.HashCacheExtensions +UiPath.Caching.ICache.GetAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.GetAsync(UiPath.Caching.CacheKey[]! cacheKeys, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask[]!> +UiPath.Caching.ICache.GetCacheEntriesAsync(UiPath.Caching.CacheKey[]! cacheKeys, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!>[]!> +UiPath.Caching.ICache.GetCacheEntryAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +UiPath.Caching.ICache.GetOrAddAsync(System.Collections.Generic.KeyValuePair[]! entries, System.Func[]!>!>! generator, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask[]!> +UiPath.Caching.ICache.GetOrAddAsync(System.Collections.Generic.KeyValuePair[]! entries, System.Func[]!>!>! generator, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask[]!> +UiPath.Caching.ICache.GetOrAddAsync(System.Collections.Generic.KeyValuePair[]! entries, System.Func[]!>!>! generator, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask[]!> +UiPath.Caching.ICache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.SetAsync(System.Collections.Generic.KeyValuePair[]! keyValues, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.SetAsync(System.Collections.Generic.KeyValuePair[]! keyValues, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.SetAsync(System.Collections.Generic.KeyValuePair[]! keyValues, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.SetAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.SetAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.SetAsync(UiPath.Caching.CacheKey cacheKey, T? value, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.TryAdd(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool UiPath.Caching.ICache.TryAdd(UiPath.Caching.CacheKey cacheKey, T? value, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool UiPath.Caching.ICache.TryAdd(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool UiPath.Caching.ICache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullCache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullCache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullCache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.IHashCache.GetAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +UiPath.Caching.IHashCache.GetAsync(UiPath.Caching.CacheKey cacheKey, string![]! fields, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +UiPath.Caching.IHashCache.GetCacheEntryAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!>!> +UiPath.Caching.IHashCache.GetItemAsync(UiPath.Caching.CacheKey cacheKey, string! field, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.IHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +UiPath.Caching.IHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.DateTimeOffset? expiration, UiPath.Caching.HashCacheSetOption? setOption, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +UiPath.Caching.IHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +UiPath.Caching.IHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +UiPath.Caching.IHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.IHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.IHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.IHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.HashCacheEntryOptions options, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.IHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.IHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.IHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.IHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, UiPath.Caching.HashCacheEntryOptions options, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullCache.GetAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullCache.GetAsync(UiPath.Caching.CacheKey[]! cacheKeys, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask[]!> +UiPath.Caching.NullCache.GetCacheEntriesAsync(UiPath.Caching.CacheKey[]! cacheKeys, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!>[]!> +UiPath.Caching.NullCache.GetCacheEntryAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +UiPath.Caching.NullCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullCache.SetAsync(System.Collections.Generic.KeyValuePair[]! keyValues, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullCache.SetAsync(System.Collections.Generic.KeyValuePair[]! keyValues, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullCache.SetAsync(System.Collections.Generic.KeyValuePair[]! keyValues, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullCache.SetAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullCache.SetAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullCache.SetAsync(UiPath.Caching.CacheKey cacheKey, T? value, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullCache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullCache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullCache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullHashCache.GetAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +UiPath.Caching.NullHashCache.GetAsync(UiPath.Caching.CacheKey cacheKey, string![]! fields, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +UiPath.Caching.NullHashCache.GetCacheEntryAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!>!> +UiPath.Caching.NullHashCache.GetItemAsync(UiPath.Caching.CacheKey cacheKey, string! field, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +UiPath.Caching.NullHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.DateTimeOffset? expiration, UiPath.Caching.HashCacheSetOption? setOption, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +UiPath.Caching.NullHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +UiPath.Caching.NullHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +UiPath.Caching.NullHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.HashCacheEntryOptions options, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, UiPath.Caching.HashCacheEntryOptions options, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.SystemJsonByteSerializerProxy UiPath.Caching.SystemJsonByteSerializerProxy.Deserialize(byte[]? value) -> T? UiPath.Caching.SystemJsonByteSerializerProxy.Serialize(object? value) -> byte[]? UiPath.Caching.SystemJsonByteSerializerProxy.SystemJsonByteSerializerProxy(System.Text.Json.JsonSerializerOptions? options = null) -> void UiPath.Caching.SystemJsonByteSerializerProxy.TryDeserialize(object? value, out T? result) -> bool UiPath.Caching.SystemJsonByteSerializerProxy.TryDeserialize(string? value, out T? result) -> bool +static UiPath.Caching.CacheExtensions.GetAsync(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.CacheExtensions.GetAsync(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey[]! cacheKeys, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask[]!> +static UiPath.Caching.CacheExtensions.GetCacheEntriesAsync(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey[]! cacheKeys, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!>[]!> +static UiPath.Caching.CacheExtensions.GetCacheEntryAsync(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +static UiPath.Caching.CacheExtensions.GetOrAddAsync(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.CacheExtensions.GetOrAddAsync(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.CacheExtensions.GetOrAddAsync(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.CacheExtensions.RefreshAsync(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.CacheExtensions.RefreshAsync(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.CacheExtensions.RefreshAsync(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.CacheExtensions.SetAsync(this UiPath.Caching.ICache! cache, System.Collections.Generic.KeyValuePair[]! keyValues, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.CacheExtensions.SetAsync(this UiPath.Caching.ICache! cache, System.Collections.Generic.KeyValuePair[]! keyValues, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.CacheExtensions.SetAsync(this UiPath.Caching.ICache! cache, System.Collections.Generic.KeyValuePair[]! keyValues, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.CacheExtensions.SetAsync(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.CacheExtensions.SetAsync(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, T? value, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.CacheExtensions.SetAsync(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.CacheExtensions.TryAddAsync(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.CacheExtensions.TryAddAsync(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, T? value, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.CacheExtensions.TryAddAsync(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static UiPath.Caching.CacheKey.DefaultCasing.get -> UiPath.Caching.CacheKeyCasing static UiPath.Caching.CacheKey.DefaultCasing.set -> void static UiPath.Caching.CacheKeyComparer.Insensitive.get -> UiPath.Caching.CacheKeyComparer! static UiPath.Caching.CacheKeyComparer.Sensitive.get -> UiPath.Caching.CacheKeyComparer! +static UiPath.Caching.HashCacheExtensions.GetAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +static UiPath.Caching.HashCacheExtensions.GetAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, string![]! fields, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +static UiPath.Caching.HashCacheExtensions.GetCacheEntryAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!>!> +static UiPath.Caching.HashCacheExtensions.GetItemAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, string! field, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.HashCacheExtensions.GetOrAddAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +static UiPath.Caching.HashCacheExtensions.GetOrAddAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.DateTimeOffset? expiration, UiPath.Caching.HashCacheSetOption? setOption, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +static UiPath.Caching.HashCacheExtensions.GetOrAddAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +static UiPath.Caching.HashCacheExtensions.GetOrAddAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +static UiPath.Caching.HashCacheExtensions.RefreshAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.HashCacheExtensions.RefreshAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.HashCacheExtensions.RefreshAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.HashCacheExtensions.RefreshAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, UiPath.Caching.HashCacheEntryOptions options, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.HashCacheExtensions.SetAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.HashCacheExtensions.SetAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.HashCacheExtensions.SetAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.HashCacheExtensions.SetAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, UiPath.Caching.HashCacheEntryOptions options, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask diff --git a/src/UiPath.Caching.Queue/ISetCache.Compat.cs b/src/UiPath.Caching.Queue/ISetCache.Compat.cs deleted file mode 100644 index 2f776c7..0000000 --- a/src/UiPath.Caching.Queue/ISetCache.Compat.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace UiPath.Caching; - -// Source-compatibility default interface methods for pre-CachePolicy call sites. Each forwards -// to the policy-bearing overload with policy=null. Excluded from coverage — these are forwarders -// with no behavior of their own; the policy-bearing impls are what tests exercise. -public partial interface ISetCache -{ - [ExcludeFromCodeCoverage] - ValueTask AddAsync(CacheKey cacheKey, T item, CancellationToken token = default) - => AddAsync(cacheKey, item, (CachePolicy?)null, token); - - [ExcludeFromCodeCoverage] - ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, CancellationToken token = default) - => AddAsync(cacheKey, items, (CachePolicy?)null, token); - - [ExcludeFromCodeCoverage] - ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, TimeSpan? expiration, CancellationToken token = default) - => AddAsync(cacheKey, items, expiration, null, token); - - [ExcludeFromCodeCoverage] - ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, DateTimeOffset? expiration, CancellationToken token = default) - => AddAsync(cacheKey, items, expiration, null, token); - - [ExcludeFromCodeCoverage] - ValueTask PopAsync(CacheKey cacheKey, CancellationToken token = default) - => PopAsync(cacheKey, (CachePolicy?)null, token); - - [ExcludeFromCodeCoverage] - ValueTask> PopAsync(CacheKey cacheKey, long count, CancellationToken token = default) - => PopAsync(cacheKey, count, null, token); - - [ExcludeFromCodeCoverage] - ValueTask> MembersAsync(CacheKey cacheKey, CancellationToken token = default) - => MembersAsync(cacheKey, (CachePolicy?)null, token); -} diff --git a/src/UiPath.Caching.Queue/ISetCache.cs b/src/UiPath.Caching.Queue/ISetCache.cs index 5b6a36e..af7c45f 100644 --- a/src/UiPath.Caching.Queue/ISetCache.cs +++ b/src/UiPath.Caching.Queue/ISetCache.cs @@ -6,7 +6,7 @@ namespace UiPath.Caching; /// insertion order and PopAsync removes a random member (Redis SPOP). Use the dedicated list /// caches when order matters. /// -public partial interface ISetCache : IDisposable +public interface ISetCache : IDisposable { string Name { get; } @@ -15,30 +15,30 @@ public partial interface ISetCache : IDisposable /// TTL). Every AddAsync call re-applies the resolved expiration, so adding any member resets the /// TTL of the entire set. /// - ValueTask AddAsync(CacheKey cacheKey, T item, CachePolicy? policy = null, CancellationToken token = default); + ValueTask AddAsync(CacheKey cacheKey, T item, CachePolicy? policy, CancellationToken token = default); /// - ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, CachePolicy? policy = null, CancellationToken token = default); + ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, CachePolicy? policy, CancellationToken token = default); /// - ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default); + ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default); /// - ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default); + ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default); /// /// Removes and returns a random member of the set (Redis SPOP). The set is unordered, so this is /// not a FIFO/LIFO dequeue — callers must not assume any insertion order. /// - ValueTask PopAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default); + ValueTask PopAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default); /// /// Removes and returns up to count random members of the set (Redis SPOP). The set is unordered, /// so this is not a FIFO/LIFO dequeue — callers must not assume any insertion order. /// - ValueTask> PopAsync(CacheKey cacheKey, long count, CachePolicy? policy = null, CancellationToken token = default); + ValueTask> PopAsync(CacheKey cacheKey, long count, CachePolicy? policy, CancellationToken token = default); - ValueTask> MembersAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default); + ValueTask> MembersAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default); ValueTask ContainsItemAsync(CacheKey cacheKey, T item, CancellationToken token = default); diff --git a/src/UiPath.Caching.Queue/MultilayerSetCache.cs b/src/UiPath.Caching.Queue/MultilayerSetCache.cs index 35355e7..1265fc4 100644 --- a/src/UiPath.Caching.Queue/MultilayerSetCache.cs +++ b/src/UiPath.Caching.Queue/MultilayerSetCache.cs @@ -51,25 +51,25 @@ inner is IConnectionState state public string Name => _name; - public async ValueTask AddAsync(CacheKey cacheKey, T item, CachePolicy? policy = null, CancellationToken token = default) + public async ValueTask AddAsync(CacheKey cacheKey, T item, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); return await InternalAddAsync(cacheKey, item, policy, token).ConfigureAwait(false); } - public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, CachePolicy? policy = null, CancellationToken token = default) => + public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, CachePolicy? policy, CancellationToken token = default) => AddAsync(cacheKey, items, expiration: (DateTimeOffset?)null, policy, token); - public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => + public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) => AddAsync(cacheKey, items, FromTtl(expiration), policy, token); - public async ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + public async ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); return await InternalAddAsync(cacheKey, Materialize(items), expiration, policy, token).ConfigureAwait(false); } - public async ValueTask PopAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default) + public async ValueTask PopAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); var key = Key(cacheKey, token); @@ -86,7 +86,7 @@ public async ValueTask AddAsync(CacheKey cacheKey, IEnumerable items return value; } - public async ValueTask> PopAsync(CacheKey cacheKey, long count, CachePolicy? policy = null, CancellationToken token = default) + public async ValueTask> PopAsync(CacheKey cacheKey, long count, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); var key = Key(cacheKey, token); @@ -102,7 +102,7 @@ public async ValueTask AddAsync(CacheKey cacheKey, IEnumerable items return values; } - public async ValueTask> MembersAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default) + public async ValueTask> MembersAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); var key = Key(cacheKey, token); diff --git a/src/UiPath.Caching.Queue/NullSetCache.cs b/src/UiPath.Caching.Queue/NullSetCache.cs index dd9f78f..c0a8728 100644 --- a/src/UiPath.Caching.Queue/NullSetCache.cs +++ b/src/UiPath.Caching.Queue/NullSetCache.cs @@ -7,23 +7,23 @@ public sealed class NullSetCache : ISetCache public string Name => "Null"; - public ValueTask AddAsync(CacheKey cacheKey, T item, CachePolicy? policy = null, CancellationToken token = default) => ReturnFalseAsync(); + public ValueTask AddAsync(CacheKey cacheKey, T item, CachePolicy? policy, CancellationToken token = default) => ReturnFalseAsync(); - public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, CachePolicy? policy = null, CancellationToken token = default) => ReturnZeroAsync(); + public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, CachePolicy? policy, CancellationToken token = default) => ReturnZeroAsync(); - public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => ReturnZeroAsync(); + public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) => ReturnZeroAsync(); - public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => ReturnZeroAsync(); + public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) => ReturnZeroAsync(); - public ValueTask PopAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask PopAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); return ValueTask.FromResult(default(T?)); } - public ValueTask> PopAsync(CacheKey cacheKey, long count, CachePolicy? policy = null, CancellationToken token = default) => EmptyAsync(); + public ValueTask> PopAsync(CacheKey cacheKey, long count, CachePolicy? policy, CancellationToken token = default) => EmptyAsync(); - public ValueTask> MembersAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default) => EmptyAsync(); + public ValueTask> MembersAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default) => EmptyAsync(); public ValueTask ContainsItemAsync(CacheKey cacheKey, T item, CancellationToken token = default) => ReturnFalseAsync(); diff --git a/src/UiPath.Caching.Queue/PublicAPI.Shipped.txt b/src/UiPath.Caching.Queue/PublicAPI.Shipped.txt index db55e99..c6abc0d 100644 --- a/src/UiPath.Caching.Queue/PublicAPI.Shipped.txt +++ b/src/UiPath.Caching.Queue/PublicAPI.Shipped.txt @@ -8,24 +8,10 @@ UiPath.Caching.IQueueCacheProvider.CreateSetCache() -> UiPath.Caching.ISetCache! UiPath.Caching.IQueueCacheProvider.Enabled.get -> bool UiPath.Caching.IQueueCacheProvider.Name.get -> string! UiPath.Caching.ISetCache -UiPath.Caching.ISetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.DateTimeOffset? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ISetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ISetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ISetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.TimeSpan? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ISetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ISetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ISetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, T item, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ISetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, T item, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ISetCache.ContainsAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ISetCache.ContainsItemAsync(UiPath.Caching.CacheKey cacheKey, T item, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ISetCache.CountAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ISetCache.MembersAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> -UiPath.Caching.ISetCache.MembersAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> UiPath.Caching.ISetCache.Name.get -> string! -UiPath.Caching.ISetCache.PopAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ISetCache.PopAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ISetCache.PopAsync(UiPath.Caching.CacheKey cacheKey, long count, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> -UiPath.Caching.ISetCache.PopAsync(UiPath.Caching.CacheKey cacheKey, long count, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> UiPath.Caching.ISetCache.RemoveAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ISetCache.RemoveItemAsync(UiPath.Caching.CacheKey cacheKey, T item, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ISetCache.RemoveItemsAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask @@ -116,19 +102,12 @@ UiPath.Caching.NullQueueCacheFactory.Dispose() -> void UiPath.Caching.NullQueueCacheFactory.NullQueueCacheFactory() -> void UiPath.Caching.NullQueueCacheFactory.ProviderNames.get -> System.Collections.Generic.IEnumerable! UiPath.Caching.NullSetCache -UiPath.Caching.NullSetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.DateTimeOffset? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullSetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.TimeSpan? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullSetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullSetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, T item, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.NullSetCache.ContainsAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.NullSetCache.ContainsItemAsync(UiPath.Caching.CacheKey cacheKey, T item, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.NullSetCache.CountAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.NullSetCache.Dispose() -> void -UiPath.Caching.NullSetCache.MembersAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> UiPath.Caching.NullSetCache.Name.get -> string! UiPath.Caching.NullSetCache.NullSetCache() -> void -UiPath.Caching.NullSetCache.PopAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullSetCache.PopAsync(UiPath.Caching.CacheKey cacheKey, long count, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> UiPath.Caching.NullSetCache.RemoveAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.NullSetCache.RemoveItemAsync(UiPath.Caching.CacheKey cacheKey, T item, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.NullSetCache.RemoveItemsAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask @@ -142,17 +121,10 @@ UiPath.Caching.QueueCacheFactory.QueueCacheFactory(Microsoft.Extensions.Options. UiPath.Caching.QueueCacheFactory.QueueCacheFactory(Microsoft.Extensions.Options.IOptions! cacheOptions, System.Collections.Generic.IEnumerable! providers) -> void UiPath.Caching.QueueCacheFactoryExtensions UiPath.Caching.Redis.RedisSetCache -UiPath.Caching.Redis.RedisSetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.DateTimeOffset? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.Redis.RedisSetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.TimeSpan? expiration = null, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.Redis.RedisSetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.Redis.RedisSetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, T item, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.Redis.RedisSetCache.ContainsAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.Redis.RedisSetCache.ContainsItemAsync(UiPath.Caching.CacheKey cacheKey, T item, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.Redis.RedisSetCache.CountAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.Redis.RedisSetCache.MembersAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> UiPath.Caching.Redis.RedisSetCache.Name.get -> string! -UiPath.Caching.Redis.RedisSetCache.PopAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.Redis.RedisSetCache.PopAsync(UiPath.Caching.CacheKey cacheKey, long count, UiPath.Caching.CachePolicy? policy = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> UiPath.Caching.Redis.RedisSetCache.RedisSetCache(UiPath.Caching.Redis.IRedisConnector! redis, UiPath.Caching.ISerializerProxy! serializer, UiPath.Caching.Policies.IResiliencePipelineProvider! resiliencePipelineProvider, UiPath.Caching.Telemetry.ICachingTelemetryProvider! telemetryProvider, UiPath.Caching.Redis.RedisCacheOptions! redisCacheOptions, UiPath.Caching.CacheOptions! cacheOptions, UiPath.Caching.Redis.RedisSetCacheOptions! setCacheOptions, UiPath.Caching.ICachePolicyFactory! policyFactory, Microsoft.Extensions.Logging.ILogger! logger) -> void UiPath.Caching.Redis.RedisSetCache.RemoveAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.Redis.RedisSetCache.RemoveItemAsync(UiPath.Caching.CacheKey cacheKey, T item, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask diff --git a/src/UiPath.Caching.Queue/PublicAPI.Unshipped.txt b/src/UiPath.Caching.Queue/PublicAPI.Unshipped.txt index 7dc5c58..7b0404a 100644 --- a/src/UiPath.Caching.Queue/PublicAPI.Unshipped.txt +++ b/src/UiPath.Caching.Queue/PublicAPI.Unshipped.txt @@ -1 +1,30 @@ #nullable enable +UiPath.Caching.ISetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ISetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ISetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ISetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, T item, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ISetCache.MembersAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +UiPath.Caching.ISetCache.PopAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ISetCache.PopAsync(UiPath.Caching.CacheKey cacheKey, long count, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +UiPath.Caching.NullSetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullSetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullSetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullSetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, T item, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullSetCache.MembersAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +UiPath.Caching.NullSetCache.PopAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullSetCache.PopAsync(UiPath.Caching.CacheKey cacheKey, long count, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +UiPath.Caching.Redis.RedisSetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.Redis.RedisSetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.Redis.RedisSetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.Redis.RedisSetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, T item, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.Redis.RedisSetCache.MembersAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +UiPath.Caching.Redis.RedisSetCache.PopAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.Redis.RedisSetCache.PopAsync(UiPath.Caching.CacheKey cacheKey, long count, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +UiPath.Caching.SetCacheExtensions +static UiPath.Caching.SetCacheExtensions.AddAsync(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.SetCacheExtensions.AddAsync(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.SetCacheExtensions.AddAsync(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.SetCacheExtensions.AddAsync(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, T item, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.SetCacheExtensions.MembersAsync(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +static UiPath.Caching.SetCacheExtensions.PopAsync(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.SetCacheExtensions.PopAsync(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, long count, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> diff --git a/src/UiPath.Caching.Queue/RedisSetCache.cs b/src/UiPath.Caching.Queue/RedisSetCache.cs index 5689253..8804097 100644 --- a/src/UiPath.Caching.Queue/RedisSetCache.cs +++ b/src/UiPath.Caching.Queue/RedisSetCache.cs @@ -37,7 +37,7 @@ public RedisSetCache( public string Name => KnownCacheProviderNames.Redis; - public async ValueTask AddAsync(CacheKey cacheKey, T item, CachePolicy? policy = null, CancellationToken token = default) + public async ValueTask AddAsync(CacheKey cacheKey, T item, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); var value = _serializer.Serialize(item); @@ -45,10 +45,10 @@ public async ValueTask AddAsync(CacheKey cacheKey, T item, CachePolicy? return added > 0; } - public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, CachePolicy? policy = null, CancellationToken token = default) => + public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, CachePolicy? policy, CancellationToken token = default) => AddAsync(cacheKey, items, expiration: (TimeSpan?)null, policy, token); - public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); ArgumentNullException.ThrowIfNull(items); @@ -56,7 +56,7 @@ public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, Time return AddManyInnerAsync(cacheKey, values, Clock.ToDateTimeOffset(ResolveExpiration(expiration, policy)), token); } - public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); ArgumentNullException.ThrowIfNull(items); @@ -120,7 +120,7 @@ private async ValueTask AddManyInnerAsync(CacheKey cacheKey, RedisValue return ret; } - public async ValueTask PopAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default) + public async ValueTask PopAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); var redisKey = ToRedisKey(cacheKey, token); @@ -159,7 +159,7 @@ private async ValueTask AddManyInnerAsync(CacheKey cacheKey, RedisValue return ret; } - public async ValueTask> PopAsync(CacheKey cacheKey, long count, CachePolicy? policy = null, CancellationToken token = default) + public async ValueTask> PopAsync(CacheKey cacheKey, long count, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); if (count <= 0) @@ -198,7 +198,7 @@ private async ValueTask AddManyInnerAsync(CacheKey cacheKey, RedisValue return ret; } - public async ValueTask> MembersAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default) + public async ValueTask> MembersAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); var redisKey = ToRedisKey(cacheKey, token); diff --git a/src/UiPath.Caching.Queue/SetCacheExtensions.cs b/src/UiPath.Caching.Queue/SetCacheExtensions.cs new file mode 100644 index 0000000..69cf8cb --- /dev/null +++ b/src/UiPath.Caching.Queue/SetCacheExtensions.cs @@ -0,0 +1,39 @@ +namespace UiPath.Caching; + +/// +/// Source-compatibility overloads for pre-CachePolicy call sites. Each forwards to the +/// policy-bearing member with policy: null. +/// +// Excluded from coverage — forwarders with no behavior of their own; the policy-bearing impls +// are what tests exercise. +[ExcludeFromCodeCoverage] +public static class SetCacheExtensions +{ + /// + public static ValueTask AddAsync(this ISetCache cache, CacheKey cacheKey, T item, CancellationToken token = default) + => cache.AddAsync(cacheKey, item, (CachePolicy?)null, token); + + /// + public static ValueTask AddAsync(this ISetCache cache, CacheKey cacheKey, IEnumerable items, CancellationToken token = default) + => cache.AddAsync(cacheKey, items, (CachePolicy?)null, token); + + /// + public static ValueTask AddAsync(this ISetCache cache, CacheKey cacheKey, IEnumerable items, TimeSpan? expiration, CancellationToken token = default) + => cache.AddAsync(cacheKey, items, expiration, null, token); + + /// + public static ValueTask AddAsync(this ISetCache cache, CacheKey cacheKey, IEnumerable items, DateTimeOffset? expiration, CancellationToken token = default) + => cache.AddAsync(cacheKey, items, expiration, null, token); + + /// + public static ValueTask PopAsync(this ISetCache cache, CacheKey cacheKey, CancellationToken token = default) + => cache.PopAsync(cacheKey, (CachePolicy?)null, token); + + /// + public static ValueTask> PopAsync(this ISetCache cache, CacheKey cacheKey, long count, CancellationToken token = default) + => cache.PopAsync(cacheKey, count, null, token); + + /// + public static ValueTask> MembersAsync(this ISetCache cache, CacheKey cacheKey, CancellationToken token = default) + => cache.MembersAsync(cacheKey, (CachePolicy?)null, token); +} diff --git a/src/UiPath.Caching/MultilayerCache.cs b/src/UiPath.Caching/MultilayerCache.cs index d2d8776..0032988 100644 --- a/src/UiPath.Caching/MultilayerCache.cs +++ b/src/UiPath.Caching/MultilayerCache.cs @@ -45,14 +45,14 @@ public MultilayerCache( _localMemorySetter = new LocalMemorySetter(cacheName, changeTokenFactory, _topicProvider, _memoryCache, logger, _clock, _multiLayerCacheOptions, memoryCacheOptions, telemetryProvider); } - public ValueTask GetAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask GetAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); policy ??= _defaultPolicy; return GetInnerAsync(_entryBuilder.BuildEntryOptions(cacheKey, _clock.ToDateTimeOffset(_multiLayerCacheOptions.DefaultExpiration), token), policy); } - public ValueTask[]> GetAsync(CacheKey[] cacheKeys, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask[]> GetAsync(CacheKey[] cacheKeys, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); policy ??= _defaultPolicy; @@ -60,7 +60,7 @@ public MultilayerCache( return GetInnerAsync(options, policy, token); } - public async ValueTask> GetCacheEntryAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default) + public async ValueTask> GetCacheEntryAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); policy ??= _defaultPolicy; @@ -68,7 +68,7 @@ public MultilayerCache( return await GetCacheEntryInnerAsync(options, policy).ConfigureAwait(false); } - public async ValueTask>[]> GetCacheEntriesAsync(CacheKey[] cacheKeys, CachePolicy? policy = null, CancellationToken token = default) + public async ValueTask>[]> GetCacheEntriesAsync(CacheKey[] cacheKeys, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); policy ??= _defaultPolicy; @@ -76,7 +76,7 @@ public MultilayerCache( return await GetCacheEntriesInnerAsync(options, policy, token).ConfigureAwait(false); } - public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, CachePolicy? policy, CancellationToken token = default) { ArgumentNullException.ThrowIfNull(generator); policy ??= _defaultPolicy; @@ -85,7 +85,7 @@ public MultilayerCache( return GetOrAddInternalAsync(cacheKey, generator, writeExpiration, duration, policy, token); } - public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) { ArgumentNullException.ThrowIfNull(generator); policy ??= _defaultPolicy; @@ -93,7 +93,7 @@ public MultilayerCache( return GetOrAddInternalAsync(cacheKey, generator, _clock.ToDateTimeOffset(ResolveWriteDuration(policy, expiration)), duration, policy, token); } - public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) { ArgumentNullException.ThrowIfNull(generator); policy ??= _defaultPolicy; @@ -111,7 +111,7 @@ public MultilayerCache( return GetOrAddInternalAsync(cacheKey, generator, expiration, duration, policy, token); } - public ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, CachePolicy? policy, CancellationToken token = default) where TState : notnull { ArgumentNullException.ThrowIfNull(entries); @@ -122,7 +122,7 @@ public MultilayerCache( return GetOrAddBatchInternalAsync(entries, generator, writeExpiration, duration, policy, token); } - public ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) where TState : notnull { ArgumentNullException.ThrowIfNull(entries); @@ -132,7 +132,7 @@ public MultilayerCache( return GetOrAddBatchInternalAsync(entries, generator, _clock.ToDateTimeOffset(ResolveWriteDuration(policy, expiration)), duration, policy, token); } - public ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) where TState : notnull { ArgumentNullException.ThrowIfNull(entries); @@ -631,19 +631,19 @@ private List> SelectEntriesToStore( return results; } - public ValueTask SetAsync(CacheKey cacheKey, T? value, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask SetAsync(CacheKey cacheKey, T? value, CachePolicy? policy, CancellationToken token = default) { policy ??= _defaultPolicy; return SetAsync(cacheKey, value, ResolveWriteDuration(policy), policy, token); } - public ValueTask SetAsync(CacheKey cacheKey, T? value, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask SetAsync(CacheKey cacheKey, T? value, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) { policy ??= _defaultPolicy; return SetAsync(cacheKey, value, _clock.ToDateTimeOffset(ResolveWriteDuration(policy, expiration)), policy, token); } - public async ValueTask SetAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + public async ValueTask SetAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); policy ??= _defaultPolicy; @@ -669,13 +669,13 @@ public async ValueTask SetAsync(CacheKey cacheKey, T? value, DateTimeOf } - public ValueTask TryAddAsync(CacheKey cacheKey, T? value, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask TryAddAsync(CacheKey cacheKey, T? value, CachePolicy? policy, CancellationToken token = default) { policy ??= _defaultPolicy; return TryAddAsync(cacheKey, value, ResolveWriteDuration(policy), policy, token); } - public ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) { policy ??= _defaultPolicy; return TryAddAsync(cacheKey, value, _clock.ToDateTimeOffset(ResolveWriteDuration(policy, expiration)), policy, token); @@ -688,7 +688,7 @@ public ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan? exp /// checks its connection first) rather than being gated on GetInnerCacheDisconnected, /// whose state also covers the broadcast transport — a dead topic must not stop a healthy Redis. /// - public async ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + public async ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); policy ??= _defaultPolicy; @@ -819,19 +819,19 @@ private async ValueTask LocalTryAddAsync(CacheEntryOptions options, T? } } - public ValueTask SetAsync(KeyValuePair[] keyValues, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask SetAsync(KeyValuePair[] keyValues, CachePolicy? policy, CancellationToken token = default) { policy ??= _defaultPolicy; return SetAsync(keyValues, ResolveWriteDuration(policy), policy, token); } - public ValueTask SetAsync(KeyValuePair[] keyValues, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask SetAsync(KeyValuePair[] keyValues, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) { policy ??= _defaultPolicy; return SetAsync(keyValues, _clock.ToDateTimeOffset(ResolveWriteDuration(policy, expiration)), policy, token); } - public async ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + public async ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); policy ??= _defaultPolicy; @@ -901,19 +901,19 @@ public ValueTask RemoveAsync(CacheKey[] cacheKey, CancellationToken tok return RemoveAsync(options, token); } - public ValueTask RefreshAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask RefreshAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default) { policy ??= _defaultPolicy; return RefreshAsync(cacheKey, ResolveWriteDuration(policy), policy, token); } - public ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) { policy ??= _defaultPolicy; return RefreshAsync(cacheKey, _clock.ToDateTimeOffset(ResolveWriteDuration(policy, expiration)), policy, token); } - public async ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + public async ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); policy ??= _defaultPolicy; diff --git a/src/UiPath.Caching/MultilayerHashCache.cs b/src/UiPath.Caching/MultilayerHashCache.cs index 1622aaf..634837d 100644 --- a/src/UiPath.Caching/MultilayerHashCache.cs +++ b/src/UiPath.Caching/MultilayerHashCache.cs @@ -34,7 +34,7 @@ public MultilayerHashCache( _localMemorySetter = new HashLocalMemorySetter(cacheName, changeTokenFactory, _topicProvider, _memoryCache, logger, _clock, _multiLayerCacheOptions, memoryCacheOptions, telemetryProvider); } - public async ValueTask GetItemAsync(CacheKey cacheKey, string field, CachePolicy? policy = null, CancellationToken token = default) + public async ValueTask GetItemAsync(CacheKey cacheKey, string field, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); policy ??= _defaultPolicy; @@ -47,7 +47,7 @@ public MultilayerHashCache( return cacheEntry.Value.TryGetValue(field, out var value) ? value : default; } - public async ValueTask> GetAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default) + public async ValueTask> GetAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); policy ??= _defaultPolicy; @@ -55,7 +55,7 @@ public MultilayerHashCache( return cacheEntry.Value ?? Empty(); } - public async ValueTask> GetAsync(CacheKey cacheKey, string[] fields, CachePolicy? policy = null, CancellationToken token = default) + public async ValueTask> GetAsync(CacheKey cacheKey, string[] fields, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); policy ??= _defaultPolicy; @@ -63,14 +63,14 @@ public MultilayerHashCache( return cacheEntry?.Value ?? Empty(); } - public ValueTask>> GetCacheEntryAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask>> GetCacheEntryAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); policy ??= _defaultPolicy; return GetCacheEntryAsync(_entryBuilder.BuildEntryOptions(cacheKey, token), policy); } - public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, CachePolicy? policy, CancellationToken token = default) { ArgumentNullException.ThrowIfNull(generator); policy ??= _defaultPolicy; @@ -79,7 +79,7 @@ public MultilayerHashCache( return GetOrAddInternalAsync(cacheKey, generator, writeExpiration, duration, HashCacheSetOption.KeyReplace, policy, token); } - public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) { ArgumentNullException.ThrowIfNull(generator); policy ??= _defaultPolicy; @@ -87,7 +87,7 @@ public MultilayerHashCache( return GetOrAddInternalAsync(cacheKey, generator, _clock.ToDateTimeOffset(ResolveWriteDuration(policy, expiration)), duration, HashCacheSetOption.KeyReplace, policy, token); } - public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) { ArgumentNullException.ThrowIfNull(generator); policy ??= _defaultPolicy; @@ -112,7 +112,7 @@ public MultilayerHashCache( /// _metadata_-as-empty-marker is present; we collapse that to for the /// caller, who can always iterate the result without a null check. /// - public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration = null, HashCacheSetOption? setOption = null, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration, HashCacheSetOption? setOption, CachePolicy? policy, CancellationToken token = default) { ArgumentNullException.ThrowIfNull(generator); policy ??= _defaultPolicy; @@ -207,19 +207,19 @@ private void TryHashRehydrate(CacheKey originalCacheKey, DateTimeOffset entry return _cacheEntryFactory.Create>(ret ?? Empty(), cacheEntryOptions.Expiration, cacheEntryOptions.Metadata); } - public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, CachePolicy? policy, CancellationToken token = default) { policy ??= _defaultPolicy; return SetAsync(cacheKey, values, ResolveWriteDuration(policy), policy, token); } - public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) { policy ??= _defaultPolicy; return SetAsync(cacheKey, values, _clock.ToDateTimeOffset(ResolveWriteDuration(policy, expiration)), policy, token); } - public async ValueTask SetAsync(CacheKey cacheKey, IDictionary values, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + public async ValueTask SetAsync(CacheKey cacheKey, IDictionary values, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); policy ??= _defaultPolicy; @@ -246,7 +246,7 @@ public async ValueTask SetAsync(CacheKey cacheKey, IDictionary SetAsync(CacheKey cacheKey, IDictionary values, HashCacheEntryOptions options, CachePolicy? policy = null, CancellationToken token = default) + public async ValueTask SetAsync(CacheKey cacheKey, IDictionary values, HashCacheEntryOptions options, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); policy ??= _defaultPolicy; @@ -290,22 +290,22 @@ public ValueTask RemoveAsync(CacheKey cacheKey, CancellationToken token return RemoveAsync(_entryBuilder.BuildEntryOptions(cacheKey, default, token: token)); } - public ValueTask RefreshAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask RefreshAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default) { policy ??= _defaultPolicy; return RefreshAsync(cacheKey, ResolveWriteDuration(policy), policy, token); } - public ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) { policy ??= _defaultPolicy; return RefreshAsync(cacheKey, _clock.ToDateTimeOffset(ResolveWriteDuration(policy, expiration)), policy, token); } - public ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => + public ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) => RefreshAsync(cacheKey, new HashCacheEntryOptions(expiration), policy, token); - public async ValueTask RefreshAsync(CacheKey cacheKey, HashCacheEntryOptions options, CachePolicy? policy = null, CancellationToken token = default) + public async ValueTask RefreshAsync(CacheKey cacheKey, HashCacheEntryOptions options, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); policy ??= _defaultPolicy; diff --git a/src/UiPath.Caching/Redis/RedisCache.cs b/src/UiPath.Caching/Redis/RedisCache.cs index f8808cc..0cabf6a 100644 --- a/src/UiPath.Caching/Redis/RedisCache.cs +++ b/src/UiPath.Caching/Redis/RedisCache.cs @@ -47,37 +47,37 @@ public RedisCache( public string Name => KnownCacheProviderNames.Redis; - public ValueTask GetAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask GetAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); return GetAsync(ToRedisKey(cacheKey, token), token); } - public ValueTask[]> GetAsync(CacheKey[] cacheKeys, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask[]> GetAsync(CacheKey[] cacheKeys, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); return GetAsyncInternal(cacheKeys, token); } - public ValueTask> GetCacheEntryAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask> GetCacheEntryAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); return GetCacheEntryInternalAsync(cacheKey, token); } - public ValueTask>[]> GetCacheEntriesAsync(CacheKey[] cacheKeys, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask>[]> GetCacheEntriesAsync(CacheKey[] cacheKeys, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); return GetCacheEntriesInternalAsync(cacheKeys, token); } - public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, CachePolicy? policy = null, CancellationToken token = default) => + public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, CachePolicy? policy, CancellationToken token = default) => GetOrAddAsync(cacheKey, generator, expiration: (TimeSpan?)null, policy, token); - public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => + public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) => GetOrAddAsync(cacheKey, generator, expiration is { } d ? d.Subtract(Clock.UtcNow) : (TimeSpan?)null, policy, token); - public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); ArgumentNullException.ThrowIfNull(generator); @@ -88,7 +88,7 @@ public RedisCache( } /// - public ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, CachePolicy? policy, CancellationToken token = default) where TState : notnull { ArgumentNullException.ThrowIfNull(generator); @@ -97,7 +97,7 @@ public RedisCache( } /// - public ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) where TState : notnull { ArgumentNullException.ThrowIfNull(generator); @@ -106,7 +106,7 @@ public RedisCache( } /// - public ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) where TState : notnull { ArgumentNullException.ThrowIfNull(generator); @@ -142,13 +142,13 @@ public RedisCache( token); } - public ValueTask RefreshAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default) => + public ValueTask RefreshAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default) => RefreshAsync(cacheKey, expiration: (TimeSpan?)null, policy, token); - public ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => + public ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) => RefreshAsync(cacheKey, Clock.ToDateTimeOffset(ResolveExpiration(expiration, policy)), policy, token); - public async ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + public async ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); var redisKey = ToRedisKey(cacheKey, token); @@ -201,54 +201,54 @@ public ValueTask RemoveAsync(CacheKey[] cacheKey, CancellationToken tok return RemoveAsync(cacheKey.Select(k => ToRedisKey(k, token)).ToArray(), token); } - public ValueTask SetAsync(CacheKey cacheKey, T? value, CachePolicy? policy = null, CancellationToken token = default) => + public ValueTask SetAsync(CacheKey cacheKey, T? value, CachePolicy? policy, CancellationToken token = default) => SetAsync(cacheKey, value, expiration: (TimeSpan?)null, policy, token); - public ValueTask SetAsync(CacheKey cacheKey, T? value, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask SetAsync(CacheKey cacheKey, T? value, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); var effective = ResolveExpiration(expiration, policy); return SetInternalAsync(ToRedisKey(cacheKey, token), value, Clock.ToTimeSpan(effective), token); } - public ValueTask SetAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask SetAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); var effective = ResolveExpiration(expiration, policy); return SetInternalAsync(ToRedisKey(cacheKey, token), value, Clock.ToTimeSpan(effective), token); } - public ValueTask SetAsync(KeyValuePair[] keyValues, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask SetAsync(KeyValuePair[] keyValues, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); return SetAsync(keyValues, expiration: (TimeSpan?)null, policy, token); } - public ValueTask SetAsync(KeyValuePair[] keyValues, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask SetAsync(KeyValuePair[] keyValues, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); var effective = ResolveExpiration(expiration, policy); return SetInternalAsync(keyValues, Clock.ToTimeSpan(effective), token); } - public ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); var effective = ResolveExpiration(expiration, policy); return SetInternalAsync(keyValues, Clock.ToTimeSpan(effective), token); } - public ValueTask TryAddAsync(CacheKey cacheKey, T? value, CachePolicy? policy = null, CancellationToken token = default) => + public ValueTask TryAddAsync(CacheKey cacheKey, T? value, CachePolicy? policy, CancellationToken token = default) => TryAddAsync(cacheKey, value, expiration: (TimeSpan?)null, policy, token); - public ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); var effective = ResolveExpiration(expiration, policy); return TryAddInternalAsync(ToRedisKey(cacheKey, token), value, Clock.ToTimeSpan(effective), token); } - public ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); var effective = ResolveExpiration(expiration, policy); diff --git a/src/UiPath.Caching/Redis/RedisHashCache.cs b/src/UiPath.Caching/Redis/RedisHashCache.cs index 0eeaa81..86dc1f8 100644 --- a/src/UiPath.Caching/Redis/RedisHashCache.cs +++ b/src/UiPath.Caching/Redis/RedisHashCache.cs @@ -46,41 +46,41 @@ public RedisHashCache( public string Name => KnownCacheProviderNames.Redis; - public async ValueTask GetItemAsync(CacheKey cacheKey, string field, CachePolicy? policy = null, CancellationToken token = default) + public async ValueTask GetItemAsync(CacheKey cacheKey, string field, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); ValidateFieldForRead(field); return await GetInnerAsync(cacheKey, field, token); } - public async ValueTask> GetAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default) + public async ValueTask> GetAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); return await GetInnerAsync(cacheKey, token); } - public async ValueTask> GetAsync(CacheKey cacheKey, string[] fields, CachePolicy? policy = null, CancellationToken token = default) + public async ValueTask> GetAsync(CacheKey cacheKey, string[] fields, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); return await GetInnerAsync(cacheKey, fields, token); } - public async ValueTask>> GetCacheEntryAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default) + public async ValueTask>> GetCacheEntryAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); return await GetInnerCacheEntryAsync(cacheKey, token); } - public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, CachePolicy? policy = null, CancellationToken token = default) => + public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, CachePolicy? policy, CancellationToken token = default) => GetOrAddAsync(cacheKey, generator, expiration: (TimeSpan?)null, policy, token); - public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => + public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) => GetOrAddAsync(cacheKey, generator, Clock.ToDateTimeOffset(ResolveExpiration(expiration, policy)), HashCacheSetOption.KeyReplace, policy, token); - public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => + public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) => GetOrAddAsync(cacheKey, generator, expiration, HashCacheSetOption.KeyReplace, policy, token); - public async ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration = null, HashCacheSetOption? setOption = null, CachePolicy? policy = null, CancellationToken token = default) + public async ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration, HashCacheSetOption? setOption, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); ArgumentNullException.ThrowIfNull(generator); @@ -224,13 +224,13 @@ public async ValueTask ContainsAsync(CacheKey cacheKey, CancellationTok return ret; } - public ValueTask RefreshAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default) => + public ValueTask RefreshAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default) => RefreshAsync(cacheKey, expiration: (TimeSpan?)null, policy, token); - public ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => + public ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) => RefreshAsync(cacheKey, Clock.ToDateTimeOffset(ResolveExpiration(expiration, policy)), policy, token); - public async ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + public async ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); var redisKey = ToRedisKey(cacheKey, token); @@ -266,7 +266,7 @@ public async ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? return ret; } - public async ValueTask RefreshAsync(CacheKey cacheKey, HashCacheEntryOptions options, CachePolicy? policy = null, CancellationToken token = default) + public async ValueTask RefreshAsync(CacheKey cacheKey, HashCacheEntryOptions options, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); var redisKey = ToRedisKey(cacheKey, token); @@ -379,13 +379,13 @@ public async ValueTask RemoveAsync(CacheKey cacheKey, CancellationToken return ret; } - public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, CachePolicy? policy = null, CancellationToken token = default) => + public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, CachePolicy? policy, CancellationToken token = default) => SetAsync(cacheKey, values, expiration: (TimeSpan?)null, policy, token); - public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => + public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) => SetAsync(cacheKey, values, Clock.ToDateTimeOffset(ResolveExpiration(expiration, policy)), policy, token); - public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); ValidateForWrite(values); @@ -400,7 +400,7 @@ public ValueTask SetAsync(CacheKey cacheKey, IDictionary va return SetInnerAsync(redisKey, hashEntries, HashCacheSetOption.KeyReplace, effective, token); } - public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, HashCacheEntryOptions options, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, HashCacheEntryOptions options, CachePolicy? policy, CancellationToken token = default) { NotCacheableException.ThrowIfNotCacheable(); ValidateForWrite(values); diff --git a/tests/UiPath.Caching.Tests/Fakes/DictionaryCache.cs b/tests/UiPath.Caching.Tests/Fakes/DictionaryCache.cs index 5e3d70a..5325d78 100644 --- a/tests/UiPath.Caching.Tests/Fakes/DictionaryCache.cs +++ b/tests/UiPath.Caching.Tests/Fakes/DictionaryCache.cs @@ -24,7 +24,7 @@ internal sealed class DictionaryCache : ICache public T? Read(CacheKey key) => _store.TryGetValue(key, out var v) ? (T?)v : default; public ValueTask>[]> GetCacheEntriesAsync( - CacheKey[] cacheKeys, CachePolicy? policy = null, CancellationToken token = default) + CacheKey[] cacheKeys, CachePolicy? policy, CancellationToken token = default) { GetCacheEntriesCalls++; var results = cacheKeys @@ -37,7 +37,7 @@ internal sealed class DictionaryCache : ICache return ValueTask.FromResult(results); } - public ValueTask SetAsync(KeyValuePair[] keyValues, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask SetAsync(KeyValuePair[] keyValues, CachePolicy? policy, CancellationToken token = default) { SetCalls++; SetKeySets.Add(keyValues.Select(kv => kv.Key).ToArray()); @@ -53,44 +53,44 @@ public ValueTask SetAsync(KeyValuePair[] keyValues, Cache return ValueTask.FromResult(true); } - public ValueTask> GetCacheEntryAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default) => + public ValueTask> GetCacheEntryAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default) => ValueTask.FromResult>(_store.TryGetValue(cacheKey, out var v) ? new TestCacheEntry { Value = (T?)v, Expiration = DateTimeOffset.MaxValue, Found = true } : new TestCacheEntry { Value = default, Expiration = DateTimeOffset.MinValue }); - public ValueTask GetAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default) => + public ValueTask GetAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default) => ValueTask.FromResult(Read(cacheKey)); - public ValueTask[]> GetAsync(CacheKey[] cacheKeys, CachePolicy? policy = null, CancellationToken token = default) => + public ValueTask[]> GetAsync(CacheKey[] cacheKeys, CachePolicy? policy, CancellationToken token = default) => ValueTask.FromResult(cacheKeys.Select(k => new KeyValuePair(k, Read(k))).ToArray()); - public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, CachePolicy? policy = null, CancellationToken token = default) => + public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, CachePolicy? policy, CancellationToken token = default) => throw new NotSupportedException(); - public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => + public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) => throw new NotSupportedException(); - public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => + public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) => throw new NotSupportedException(); - public ValueTask SetAsync(CacheKey cacheKey, T? value, CachePolicy? policy = null, CancellationToken token = default) => + public ValueTask SetAsync(CacheKey cacheKey, T? value, CachePolicy? policy, CancellationToken token = default) => SetAsync([new KeyValuePair(cacheKey, value)], policy, token); - public ValueTask SetAsync(CacheKey cacheKey, T? value, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => + public ValueTask SetAsync(CacheKey cacheKey, T? value, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) => SetAsync([new KeyValuePair(cacheKey, value)], policy, token); - public ValueTask SetAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => + public ValueTask SetAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) => SetAsync([new KeyValuePair(cacheKey, value)], policy, token); - public ValueTask SetAsync(KeyValuePair[] keyValues, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => + public ValueTask SetAsync(KeyValuePair[] keyValues, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) => SetAsync(keyValues, policy, token); - public ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => + public ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) => SetAsync(keyValues, policy, token); // A real conditional add: the dictionary itself decides, so this fake can stand in for a store // that supports NX. - public ValueTask TryAddAsync(CacheKey cacheKey, T? value, CachePolicy? policy = null, CancellationToken token = default) + public ValueTask TryAddAsync(CacheKey cacheKey, T? value, CachePolicy? policy, CancellationToken token = default) { TryAddCalls++; if (value is null && !CacheNullValues) @@ -100,10 +100,10 @@ public ValueTask TryAddAsync(CacheKey cacheKey, T? value, CachePolicy? return ValueTask.FromResult(_store.TryAdd(cacheKey, value)); } - public ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => + public ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) => TryAddAsync(cacheKey, value, policy, token); - public ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => + public ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) => TryAddAsync(cacheKey, value, policy, token); public ValueTask RemoveAsync(CacheKey cacheKey, CancellationToken token = default) => @@ -115,11 +115,11 @@ public ValueTask RemoveAsync(CacheKey[] cacheKey, CancellationToken tok return ValueTask.FromResult(true); } - public ValueTask RefreshAsync(CacheKey cacheKey, CachePolicy? policy = null, CancellationToken token = default) => ValueTask.FromResult(true); + public ValueTask RefreshAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default) => ValueTask.FromResult(true); - public ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => ValueTask.FromResult(true); + public ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) => ValueTask.FromResult(true); - public ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration = null, CachePolicy? policy = null, CancellationToken token = default) => ValueTask.FromResult(true); + public ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) => ValueTask.FromResult(true); public ValueTask ContainsAsync(CacheKey cacheKey, CancellationToken token = default) => ValueTask.FromResult(_store.ContainsKey(cacheKey)); From 57d5203073bf3047550ff21d60489dc58c47d024 Mon Sep 17 00:00:00 2001 From: Cosmin Staicu Date: Wed, 2 Sep 2026 21:55:58 +0300 Subject: [PATCH 6/9] refactor(cache)!: move the sync forwarders to extension methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same treatment as the Compat partials, applied to ICacheOfT.Sync.cs, IHashCacheOfT.Sync.cs and ISetCacheOfT.Sync.cs. The 59 blocking forwarders they carried as default interface methods — Get, GetOrAdd, Set, TryAdd, Refresh, Remove, Contains, TimeToLive, ExpireTime, the hash surface's GetItem, GetCacheEntry, GetMetadata and SetMetadata, and the set surface's Add, Pop, Members, ContainsItem, Count, RemoveItem and RemoveItems — now live on CacheSyncExtensions, HashCacheSyncExtensions and SetCacheSyncExtensions. Each still blocks on the async member via .AsTask().GetAwaiter().GetResult(); nothing about the blocking behavior changed. T becomes a method type parameter inferred from the receiver, so call sites are unchanged, and the forwarders stay reachable through the concrete Cache / HashCache / SetCache classes as well as the interfaces. No implementation declared them, so nothing had to be rewritten. partial comes off ICache, IHashCache and ISetCache, which nothing else extends now. That leaves all three as pure async contracts: an implementation writes only the members it implements rather than inheriting blocking forwarders it could accidentally override. Unlike the Compat move this needs no signature change, because the forwarders are distinct names (Get, not GetAsync) rather than overloads of the members they forward to — so no instance member shadows them. Verified: Release build clean (16 warnings across both TFMs, all pre-existing CS0618), 1503/1503 tests on net8.0 and net10.0. Compile-probed all 59 sync call shapes, including through the concrete Cache and SetCache, then removed the probes. 56 entries leave PublicAPI.Shipped.txt (43 Abstractions, 13 Queue) and 3 leave Unshipped.txt, replaced by 62 extension entries. One call shape does not compile, before or after: Set(pairs) with a single argument, where the KeyValuePair[] overloads with `TimeSpan? expiration = null` and `DateTimeOffset? expiration = null` tie with the token-only overload. That ambiguity is pre-existing and was verified against a default-interface-method reproduction of the old shape — no call site uses it, and the async twin SetAsync(pairs) has the same wart on ICache. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_015RLSnsYiZsqeaHHYYybXW1 Signed-off-by: Cosmin Staicu --- CHANGELOG.md | 13 ++ docs/reference/interfaces.md | 10 +- .../CacheSyncExtensions.cs | 104 +++++++++++++++ .../HashCacheSyncExtensions.cs | 75 +++++++++++ .../ICacheOfT.Sync.cs | 122 ------------------ src/UiPath.Caching.Abstractions/ICacheOfT.cs | 2 +- .../IHashCacheOfT.Sync.cs | 92 ------------- .../IHashCacheOfT.cs | 2 +- .../PublicAPI.Shipped.txt | 43 ------ .../PublicAPI.Unshipped.txt | 51 +++++++- src/UiPath.Caching.Queue/ISetCacheOfT.Sync.cs | 60 --------- src/UiPath.Caching.Queue/ISetCacheOfT.cs | 2 +- .../PublicAPI.Shipped.txt | 13 -- .../PublicAPI.Unshipped.txt | 14 ++ .../SetCacheSyncExtensions.cs | 57 ++++++++ 15 files changed, 319 insertions(+), 341 deletions(-) create mode 100644 src/UiPath.Caching.Abstractions/CacheSyncExtensions.cs create mode 100644 src/UiPath.Caching.Abstractions/HashCacheSyncExtensions.cs delete mode 100644 src/UiPath.Caching.Abstractions/ICacheOfT.Sync.cs delete mode 100644 src/UiPath.Caching.Abstractions/IHashCacheOfT.Sync.cs delete mode 100644 src/UiPath.Caching.Queue/ISetCacheOfT.Sync.cs create mode 100644 src/UiPath.Caching.Queue/SetCacheSyncExtensions.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 16f188f..b14a453 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -110,6 +110,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ships in `UiPath.Caching.Queue`, alongside `ISetCache`). Call sites are unchanged and need no edit; the interfaces shrink to just the policy-bearing members, so an implementation now has one member to write per operation instead of one plus an inherited forwarder it could accidentally override. +- **BREAKING:** `ICacheOfT.Sync.cs`, `IHashCacheOfT.Sync.cs` and `ISetCacheOfT.Sync.cs` are gone the + same way. The 59 blocking forwarders they carried as default interface methods — `Get`, `GetOrAdd`, + `Set`, `TryAdd`, `Refresh`, `Remove`, `Contains`, `TimeToLive`, `ExpireTime`, the hash surface's + `GetItem`, `GetCacheEntry`, `GetMetadata` and `SetMetadata`, and the set surface's `Add`, `Pop`, + `Members`, `ContainsItem`, `Count`, `RemoveItem` and `RemoveItems` — now live on the new + `CacheSyncExtensions`, `HashCacheSyncExtensions` and `SetCacheSyncExtensions` static classes + (`SetCacheSyncExtensions` ships in `UiPath.Caching.Queue`). Each still blocks on the async member + via `.AsTask().GetAwaiter().GetResult()`; nothing about the blocking behavior changed. `T` becomes a + method type parameter inferred from the receiver, so call sites are unchanged, and they stay + reachable through the concrete `Cache` / `HashCache` / `SetCache` classes as well as the + interfaces. `partial` comes off `ICache`, `IHashCache` and `ISetCache`, which nothing else + extends now, leaving all three as pure async contracts: an implementation writes only the members it + actually implements, rather than inheriting blocking forwarders it could accidentally override. - **BREAKING:** `CachePolicy? policy` is now a **required** parameter on every `ICache`, `IHashCache` and `ISetCache` member that takes one, along with the `expiration` / `setOption` parameters that precede it — the `= null` defaults are removed. `CacheExtensions` / diff --git a/docs/reference/interfaces.md b/docs/reference/interfaces.md index 6ffc433..7723a5b 100644 --- a/docs/reference/interfaces.md +++ b/docs/reference/interfaces.md @@ -17,7 +17,7 @@ The library's public interface surface. Each entry shows the namespace, signatur **Namespace:** `UiPath.Caching` ```csharp -public partial interface ICache +public interface ICache { string Name { get; } @@ -73,9 +73,9 @@ public partial interface ICache } ``` -`ICache` is the primary typed cache surface for single-value key/value caching. The type parameter `T` fixes the value type for the lifetime of the cache instance, which lets the library resolve `CachePolicy` by `typeof(T).FullName` and apply a single key strategy per cache. Every operation accepts a `CancellationToken` and returns a `ValueTask`, so callers integrate naturally into async pipelines without heap allocation in the hot path. Sync overloads (`Get`, `GetOrAdd`, `Set`, `Remove`, `Refresh`, `Contains`, `TimeToLive`, `ExpireTime`) are provided as blocking default interface methods for call sites that cannot use `await`; `GetOrAdd` covers both the single-key generator shape and a key-only multi-key shape (see below). +`ICache` is the primary typed cache surface for single-value key/value caching. The type parameter `T` fixes the value type for the lifetime of the cache instance, which lets the library resolve `CachePolicy` by `typeof(T).FullName` and apply a single key strategy per cache. Every operation accepts a `CancellationToken` and returns a `ValueTask`, so callers integrate naturally into async pipelines without heap allocation in the hot path. Blocking forwarders (`Get`, `GetOrAdd`, `Set`, `TryAdd`, `Remove`, `Refresh`, `Contains`, `TimeToLive`, `ExpireTime`) live on `CacheSyncExtensions` for call sites that cannot use `await`; each blocks on the async member via `.AsTask().GetAwaiter().GetResult()`, so use them only where the thread-blocking cost is acceptable. `GetOrAdd` covers both the single-key generator shape and a key-only multi-key shape (see below). -The multi-key `GetOrAddAsync` overloads pair each key with an opaque caller state (`TState`) — a database id, a request object, whatever identifies the entry in the caller's own vocabulary. The generator is invoked at most once, with only the states of the entries that missed, and results come back keyed by state. These three overloads are **abstract** on `ICache` — unlike the equivalent members on `ICache`, there is no default body, because `ICache` has no `GetCacheEntriesAsync` and so cannot distinguish a genuine miss from a cached `null` inside a default implementation. A hand-written `ICache` implementation (a test fake, typically) must add all three. There is no key-only convenience overload on the async surface: a caller whose keys are their own identity pairs each key with itself (`TState = CacheKey`). The blocking `GetOrAdd` facade above is the one place that accepts `CacheKey[]` directly and does that pairing for you. +The multi-key `GetOrAddAsync` overloads pair each key with an opaque caller state (`TState`) — a database id, a request object, whatever identifies the entry in the caller's own vocabulary. The generator is invoked at most once, with only the states of the entries that missed, and results come back keyed by state. These three overloads are **abstract** on `ICache` — unlike the equivalent members on `ICache`, there is no default body, because `ICache` has no `GetCacheEntriesAsync` and so cannot distinguish a genuine miss from a cached `null` inside a default implementation. A hand-written `ICache` implementation (a test fake, typically) must add all three. There is no key-only convenience overload on the async surface: a caller whose keys are their own identity pairs each key with itself (`TState = CacheKey`). The blocking `CacheSyncExtensions.GetOrAdd` forwarder is the one place that accepts `CacheKey[]` directly and does that pairing for you. `TryAddAsync` is the conditional-add (create-if-absent) member: it writes only when the key does not already exist, and returns `true` only to the caller that created it. On a Redis-backed cache it maps to StackExchange.Redis `When.NotExists` (`SET key value EX … NX`) — a single atomic round-trip, so exactly one caller across all nodes wins a given key. It is the primitive to reach for when you need at-most-once semantics keyed by something: a dedup marker, an idempotency key, a "who runs this job" election. See [`ICache.TryAddAsync`](#icache) for the full contract, including what a `false` return does and does not tell you. @@ -238,7 +238,7 @@ The three overloads are **default interface methods**, so existing `ICache` impl **Namespace:** `UiPath.Caching` ```csharp -public partial interface IHashCache +public interface IHashCache { string Name { get; } @@ -286,7 +286,7 @@ public partial interface IHashCache } ``` -`IHashCache` is the typed hash-cache surface. Each cache key maps to a dictionary of named fields rather than a single value — the backing store is a Redis hash (or an in-memory equivalent). `GetItemAsync` retrieves a single field by name; `GetAsync` retrieves all fields or a subset. `SetAsync` accepts a `HashCacheEntryOptions` overload for per-write control of expiration, metadata, and write scope — `HashCacheSetOption.HashReplace` merges the given fields into the existing hash, `KeyReplace` drops the key first so the written fields are the whole hash. Note that this is write *scope*, not a precondition: the hash surface has no conditional-add member, and `TryAddAsync` exists only on [`ICache`](#icache) / [`ICache`](#icachet). Metadata (`GetMetadataAsync` / `SetMetadataAsync`) provides a side-channel string dictionary attached to the same key, useful for audit or versioning data. Sync overloads (`Get`, `GetItem`, `GetOrAdd`, `Set`, `Refresh`, `Remove`, `Contains`, etc.) are provided as blocking default interface methods. +`IHashCache` is the typed hash-cache surface. Each cache key maps to a dictionary of named fields rather than a single value — the backing store is a Redis hash (or an in-memory equivalent). `GetItemAsync` retrieves a single field by name; `GetAsync` retrieves all fields or a subset. `SetAsync` accepts a `HashCacheEntryOptions` overload for per-write control of expiration, metadata, and write scope — `HashCacheSetOption.HashReplace` merges the given fields into the existing hash, `KeyReplace` drops the key first so the written fields are the whole hash. Note that this is write *scope*, not a precondition: the hash surface has no conditional-add member, and `TryAddAsync` exists only on [`ICache`](#icache) / [`ICache`](#icachet). Metadata (`GetMetadataAsync` / `SetMetadataAsync`) provides a side-channel string dictionary attached to the same key, useful for audit or versioning data. Blocking forwarders (`Get`, `GetItem`, `GetOrAdd`, `Set`, `Refresh`, `Remove`, `Contains`, etc.) live on `HashCacheSyncExtensions`, each blocking on the async member via `.AsTask().GetAwaiter().GetResult()`. > **Typical vs. power-user surface:** `IHashCache` is the standard typed hash surface. If you need to vary the value type per call, use [`IHashCache`](#ihashcache) instead. The two surfaces are different shapes for different problems. diff --git a/src/UiPath.Caching.Abstractions/CacheSyncExtensions.cs b/src/UiPath.Caching.Abstractions/CacheSyncExtensions.cs new file mode 100644 index 0000000..b4d1ba7 --- /dev/null +++ b/src/UiPath.Caching.Abstractions/CacheSyncExtensions.cs @@ -0,0 +1,104 @@ +namespace UiPath.Caching; + +/// +/// Blocking forwarders over the async API. Each blocks on the underlying +/// call via .AsTask().GetAwaiter().GetResult() — use only from sync call sites that can +/// tolerate the thread-blocking cost. +/// +// Excluded from coverage — forwarders with no behavior of their own; the async impls are what +// tests exercise. +[ExcludeFromCodeCoverage] +public static class CacheSyncExtensions +{ + public static T? Get(this ICache cache, CacheKey cacheKey, CancellationToken token = default) + => cache.GetAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); + + public static KeyValuePair[] Get(this ICache cache, CacheKey[] cacheKeys, CancellationToken token = default) + => cache.GetAsync(cacheKeys, token).AsTask().GetAwaiter().GetResult(); + + public static T? GetOrAdd(this ICache cache, CacheKey cacheKey, Func generator, CancellationToken token = default) + => cache.GetOrAddAsync(cacheKey, _ => Task.FromResult(generator()), token).AsTask().GetAwaiter().GetResult(); + + public static T? GetOrAdd(this ICache cache, CacheKey cacheKey, Func generator, TimeSpan? expiration, CancellationToken token = default) + => cache.GetOrAddAsync(cacheKey, _ => Task.FromResult(generator()), expiration, token).AsTask().GetAwaiter().GetResult(); + + public static T? GetOrAdd(this ICache cache, CacheKey cacheKey, Func generator, DateTimeOffset? expiration, CancellationToken token = default) + => cache.GetOrAddAsync(cacheKey, _ => Task.FromResult(generator()), expiration, token).AsTask().GetAwaiter().GetResult(); + + public static KeyValuePair[] GetOrAdd(this ICache cache, CacheKey[] cacheKeys, Func[]> generator, CancellationToken token = default) + => cache.GetOrAddAsync( + Array.ConvertAll(cacheKeys, k => new KeyValuePair(k, k)), + (keys, _) => Task.FromResult(generator(keys)), + token) + .AsTask().GetAwaiter().GetResult(); + + public static KeyValuePair[] GetOrAdd(this ICache cache, CacheKey[] cacheKeys, Func[]> generator, TimeSpan? expiration, CancellationToken token = default) + => cache.GetOrAddAsync( + Array.ConvertAll(cacheKeys, k => new KeyValuePair(k, k)), + (keys, _) => Task.FromResult(generator(keys)), + expiration, + token) + .AsTask().GetAwaiter().GetResult(); + + public static KeyValuePair[] GetOrAdd(this ICache cache, CacheKey[] cacheKeys, Func[]> generator, DateTimeOffset? expiration, CancellationToken token = default) + => cache.GetOrAddAsync( + Array.ConvertAll(cacheKeys, k => new KeyValuePair(k, k)), + (keys, _) => Task.FromResult(generator(keys)), + expiration, + token) + .AsTask().GetAwaiter().GetResult(); + + public static bool Remove(this ICache cache, CacheKey cacheKey, CancellationToken token = default) + => cache.RemoveAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); + + public static bool Remove(this ICache cache, CacheKey[] cacheKeys, CancellationToken token = default) + => cache.RemoveAsync(cacheKeys, token).AsTask().GetAwaiter().GetResult(); + + public static bool Set(this ICache cache, CacheKey cacheKey, T? value, CancellationToken token = default) + => cache.SetAsync(cacheKey, value, token).AsTask().GetAwaiter().GetResult(); + + public static bool Set(this ICache cache, CacheKey cacheKey, T? value, TimeSpan? expiration, CancellationToken token = default) + => cache.SetAsync(cacheKey, value, expiration, token).AsTask().GetAwaiter().GetResult(); + + public static bool Set(this ICache cache, CacheKey cacheKey, T? value, DateTimeOffset? expiration, CancellationToken token = default) + => cache.SetAsync(cacheKey, value, expiration, token).AsTask().GetAwaiter().GetResult(); + + public static bool Set(this ICache cache, KeyValuePair[] keyValues, CancellationToken token = default) + => cache.SetAsync(keyValues, token).AsTask().GetAwaiter().GetResult(); + + public static bool Set(this ICache cache, KeyValuePair[] keyValues, TimeSpan? expiration = null, CancellationToken token = default) + => cache.SetAsync(keyValues, expiration, token).AsTask().GetAwaiter().GetResult(); + + public static bool Set(this ICache cache, KeyValuePair[] keyValues, DateTimeOffset? expiration = null, CancellationToken token = default) + => cache.SetAsync(keyValues, expiration, token).AsTask().GetAwaiter().GetResult(); + + /// + public static bool TryAdd(this ICache cache, CacheKey cacheKey, T? value, CancellationToken token = default) + => cache.TryAddAsync(cacheKey, value, token).AsTask().GetAwaiter().GetResult(); + + /// + public static bool TryAdd(this ICache cache, CacheKey cacheKey, T? value, TimeSpan? expiration, CancellationToken token = default) + => cache.TryAddAsync(cacheKey, value, expiration, token).AsTask().GetAwaiter().GetResult(); + + /// + public static bool TryAdd(this ICache cache, CacheKey cacheKey, T? value, DateTimeOffset? expiration, CancellationToken token = default) + => cache.TryAddAsync(cacheKey, value, expiration, token).AsTask().GetAwaiter().GetResult(); + + public static bool Refresh(this ICache cache, CacheKey cacheKey, CancellationToken token = default) + => cache.RefreshAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); + + public static bool Refresh(this ICache cache, CacheKey cacheKey, TimeSpan? expiration, CancellationToken token = default) + => cache.RefreshAsync(cacheKey, expiration, token).AsTask().GetAwaiter().GetResult(); + + public static bool Refresh(this ICache cache, CacheKey cacheKey, DateTimeOffset? expiration, CancellationToken token = default) + => cache.RefreshAsync(cacheKey, expiration, token).AsTask().GetAwaiter().GetResult(); + + public static bool Contains(this ICache cache, CacheKey cacheKey, CancellationToken token = default) + => cache.ContainsAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); + + public static TimeSpan? TimeToLive(this ICache cache, CacheKey cacheKey, CancellationToken token = default) + => cache.TimeToLiveAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); + + public static DateTimeOffset? ExpireTime(this ICache cache, CacheKey cacheKey, CancellationToken token = default) + => cache.ExpireTimeAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); +} diff --git a/src/UiPath.Caching.Abstractions/HashCacheSyncExtensions.cs b/src/UiPath.Caching.Abstractions/HashCacheSyncExtensions.cs new file mode 100644 index 0000000..5c1f051 --- /dev/null +++ b/src/UiPath.Caching.Abstractions/HashCacheSyncExtensions.cs @@ -0,0 +1,75 @@ +namespace UiPath.Caching; + +/// +/// Blocking forwarders over the async API. Each blocks on the +/// underlying call via .AsTask().GetAwaiter().GetResult() — use only from sync call sites +/// that can tolerate the thread-blocking cost. +/// +// Excluded from coverage — forwarders with no behavior of their own; the async impls are what +// tests exercise. +[ExcludeFromCodeCoverage] +public static class HashCacheSyncExtensions +{ + public static T? GetItem(this IHashCache cache, CacheKey cacheKey, string field, CancellationToken token = default) + => cache.GetItemAsync(cacheKey, field, token).AsTask().GetAwaiter().GetResult(); + + public static IDictionary Get(this IHashCache cache, CacheKey cacheKey, CancellationToken token = default) + => cache.GetAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); + + public static IDictionary Get(this IHashCache cache, CacheKey cacheKey, string[] fields, CancellationToken token = default) + => cache.GetAsync(cacheKey, fields, token).AsTask().GetAwaiter().GetResult(); + + public static IDictionary GetOrAdd(this IHashCache cache, CacheKey cacheKey, Func> generator, CancellationToken token = default) + => cache.GetOrAddAsync(cacheKey, _ => Task.FromResult(generator()), token).AsTask().GetAwaiter().GetResult(); + + public static IDictionary GetOrAdd(this IHashCache cache, CacheKey cacheKey, Func> generator, TimeSpan? expiration, CancellationToken token = default) + => cache.GetOrAddAsync(cacheKey, _ => Task.FromResult(generator()), expiration, token).AsTask().GetAwaiter().GetResult(); + + public static IDictionary GetOrAdd(this IHashCache cache, CacheKey cacheKey, Func> generator, DateTimeOffset? expiration, CancellationToken token = default) + => cache.GetOrAddAsync(cacheKey, _ => Task.FromResult(generator()), expiration, token).AsTask().GetAwaiter().GetResult(); + + public static ICacheEntry> GetCacheEntry(this IHashCache cache, CacheKey cacheKey, CancellationToken token = default) + => cache.GetCacheEntryAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); + + public static bool Set(this IHashCache cache, CacheKey cacheKey, IDictionary values, CancellationToken token = default) + => cache.SetAsync(cacheKey, values, token).AsTask().GetAwaiter().GetResult(); + + public static bool Set(this IHashCache cache, CacheKey cacheKey, IDictionary values, TimeSpan? expiration, CancellationToken token = default) + => cache.SetAsync(cacheKey, values, expiration, token).AsTask().GetAwaiter().GetResult(); + + public static bool Set(this IHashCache cache, CacheKey cacheKey, IDictionary values, DateTimeOffset? expiration, CancellationToken token = default) + => cache.SetAsync(cacheKey, values, expiration, token).AsTask().GetAwaiter().GetResult(); + + public static bool Set(this IHashCache cache, CacheKey cacheKey, IDictionary values, HashCacheEntryOptions options, CancellationToken token = default) + => cache.SetAsync(cacheKey, values, options, token).AsTask().GetAwaiter().GetResult(); + + public static bool Refresh(this IHashCache cache, CacheKey cacheKey, CancellationToken token = default) + => cache.RefreshAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); + + public static bool Refresh(this IHashCache cache, CacheKey cacheKey, TimeSpan? expiration, CancellationToken token = default) + => cache.RefreshAsync(cacheKey, expiration, token).AsTask().GetAwaiter().GetResult(); + + public static bool Refresh(this IHashCache cache, CacheKey cacheKey, DateTimeOffset? expiration, CancellationToken token = default) + => cache.RefreshAsync(cacheKey, expiration, token).AsTask().GetAwaiter().GetResult(); + + public static bool Refresh(this IHashCache cache, CacheKey cacheKey, HashCacheEntryOptions options, CancellationToken token = default) + => cache.RefreshAsync(cacheKey, options, token).AsTask().GetAwaiter().GetResult(); + + public static bool Remove(this IHashCache cache, CacheKey cacheKey, CancellationToken token = default) + => cache.RemoveAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); + + public static bool Contains(this IHashCache cache, CacheKey cacheKey, CancellationToken token = default) + => cache.ContainsAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); + + public static TimeSpan? TimeToLive(this IHashCache cache, CacheKey cacheKey, CancellationToken token = default) + => cache.TimeToLiveAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); + + public static DateTimeOffset? ExpireTime(this IHashCache cache, CacheKey cacheKey, CancellationToken token = default) + => cache.ExpireTimeAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); + + public static IDictionary? GetMetadata(this IHashCache cache, CacheKey cacheKey, CancellationToken token = default) + => cache.GetMetadataAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); + + public static bool SetMetadata(this IHashCache cache, CacheKey cacheKey, IDictionary metadata, CancellationToken token = default) + => cache.SetMetadataAsync(cacheKey, metadata, token).AsTask().GetAwaiter().GetResult(); +} diff --git a/src/UiPath.Caching.Abstractions/ICacheOfT.Sync.cs b/src/UiPath.Caching.Abstractions/ICacheOfT.Sync.cs deleted file mode 100644 index 24c1aa1..0000000 --- a/src/UiPath.Caching.Abstractions/ICacheOfT.Sync.cs +++ /dev/null @@ -1,122 +0,0 @@ -namespace UiPath.Caching; - -// Sync forwarders over the async API. Each default interface method blocks on the underlying -// async call via .AsTask().GetAwaiter().GetResult() — use only from sync call sites that can -// tolerate the thread-blocking cost. Excluded from coverage — forwarders with no behavior of -// their own; the async impls are what tests exercise. -public partial interface ICache -{ - [ExcludeFromCodeCoverage] - T? Get(CacheKey cacheKey, CancellationToken token = default) - => GetAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - KeyValuePair[] Get(CacheKey[] cacheKeys, CancellationToken token = default) - => GetAsync(cacheKeys, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - T? GetOrAdd(CacheKey cacheKey, Func generator, CancellationToken token = default) - => GetOrAddAsync(cacheKey, _ => Task.FromResult(generator()), token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - T? GetOrAdd(CacheKey cacheKey, Func generator, TimeSpan? expiration, CancellationToken token = default) - => GetOrAddAsync(cacheKey, _ => Task.FromResult(generator()), expiration, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - T? GetOrAdd(CacheKey cacheKey, Func generator, DateTimeOffset? expiration, CancellationToken token = default) - => GetOrAddAsync(cacheKey, _ => Task.FromResult(generator()), expiration, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - KeyValuePair[] GetOrAdd(CacheKey[] cacheKeys, Func[]> generator, CancellationToken token = default) - => GetOrAddAsync( - Array.ConvertAll(cacheKeys, k => new KeyValuePair(k, k)), - (keys, _) => Task.FromResult(generator(keys)), - token) - .AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - KeyValuePair[] GetOrAdd(CacheKey[] cacheKeys, Func[]> generator, TimeSpan? expiration, CancellationToken token = default) - => GetOrAddAsync( - Array.ConvertAll(cacheKeys, k => new KeyValuePair(k, k)), - (keys, _) => Task.FromResult(generator(keys)), - expiration, - token) - .AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - KeyValuePair[] GetOrAdd(CacheKey[] cacheKeys, Func[]> generator, DateTimeOffset? expiration, CancellationToken token = default) - => GetOrAddAsync( - Array.ConvertAll(cacheKeys, k => new KeyValuePair(k, k)), - (keys, _) => Task.FromResult(generator(keys)), - expiration, - token) - .AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - bool Remove(CacheKey cacheKey, CancellationToken token = default) - => RemoveAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - bool Remove(CacheKey[] cacheKeys, CancellationToken token = default) - => RemoveAsync(cacheKeys, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - bool Set(CacheKey cacheKey, T? value, CancellationToken token = default) - => SetAsync(cacheKey, value, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - bool Set(CacheKey cacheKey, T? value, TimeSpan? expiration, CancellationToken token = default) - => SetAsync(cacheKey, value, expiration, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - bool Set(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CancellationToken token = default) - => SetAsync(cacheKey, value, expiration, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - bool Set(KeyValuePair[] keyValues, CancellationToken token = default) - => SetAsync(keyValues, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - bool Set(KeyValuePair[] keyValues, TimeSpan? expiration = null, CancellationToken token = default) - => SetAsync(keyValues, expiration, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - bool Set(KeyValuePair[] keyValues, DateTimeOffset? expiration = null, CancellationToken token = default) - => SetAsync(keyValues, expiration, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - bool TryAdd(CacheKey cacheKey, T? value, CancellationToken token = default) - => TryAddAsync(cacheKey, value, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - bool TryAdd(CacheKey cacheKey, T? value, TimeSpan? expiration, CancellationToken token = default) - => TryAddAsync(cacheKey, value, expiration, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - bool TryAdd(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CancellationToken token = default) - => TryAddAsync(cacheKey, value, expiration, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - bool Refresh(CacheKey cacheKey, CancellationToken token = default) - => RefreshAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - bool Refresh(CacheKey cacheKey, TimeSpan? expiration, CancellationToken token = default) - => RefreshAsync(cacheKey, expiration, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - bool Refresh(CacheKey cacheKey, DateTimeOffset? expiration, CancellationToken token = default) - => RefreshAsync(cacheKey, expiration, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - bool Contains(CacheKey cacheKey, CancellationToken token = default) - => ContainsAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - TimeSpan? TimeToLive(CacheKey cacheKey, CancellationToken token = default) - => TimeToLiveAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - DateTimeOffset? ExpireTime(CacheKey cacheKey, CancellationToken token = default) - => ExpireTimeAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); -} diff --git a/src/UiPath.Caching.Abstractions/ICacheOfT.cs b/src/UiPath.Caching.Abstractions/ICacheOfT.cs index 02e26fc..0eb0e69 100644 --- a/src/UiPath.Caching.Abstractions/ICacheOfT.cs +++ b/src/UiPath.Caching.Abstractions/ICacheOfT.cs @@ -1,5 +1,5 @@ namespace UiPath.Caching; -public partial interface ICache +public interface ICache { string Name { get; } diff --git a/src/UiPath.Caching.Abstractions/IHashCacheOfT.Sync.cs b/src/UiPath.Caching.Abstractions/IHashCacheOfT.Sync.cs deleted file mode 100644 index 94a23ca..0000000 --- a/src/UiPath.Caching.Abstractions/IHashCacheOfT.Sync.cs +++ /dev/null @@ -1,92 +0,0 @@ -namespace UiPath.Caching; - -// Sync forwarders over the async API. Each default interface method blocks on the underlying -// async call via .AsTask().GetAwaiter().GetResult() — use only from sync call sites that can -// tolerate the thread-blocking cost. Excluded from coverage — forwarders with no behavior of -// their own; the async impls are what tests exercise. -public partial interface IHashCache -{ - [ExcludeFromCodeCoverage] - T? GetItem(CacheKey cacheKey, string field, CancellationToken token = default) - => GetItemAsync(cacheKey, field, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - IDictionary Get(CacheKey cacheKey, CancellationToken token = default) - => GetAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - IDictionary Get(CacheKey cacheKey, string[] fields, CancellationToken token = default) - => GetAsync(cacheKey, fields, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - IDictionary GetOrAdd(CacheKey cacheKey, Func> generator, CancellationToken token = default) - => GetOrAddAsync(cacheKey, _ => Task.FromResult(generator()), token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - IDictionary GetOrAdd(CacheKey cacheKey, Func> generator, TimeSpan? expiration, CancellationToken token = default) - => GetOrAddAsync(cacheKey, _ => Task.FromResult(generator()), expiration, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - IDictionary GetOrAdd(CacheKey cacheKey, Func> generator, DateTimeOffset? expiration, CancellationToken token = default) - => GetOrAddAsync(cacheKey, _ => Task.FromResult(generator()), expiration, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - ICacheEntry> GetCacheEntry(CacheKey cacheKey, CancellationToken token = default) - => GetCacheEntryAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - bool Set(CacheKey cacheKey, IDictionary values, CancellationToken token = default) - => SetAsync(cacheKey, values, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - bool Set(CacheKey cacheKey, IDictionary values, TimeSpan? expiration, CancellationToken token = default) - => SetAsync(cacheKey, values, expiration, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - bool Set(CacheKey cacheKey, IDictionary values, DateTimeOffset? expiration, CancellationToken token = default) - => SetAsync(cacheKey, values, expiration, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - bool Set(CacheKey cacheKey, IDictionary values, HashCacheEntryOptions options, CancellationToken token = default) - => SetAsync(cacheKey, values, options, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - bool Refresh(CacheKey cacheKey, CancellationToken token = default) - => RefreshAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - bool Refresh(CacheKey cacheKey, TimeSpan? expiration, CancellationToken token = default) - => RefreshAsync(cacheKey, expiration, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - bool Refresh(CacheKey cacheKey, DateTimeOffset? expiration, CancellationToken token = default) - => RefreshAsync(cacheKey, expiration, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - bool Refresh(CacheKey cacheKey, HashCacheEntryOptions options, CancellationToken token = default) - => RefreshAsync(cacheKey, options, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - bool Remove(CacheKey cacheKey, CancellationToken token = default) - => RemoveAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - bool Contains(CacheKey cacheKey, CancellationToken token = default) - => ContainsAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - TimeSpan? TimeToLive(CacheKey cacheKey, CancellationToken token = default) - => TimeToLiveAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - DateTimeOffset? ExpireTime(CacheKey cacheKey, CancellationToken token = default) - => ExpireTimeAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - IDictionary? GetMetadata(CacheKey cacheKey, CancellationToken token = default) - => GetMetadataAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - bool SetMetadata(CacheKey cacheKey, IDictionary metadata, CancellationToken token = default) - => SetMetadataAsync(cacheKey, metadata, token).AsTask().GetAwaiter().GetResult(); -} diff --git a/src/UiPath.Caching.Abstractions/IHashCacheOfT.cs b/src/UiPath.Caching.Abstractions/IHashCacheOfT.cs index 566922e..9560f79 100644 --- a/src/UiPath.Caching.Abstractions/IHashCacheOfT.cs +++ b/src/UiPath.Caching.Abstractions/IHashCacheOfT.cs @@ -1,6 +1,6 @@ namespace UiPath.Caching; -public partial interface IHashCache +public interface IHashCache { string Name { get; } diff --git a/src/UiPath.Caching.Abstractions/PublicAPI.Shipped.txt b/src/UiPath.Caching.Abstractions/PublicAPI.Shipped.txt index 074b775..5d46a07 100644 --- a/src/UiPath.Caching.Abstractions/PublicAPI.Shipped.txt +++ b/src/UiPath.Caching.Abstractions/PublicAPI.Shipped.txt @@ -239,20 +239,10 @@ UiPath.Caching.ICache.RemoveAsync(UiPath.Caching.CacheKey cacheKey, System.Th UiPath.Caching.ICache.RemoveAsync(UiPath.Caching.CacheKey[]! cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.TimeToLiveAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache -UiPath.Caching.ICache.Contains(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool UiPath.Caching.ICache.ContainsAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.ExpireTime(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.DateTimeOffset? UiPath.Caching.ICache.ExpireTimeAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.Get(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> T? -UiPath.Caching.ICache.Get(UiPath.Caching.CacheKey[]! cacheKeys, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Collections.Generic.KeyValuePair[]! UiPath.Caching.ICache.GetAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.GetAsync(UiPath.Caching.CacheKey[]! cacheKeys, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask[]!> -UiPath.Caching.ICache.GetOrAdd(UiPath.Caching.CacheKey cacheKey, System.Func! generator, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> T? -UiPath.Caching.ICache.GetOrAdd(UiPath.Caching.CacheKey cacheKey, System.Func! generator, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> T? -UiPath.Caching.ICache.GetOrAdd(UiPath.Caching.CacheKey cacheKey, System.Func! generator, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> T? -UiPath.Caching.ICache.GetOrAdd(UiPath.Caching.CacheKey[]! cacheKeys, System.Func[]!>! generator, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Collections.Generic.KeyValuePair[]! -UiPath.Caching.ICache.GetOrAdd(UiPath.Caching.CacheKey[]! cacheKeys, System.Func[]!>! generator, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Collections.Generic.KeyValuePair[]! -UiPath.Caching.ICache.GetOrAdd(UiPath.Caching.CacheKey[]! cacheKeys, System.Func[]!>! generator, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Collections.Generic.KeyValuePair[]! UiPath.Caching.ICache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask @@ -260,29 +250,17 @@ UiPath.Caching.ICache.GetOrAddAsync(System.Collections.Generic.KeyVal UiPath.Caching.ICache.GetOrAddAsync(System.Collections.Generic.KeyValuePair[]! entries, System.Func[]!>!>! generator, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask[]!> UiPath.Caching.ICache.GetOrAddAsync(System.Collections.Generic.KeyValuePair[]! entries, System.Func[]!>!>! generator, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask[]!> UiPath.Caching.ICache.Name.get -> string! -UiPath.Caching.ICache.Refresh(UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool -UiPath.Caching.ICache.Refresh(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool -UiPath.Caching.ICache.Refresh(UiPath.Caching.CacheKey cacheKey, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool UiPath.Caching.ICache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.Remove(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool -UiPath.Caching.ICache.Remove(UiPath.Caching.CacheKey[]! cacheKeys, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool UiPath.Caching.ICache.RemoveAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.RemoveAsync(UiPath.Caching.CacheKey[]! cacheKeys, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.Set(System.Collections.Generic.KeyValuePair[]! keyValues, System.DateTimeOffset? expiration = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool -UiPath.Caching.ICache.Set(System.Collections.Generic.KeyValuePair[]! keyValues, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool -UiPath.Caching.ICache.Set(System.Collections.Generic.KeyValuePair[]! keyValues, System.TimeSpan? expiration = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool -UiPath.Caching.ICache.Set(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool -UiPath.Caching.ICache.Set(UiPath.Caching.CacheKey cacheKey, T? value, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool -UiPath.Caching.ICache.Set(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool UiPath.Caching.ICache.SetAsync(System.Collections.Generic.KeyValuePair[]! keyValues, System.DateTimeOffset? expiration = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.SetAsync(System.Collections.Generic.KeyValuePair[]! keyValues, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.SetAsync(System.Collections.Generic.KeyValuePair[]! keyValues, System.TimeSpan? expiration = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.SetAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.SetAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.SetAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.TimeToLive(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.TimeSpan? UiPath.Caching.ICache.TimeToLiveAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICacheChangeToken UiPath.Caching.ICacheChangeToken.Expiration.get -> System.DateTimeOffset? @@ -330,48 +308,27 @@ UiPath.Caching.IHashCache.RemoveAsync(UiPath.Caching.CacheKey cacheKey, Syste UiPath.Caching.IHashCache.SetMetadataAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! metadata, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.IHashCache.TimeToLiveAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.IHashCache -UiPath.Caching.IHashCache.Contains(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool UiPath.Caching.IHashCache.ContainsAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.IHashCache.ExpireTime(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.DateTimeOffset? UiPath.Caching.IHashCache.ExpireTimeAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.IHashCache.Get(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IDictionary! -UiPath.Caching.IHashCache.Get(UiPath.Caching.CacheKey cacheKey, string![]! fields, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IDictionary! UiPath.Caching.IHashCache.GetAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> UiPath.Caching.IHashCache.GetAsync(UiPath.Caching.CacheKey cacheKey, string![]! fields, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> -UiPath.Caching.IHashCache.GetCacheEntry(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> UiPath.Caching.ICacheEntry!>! UiPath.Caching.IHashCache.GetCacheEntryAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!>!> -UiPath.Caching.IHashCache.GetItem(UiPath.Caching.CacheKey cacheKey, string! field, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> T? UiPath.Caching.IHashCache.GetItemAsync(UiPath.Caching.CacheKey cacheKey, string! field, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.IHashCache.GetMetadata(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IDictionary? UiPath.Caching.IHashCache.GetMetadataAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask?> -UiPath.Caching.IHashCache.GetOrAdd(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IDictionary! -UiPath.Caching.IHashCache.GetOrAdd(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IDictionary! -UiPath.Caching.IHashCache.GetOrAdd(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IDictionary! UiPath.Caching.IHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> UiPath.Caching.IHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> UiPath.Caching.IHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> UiPath.Caching.IHashCache.Name.get -> string! -UiPath.Caching.IHashCache.Refresh(UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool -UiPath.Caching.IHashCache.Refresh(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool -UiPath.Caching.IHashCache.Refresh(UiPath.Caching.CacheKey cacheKey, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool -UiPath.Caching.IHashCache.Refresh(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.HashCacheEntryOptions options, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool UiPath.Caching.IHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.IHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.IHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.IHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.HashCacheEntryOptions options, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.IHashCache.Remove(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool UiPath.Caching.IHashCache.RemoveAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.IHashCache.Set(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool -UiPath.Caching.IHashCache.Set(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool -UiPath.Caching.IHashCache.Set(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool -UiPath.Caching.IHashCache.Set(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, UiPath.Caching.HashCacheEntryOptions options, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool UiPath.Caching.IHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.IHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.IHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.IHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, UiPath.Caching.HashCacheEntryOptions options, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.IHashCache.SetMetadata(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! metadata, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool UiPath.Caching.IHashCache.SetMetadataAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! metadata, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.IHashCache.TimeToLive(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.TimeSpan? UiPath.Caching.IHashCache.TimeToLiveAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ISerializerProxy UiPath.Caching.ISerializerProxy.Deserialize(T1? value) -> T? diff --git a/src/UiPath.Caching.Abstractions/PublicAPI.Unshipped.txt b/src/UiPath.Caching.Abstractions/PublicAPI.Unshipped.txt index e7609d2..3651025 100644 --- a/src/UiPath.Caching.Abstractions/PublicAPI.Unshipped.txt +++ b/src/UiPath.Caching.Abstractions/PublicAPI.Unshipped.txt @@ -13,7 +13,9 @@ UiPath.Caching.CacheKeyComparer UiPath.Caching.CacheKeyComparer.CacheKeyComparer() -> void UiPath.Caching.CacheOptions.KeyCasing.get -> UiPath.Caching.CacheKeyCasing UiPath.Caching.CacheOptions.KeyCasing.set -> void +UiPath.Caching.CacheSyncExtensions UiPath.Caching.HashCacheExtensions +UiPath.Caching.HashCacheSyncExtensions UiPath.Caching.ICache.GetAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.GetAsync(UiPath.Caching.CacheKey[]! cacheKeys, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask[]!> UiPath.Caching.ICache.GetCacheEntriesAsync(UiPath.Caching.CacheKey[]! cacheKeys, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!>[]!> @@ -36,9 +38,6 @@ UiPath.Caching.ICache.SetAsync(UiPath.Caching.CacheKey cacheKey, T? value, Ui UiPath.Caching.ICache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.TryAdd(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool -UiPath.Caching.ICache.TryAdd(UiPath.Caching.CacheKey cacheKey, T? value, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool -UiPath.Caching.ICache.TryAdd(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool UiPath.Caching.ICache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask @@ -122,6 +121,31 @@ static UiPath.Caching.CacheKey.DefaultCasing.get -> UiPath.Caching.CacheKeyCasin static UiPath.Caching.CacheKey.DefaultCasing.set -> void static UiPath.Caching.CacheKeyComparer.Insensitive.get -> UiPath.Caching.CacheKeyComparer! static UiPath.Caching.CacheKeyComparer.Sensitive.get -> UiPath.Caching.CacheKeyComparer! +static UiPath.Caching.CacheSyncExtensions.Contains(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.CacheSyncExtensions.ExpireTime(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.DateTimeOffset? +static UiPath.Caching.CacheSyncExtensions.Get(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> T? +static UiPath.Caching.CacheSyncExtensions.Get(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey[]! cacheKeys, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Collections.Generic.KeyValuePair[]! +static UiPath.Caching.CacheSyncExtensions.GetOrAdd(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.Func! generator, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> T? +static UiPath.Caching.CacheSyncExtensions.GetOrAdd(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.Func! generator, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> T? +static UiPath.Caching.CacheSyncExtensions.GetOrAdd(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.Func! generator, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> T? +static UiPath.Caching.CacheSyncExtensions.GetOrAdd(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey[]! cacheKeys, System.Func[]!>! generator, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Collections.Generic.KeyValuePair[]! +static UiPath.Caching.CacheSyncExtensions.GetOrAdd(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey[]! cacheKeys, System.Func[]!>! generator, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Collections.Generic.KeyValuePair[]! +static UiPath.Caching.CacheSyncExtensions.GetOrAdd(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey[]! cacheKeys, System.Func[]!>! generator, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Collections.Generic.KeyValuePair[]! +static UiPath.Caching.CacheSyncExtensions.Refresh(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.CacheSyncExtensions.Refresh(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.CacheSyncExtensions.Refresh(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.CacheSyncExtensions.Remove(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.CacheSyncExtensions.Remove(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey[]! cacheKeys, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.CacheSyncExtensions.Set(this UiPath.Caching.ICache! cache, System.Collections.Generic.KeyValuePair[]! keyValues, System.DateTimeOffset? expiration = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.CacheSyncExtensions.Set(this UiPath.Caching.ICache! cache, System.Collections.Generic.KeyValuePair[]! keyValues, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.CacheSyncExtensions.Set(this UiPath.Caching.ICache! cache, System.Collections.Generic.KeyValuePair[]! keyValues, System.TimeSpan? expiration = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.CacheSyncExtensions.Set(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.CacheSyncExtensions.Set(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, T? value, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.CacheSyncExtensions.Set(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.CacheSyncExtensions.TimeToLive(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.TimeSpan? +static UiPath.Caching.CacheSyncExtensions.TryAdd(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.CacheSyncExtensions.TryAdd(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, T? value, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.CacheSyncExtensions.TryAdd(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool static UiPath.Caching.HashCacheExtensions.GetAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> static UiPath.Caching.HashCacheExtensions.GetAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, string![]! fields, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> static UiPath.Caching.HashCacheExtensions.GetCacheEntryAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!>!> @@ -138,3 +162,24 @@ static UiPath.Caching.HashCacheExtensions.SetAsync(this UiPath.Caching.IHashC static UiPath.Caching.HashCacheExtensions.SetAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static UiPath.Caching.HashCacheExtensions.SetAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static UiPath.Caching.HashCacheExtensions.SetAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, UiPath.Caching.HashCacheEntryOptions options, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.HashCacheSyncExtensions.Contains(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.HashCacheSyncExtensions.ExpireTime(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.DateTimeOffset? +static UiPath.Caching.HashCacheSyncExtensions.Get(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IDictionary! +static UiPath.Caching.HashCacheSyncExtensions.Get(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, string![]! fields, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IDictionary! +static UiPath.Caching.HashCacheSyncExtensions.GetCacheEntry(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> UiPath.Caching.ICacheEntry!>! +static UiPath.Caching.HashCacheSyncExtensions.GetItem(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, string! field, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> T? +static UiPath.Caching.HashCacheSyncExtensions.GetMetadata(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IDictionary? +static UiPath.Caching.HashCacheSyncExtensions.GetOrAdd(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IDictionary! +static UiPath.Caching.HashCacheSyncExtensions.GetOrAdd(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IDictionary! +static UiPath.Caching.HashCacheSyncExtensions.GetOrAdd(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IDictionary! +static UiPath.Caching.HashCacheSyncExtensions.Refresh(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.HashCacheSyncExtensions.Refresh(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.HashCacheSyncExtensions.Refresh(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.HashCacheSyncExtensions.Refresh(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, UiPath.Caching.HashCacheEntryOptions options, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.HashCacheSyncExtensions.Remove(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.HashCacheSyncExtensions.Set(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.HashCacheSyncExtensions.Set(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.HashCacheSyncExtensions.Set(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.HashCacheSyncExtensions.Set(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, UiPath.Caching.HashCacheEntryOptions options, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.HashCacheSyncExtensions.SetMetadata(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! metadata, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.HashCacheSyncExtensions.TimeToLive(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.TimeSpan? diff --git a/src/UiPath.Caching.Queue/ISetCacheOfT.Sync.cs b/src/UiPath.Caching.Queue/ISetCacheOfT.Sync.cs deleted file mode 100644 index 716dbf8..0000000 --- a/src/UiPath.Caching.Queue/ISetCacheOfT.Sync.cs +++ /dev/null @@ -1,60 +0,0 @@ -namespace UiPath.Caching; - -// Sync forwarders over the async API. Each default interface method blocks on the underlying -// async call via .AsTask().GetAwaiter().GetResult() — use only from sync call sites that can -// tolerate the thread-blocking cost. Excluded from coverage — forwarders with no behavior of -// their own; the async impls are what tests exercise. -public partial interface ISetCache -{ - [ExcludeFromCodeCoverage] - bool Add(CacheKey cacheKey, T item, CancellationToken token = default) - => AddAsync(cacheKey, item, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - long Add(CacheKey cacheKey, IEnumerable items, CancellationToken token = default) - => AddAsync(cacheKey, items, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - long Add(CacheKey cacheKey, IEnumerable items, TimeSpan? expiration, CancellationToken token = default) - => AddAsync(cacheKey, items, expiration, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - long Add(CacheKey cacheKey, IEnumerable items, DateTimeOffset? expiration, CancellationToken token = default) - => AddAsync(cacheKey, items, expiration, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - T? Pop(CacheKey cacheKey, CancellationToken token = default) - => PopAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - IReadOnlyCollection Pop(CacheKey cacheKey, long count, CancellationToken token = default) - => PopAsync(cacheKey, count, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - IReadOnlyCollection Members(CacheKey cacheKey, CancellationToken token = default) - => MembersAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - bool ContainsItem(CacheKey cacheKey, T item, CancellationToken token = default) - => ContainsItemAsync(cacheKey, item, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - long Count(CacheKey cacheKey, CancellationToken token = default) - => CountAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - bool RemoveItem(CacheKey cacheKey, T item, CancellationToken token = default) - => RemoveItemAsync(cacheKey, item, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - long RemoveItems(CacheKey cacheKey, IEnumerable items, CancellationToken token = default) - => RemoveItemsAsync(cacheKey, items, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - bool Remove(CacheKey cacheKey, CancellationToken token = default) - => RemoveAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); - - [ExcludeFromCodeCoverage] - bool Contains(CacheKey cacheKey, CancellationToken token = default) - => ContainsAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); -} diff --git a/src/UiPath.Caching.Queue/ISetCacheOfT.cs b/src/UiPath.Caching.Queue/ISetCacheOfT.cs index 3a7050a..875783b 100644 --- a/src/UiPath.Caching.Queue/ISetCacheOfT.cs +++ b/src/UiPath.Caching.Queue/ISetCacheOfT.cs @@ -6,7 +6,7 @@ namespace UiPath.Caching; /// insertion order and PopAsync removes a random member (Redis SPOP). Use the dedicated list caches /// when order matters. /// -public partial interface ISetCache +public interface ISetCache { string Name { get; } diff --git a/src/UiPath.Caching.Queue/PublicAPI.Shipped.txt b/src/UiPath.Caching.Queue/PublicAPI.Shipped.txt index c6abc0d..309ca31 100644 --- a/src/UiPath.Caching.Queue/PublicAPI.Shipped.txt +++ b/src/UiPath.Caching.Queue/PublicAPI.Shipped.txt @@ -16,32 +16,19 @@ UiPath.Caching.ISetCache.RemoveAsync(UiPath.Caching.CacheKey cacheKey, System UiPath.Caching.ISetCache.RemoveItemAsync(UiPath.Caching.CacheKey cacheKey, T item, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ISetCache.RemoveItemsAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ISetCache -UiPath.Caching.ISetCache.Add(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> long -UiPath.Caching.ISetCache.Add(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> long -UiPath.Caching.ISetCache.Add(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> long -UiPath.Caching.ISetCache.Add(UiPath.Caching.CacheKey cacheKey, T item, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool UiPath.Caching.ISetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ISetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ISetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ISetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, T item, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ISetCache.Contains(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool UiPath.Caching.ISetCache.ContainsAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ISetCache.ContainsItem(UiPath.Caching.CacheKey cacheKey, T item, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool UiPath.Caching.ISetCache.ContainsItemAsync(UiPath.Caching.CacheKey cacheKey, T item, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ISetCache.Count(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> long UiPath.Caching.ISetCache.CountAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ISetCache.Members(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IReadOnlyCollection! UiPath.Caching.ISetCache.MembersAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> UiPath.Caching.ISetCache.Name.get -> string! -UiPath.Caching.ISetCache.Pop(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> T? -UiPath.Caching.ISetCache.Pop(UiPath.Caching.CacheKey cacheKey, long count, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IReadOnlyCollection! UiPath.Caching.ISetCache.PopAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ISetCache.PopAsync(UiPath.Caching.CacheKey cacheKey, long count, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> -UiPath.Caching.ISetCache.Remove(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool UiPath.Caching.ISetCache.RemoveAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ISetCache.RemoveItem(UiPath.Caching.CacheKey cacheKey, T item, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool UiPath.Caching.ISetCache.RemoveItemAsync(UiPath.Caching.CacheKey cacheKey, T item, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ISetCache.RemoveItems(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> long UiPath.Caching.ISetCache.RemoveItemsAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.InMemoryQueueCacheOptions UiPath.Caching.InMemoryQueueCacheOptions.CompactionPercentage.get -> double? diff --git a/src/UiPath.Caching.Queue/PublicAPI.Unshipped.txt b/src/UiPath.Caching.Queue/PublicAPI.Unshipped.txt index 7b0404a..6b61b88 100644 --- a/src/UiPath.Caching.Queue/PublicAPI.Unshipped.txt +++ b/src/UiPath.Caching.Queue/PublicAPI.Unshipped.txt @@ -21,6 +21,7 @@ UiPath.Caching.Redis.RedisSetCache.MembersAsync(UiPath.Caching.CacheKey cache UiPath.Caching.Redis.RedisSetCache.PopAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.Redis.RedisSetCache.PopAsync(UiPath.Caching.CacheKey cacheKey, long count, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> UiPath.Caching.SetCacheExtensions +UiPath.Caching.SetCacheSyncExtensions static UiPath.Caching.SetCacheExtensions.AddAsync(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static UiPath.Caching.SetCacheExtensions.AddAsync(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static UiPath.Caching.SetCacheExtensions.AddAsync(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask @@ -28,3 +29,16 @@ static UiPath.Caching.SetCacheExtensions.AddAsync(this UiPath.Caching.ISetCac static UiPath.Caching.SetCacheExtensions.MembersAsync(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> static UiPath.Caching.SetCacheExtensions.PopAsync(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static UiPath.Caching.SetCacheExtensions.PopAsync(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, long count, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +static UiPath.Caching.SetCacheSyncExtensions.Add(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> long +static UiPath.Caching.SetCacheSyncExtensions.Add(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> long +static UiPath.Caching.SetCacheSyncExtensions.Add(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> long +static UiPath.Caching.SetCacheSyncExtensions.Add(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, T item, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.SetCacheSyncExtensions.Contains(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.SetCacheSyncExtensions.ContainsItem(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, T item, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.SetCacheSyncExtensions.Count(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> long +static UiPath.Caching.SetCacheSyncExtensions.Members(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IReadOnlyCollection! +static UiPath.Caching.SetCacheSyncExtensions.Pop(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> T? +static UiPath.Caching.SetCacheSyncExtensions.Pop(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, long count, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IReadOnlyCollection! +static UiPath.Caching.SetCacheSyncExtensions.Remove(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.SetCacheSyncExtensions.RemoveItem(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, T item, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.SetCacheSyncExtensions.RemoveItems(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> long diff --git a/src/UiPath.Caching.Queue/SetCacheSyncExtensions.cs b/src/UiPath.Caching.Queue/SetCacheSyncExtensions.cs new file mode 100644 index 0000000..33b27b0 --- /dev/null +++ b/src/UiPath.Caching.Queue/SetCacheSyncExtensions.cs @@ -0,0 +1,57 @@ +namespace UiPath.Caching; + +/// +/// Blocking forwarders over the async API. Each blocks on the +/// underlying call via .AsTask().GetAwaiter().GetResult() — use only from sync call sites +/// that can tolerate the thread-blocking cost. +/// +// Excluded from coverage — forwarders with no behavior of their own; the async impls are what +// tests exercise. +[ExcludeFromCodeCoverage] +public static class SetCacheSyncExtensions +{ + /// + public static bool Add(this ISetCache cache, CacheKey cacheKey, T item, CancellationToken token = default) + => cache.AddAsync(cacheKey, item, token).AsTask().GetAwaiter().GetResult(); + + /// + public static long Add(this ISetCache cache, CacheKey cacheKey, IEnumerable items, CancellationToken token = default) + => cache.AddAsync(cacheKey, items, token).AsTask().GetAwaiter().GetResult(); + + /// + public static long Add(this ISetCache cache, CacheKey cacheKey, IEnumerable items, TimeSpan? expiration, CancellationToken token = default) + => cache.AddAsync(cacheKey, items, expiration, token).AsTask().GetAwaiter().GetResult(); + + /// + public static long Add(this ISetCache cache, CacheKey cacheKey, IEnumerable items, DateTimeOffset? expiration, CancellationToken token = default) + => cache.AddAsync(cacheKey, items, expiration, token).AsTask().GetAwaiter().GetResult(); + + /// + public static T? Pop(this ISetCache cache, CacheKey cacheKey, CancellationToken token = default) + => cache.PopAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); + + /// + public static IReadOnlyCollection Pop(this ISetCache cache, CacheKey cacheKey, long count, CancellationToken token = default) + => cache.PopAsync(cacheKey, count, token).AsTask().GetAwaiter().GetResult(); + + public static IReadOnlyCollection Members(this ISetCache cache, CacheKey cacheKey, CancellationToken token = default) + => cache.MembersAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); + + public static bool ContainsItem(this ISetCache cache, CacheKey cacheKey, T item, CancellationToken token = default) + => cache.ContainsItemAsync(cacheKey, item, token).AsTask().GetAwaiter().GetResult(); + + public static long Count(this ISetCache cache, CacheKey cacheKey, CancellationToken token = default) + => cache.CountAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); + + public static bool RemoveItem(this ISetCache cache, CacheKey cacheKey, T item, CancellationToken token = default) + => cache.RemoveItemAsync(cacheKey, item, token).AsTask().GetAwaiter().GetResult(); + + public static long RemoveItems(this ISetCache cache, CacheKey cacheKey, IEnumerable items, CancellationToken token = default) + => cache.RemoveItemsAsync(cacheKey, items, token).AsTask().GetAwaiter().GetResult(); + + public static bool Remove(this ISetCache cache, CacheKey cacheKey, CancellationToken token = default) + => cache.RemoveAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); + + public static bool Contains(this ISetCache cache, CacheKey cacheKey, CancellationToken token = default) + => cache.ContainsAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); +} From 628b6fde426b53170ff862940f333db308d4faf5 Mon Sep 17 00:00:00 2001 From: Cosmin Staicu Date: Wed, 2 Sep 2026 22:28:35 +0300 Subject: [PATCH 7/9] test: close a data race on the quarantine test's fail flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found while investigating a third intermittent net10.0 failure: RedisStreamSubjectWriterTests.Unknown_command_quarantine_is_lifted_when_the_connection_reconnects flips a captured `fail` bool from the test thread while the writer's fetch loop reads it from its own thread, with no barrier. The neighbouring test in the same file already reads its `attempts` counter through Interlocked/Volatile; this flag was the outlier. Now written with Volatile.Write before the retry-gate release and read with Volatile.Read, so a thread observing the release also observes the flip. This does NOT fix the flake. The test was still seen failing under parallel load after this change, with `recovered` false after its 10s budget and the same ~21s duration. Two hypotheses are ruled out: the wake is not lost (the retry gate is a SemaphoreSlim, so a Release preceding WaitAsync is preserved — and ReleaseRetryGate deliberately swallows SemaphoreFullException for exactly that case), and it is not this data race. I could not capture the assertion message: the failure did not reproduce in five subsequent loaded runs, so the root cause is still open. Committing the race fix on its own merits rather than leaving an unsynchronized cross-thread flag in place. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_015RLSnsYiZsqeaHHYYybXW1 Signed-off-by: Cosmin Staicu --- .../Broadcast/RedisStreamSubjectWriterTests.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/UiPath.Caching.Tests/Broadcast/RedisStreamSubjectWriterTests.cs b/tests/UiPath.Caching.Tests/Broadcast/RedisStreamSubjectWriterTests.cs index 688d580..2f75a9c 100644 --- a/tests/UiPath.Caching.Tests/Broadcast/RedisStreamSubjectWriterTests.cs +++ b/tests/UiPath.Caching.Tests/Broadcast/RedisStreamSubjectWriterTests.cs @@ -314,9 +314,14 @@ public async Task Unknown_command_quarantine_is_lifted_when_the_connection_recon } }; + // Read through Volatile, like `attempts` above: this flag is written by the test thread and + // read by the fetch loop's thread, so a plain capture is a data race. Closing it does not + // fully de-flake this test — it was still seen failing under parallel load afterwards, with + // `recovered` false after the 10s budget — but an unsynchronized cross-thread flag is not + // something to leave in place while chasing that. var fail = true; _database.StreamReadGroupAsync(_context.Topic, _context.ConsumerGroup, _context.ConsumerName, ">", _context.PollBatchSize) - .ReturnsForAnyArgs(_ => fail + .ReturnsForAnyArgs(_ => Volatile.Read(ref fail) ? throw UnknownCommandError("ERR unknown command 'XREADGROUP'") : Task.FromResult(Array.Empty())); @@ -340,7 +345,8 @@ public async Task Unknown_command_quarantine_is_lifted_when_the_connection_recon await loggedCritical.Task.WaitAsync(WaitTimeout, TestContext.Current.CancellationToken); // Server now answers XREADGROUP; the reconnect must wake the loop instead of waiting out the backoff. - fail = false; + // Written before the release below, so a thread that observes the release also observes this. + Volatile.Write(ref fail, false); connectionState.OnReconnected += Raise.Event(connectionState, EventArgs.Empty); var recovered = await WaitUntil(() => recordingLogger.Records.Any( From 9f0dcfb13392800b2295707f4616d92b4f69bea8 Mon Sep 17 00:00:00 2001 From: Cosmin Staicu Date: Thu, 3 Sep 2026 13:13:54 +0300 Subject: [PATCH 8/9] feat(cache)!: make the per-call expiration non-nullable and reject one that cannot be honored MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `TimeSpan? expiration` / `DateTimeOffset? expiration` become `TimeSpan` / `DateTimeOffset` on every write across `ICache`, `ICache`, `IHashCache`, `IHashCache`, `ISetCache`, `ISetCache`, their extension surfaces and every implementation. The nullable was a redundant third state. Each of these members already has a sibling overload with no `expiration` parameter, and `null` resolved through the exact same chain as omitting it: `expiration ?? policy.DistributedExpiration ?? options.DefaultExpiration`. It also made `SetAsync(key, value, null)` ambiguous (CS0121) between the `TimeSpan?` and `DateTimeOffset?` overloads, which is why the forwarders had to spell out `(CachePolicy?)null` — and why `SetAsync(pairs)` with a single argument did not compile at all. With the third state gone there is nothing left for a value that cannot be honored to mean, so it is refused at the boundary instead of absorbed: a duration that is not strictly positive, or a deadline at or before the cache's current time, raises `ArgumentOutOfRangeException` and nothing is written. Previously such a value was quietly treated as "no expiration" and, on `TryAddAsync`, answered `false` — indistinguishable from "somebody else holds the key", the one confusion that API's contract asks callers to design around. `TimeSpan.MaxValue` / `DateTimeOffset.MaxValue` stay valid: they are how the providers spell "no TTL". The new public `CacheExpiration` (`ThrowIfNotPositive`, `ThrowIfNotFuture`, `ToDuration`) holds the guard for out-of-tree implementations. `RedisCacheBase` and `MultilayerCacheBase` grew `PolicyDuration` / `PolicyDeadline` for the no-expiration path and `CallerDuration` / `CallerDeadline` / `CallerWrite` for the validated one, which is what lets each write overload resolve its lifetime directly rather than threading a nullable through a shared body. The implementations shrink accordingly — `MultilayerCache` and `MultilayerHashCache` lose the `if (expiration.HasValue) … else …` blocks entirely. Nullability stays where it means *inherit*: `CachePolicy.LocalExpiration` / `DistributedExpiration`, the providers' `DefaultExpiration`, and `HashCacheEntryOptions.ExpireTime` / `TimeToLive`. Reads stay nullable too — `TimeToLiveAsync` / `ExpireTimeAsync` still return `null` for a key with no TTL. `NullCache`, `NullHashCache` and `NullSetCache` read no argument at all — not the key, not the type, not the expiration — so they enforce nothing and keep degrading to "caching is off, carry on". `UiPathDistributedCache` now expresses "no caller TTL and no adapter default" by calling the overload that carries no expiration, rather than by passing null. 123 entries leave `PublicAPI.Shipped.txt` (107 in `Abstractions`, 14 in `Queue`, 2 in `UiPath.Caching`); `ICacheEntry.NewEntry(DateTimeOffset?)` keeps its nullable, being a read-side derive rather than a write. Signed-off-by: Cosmin Staicu Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_011QeZZPMo8bFYz98AuPZU2V Signed-off-by: Cosmin Staicu --- CHANGELOG.md | 23 ++ docs/recipes/conditional-add.md | 8 +- docs/reference/interfaces.md | 101 +++++---- .../CacheExpiration.cs | 62 ++++++ .../CacheExtensions.cs | 24 +- src/UiPath.Caching.Abstractions/CacheOfT.cs | 24 +- .../CacheSyncExtensions.cs | 28 +-- .../HashCacheExtensions.cs | 14 +- .../HashCacheOfT.cs | 12 +- .../HashCacheSyncExtensions.cs | 12 +- src/UiPath.Caching.Abstractions/ICache.cs | 41 ++-- src/UiPath.Caching.Abstractions/ICacheOfT.cs | 28 +-- src/UiPath.Caching.Abstractions/IHashCache.cs | 23 +- .../IHashCacheOfT.cs | 12 +- src/UiPath.Caching.Abstractions/NullCache.cs | 20 +- .../NullHashCache.cs | 14 +- .../PublicAPI.Shipped.txt | 32 --- .../PublicAPI.Unshipped.txt | 186 +++++++++------- src/UiPath.Caching.Queue/ISetCache.cs | 13 +- src/UiPath.Caching.Queue/ISetCacheOfT.cs | 4 +- .../MultilayerSetCache.cs | 17 +- src/UiPath.Caching.Queue/NullSetCache.cs | 4 +- .../PublicAPI.Shipped.txt | 4 - .../PublicAPI.Unshipped.txt | 24 +- src/UiPath.Caching.Queue/RedisSetCache.cs | 20 +- .../SetCacheExtensions.cs | 4 +- src/UiPath.Caching.Queue/SetCacheOfT.cs | 4 +- .../SetCacheSyncExtensions.cs | 4 +- .../Distributed/UiPathDistributedCache.cs | 19 +- src/UiPath.Caching/MultilayerCache.cs | 134 +++++------- src/UiPath.Caching/MultilayerCacheBase.cs | 31 +++ src/UiPath.Caching/MultilayerHashCache.cs | 75 ++----- src/UiPath.Caching/PublicAPI.Shipped.txt | 8 +- src/UiPath.Caching/PublicAPI.Unshipped.txt | 6 + src/UiPath.Caching/Redis/RedisCache.cs | 91 ++++---- src/UiPath.Caching/Redis/RedisCacheBase.cs | 42 +++- src/UiPath.Caching/Redis/RedisHashCache.cs | 54 ++--- .../CacheExpirationTests.cs | 207 ++++++++++++++++++ .../DistributedCacheRedisIntegrationTests.cs | 4 +- .../UiPathDistributedCacheTests.cs | 47 ++-- .../Fakes/DictionaryCache.cs | 20 +- .../InMemorySetCacheTests.cs | 6 +- .../MultilayerCacheBatchGetOrAddLockTests.cs | 4 +- .../MultilayerCacheGetOrAddLockTests.cs | 2 +- .../MemoryCacheSetterTests.cs | 2 +- .../MultilayerCacheBatchGetOrAddTests.cs | 4 +- .../MultilayerCacheBatchRehydrateTests.cs | 2 +- ...MultilayerCachePerNamePolicyWiringTests.cs | 73 +++--- .../MultilayerCacheRehydrateTests.cs | 16 +- .../MultilayerCacheTests.cs | 28 +-- .../MultilayerCacheTryAddTests.cs | 75 ++++--- .../MultilayerHashCachePerNameJitterTests.cs | 4 +- .../MultilayerHashCacheTests.cs | 12 +- .../MultilayerSetCacheTests.cs | 2 +- .../NullCacheConditionalAddTests.cs | 11 +- .../Redis/RedisCacheTests.cs | 51 +++-- .../Redis/RedisCacheTryAddTests.cs | 11 +- .../Redis/RedisHashCacheTests.cs | 34 +-- 58 files changed, 1112 insertions(+), 725 deletions(-) create mode 100644 src/UiPath.Caching.Abstractions/CacheExpiration.cs create mode 100644 tests/UiPath.Caching.Tests/CacheExpirationTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index b14a453..eee5d46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -101,6 +101,29 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ### Changed +- **BREAKING:** the per-call `expiration` is no longer nullable. Every write on `ICache`, + `ICache`, `IHashCache`, `IHashCache`, `ISetCache`, `ISetCache` and their extension + surfaces takes `TimeSpan` / `DateTimeOffset` instead of `TimeSpan?` / `DateTimeOffset?`. The + nullable was a redundant third state: each of these members already has a sibling overload with no + `expiration` parameter, and passing `null` meant exactly the same thing as not passing it — fall + back to `CachePolicy.DistributedExpiration`, then the provider's `DefaultExpiration`. It also made + `cache.SetAsync(key, value, null)` ambiguous (`CS0121`) between the `TimeSpan?` and + `DateTimeOffset?` overloads, which is why the forwarders had to spell out `(CachePolicy?)null`. + Callers passing a real value are unaffected; a caller forwarding its own `TimeSpan?` now branches + on it and calls the overload without an expiration for the null case. Nullability stays where it + means *inherit* — `CachePolicy.LocalExpiration` / `DistributedExpiration`, the providers' + `DefaultExpiration`, `HashCacheEntryOptions.ExpireTime` / `TimeToLive` — and on reads, where + `TimeToLiveAsync` / `ExpireTimeAsync` still return `null` for a key with no TTL. +- **BREAKING:** a per-call `expiration` that cannot be honored is now rejected instead of silently + absorbed. A `TimeSpan` that is not strictly positive, or a `DateTimeOffset` at or before the + cache's current time, raises `ArgumentOutOfRangeException` (`ParamName` `"expiration"`) and nothing + is written. Previously such a value was quietly treated as "no expiration" and, on `TryAddAsync`, + answered `false` — the same answer as "somebody else holds the key". With the nullable gone there + is no third state left to carry that meaning, so the argument is refused at the boundary. The new + public `CacheExpiration` helper (`ThrowIfNotPositive`, `ThrowIfNotFuture`, `ToDuration`) holds the + guard for out-of-tree implementations. `TimeSpan.MaxValue` and `DateTimeOffset.MaxValue` remain + valid — they are how the providers spell "no TTL". `NullCache`, `NullHashCache` and `NullSetCache` + read no argument at all and so enforce nothing, keeping "caching is off, carry on". - **BREAKING:** `ICache.Compat.cs`, `IHashCache.Compat.cs` and `ISetCache.Compat.cs` are gone. The pre-`CachePolicy` convenience overloads they carried as default interface methods — `GetAsync(key, token)`, `SetAsync(key, value, expiration, token)`, diff --git a/docs/recipes/conditional-add.md b/docs/recipes/conditional-add.md index ef56df8..dbe6047 100644 --- a/docs/recipes/conditional-add.md +++ b/docs/recipes/conditional-add.md @@ -107,9 +107,11 @@ guarantee the method makes. change either side of the call anyway. `IDistributedLock.TryAcquireAsync` does not either; it documents backend-unavailable and already-held as the same `null`. If the two readings need different handling, you need a primitive with a richer result than a `bool`. -- **A non-positive TTL claims nothing.** An expiration that is not in the future returns `false` on - every tier rather than a win, because the entry would be evicted on arrival and the next caller - would be told it won too. +- **A non-positive TTL is a bad argument, not a loss.** `expiration` is non-nullable, so a duration + that is not strictly positive — or a deadline already past — raises `ArgumentOutOfRangeException` + and nothing is written. Returning `false` would be indistinguishable from "somebody else holds the + key", which is exactly the confusion the ambiguity above asks you to design around. To inherit the + policy's TTL, call the overload that has no `expiration` parameter. - **Caching switched off means nobody wins.** `NullCache.TryAddAsync` returns `false` — it cannot complete the write — and it is what `ICacheFactory.CreateCache` falls back to when the requested provider is missing or has `Enabled=false`. That is fail-closed rather than silently at-least-once, diff --git a/docs/reference/interfaces.md b/docs/reference/interfaces.md index 7723a5b..ed74e26 100644 --- a/docs/reference/interfaces.md +++ b/docs/reference/interfaces.md @@ -27,15 +27,15 @@ public interface ICache ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, CancellationToken token = default); - ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, TimeSpan? expiration, CancellationToken token = default); + ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, TimeSpan expiration, CancellationToken token = default); - ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, DateTimeOffset? expiration, CancellationToken token = default); + ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, DateTimeOffset expiration, CancellationToken token = default); ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, CancellationToken token = default) where TState : notnull; - ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, TimeSpan? expiration, CancellationToken token = default) where TState : notnull; + ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, TimeSpan expiration, CancellationToken token = default) where TState : notnull; - ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, DateTimeOffset? expiration, CancellationToken token = default) where TState : notnull; + ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, DateTimeOffset expiration, CancellationToken token = default) where TState : notnull; ValueTask RemoveAsync(CacheKey cacheKey, CancellationToken token = default); @@ -43,27 +43,27 @@ public interface ICache ValueTask SetAsync(CacheKey cacheKey, T? value, CancellationToken token = default); - ValueTask SetAsync(CacheKey cacheKey, T? value, TimeSpan? expiration, CancellationToken token = default); + ValueTask SetAsync(CacheKey cacheKey, T? value, TimeSpan expiration, CancellationToken token = default); - ValueTask SetAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CancellationToken token = default); + ValueTask SetAsync(CacheKey cacheKey, T? value, DateTimeOffset expiration, CancellationToken token = default); ValueTask SetAsync(KeyValuePair[] keyValues, CancellationToken token = default); - ValueTask SetAsync(KeyValuePair[] keyValues, TimeSpan? expiration = null, CancellationToken token = default); + ValueTask SetAsync(KeyValuePair[] keyValues, TimeSpan expiration, CancellationToken token = default); - ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset? expiration = null, CancellationToken token = default); + ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset expiration, CancellationToken token = default); ValueTask TryAddAsync(CacheKey cacheKey, T? value, CancellationToken token = default); - ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan? expiration, CancellationToken token = default); + ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan expiration, CancellationToken token = default); - ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CancellationToken token = default); + ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset expiration, CancellationToken token = default); ValueTask RefreshAsync(CacheKey cacheKey, CancellationToken token = default); - ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration, CancellationToken token = default); + ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan expiration, CancellationToken token = default); - ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration, CancellationToken token = default); + ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset expiration, CancellationToken token = default); ValueTask ContainsAsync(CacheKey cacheKey, CancellationToken token = default); @@ -116,19 +116,19 @@ public interface ICache : IDisposable ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, CachePolicy? policy, CancellationToken token = default); - ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default); + ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default); + ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default); ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, CachePolicy? policy, CancellationToken token = default) where TState : notnull => BatchGetOrAdd.RunAsync(this, entries, generator, (pairs, t) => SetAsync(pairs, policy, t), policy, token); - ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) + ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default) where TState : notnull => BatchGetOrAdd.RunAsync(this, entries, generator, (pairs, t) => SetAsync(pairs, expiration, policy, t), policy, token); - ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) + ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default) where TState : notnull => BatchGetOrAdd.RunAsync(this, entries, generator, (pairs, t) => SetAsync(pairs, expiration, policy, t), policy, token); @@ -138,27 +138,27 @@ public interface ICache : IDisposable ValueTask SetAsync(CacheKey cacheKey, T? value, CachePolicy? policy, CancellationToken token = default); - ValueTask SetAsync(CacheKey cacheKey, T? value, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default); + ValueTask SetAsync(CacheKey cacheKey, T? value, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask SetAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default); + ValueTask SetAsync(CacheKey cacheKey, T? value, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default); ValueTask SetAsync(KeyValuePair[] keyValues, CachePolicy? policy, CancellationToken token = default); - ValueTask SetAsync(KeyValuePair[] keyValues, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default); + ValueTask SetAsync(KeyValuePair[] keyValues, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default); + ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default); ValueTask TryAddAsync(CacheKey cacheKey, T? value, CachePolicy? policy, CancellationToken token = default); - ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default); + ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default); + ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default); ValueTask RefreshAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default); - ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default); + ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default); + ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default); ValueTask ContainsAsync(CacheKey cacheKey, CancellationToken token = default); @@ -170,6 +170,31 @@ public interface ICache : IDisposable `ICache` is the dynamic-key, dynamic-type cache surface. Unlike `ICache`, the value type is specified as a generic type argument on each method call rather than fixed at cache-creation time, and a `CachePolicy` can be supplied per call rather than resolved by `typeof(T).FullName`. It also exposes `GetCacheEntryAsync` for callers that need cache-entry metadata (hit/miss status, expiration) in addition to the value. `ICache` implements `IDisposable`, but instances returned by `ICacheFactory.CreateCache(...)` are provider-owned (typically singletons resolved through a `Lazy<>`); their lifetime is managed by the provider and the DI container, so callers should not dispose them per use. +### Expiration + +`expiration` is **non-nullable** everywhere it appears on a write — `TimeSpan` or `DateTimeOffset`, never `TimeSpan?`/`DateTimeOffset?`. A caller with nothing to say about lifetime calls the overload that has no `expiration` parameter; a caller that passes one means it. + +```csharp +// I want this lifetime. +await cache.SetAsync(key, order, TimeSpan.FromMinutes(5), policy, token); + +// I have no opinion: resolve it from the policy, then the provider default. +await cache.SetAsync(key, order, policy, token); +``` + +That leaves one resolution chain with no redundant state in it: + +| What the caller does | Lifetime used | +| --- | --- | +| passes `expiration` | exactly that value, no jitter | +| omits `expiration` | `CachePolicy.DistributedExpiration`, jittered by `CachePolicy.JitterMaxDuration` | +| omits it, policy has no TTL | the provider's `DefaultExpiration`, jittered | +| omits it, nothing configured | unbounded — `TimeSpan.MaxValue` / `DateTimeOffset.MaxValue`, which the providers store as "no TTL" | + +Because the argument can no longer be `null`, there is nothing left for a meaningless value to mean, so it is rejected rather than absorbed: a duration that is not strictly positive, or a deadline at or before the cache's current time, raises `ArgumentOutOfRangeException` with `ParamName` `"expiration"` and nothing is written. `TimeSpan.MaxValue` and `DateTimeOffset.MaxValue` stay valid — they are how the providers spell "no TTL". `CacheExpiration` holds the guard if you need it in your own implementation. The no-op caches (`NullCache`, `NullHashCache`, `NullSetCache`) read no argument at all and so enforce nothing; they keep degrading to "caching is off, carry on". + +Nullability stays where it means *inherit*: `CachePolicy.LocalExpiration` / `DistributedExpiration`, the providers' `DefaultExpiration`, and the lifetime fields on `HashCacheEntryOptions`. Reads stay nullable too — `TimeToLiveAsync` and `ExpireTimeAsync` return `null` for a key with no TTL. + Every policy-bearing member takes `policy` as a **required** parameter — there is no `= null` default on the interface. Call sites that do not want a per-call policy use the `CacheExtensions` overloads instead, which omit `policy` (and, where the interface pairs the two, `expiration`) and forward with `policy: null`: ```csharp @@ -190,7 +215,7 @@ The multi-key `GetOrAddAsync` overloads shown above pair each key wit - **`false` is deliberately ambiguous.** It means "you did not create this key" — either it already existed, or the write could not be completed (backing store disconnected, write threw, or the value was a `null`/`default` that the cache has no way to represent). This is fail-closed by design: a caller treating `true` as "I own this key" is never wrongly told it won. The ambiguity is not recoverable: a serialization or command failure also returns `false`, with [`IConnectionState.IsConnected`](#iconnectionstate) still reporting healthy, and [`IDistributedLock.TryAcquireAsync`](#idistributedlock) conflates backend-unavailable with already-held in the same way. Design the `false` branch so that not proceeding is safe; if the two readings must be handled differently, the caller needs a primitive with a richer result than a `bool`. - **It never deletes.** Where `SetAsync` removes the key when handed a `null` and `CacheNullValues` is off, `TryAddAsync` returns `false` and leaves the key untouched. With `CacheNullValues` on, a `null` claims the key via the cached-null sentinel. - **It is a cache primitive, not a lock.** The entry expires on its own TTL, there is no ownership token, and any later `SetAsync`/`RemoveAsync` on the key ignores the claim. For mutual exclusion with a fencing token and explicit release, use [`IDistributedLock`](#idistributedlock). -- **An expiration that is not in the future claims nothing** and reports `false` on every tier. The entry would be evicted on arrival, so answering `true` would hand the same key to every later caller as well. +- **An expiration that is not in the future is rejected**, not answered. `expiration` is non-nullable, so a non-positive duration or a deadline already past is a bad argument and raises `ArgumentOutOfRangeException` — reporting `false` would be indistinguishable from "somebody else holds the key". See [Expiration](#expiration). Tier behavior follows from the same rule — L1 can never arbitrate while an L2 exists, because a key absent locally may well be present in the shared store: @@ -250,25 +275,25 @@ public interface IHashCache ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, CancellationToken token = default); - ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, TimeSpan? expiration, CancellationToken token = default); + ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, TimeSpan expiration, CancellationToken token = default); - ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration, CancellationToken token = default); + ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset expiration, CancellationToken token = default); ValueTask>> GetCacheEntryAsync(CacheKey cacheKey, CancellationToken token = default); ValueTask SetAsync(CacheKey cacheKey, IDictionary values, CancellationToken token = default); - ValueTask SetAsync(CacheKey cacheKey, IDictionary values, TimeSpan? expiration, CancellationToken token = default); + ValueTask SetAsync(CacheKey cacheKey, IDictionary values, TimeSpan expiration, CancellationToken token = default); - ValueTask SetAsync(CacheKey cacheKey, IDictionary values, DateTimeOffset? expiration, CancellationToken token = default); + ValueTask SetAsync(CacheKey cacheKey, IDictionary values, DateTimeOffset expiration, CancellationToken token = default); ValueTask SetAsync(CacheKey cacheKey, IDictionary values, HashCacheEntryOptions options, CancellationToken token = default); ValueTask RefreshAsync(CacheKey cacheKey, CancellationToken token = default); - ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration, CancellationToken token = default); + ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan expiration, CancellationToken token = default); - ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration, CancellationToken token = default); + ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset expiration, CancellationToken token = default); ValueTask RefreshAsync(CacheKey cacheKey, HashCacheEntryOptions options, CancellationToken token = default); @@ -325,25 +350,25 @@ public interface IHashCache : IDisposable ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, CachePolicy? policy, CancellationToken token = default); - ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default); + ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default); + ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration, HashCacheSetOption? setOption, CachePolicy? policy, CancellationToken token = default); + ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset expiration, HashCacheSetOption? setOption, CachePolicy? policy, CancellationToken token = default); ValueTask SetAsync(CacheKey cacheKey, IDictionary values, CachePolicy? policy, CancellationToken token = default); - ValueTask SetAsync(CacheKey cacheKey, IDictionary values, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default); + ValueTask SetAsync(CacheKey cacheKey, IDictionary values, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask SetAsync(CacheKey cacheKey, IDictionary values, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default); + ValueTask SetAsync(CacheKey cacheKey, IDictionary values, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default); ValueTask SetAsync(CacheKey cacheKey, IDictionary values, HashCacheEntryOptions options, CachePolicy? policy, CancellationToken token = default); ValueTask RefreshAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default); - ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default); + ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default); + ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default); ValueTask RefreshAsync(CacheKey cacheKey, HashCacheEntryOptions options, CachePolicy? policy, CancellationToken token = default); diff --git a/src/UiPath.Caching.Abstractions/CacheExpiration.cs b/src/UiPath.Caching.Abstractions/CacheExpiration.cs new file mode 100644 index 0000000..7a5b905 --- /dev/null +++ b/src/UiPath.Caching.Abstractions/CacheExpiration.cs @@ -0,0 +1,62 @@ +using System.Runtime.CompilerServices; + +namespace UiPath.Caching; + +/// +/// Argument validation for the per-call expiration on the write surface. +/// +/// +/// The write members take a non-nullable / , so +/// there is no null left to absorb a nonsensical value: a caller with nothing to say about +/// lifetime calls the overload that has no expiration parameter and gets +/// , then the cache default. What remains is a value +/// the caller meant, and a duration that is not positive — or a deadline that has already passed — +/// cannot be honored, so it is rejected rather than silently swallowed. +/// +/// This does not police the resolved default: a policy or provider default that leaves entries +/// unbounded still yields / , +/// which the providers read as "no TTL". Those two sentinels stay valid inputs here. +/// +/// +/// Enforcement sits in the implementations that honor the lifetime. and its +/// siblings read no argument at all — not the key, not the type, not the expiration — so they keep +/// degrading to "caching is off, carry on" rather than throwing on a value they never look at. +/// +/// +public static class CacheExpiration +{ + /// Returns , or throws if it is not a positive duration. + /// is zero or negative. + public static TimeSpan ThrowIfNotPositive(TimeSpan expiration, [CallerArgumentExpression(nameof(expiration))] string? paramName = null) + { + if (expiration <= TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException( + paramName, + expiration, + "The cache expiration must be a positive duration. To inherit the policy's DistributedExpiration or the cache default, call the overload without an expiration argument."); + } + + return expiration; + } + + /// Returns , or throws if it is not later than . + /// is at or before . + public static DateTimeOffset ThrowIfNotFuture(DateTimeOffset expiration, DateTimeOffset now, [CallerArgumentExpression(nameof(expiration))] string? paramName = null) + { + if (expiration <= now) + { + throw new ArgumentOutOfRangeException( + paramName, + expiration, + "The cache expiration must be later than the cache's current time. To inherit the policy's DistributedExpiration or the cache default, call the overload without an expiration argument."); + } + + return expiration; + } + + /// Validates a caller deadline against and returns it as a duration from . + /// is at or before . + public static TimeSpan ToDuration(DateTimeOffset expiration, DateTimeOffset now, [CallerArgumentExpression(nameof(expiration))] string? paramName = null) => + ThrowIfNotFuture(expiration, now, paramName) - now; +} diff --git a/src/UiPath.Caching.Abstractions/CacheExtensions.cs b/src/UiPath.Caching.Abstractions/CacheExtensions.cs index e2195b3..c8330c1 100644 --- a/src/UiPath.Caching.Abstractions/CacheExtensions.cs +++ b/src/UiPath.Caching.Abstractions/CacheExtensions.cs @@ -24,48 +24,48 @@ public static class CacheExtensions public static ValueTask GetOrAddAsync(this ICache cache, CacheKey cacheKey, Func> generator, CancellationToken token = default) => cache.GetOrAddAsync(cacheKey, generator, (CachePolicy?)null, token); - public static ValueTask GetOrAddAsync(this ICache cache, CacheKey cacheKey, Func> generator, TimeSpan? expiration, CancellationToken token = default) + public static ValueTask GetOrAddAsync(this ICache cache, CacheKey cacheKey, Func> generator, TimeSpan expiration, CancellationToken token = default) => cache.GetOrAddAsync(cacheKey, generator, expiration, null, token); - public static ValueTask GetOrAddAsync(this ICache cache, CacheKey cacheKey, Func> generator, DateTimeOffset? expiration, CancellationToken token = default) + public static ValueTask GetOrAddAsync(this ICache cache, CacheKey cacheKey, Func> generator, DateTimeOffset expiration, CancellationToken token = default) => cache.GetOrAddAsync(cacheKey, generator, expiration, null, token); public static ValueTask SetAsync(this ICache cache, CacheKey cacheKey, T? value, CancellationToken token = default) => cache.SetAsync(cacheKey, value, (CachePolicy?)null, token); - public static ValueTask SetAsync(this ICache cache, CacheKey cacheKey, T? value, TimeSpan? expiration, CancellationToken token = default) + public static ValueTask SetAsync(this ICache cache, CacheKey cacheKey, T? value, TimeSpan expiration, CancellationToken token = default) => cache.SetAsync(cacheKey, value, expiration, null, token); - public static ValueTask SetAsync(this ICache cache, CacheKey cacheKey, T? value, DateTimeOffset? expiration, CancellationToken token = default) + public static ValueTask SetAsync(this ICache cache, CacheKey cacheKey, T? value, DateTimeOffset expiration, CancellationToken token = default) => cache.SetAsync(cacheKey, value, expiration, null, token); public static ValueTask SetAsync(this ICache cache, KeyValuePair[] keyValues, CancellationToken token = default) => cache.SetAsync(keyValues, (CachePolicy?)null, token); - public static ValueTask SetAsync(this ICache cache, KeyValuePair[] keyValues, TimeSpan? expiration, CancellationToken token = default) + public static ValueTask SetAsync(this ICache cache, KeyValuePair[] keyValues, TimeSpan expiration, CancellationToken token = default) => cache.SetAsync(keyValues, expiration, null, token); - public static ValueTask SetAsync(this ICache cache, KeyValuePair[] keyValues, DateTimeOffset? expiration, CancellationToken token = default) + public static ValueTask SetAsync(this ICache cache, KeyValuePair[] keyValues, DateTimeOffset expiration, CancellationToken token = default) => cache.SetAsync(keyValues, expiration, null, token); /// public static ValueTask TryAddAsync(this ICache cache, CacheKey cacheKey, T? value, CancellationToken token = default) => cache.TryAddAsync(cacheKey, value, (CachePolicy?)null, token); - /// - public static ValueTask TryAddAsync(this ICache cache, CacheKey cacheKey, T? value, TimeSpan? expiration, CancellationToken token = default) + /// + public static ValueTask TryAddAsync(this ICache cache, CacheKey cacheKey, T? value, TimeSpan expiration, CancellationToken token = default) => cache.TryAddAsync(cacheKey, value, expiration, null, token); - /// - public static ValueTask TryAddAsync(this ICache cache, CacheKey cacheKey, T? value, DateTimeOffset? expiration, CancellationToken token = default) + /// + public static ValueTask TryAddAsync(this ICache cache, CacheKey cacheKey, T? value, DateTimeOffset expiration, CancellationToken token = default) => cache.TryAddAsync(cacheKey, value, expiration, null, token); public static ValueTask RefreshAsync(this ICache cache, CacheKey cacheKey, CancellationToken token = default) => cache.RefreshAsync(cacheKey, (CachePolicy?)null, token); - public static ValueTask RefreshAsync(this ICache cache, CacheKey cacheKey, TimeSpan? expiration, CancellationToken token = default) + public static ValueTask RefreshAsync(this ICache cache, CacheKey cacheKey, TimeSpan expiration, CancellationToken token = default) => cache.RefreshAsync(cacheKey, expiration, null, token); - public static ValueTask RefreshAsync(this ICache cache, CacheKey cacheKey, DateTimeOffset? expiration, CancellationToken token = default) + public static ValueTask RefreshAsync(this ICache cache, CacheKey cacheKey, DateTimeOffset expiration, CancellationToken token = default) => cache.RefreshAsync(cacheKey, expiration, null, token); } diff --git a/src/UiPath.Caching.Abstractions/CacheOfT.cs b/src/UiPath.Caching.Abstractions/CacheOfT.cs index bbb79b1..879c6b3 100644 --- a/src/UiPath.Caching.Abstractions/CacheOfT.cs +++ b/src/UiPath.Caching.Abstractions/CacheOfT.cs @@ -46,21 +46,21 @@ public ValueTask ContainsAsync(CacheKey cacheKey, CancellationToken token public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, CancellationToken token = default) => _cache.GetOrAddAsync(GetCacheKey(cacheKey), generator, policy: Policy, token: token); - public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, TimeSpan? expiration, CancellationToken token = default) => + public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, TimeSpan expiration, CancellationToken token = default) => _cache.GetOrAddAsync(GetCacheKey(cacheKey), generator, expiration, Policy, token); - public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, DateTimeOffset? expiration, CancellationToken token = default) => + public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, DateTimeOffset expiration, CancellationToken token = default) => _cache.GetOrAddAsync(GetCacheKey(cacheKey), generator, expiration, Policy, token); public ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, CancellationToken token = default) where TState : notnull => _cache.GetOrAddAsync(MapKeys(entries), generator, policy: Policy, token: token); - public ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, TimeSpan? expiration, CancellationToken token = default) + public ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, TimeSpan expiration, CancellationToken token = default) where TState : notnull => _cache.GetOrAddAsync(MapKeys(entries), generator, expiration, Policy, token); - public ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, DateTimeOffset? expiration, CancellationToken token = default) + public ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, DateTimeOffset expiration, CancellationToken token = default) where TState : notnull => _cache.GetOrAddAsync(MapKeys(entries), generator, expiration, Policy, token); @@ -74,10 +74,10 @@ private KeyValuePair[] MapKeys(KeyValuePair RefreshAsync(CacheKey cacheKey, CancellationToken token = default) => _cache.RefreshAsync(GetCacheKey(cacheKey), policy: Policy, token: token); - public ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration, CancellationToken token = default) => + public ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan expiration, CancellationToken token = default) => _cache.RefreshAsync(GetCacheKey(cacheKey), expiration, Policy, token); - public ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration, CancellationToken token = default) => + public ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset expiration, CancellationToken token = default) => _cache.RefreshAsync(GetCacheKey(cacheKey), expiration, Policy, token); public ValueTask RemoveAsync(CacheKey cacheKey, CancellationToken token = default) => @@ -89,29 +89,29 @@ public ValueTask RemoveAsync(CacheKey[] cacheKeys, CancellationToken token public ValueTask SetAsync(CacheKey cacheKey, T? value, CancellationToken token = default) => _cache.SetAsync(GetCacheKey(cacheKey), value, policy: Policy, token: token); - public ValueTask SetAsync(CacheKey cacheKey, T? value, TimeSpan? expiration, CancellationToken token = default) => + public ValueTask SetAsync(CacheKey cacheKey, T? value, TimeSpan expiration, CancellationToken token = default) => _cache.SetAsync(GetCacheKey(cacheKey), value, expiration, Policy, token); - public ValueTask SetAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CancellationToken token = default) => + public ValueTask SetAsync(CacheKey cacheKey, T? value, DateTimeOffset expiration, CancellationToken token = default) => _cache.SetAsync(GetCacheKey(cacheKey), value, expiration, Policy, token); public ValueTask TryAddAsync(CacheKey cacheKey, T? value, CancellationToken token = default) => _cache.TryAddAsync(GetCacheKey(cacheKey), value, policy: Policy, token: token); - public ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan? expiration, CancellationToken token = default) => + public ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan expiration, CancellationToken token = default) => _cache.TryAddAsync(GetCacheKey(cacheKey), value, expiration, Policy, token); - public ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CancellationToken token = default) => + public ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset expiration, CancellationToken token = default) => _cache.TryAddAsync(GetCacheKey(cacheKey), value, expiration, Policy, token); public ValueTask SetAsync(KeyValuePair[] keyValues, CancellationToken token = default) => _cache.SetAsync(GetKeyValuePairs(keyValues), policy: Policy, token: token); - public ValueTask SetAsync(KeyValuePair[] keyValues, TimeSpan? expiration = null, CancellationToken token = default) => + public ValueTask SetAsync(KeyValuePair[] keyValues, TimeSpan expiration, CancellationToken token = default) => _cache.SetAsync(GetKeyValuePairs(keyValues), expiration, Policy, token); - public ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset? expiration = null, CancellationToken token = default) => + public ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset expiration, CancellationToken token = default) => _cache.SetAsync(GetKeyValuePairs(keyValues), expiration, Policy, token); public ValueTask TimeToLiveAsync(CacheKey cacheKey, CancellationToken token = default) => diff --git a/src/UiPath.Caching.Abstractions/CacheSyncExtensions.cs b/src/UiPath.Caching.Abstractions/CacheSyncExtensions.cs index b4d1ba7..c11dc82 100644 --- a/src/UiPath.Caching.Abstractions/CacheSyncExtensions.cs +++ b/src/UiPath.Caching.Abstractions/CacheSyncExtensions.cs @@ -19,10 +19,10 @@ public static class CacheSyncExtensions public static T? GetOrAdd(this ICache cache, CacheKey cacheKey, Func generator, CancellationToken token = default) => cache.GetOrAddAsync(cacheKey, _ => Task.FromResult(generator()), token).AsTask().GetAwaiter().GetResult(); - public static T? GetOrAdd(this ICache cache, CacheKey cacheKey, Func generator, TimeSpan? expiration, CancellationToken token = default) + public static T? GetOrAdd(this ICache cache, CacheKey cacheKey, Func generator, TimeSpan expiration, CancellationToken token = default) => cache.GetOrAddAsync(cacheKey, _ => Task.FromResult(generator()), expiration, token).AsTask().GetAwaiter().GetResult(); - public static T? GetOrAdd(this ICache cache, CacheKey cacheKey, Func generator, DateTimeOffset? expiration, CancellationToken token = default) + public static T? GetOrAdd(this ICache cache, CacheKey cacheKey, Func generator, DateTimeOffset expiration, CancellationToken token = default) => cache.GetOrAddAsync(cacheKey, _ => Task.FromResult(generator()), expiration, token).AsTask().GetAwaiter().GetResult(); public static KeyValuePair[] GetOrAdd(this ICache cache, CacheKey[] cacheKeys, Func[]> generator, CancellationToken token = default) @@ -32,7 +32,7 @@ public static class CacheSyncExtensions token) .AsTask().GetAwaiter().GetResult(); - public static KeyValuePair[] GetOrAdd(this ICache cache, CacheKey[] cacheKeys, Func[]> generator, TimeSpan? expiration, CancellationToken token = default) + public static KeyValuePair[] GetOrAdd(this ICache cache, CacheKey[] cacheKeys, Func[]> generator, TimeSpan expiration, CancellationToken token = default) => cache.GetOrAddAsync( Array.ConvertAll(cacheKeys, k => new KeyValuePair(k, k)), (keys, _) => Task.FromResult(generator(keys)), @@ -40,7 +40,7 @@ public static class CacheSyncExtensions token) .AsTask().GetAwaiter().GetResult(); - public static KeyValuePair[] GetOrAdd(this ICache cache, CacheKey[] cacheKeys, Func[]> generator, DateTimeOffset? expiration, CancellationToken token = default) + public static KeyValuePair[] GetOrAdd(this ICache cache, CacheKey[] cacheKeys, Func[]> generator, DateTimeOffset expiration, CancellationToken token = default) => cache.GetOrAddAsync( Array.ConvertAll(cacheKeys, k => new KeyValuePair(k, k)), (keys, _) => Task.FromResult(generator(keys)), @@ -57,40 +57,40 @@ public static bool Remove(this ICache cache, CacheKey[] cacheKeys, Cancell public static bool Set(this ICache cache, CacheKey cacheKey, T? value, CancellationToken token = default) => cache.SetAsync(cacheKey, value, token).AsTask().GetAwaiter().GetResult(); - public static bool Set(this ICache cache, CacheKey cacheKey, T? value, TimeSpan? expiration, CancellationToken token = default) + public static bool Set(this ICache cache, CacheKey cacheKey, T? value, TimeSpan expiration, CancellationToken token = default) => cache.SetAsync(cacheKey, value, expiration, token).AsTask().GetAwaiter().GetResult(); - public static bool Set(this ICache cache, CacheKey cacheKey, T? value, DateTimeOffset? expiration, CancellationToken token = default) + public static bool Set(this ICache cache, CacheKey cacheKey, T? value, DateTimeOffset expiration, CancellationToken token = default) => cache.SetAsync(cacheKey, value, expiration, token).AsTask().GetAwaiter().GetResult(); public static bool Set(this ICache cache, KeyValuePair[] keyValues, CancellationToken token = default) => cache.SetAsync(keyValues, token).AsTask().GetAwaiter().GetResult(); - public static bool Set(this ICache cache, KeyValuePair[] keyValues, TimeSpan? expiration = null, CancellationToken token = default) + public static bool Set(this ICache cache, KeyValuePair[] keyValues, TimeSpan expiration, CancellationToken token = default) => cache.SetAsync(keyValues, expiration, token).AsTask().GetAwaiter().GetResult(); - public static bool Set(this ICache cache, KeyValuePair[] keyValues, DateTimeOffset? expiration = null, CancellationToken token = default) + public static bool Set(this ICache cache, KeyValuePair[] keyValues, DateTimeOffset expiration, CancellationToken token = default) => cache.SetAsync(keyValues, expiration, token).AsTask().GetAwaiter().GetResult(); /// public static bool TryAdd(this ICache cache, CacheKey cacheKey, T? value, CancellationToken token = default) => cache.TryAddAsync(cacheKey, value, token).AsTask().GetAwaiter().GetResult(); - /// - public static bool TryAdd(this ICache cache, CacheKey cacheKey, T? value, TimeSpan? expiration, CancellationToken token = default) + /// + public static bool TryAdd(this ICache cache, CacheKey cacheKey, T? value, TimeSpan expiration, CancellationToken token = default) => cache.TryAddAsync(cacheKey, value, expiration, token).AsTask().GetAwaiter().GetResult(); - /// - public static bool TryAdd(this ICache cache, CacheKey cacheKey, T? value, DateTimeOffset? expiration, CancellationToken token = default) + /// + public static bool TryAdd(this ICache cache, CacheKey cacheKey, T? value, DateTimeOffset expiration, CancellationToken token = default) => cache.TryAddAsync(cacheKey, value, expiration, token).AsTask().GetAwaiter().GetResult(); public static bool Refresh(this ICache cache, CacheKey cacheKey, CancellationToken token = default) => cache.RefreshAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); - public static bool Refresh(this ICache cache, CacheKey cacheKey, TimeSpan? expiration, CancellationToken token = default) + public static bool Refresh(this ICache cache, CacheKey cacheKey, TimeSpan expiration, CancellationToken token = default) => cache.RefreshAsync(cacheKey, expiration, token).AsTask().GetAwaiter().GetResult(); - public static bool Refresh(this ICache cache, CacheKey cacheKey, DateTimeOffset? expiration, CancellationToken token = default) + public static bool Refresh(this ICache cache, CacheKey cacheKey, DateTimeOffset expiration, CancellationToken token = default) => cache.RefreshAsync(cacheKey, expiration, token).AsTask().GetAwaiter().GetResult(); public static bool Contains(this ICache cache, CacheKey cacheKey, CancellationToken token = default) diff --git a/src/UiPath.Caching.Abstractions/HashCacheExtensions.cs b/src/UiPath.Caching.Abstractions/HashCacheExtensions.cs index cdd4c07..f910096 100644 --- a/src/UiPath.Caching.Abstractions/HashCacheExtensions.cs +++ b/src/UiPath.Caching.Abstractions/HashCacheExtensions.cs @@ -24,22 +24,22 @@ public static class HashCacheExtensions public static ValueTask> GetOrAddAsync(this IHashCache cache, CacheKey cacheKey, Func>> generator, CancellationToken token = default) => cache.GetOrAddAsync(cacheKey, generator, (CachePolicy?)null, token); - public static ValueTask> GetOrAddAsync(this IHashCache cache, CacheKey cacheKey, Func>> generator, TimeSpan? expiration, CancellationToken token = default) + public static ValueTask> GetOrAddAsync(this IHashCache cache, CacheKey cacheKey, Func>> generator, TimeSpan expiration, CancellationToken token = default) => cache.GetOrAddAsync(cacheKey, generator, expiration, null, token); - public static ValueTask> GetOrAddAsync(this IHashCache cache, CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration, CancellationToken token = default) + public static ValueTask> GetOrAddAsync(this IHashCache cache, CacheKey cacheKey, Func>> generator, DateTimeOffset expiration, CancellationToken token = default) => cache.GetOrAddAsync(cacheKey, generator, expiration, (CachePolicy?)null, token); - public static ValueTask> GetOrAddAsync(this IHashCache cache, CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration, HashCacheSetOption? setOption, CancellationToken token = default) + public static ValueTask> GetOrAddAsync(this IHashCache cache, CacheKey cacheKey, Func>> generator, DateTimeOffset expiration, HashCacheSetOption? setOption, CancellationToken token = default) => cache.GetOrAddAsync(cacheKey, generator, expiration, setOption, null, token); public static ValueTask SetAsync(this IHashCache cache, CacheKey cacheKey, IDictionary values, CancellationToken token = default) => cache.SetAsync(cacheKey, values, (CachePolicy?)null, token); - public static ValueTask SetAsync(this IHashCache cache, CacheKey cacheKey, IDictionary values, TimeSpan? expiration, CancellationToken token = default) + public static ValueTask SetAsync(this IHashCache cache, CacheKey cacheKey, IDictionary values, TimeSpan expiration, CancellationToken token = default) => cache.SetAsync(cacheKey, values, expiration, null, token); - public static ValueTask SetAsync(this IHashCache cache, CacheKey cacheKey, IDictionary values, DateTimeOffset? expiration, CancellationToken token = default) + public static ValueTask SetAsync(this IHashCache cache, CacheKey cacheKey, IDictionary values, DateTimeOffset expiration, CancellationToken token = default) => cache.SetAsync(cacheKey, values, expiration, null, token); public static ValueTask SetAsync(this IHashCache cache, CacheKey cacheKey, IDictionary values, HashCacheEntryOptions options, CancellationToken token = default) @@ -48,10 +48,10 @@ public static ValueTask SetAsync(this IHashCache cache, CacheKey cacheK public static ValueTask RefreshAsync(this IHashCache cache, CacheKey cacheKey, CancellationToken token = default) => cache.RefreshAsync(cacheKey, (CachePolicy?)null, token); - public static ValueTask RefreshAsync(this IHashCache cache, CacheKey cacheKey, TimeSpan? expiration, CancellationToken token = default) + public static ValueTask RefreshAsync(this IHashCache cache, CacheKey cacheKey, TimeSpan expiration, CancellationToken token = default) => cache.RefreshAsync(cacheKey, expiration, null, token); - public static ValueTask RefreshAsync(this IHashCache cache, CacheKey cacheKey, DateTimeOffset? expiration, CancellationToken token = default) + public static ValueTask RefreshAsync(this IHashCache cache, CacheKey cacheKey, DateTimeOffset expiration, CancellationToken token = default) => cache.RefreshAsync(cacheKey, expiration, null, token); public static ValueTask RefreshAsync(this IHashCache cache, CacheKey cacheKey, HashCacheEntryOptions options, CancellationToken token = default) diff --git a/src/UiPath.Caching.Abstractions/HashCacheOfT.cs b/src/UiPath.Caching.Abstractions/HashCacheOfT.cs index cf24ac5..a0259d9 100644 --- a/src/UiPath.Caching.Abstractions/HashCacheOfT.cs +++ b/src/UiPath.Caching.Abstractions/HashCacheOfT.cs @@ -46,10 +46,10 @@ public HashCache( public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, CancellationToken token = default) => _cache.GetOrAddAsync(GetCacheKey(cacheKey), generator, policy: Policy, token: token); - public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, TimeSpan? expiration, CancellationToken token = default) => + public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, TimeSpan expiration, CancellationToken token = default) => _cache.GetOrAddAsync(GetCacheKey(cacheKey), generator, expiration, Policy, token); - public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration, CancellationToken token = default) => + public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset expiration, CancellationToken token = default) => _cache.GetOrAddAsync(GetCacheKey(cacheKey), generator, expiration, Policy, token); public ValueTask>> GetCacheEntryAsync(CacheKey cacheKey, CancellationToken token = default) => @@ -58,10 +58,10 @@ public HashCache( public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, CancellationToken token = default) => _cache.SetAsync(GetCacheKey(cacheKey), values, policy: Policy, token: token); - public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, TimeSpan? expiration, CancellationToken token = default) => + public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, TimeSpan expiration, CancellationToken token = default) => _cache.SetAsync(GetCacheKey(cacheKey), values, expiration, Policy, token); - public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, DateTimeOffset? expiration, CancellationToken token = default) => + public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, DateTimeOffset expiration, CancellationToken token = default) => _cache.SetAsync(GetCacheKey(cacheKey), values, expiration, Policy, token); public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, HashCacheEntryOptions options, CancellationToken token = default) => @@ -70,10 +70,10 @@ public ValueTask SetAsync(CacheKey cacheKey, IDictionary value public ValueTask RefreshAsync(CacheKey cacheKey, CancellationToken token = default) => _cache.RefreshAsync(GetCacheKey(cacheKey), policy: Policy, token: token); - public ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration, CancellationToken token = default) => + public ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan expiration, CancellationToken token = default) => _cache.RefreshAsync(GetCacheKey(cacheKey), expiration, Policy, token); - public ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration, CancellationToken token = default) => + public ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset expiration, CancellationToken token = default) => _cache.RefreshAsync(GetCacheKey(cacheKey), expiration, Policy, token); public ValueTask RefreshAsync(CacheKey cacheKey, HashCacheEntryOptions options, CancellationToken token = default) => diff --git a/src/UiPath.Caching.Abstractions/HashCacheSyncExtensions.cs b/src/UiPath.Caching.Abstractions/HashCacheSyncExtensions.cs index 5c1f051..4923d4c 100644 --- a/src/UiPath.Caching.Abstractions/HashCacheSyncExtensions.cs +++ b/src/UiPath.Caching.Abstractions/HashCacheSyncExtensions.cs @@ -22,10 +22,10 @@ public static class HashCacheSyncExtensions public static IDictionary GetOrAdd(this IHashCache cache, CacheKey cacheKey, Func> generator, CancellationToken token = default) => cache.GetOrAddAsync(cacheKey, _ => Task.FromResult(generator()), token).AsTask().GetAwaiter().GetResult(); - public static IDictionary GetOrAdd(this IHashCache cache, CacheKey cacheKey, Func> generator, TimeSpan? expiration, CancellationToken token = default) + public static IDictionary GetOrAdd(this IHashCache cache, CacheKey cacheKey, Func> generator, TimeSpan expiration, CancellationToken token = default) => cache.GetOrAddAsync(cacheKey, _ => Task.FromResult(generator()), expiration, token).AsTask().GetAwaiter().GetResult(); - public static IDictionary GetOrAdd(this IHashCache cache, CacheKey cacheKey, Func> generator, DateTimeOffset? expiration, CancellationToken token = default) + public static IDictionary GetOrAdd(this IHashCache cache, CacheKey cacheKey, Func> generator, DateTimeOffset expiration, CancellationToken token = default) => cache.GetOrAddAsync(cacheKey, _ => Task.FromResult(generator()), expiration, token).AsTask().GetAwaiter().GetResult(); public static ICacheEntry> GetCacheEntry(this IHashCache cache, CacheKey cacheKey, CancellationToken token = default) @@ -34,10 +34,10 @@ public static class HashCacheSyncExtensions public static bool Set(this IHashCache cache, CacheKey cacheKey, IDictionary values, CancellationToken token = default) => cache.SetAsync(cacheKey, values, token).AsTask().GetAwaiter().GetResult(); - public static bool Set(this IHashCache cache, CacheKey cacheKey, IDictionary values, TimeSpan? expiration, CancellationToken token = default) + public static bool Set(this IHashCache cache, CacheKey cacheKey, IDictionary values, TimeSpan expiration, CancellationToken token = default) => cache.SetAsync(cacheKey, values, expiration, token).AsTask().GetAwaiter().GetResult(); - public static bool Set(this IHashCache cache, CacheKey cacheKey, IDictionary values, DateTimeOffset? expiration, CancellationToken token = default) + public static bool Set(this IHashCache cache, CacheKey cacheKey, IDictionary values, DateTimeOffset expiration, CancellationToken token = default) => cache.SetAsync(cacheKey, values, expiration, token).AsTask().GetAwaiter().GetResult(); public static bool Set(this IHashCache cache, CacheKey cacheKey, IDictionary values, HashCacheEntryOptions options, CancellationToken token = default) @@ -46,10 +46,10 @@ public static bool Set(this IHashCache cache, CacheKey cacheKey, IDictiona public static bool Refresh(this IHashCache cache, CacheKey cacheKey, CancellationToken token = default) => cache.RefreshAsync(cacheKey, token).AsTask().GetAwaiter().GetResult(); - public static bool Refresh(this IHashCache cache, CacheKey cacheKey, TimeSpan? expiration, CancellationToken token = default) + public static bool Refresh(this IHashCache cache, CacheKey cacheKey, TimeSpan expiration, CancellationToken token = default) => cache.RefreshAsync(cacheKey, expiration, token).AsTask().GetAwaiter().GetResult(); - public static bool Refresh(this IHashCache cache, CacheKey cacheKey, DateTimeOffset? expiration, CancellationToken token = default) + public static bool Refresh(this IHashCache cache, CacheKey cacheKey, DateTimeOffset expiration, CancellationToken token = default) => cache.RefreshAsync(cacheKey, expiration, token).AsTask().GetAwaiter().GetResult(); public static bool Refresh(this IHashCache cache, CacheKey cacheKey, HashCacheEntryOptions options, CancellationToken token = default) diff --git a/src/UiPath.Caching.Abstractions/ICache.cs b/src/UiPath.Caching.Abstractions/ICache.cs index ff71b90..49f46a4 100644 --- a/src/UiPath.Caching.Abstractions/ICache.cs +++ b/src/UiPath.Caching.Abstractions/ICache.cs @@ -1,5 +1,14 @@ namespace UiPath.Caching; +/// +/// Expiration. The expiration parameter is not nullable. A caller with nothing to +/// say about lifetime calls the overload that has no expiration parameter and gets +/// , then the cache default; a caller that passes one +/// means it, so a duration that is not positive — or a deadline that has already passed — is +/// rejected with rather than silently ignored. See +/// . The no-op implementations in this package read no argument at all +/// and so enforce nothing. +/// public interface ICache : IDisposable { string Name { get; } @@ -14,19 +23,19 @@ public interface ICache : IDisposable ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, CachePolicy? policy, CancellationToken token = default); - ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default); + ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default); + ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default); ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, CachePolicy? policy, CancellationToken token = default) where TState : notnull => BatchGetOrAdd.RunAsync(this, entries, generator, (pairs, t) => SetAsync(pairs, policy, t), policy, token); - ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) + ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default) where TState : notnull => BatchGetOrAdd.RunAsync(this, entries, generator, (pairs, t) => SetAsync(pairs, expiration, policy, t), policy, token); - ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) + ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default) where TState : notnull => BatchGetOrAdd.RunAsync(this, entries, generator, (pairs, t) => SetAsync(pairs, expiration, policy, t), policy, token); @@ -36,15 +45,15 @@ public interface ICache : IDisposable ValueTask SetAsync(CacheKey cacheKey, T? value, CachePolicy? policy, CancellationToken token = default); - ValueTask SetAsync(CacheKey cacheKey, T? value, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default); + ValueTask SetAsync(CacheKey cacheKey, T? value, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask SetAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default); + ValueTask SetAsync(CacheKey cacheKey, T? value, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default); ValueTask SetAsync(KeyValuePair[] keyValues, CachePolicy? policy, CancellationToken token = default); - ValueTask SetAsync(KeyValuePair[] keyValues, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default); + ValueTask SetAsync(KeyValuePair[] keyValues, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default); + ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default); /// /// Conditional add: writes only if is @@ -62,21 +71,23 @@ ValueTask TryAddAsync(CacheKey cacheKey, T? value, CachePolicy? policy, /// /// - /// Lifetime of the entry if it is created, applied by the same atomic command. Falls back to - /// CachePolicy.DistributedExpiration then the cache default. Not in the future: no-op. + /// Lifetime of the entry if it is created, applied by the same atomic command. Must be a + /// positive duration; to inherit CachePolicy.DistributedExpiration and then the cache + /// default, call the overload without this parameter. /// - ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) + /// is not positive. + ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default) => throw ConditionalAdd.NotSupported(Name, GetType().FullName ?? nameof(ICache)); - /// - ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) + /// + ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default) => throw ConditionalAdd.NotSupported(Name, GetType().FullName ?? nameof(ICache)); ValueTask RefreshAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default); - ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default); + ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default); + ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default); ValueTask ContainsAsync(CacheKey cacheKey, CancellationToken token = default); diff --git a/src/UiPath.Caching.Abstractions/ICacheOfT.cs b/src/UiPath.Caching.Abstractions/ICacheOfT.cs index 0eb0e69..fe884e8 100644 --- a/src/UiPath.Caching.Abstractions/ICacheOfT.cs +++ b/src/UiPath.Caching.Abstractions/ICacheOfT.cs @@ -9,15 +9,15 @@ public interface ICache ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, CancellationToken token = default); - ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, TimeSpan? expiration, CancellationToken token = default); + ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, TimeSpan expiration, CancellationToken token = default); - ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, DateTimeOffset? expiration, CancellationToken token = default); + ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, DateTimeOffset expiration, CancellationToken token = default); ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, CancellationToken token = default) where TState : notnull; - ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, TimeSpan? expiration, CancellationToken token = default) where TState : notnull; + ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, TimeSpan expiration, CancellationToken token = default) where TState : notnull; - ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, DateTimeOffset? expiration, CancellationToken token = default) where TState : notnull; + ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, DateTimeOffset expiration, CancellationToken token = default) where TState : notnull; ValueTask RemoveAsync(CacheKey cacheKey, CancellationToken token = default); @@ -25,19 +25,19 @@ public interface ICache ValueTask SetAsync(CacheKey cacheKey, T? value, CancellationToken token = default); - ValueTask SetAsync(CacheKey cacheKey, T? value, TimeSpan? expiration, CancellationToken token = default); + ValueTask SetAsync(CacheKey cacheKey, T? value, TimeSpan expiration, CancellationToken token = default); - ValueTask SetAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CancellationToken token = default); + ValueTask SetAsync(CacheKey cacheKey, T? value, DateTimeOffset expiration, CancellationToken token = default); ValueTask SetAsync(KeyValuePair[] keyValues, CancellationToken token = default); - ValueTask SetAsync(KeyValuePair[] keyValues, TimeSpan? expiration = null, CancellationToken token = default); + ValueTask SetAsync(KeyValuePair[] keyValues, TimeSpan expiration, CancellationToken token = default); - ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset? expiration = null, CancellationToken token = default); + ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset expiration, CancellationToken token = default); /// /// Typed façade over - /// ; + /// ; /// see that member for the contract. /// ValueTask TryAddAsync(CacheKey cacheKey, T? value, CancellationToken token = default) @@ -46,18 +46,18 @@ ValueTask TryAddAsync(CacheKey cacheKey, T? value, CancellationToken token /// /// Lifetime of the entry if it is created. - ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan? expiration, CancellationToken token = default) + ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan expiration, CancellationToken token = default) => throw ConditionalAdd.NotSupported(Name, GetType().FullName ?? nameof(ICache)); - /// - ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CancellationToken token = default) + /// + ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset expiration, CancellationToken token = default) => throw ConditionalAdd.NotSupported(Name, GetType().FullName ?? nameof(ICache)); ValueTask RefreshAsync(CacheKey cacheKey, CancellationToken token = default); - ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration, CancellationToken token = default); + ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan expiration, CancellationToken token = default); - ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration, CancellationToken token = default); + ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset expiration, CancellationToken token = default); ValueTask ContainsAsync(CacheKey cacheKey, CancellationToken token = default); diff --git a/src/UiPath.Caching.Abstractions/IHashCache.cs b/src/UiPath.Caching.Abstractions/IHashCache.cs index 3819040..b10d3b8 100644 --- a/src/UiPath.Caching.Abstractions/IHashCache.cs +++ b/src/UiPath.Caching.Abstractions/IHashCache.cs @@ -1,5 +1,14 @@ namespace UiPath.Caching; +/// +/// Expiration. The expiration parameter is not nullable. A caller with nothing to +/// say about lifetime calls the overload that has no expiration parameter and gets +/// , then the cache default; a caller that passes one +/// means it, so a duration that is not positive — or a deadline that has already passed — is +/// rejected with rather than silently ignored. See +/// . The no-op implementations in this package read no argument at all +/// and so enforce nothing. +/// public interface IHashCache : IDisposable { string Name { get; } @@ -14,25 +23,25 @@ public interface IHashCache : IDisposable ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, CachePolicy? policy, CancellationToken token = default); - ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default); + ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default); + ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration, HashCacheSetOption? setOption, CachePolicy? policy, CancellationToken token = default); + ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset expiration, HashCacheSetOption? setOption, CachePolicy? policy, CancellationToken token = default); ValueTask SetAsync(CacheKey cacheKey, IDictionary values, CachePolicy? policy, CancellationToken token = default); - ValueTask SetAsync(CacheKey cacheKey, IDictionary values, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default); + ValueTask SetAsync(CacheKey cacheKey, IDictionary values, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask SetAsync(CacheKey cacheKey, IDictionary values, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default); + ValueTask SetAsync(CacheKey cacheKey, IDictionary values, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default); ValueTask SetAsync(CacheKey cacheKey, IDictionary values, HashCacheEntryOptions options, CachePolicy? policy, CancellationToken token = default); ValueTask RefreshAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default); - ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default); + ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default); - ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default); + ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default); ValueTask RefreshAsync(CacheKey cacheKey, HashCacheEntryOptions options, CachePolicy? policy, CancellationToken token = default); diff --git a/src/UiPath.Caching.Abstractions/IHashCacheOfT.cs b/src/UiPath.Caching.Abstractions/IHashCacheOfT.cs index 9560f79..5684549 100644 --- a/src/UiPath.Caching.Abstractions/IHashCacheOfT.cs +++ b/src/UiPath.Caching.Abstractions/IHashCacheOfT.cs @@ -12,25 +12,25 @@ public interface IHashCache ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, CancellationToken token = default); - ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, TimeSpan? expiration, CancellationToken token = default); + ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, TimeSpan expiration, CancellationToken token = default); - ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration, CancellationToken token = default); + ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset expiration, CancellationToken token = default); ValueTask>> GetCacheEntryAsync(CacheKey cacheKey, CancellationToken token = default); ValueTask SetAsync(CacheKey cacheKey, IDictionary values, CancellationToken token = default); - ValueTask SetAsync(CacheKey cacheKey, IDictionary values, TimeSpan? expiration, CancellationToken token = default); + ValueTask SetAsync(CacheKey cacheKey, IDictionary values, TimeSpan expiration, CancellationToken token = default); - ValueTask SetAsync(CacheKey cacheKey, IDictionary values, DateTimeOffset? expiration, CancellationToken token = default); + ValueTask SetAsync(CacheKey cacheKey, IDictionary values, DateTimeOffset expiration, CancellationToken token = default); ValueTask SetAsync(CacheKey cacheKey, IDictionary values, HashCacheEntryOptions options, CancellationToken token = default); ValueTask RefreshAsync(CacheKey cacheKey, CancellationToken token = default); - ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration, CancellationToken token = default); + ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan expiration, CancellationToken token = default); - ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration, CancellationToken token = default); + ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset expiration, CancellationToken token = default); ValueTask RefreshAsync(CacheKey cacheKey, HashCacheEntryOptions options, CancellationToken token = default); diff --git a/src/UiPath.Caching.Abstractions/NullCache.cs b/src/UiPath.Caching.Abstractions/NullCache.cs index f518028..ff5c408 100644 --- a/src/UiPath.Caching.Abstractions/NullCache.cs +++ b/src/UiPath.Caching.Abstractions/NullCache.cs @@ -48,17 +48,17 @@ public ValueTask ContainsAsync(CacheKey cacheKey, CancellationToken tok public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, CachePolicy? policy, CancellationToken token = default) => ReturnGeneratorAsync(generator, token); - public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) => + public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default) => ReturnGeneratorAsync(generator, token); - public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) => + public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default) => ReturnGeneratorAsync(generator, token); public ValueTask RefreshAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); - public ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); + public ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); - public ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); + public ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); public ValueTask RemoveAsync(CacheKey cacheKey, CancellationToken token = default) => ReturnTrueAsync(); @@ -66,15 +66,15 @@ public ValueTask ContainsAsync(CacheKey cacheKey, CancellationToken tok public ValueTask SetAsync(CacheKey cacheKey, T? value, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); - public ValueTask SetAsync(CacheKey cacheKey, T? value, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); + public ValueTask SetAsync(CacheKey cacheKey, T? value, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); - public ValueTask SetAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); + public ValueTask SetAsync(CacheKey cacheKey, T? value, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); public ValueTask SetAsync(KeyValuePair[] keyValues, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); - public ValueTask SetAsync(KeyValuePair[] keyValues, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); + public ValueTask SetAsync(KeyValuePair[] keyValues, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); - public ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); + public ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); /// /// Always false: retaining nothing, this store cannot arbitrate a conditional add, and @@ -85,10 +85,10 @@ public ValueTask ContainsAsync(CacheKey cacheKey, CancellationToken tok public ValueTask TryAddAsync(CacheKey cacheKey, T? value, CachePolicy? policy, CancellationToken token = default) => ReturnFalseAsync(); /// - public ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) => ReturnFalseAsync(); + public ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default) => ReturnFalseAsync(); /// - public ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) => ReturnFalseAsync(); + public ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default) => ReturnFalseAsync(); public ValueTask TimeToLiveAsync(CacheKey cacheKey, CancellationToken token = default) diff --git a/src/UiPath.Caching.Abstractions/NullHashCache.cs b/src/UiPath.Caching.Abstractions/NullHashCache.cs index e15a9e0..7f3585c 100644 --- a/src/UiPath.Caching.Abstractions/NullHashCache.cs +++ b/src/UiPath.Caching.Abstractions/NullHashCache.cs @@ -54,20 +54,20 @@ public ValueTask ContainsAsync(CacheKey cacheKey, CancellationToken tok public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, CachePolicy? policy, CancellationToken token = default) => ReturnGeneratorAsync(generator, token); - public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) => + public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default) => ReturnGeneratorAsync(generator, token); - public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) => + public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default) => ReturnGeneratorAsync(generator, token); - public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration, HashCacheSetOption? setOption, CachePolicy? policy, CancellationToken token = default) => + public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset expiration, HashCacheSetOption? setOption, CachePolicy? policy, CancellationToken token = default) => ReturnGeneratorAsync(generator, token); public ValueTask RefreshAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); - public ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); + public ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); - public ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); + public ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); public ValueTask RefreshAsync(CacheKey cacheKey, HashCacheEntryOptions options, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); @@ -75,9 +75,9 @@ public ValueTask ContainsAsync(CacheKey cacheKey, CancellationToken tok public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); - public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); + public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); - public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); + public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, HashCacheEntryOptions options, CachePolicy? policy, CancellationToken token = default) => ReturnTrueAsync(); diff --git a/src/UiPath.Caching.Abstractions/PublicAPI.Shipped.txt b/src/UiPath.Caching.Abstractions/PublicAPI.Shipped.txt index 5d46a07..3ac8107 100644 --- a/src/UiPath.Caching.Abstractions/PublicAPI.Shipped.txt +++ b/src/UiPath.Caching.Abstractions/PublicAPI.Shipped.txt @@ -91,25 +91,15 @@ UiPath.Caching.Cache.ContainsAsync(UiPath.Caching.CacheKey cacheKey, System.T UiPath.Caching.Cache.ExpireTimeAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.Cache.GetAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.Cache.GetAsync(UiPath.Caching.CacheKey[]! cacheKeys, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask[]!> -UiPath.Caching.Cache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.Cache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.Cache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.Cache.GetOrAddAsync(System.Collections.Generic.KeyValuePair[]! entries, System.Func[]!>!>! generator, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask[]!> UiPath.Caching.Cache.GetOrAddAsync(System.Collections.Generic.KeyValuePair[]! entries, System.Func[]!>!>! generator, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask[]!> -UiPath.Caching.Cache.GetOrAddAsync(System.Collections.Generic.KeyValuePair[]! entries, System.Func[]!>!>! generator, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask[]!> UiPath.Caching.Cache.Name.get -> string! UiPath.Caching.Cache.Policy.get -> UiPath.Caching.CachePolicy? -UiPath.Caching.Cache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.Cache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.Cache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.Cache.RemoveAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.Cache.RemoveAsync(UiPath.Caching.CacheKey[]! cacheKeys, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.Cache.SetAsync(System.Collections.Generic.KeyValuePair[]! keyValues, System.DateTimeOffset? expiration = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.Cache.SetAsync(System.Collections.Generic.KeyValuePair[]! keyValues, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.Cache.SetAsync(System.Collections.Generic.KeyValuePair[]! keyValues, System.TimeSpan? expiration = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.Cache.SetAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.Cache.SetAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.Cache.SetAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.Cache.TimeToLiveAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.CacheFactoryExtensions UiPath.Caching.CacheKey @@ -197,21 +187,15 @@ UiPath.Caching.HashCache.GetAsync(UiPath.Caching.CacheKey cacheKey, string![] UiPath.Caching.HashCache.GetCacheEntryAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!>!> UiPath.Caching.HashCache.GetItemAsync(UiPath.Caching.CacheKey cacheKey, string! field, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.HashCache.GetMetadataAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask?> -UiPath.Caching.HashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> UiPath.Caching.HashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> -UiPath.Caching.HashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> UiPath.Caching.HashCache.HashCache(UiPath.Caching.ICacheFactory! cacheFactory, UiPath.Caching.ICacheKeyStrategy? cacheKeyStrategy = null, UiPath.Caching.ICachePolicyFactory? policyFactory = null, string? policyName = null) -> void UiPath.Caching.HashCache.HashCache(UiPath.Caching.IHashCache! cache, UiPath.Caching.ICacheKeyStrategy? cacheKeyStrategy = null, UiPath.Caching.CachePolicy? policy = null) -> void UiPath.Caching.HashCache.Name.get -> string! UiPath.Caching.HashCache.Policy.get -> UiPath.Caching.CachePolicy? -UiPath.Caching.HashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.HashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.HashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.HashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.HashCacheEntryOptions options, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.HashCache.RemoveAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.HashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.HashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.HashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.HashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, UiPath.Caching.HashCacheEntryOptions options, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.HashCache.SetMetadataAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! metadata, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.HashCache.TimeToLiveAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask @@ -243,24 +227,14 @@ UiPath.Caching.ICache.ContainsAsync(UiPath.Caching.CacheKey cacheKey, System. UiPath.Caching.ICache.ExpireTimeAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.GetAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.GetAsync(UiPath.Caching.CacheKey[]! cacheKeys, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask[]!> -UiPath.Caching.ICache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.GetOrAddAsync(System.Collections.Generic.KeyValuePair[]! entries, System.Func[]!>!>! generator, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask[]!> UiPath.Caching.ICache.GetOrAddAsync(System.Collections.Generic.KeyValuePair[]! entries, System.Func[]!>!>! generator, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask[]!> -UiPath.Caching.ICache.GetOrAddAsync(System.Collections.Generic.KeyValuePair[]! entries, System.Func[]!>!>! generator, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask[]!> UiPath.Caching.ICache.Name.get -> string! -UiPath.Caching.ICache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.RemoveAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.RemoveAsync(UiPath.Caching.CacheKey[]! cacheKeys, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.SetAsync(System.Collections.Generic.KeyValuePair[]! keyValues, System.DateTimeOffset? expiration = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.SetAsync(System.Collections.Generic.KeyValuePair[]! keyValues, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.SetAsync(System.Collections.Generic.KeyValuePair[]! keyValues, System.TimeSpan? expiration = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.SetAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.SetAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.SetAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.TimeToLiveAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICacheChangeToken UiPath.Caching.ICacheChangeToken.Expiration.get -> System.DateTimeOffset? @@ -315,18 +289,12 @@ UiPath.Caching.IHashCache.GetAsync(UiPath.Caching.CacheKey cacheKey, string![ UiPath.Caching.IHashCache.GetCacheEntryAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!>!> UiPath.Caching.IHashCache.GetItemAsync(UiPath.Caching.CacheKey cacheKey, string! field, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.IHashCache.GetMetadataAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask?> -UiPath.Caching.IHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> UiPath.Caching.IHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> -UiPath.Caching.IHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> UiPath.Caching.IHashCache.Name.get -> string! -UiPath.Caching.IHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.IHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.IHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.IHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.HashCacheEntryOptions options, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.IHashCache.RemoveAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.IHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.IHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.IHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.IHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, UiPath.Caching.HashCacheEntryOptions options, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.IHashCache.SetMetadataAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! metadata, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.IHashCache.TimeToLiveAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask diff --git a/src/UiPath.Caching.Abstractions/PublicAPI.Unshipped.txt b/src/UiPath.Caching.Abstractions/PublicAPI.Unshipped.txt index 3651025..cd96e0c 100644 --- a/src/UiPath.Caching.Abstractions/PublicAPI.Unshipped.txt +++ b/src/UiPath.Caching.Abstractions/PublicAPI.Unshipped.txt @@ -1,7 +1,18 @@ #nullable enable -UiPath.Caching.Cache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.Cache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.DateTimeOffset expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.Cache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.TimeSpan expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.Cache.GetOrAddAsync(System.Collections.Generic.KeyValuePair[]! entries, System.Func[]!>!>! generator, System.DateTimeOffset expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask[]!> +UiPath.Caching.Cache.GetOrAddAsync(System.Collections.Generic.KeyValuePair[]! entries, System.Func[]!>!>! generator, System.TimeSpan expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask[]!> +UiPath.Caching.Cache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.Cache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.TimeSpan expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.Cache.SetAsync(System.Collections.Generic.KeyValuePair[]! keyValues, System.DateTimeOffset expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.Cache.SetAsync(System.Collections.Generic.KeyValuePair[]! keyValues, System.TimeSpan expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.Cache.SetAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.Cache.SetAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.Cache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.Cache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.Cache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.Cache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.CacheExpiration UiPath.Caching.CacheExtensions UiPath.Caching.CacheKey.CacheKey(string? name, UiPath.Caching.CacheKeyCasing casing) -> void UiPath.Caching.CacheKey.Casing.get -> UiPath.Caching.CacheKeyCasing @@ -14,82 +25,104 @@ UiPath.Caching.CacheKeyComparer.CacheKeyComparer() -> void UiPath.Caching.CacheOptions.KeyCasing.get -> UiPath.Caching.CacheKeyCasing UiPath.Caching.CacheOptions.KeyCasing.set -> void UiPath.Caching.CacheSyncExtensions +UiPath.Caching.HashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.DateTimeOffset expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +UiPath.Caching.HashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.TimeSpan expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +UiPath.Caching.HashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.HashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.TimeSpan expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.HashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.DateTimeOffset expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.HashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.TimeSpan expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.HashCacheExtensions UiPath.Caching.HashCacheSyncExtensions UiPath.Caching.ICache.GetAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.GetAsync(UiPath.Caching.CacheKey[]! cacheKeys, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask[]!> UiPath.Caching.ICache.GetCacheEntriesAsync(UiPath.Caching.CacheKey[]! cacheKeys, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!>[]!> UiPath.Caching.ICache.GetCacheEntryAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> -UiPath.Caching.ICache.GetOrAddAsync(System.Collections.Generic.KeyValuePair[]! entries, System.Func[]!>!>! generator, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask[]!> -UiPath.Caching.ICache.GetOrAddAsync(System.Collections.Generic.KeyValuePair[]! entries, System.Func[]!>!>! generator, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask[]!> +UiPath.Caching.ICache.GetOrAddAsync(System.Collections.Generic.KeyValuePair[]! entries, System.Func[]!>!>! generator, System.DateTimeOffset expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask[]!> +UiPath.Caching.ICache.GetOrAddAsync(System.Collections.Generic.KeyValuePair[]! entries, System.Func[]!>!>! generator, System.TimeSpan expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask[]!> UiPath.Caching.ICache.GetOrAddAsync(System.Collections.Generic.KeyValuePair[]! entries, System.Func[]!>!>! generator, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask[]!> -UiPath.Caching.ICache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.DateTimeOffset expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.TimeSpan expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.TimeSpan expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.SetAsync(System.Collections.Generic.KeyValuePair[]! keyValues, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.SetAsync(System.Collections.Generic.KeyValuePair[]! keyValues, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.SetAsync(System.Collections.Generic.KeyValuePair[]! keyValues, System.DateTimeOffset expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.SetAsync(System.Collections.Generic.KeyValuePair[]! keyValues, System.TimeSpan expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.SetAsync(System.Collections.Generic.KeyValuePair[]! keyValues, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.SetAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.SetAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.SetAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.SetAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.SetAsync(UiPath.Caching.CacheKey cacheKey, T? value, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.DateTimeOffset expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.TimeSpan expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.GetOrAddAsync(System.Collections.Generic.KeyValuePair[]! entries, System.Func[]!>!>! generator, System.DateTimeOffset expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask[]!> +UiPath.Caching.ICache.GetOrAddAsync(System.Collections.Generic.KeyValuePair[]! entries, System.Func[]!>!>! generator, System.TimeSpan expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask[]!> +UiPath.Caching.ICache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.TimeSpan expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.SetAsync(System.Collections.Generic.KeyValuePair[]! keyValues, System.DateTimeOffset expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.SetAsync(System.Collections.Generic.KeyValuePair[]! keyValues, System.TimeSpan expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.SetAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.SetAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ICache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ICache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ICache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.IHashCache.GetAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> UiPath.Caching.IHashCache.GetAsync(UiPath.Caching.CacheKey cacheKey, string![]! fields, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> UiPath.Caching.IHashCache.GetCacheEntryAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!>!> UiPath.Caching.IHashCache.GetItemAsync(UiPath.Caching.CacheKey cacheKey, string! field, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.IHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> -UiPath.Caching.IHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.DateTimeOffset? expiration, UiPath.Caching.HashCacheSetOption? setOption, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> -UiPath.Caching.IHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +UiPath.Caching.IHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.DateTimeOffset expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +UiPath.Caching.IHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.DateTimeOffset expiration, UiPath.Caching.HashCacheSetOption? setOption, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +UiPath.Caching.IHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.TimeSpan expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> UiPath.Caching.IHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> -UiPath.Caching.IHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.IHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.IHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.IHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.TimeSpan expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.IHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.IHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.HashCacheEntryOptions options, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.IHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.IHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.IHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.DateTimeOffset expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.IHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.TimeSpan expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.IHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.IHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, UiPath.Caching.HashCacheEntryOptions options, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.IHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.DateTimeOffset expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +UiPath.Caching.IHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.TimeSpan expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +UiPath.Caching.IHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.IHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.TimeSpan expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.IHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.DateTimeOffset expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.IHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.TimeSpan expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.NullCache.GetAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.NullCache.GetAsync(UiPath.Caching.CacheKey[]! cacheKeys, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask[]!> UiPath.Caching.NullCache.GetCacheEntriesAsync(UiPath.Caching.CacheKey[]! cacheKeys, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!>[]!> UiPath.Caching.NullCache.GetCacheEntryAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> -UiPath.Caching.NullCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.DateTimeOffset expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.TimeSpan expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.NullCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.TimeSpan expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.NullCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullCache.SetAsync(System.Collections.Generic.KeyValuePair[]! keyValues, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullCache.SetAsync(System.Collections.Generic.KeyValuePair[]! keyValues, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullCache.SetAsync(System.Collections.Generic.KeyValuePair[]! keyValues, System.DateTimeOffset expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullCache.SetAsync(System.Collections.Generic.KeyValuePair[]! keyValues, System.TimeSpan expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.NullCache.SetAsync(System.Collections.Generic.KeyValuePair[]! keyValues, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullCache.SetAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullCache.SetAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullCache.SetAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullCache.SetAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.NullCache.SetAsync(UiPath.Caching.CacheKey cacheKey, T? value, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullCache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullCache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullCache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullCache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.NullCache.TryAddAsync(UiPath.Caching.CacheKey cacheKey, T? value, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.NullHashCache.GetAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> UiPath.Caching.NullHashCache.GetAsync(UiPath.Caching.CacheKey cacheKey, string![]! fields, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> UiPath.Caching.NullHashCache.GetCacheEntryAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!>!> UiPath.Caching.NullHashCache.GetItemAsync(UiPath.Caching.CacheKey cacheKey, string! field, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> -UiPath.Caching.NullHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.DateTimeOffset? expiration, UiPath.Caching.HashCacheSetOption? setOption, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> -UiPath.Caching.NullHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +UiPath.Caching.NullHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.DateTimeOffset expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +UiPath.Caching.NullHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.DateTimeOffset expiration, UiPath.Caching.HashCacheSetOption? setOption, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +UiPath.Caching.NullHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.TimeSpan expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> UiPath.Caching.NullHashCache.GetOrAddAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> -UiPath.Caching.NullHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, System.TimeSpan expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.NullHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.NullHashCache.RefreshAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.HashCacheEntryOptions options, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.DateTimeOffset expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.TimeSpan expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.NullHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.NullHashCache.SetAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, UiPath.Caching.HashCacheEntryOptions options, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.SystemJsonByteSerializerProxy @@ -98,25 +131,28 @@ UiPath.Caching.SystemJsonByteSerializerProxy.Serialize(object? value) -> byte[]? UiPath.Caching.SystemJsonByteSerializerProxy.SystemJsonByteSerializerProxy(System.Text.Json.JsonSerializerOptions? options = null) -> void UiPath.Caching.SystemJsonByteSerializerProxy.TryDeserialize(object? value, out T? result) -> bool UiPath.Caching.SystemJsonByteSerializerProxy.TryDeserialize(string? value, out T? result) -> bool +static UiPath.Caching.CacheExpiration.ThrowIfNotFuture(System.DateTimeOffset expiration, System.DateTimeOffset now, string? paramName = null) -> System.DateTimeOffset +static UiPath.Caching.CacheExpiration.ThrowIfNotPositive(System.TimeSpan expiration, string? paramName = null) -> System.TimeSpan +static UiPath.Caching.CacheExpiration.ToDuration(System.DateTimeOffset expiration, System.DateTimeOffset now, string? paramName = null) -> System.TimeSpan static UiPath.Caching.CacheExtensions.GetAsync(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static UiPath.Caching.CacheExtensions.GetAsync(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey[]! cacheKeys, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask[]!> static UiPath.Caching.CacheExtensions.GetCacheEntriesAsync(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey[]! cacheKeys, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!>[]!> static UiPath.Caching.CacheExtensions.GetCacheEntryAsync(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> -static UiPath.Caching.CacheExtensions.GetOrAddAsync(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.CacheExtensions.GetOrAddAsync(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.DateTimeOffset expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static UiPath.Caching.CacheExtensions.GetOrAddAsync(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -static UiPath.Caching.CacheExtensions.GetOrAddAsync(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -static UiPath.Caching.CacheExtensions.RefreshAsync(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.CacheExtensions.GetOrAddAsync(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.TimeSpan expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.CacheExtensions.RefreshAsync(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static UiPath.Caching.CacheExtensions.RefreshAsync(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -static UiPath.Caching.CacheExtensions.RefreshAsync(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -static UiPath.Caching.CacheExtensions.SetAsync(this UiPath.Caching.ICache! cache, System.Collections.Generic.KeyValuePair[]! keyValues, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.CacheExtensions.RefreshAsync(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.TimeSpan expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.CacheExtensions.SetAsync(this UiPath.Caching.ICache! cache, System.Collections.Generic.KeyValuePair[]! keyValues, System.DateTimeOffset expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static UiPath.Caching.CacheExtensions.SetAsync(this UiPath.Caching.ICache! cache, System.Collections.Generic.KeyValuePair[]! keyValues, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -static UiPath.Caching.CacheExtensions.SetAsync(this UiPath.Caching.ICache! cache, System.Collections.Generic.KeyValuePair[]! keyValues, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -static UiPath.Caching.CacheExtensions.SetAsync(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.CacheExtensions.SetAsync(this UiPath.Caching.ICache! cache, System.Collections.Generic.KeyValuePair[]! keyValues, System.TimeSpan expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.CacheExtensions.SetAsync(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static UiPath.Caching.CacheExtensions.SetAsync(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, T? value, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -static UiPath.Caching.CacheExtensions.SetAsync(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -static UiPath.Caching.CacheExtensions.TryAddAsync(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.CacheExtensions.SetAsync(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.CacheExtensions.TryAddAsync(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static UiPath.Caching.CacheExtensions.TryAddAsync(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, T? value, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -static UiPath.Caching.CacheExtensions.TryAddAsync(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.CacheExtensions.TryAddAsync(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static UiPath.Caching.CacheKey.DefaultCasing.get -> UiPath.Caching.CacheKeyCasing static UiPath.Caching.CacheKey.DefaultCasing.set -> void static UiPath.Caching.CacheKeyComparer.Insensitive.get -> UiPath.Caching.CacheKeyComparer! @@ -125,42 +161,42 @@ static UiPath.Caching.CacheSyncExtensions.Contains(this UiPath.Caching.ICache static UiPath.Caching.CacheSyncExtensions.ExpireTime(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.DateTimeOffset? static UiPath.Caching.CacheSyncExtensions.Get(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> T? static UiPath.Caching.CacheSyncExtensions.Get(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey[]! cacheKeys, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Collections.Generic.KeyValuePair[]! -static UiPath.Caching.CacheSyncExtensions.GetOrAdd(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.Func! generator, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> T? +static UiPath.Caching.CacheSyncExtensions.GetOrAdd(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.Func! generator, System.DateTimeOffset expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> T? static UiPath.Caching.CacheSyncExtensions.GetOrAdd(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.Func! generator, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> T? -static UiPath.Caching.CacheSyncExtensions.GetOrAdd(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.Func! generator, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> T? -static UiPath.Caching.CacheSyncExtensions.GetOrAdd(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey[]! cacheKeys, System.Func[]!>! generator, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Collections.Generic.KeyValuePair[]! +static UiPath.Caching.CacheSyncExtensions.GetOrAdd(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.Func! generator, System.TimeSpan expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> T? +static UiPath.Caching.CacheSyncExtensions.GetOrAdd(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey[]! cacheKeys, System.Func[]!>! generator, System.DateTimeOffset expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Collections.Generic.KeyValuePair[]! static UiPath.Caching.CacheSyncExtensions.GetOrAdd(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey[]! cacheKeys, System.Func[]!>! generator, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Collections.Generic.KeyValuePair[]! -static UiPath.Caching.CacheSyncExtensions.GetOrAdd(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey[]! cacheKeys, System.Func[]!>! generator, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Collections.Generic.KeyValuePair[]! -static UiPath.Caching.CacheSyncExtensions.Refresh(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.CacheSyncExtensions.GetOrAdd(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey[]! cacheKeys, System.Func[]!>! generator, System.TimeSpan expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Collections.Generic.KeyValuePair[]! +static UiPath.Caching.CacheSyncExtensions.Refresh(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool static UiPath.Caching.CacheSyncExtensions.Refresh(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool -static UiPath.Caching.CacheSyncExtensions.Refresh(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.CacheSyncExtensions.Refresh(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.TimeSpan expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool static UiPath.Caching.CacheSyncExtensions.Remove(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool static UiPath.Caching.CacheSyncExtensions.Remove(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey[]! cacheKeys, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool -static UiPath.Caching.CacheSyncExtensions.Set(this UiPath.Caching.ICache! cache, System.Collections.Generic.KeyValuePair[]! keyValues, System.DateTimeOffset? expiration = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.CacheSyncExtensions.Set(this UiPath.Caching.ICache! cache, System.Collections.Generic.KeyValuePair[]! keyValues, System.DateTimeOffset expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool static UiPath.Caching.CacheSyncExtensions.Set(this UiPath.Caching.ICache! cache, System.Collections.Generic.KeyValuePair[]! keyValues, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool -static UiPath.Caching.CacheSyncExtensions.Set(this UiPath.Caching.ICache! cache, System.Collections.Generic.KeyValuePair[]! keyValues, System.TimeSpan? expiration = null, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool -static UiPath.Caching.CacheSyncExtensions.Set(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.CacheSyncExtensions.Set(this UiPath.Caching.ICache! cache, System.Collections.Generic.KeyValuePair[]! keyValues, System.TimeSpan expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.CacheSyncExtensions.Set(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool static UiPath.Caching.CacheSyncExtensions.Set(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, T? value, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool -static UiPath.Caching.CacheSyncExtensions.Set(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.CacheSyncExtensions.Set(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool static UiPath.Caching.CacheSyncExtensions.TimeToLive(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.TimeSpan? -static UiPath.Caching.CacheSyncExtensions.TryAdd(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.CacheSyncExtensions.TryAdd(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, T? value, System.DateTimeOffset expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool static UiPath.Caching.CacheSyncExtensions.TryAdd(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, T? value, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool -static UiPath.Caching.CacheSyncExtensions.TryAdd(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.CacheSyncExtensions.TryAdd(this UiPath.Caching.ICache! cache, UiPath.Caching.CacheKey cacheKey, T? value, System.TimeSpan expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool static UiPath.Caching.HashCacheExtensions.GetAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> static UiPath.Caching.HashCacheExtensions.GetAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, string![]! fields, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> static UiPath.Caching.HashCacheExtensions.GetCacheEntryAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!>!> static UiPath.Caching.HashCacheExtensions.GetItemAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, string! field, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -static UiPath.Caching.HashCacheExtensions.GetOrAddAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> -static UiPath.Caching.HashCacheExtensions.GetOrAddAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.DateTimeOffset? expiration, UiPath.Caching.HashCacheSetOption? setOption, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +static UiPath.Caching.HashCacheExtensions.GetOrAddAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.DateTimeOffset expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +static UiPath.Caching.HashCacheExtensions.GetOrAddAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.DateTimeOffset expiration, UiPath.Caching.HashCacheSetOption? setOption, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> static UiPath.Caching.HashCacheExtensions.GetOrAddAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> -static UiPath.Caching.HashCacheExtensions.GetOrAddAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> -static UiPath.Caching.HashCacheExtensions.RefreshAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.HashCacheExtensions.GetOrAddAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Func!>!>! generator, System.TimeSpan expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +static UiPath.Caching.HashCacheExtensions.RefreshAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static UiPath.Caching.HashCacheExtensions.RefreshAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -static UiPath.Caching.HashCacheExtensions.RefreshAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.HashCacheExtensions.RefreshAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.TimeSpan expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static UiPath.Caching.HashCacheExtensions.RefreshAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, UiPath.Caching.HashCacheEntryOptions options, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -static UiPath.Caching.HashCacheExtensions.SetAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.HashCacheExtensions.SetAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.DateTimeOffset expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static UiPath.Caching.HashCacheExtensions.SetAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -static UiPath.Caching.HashCacheExtensions.SetAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.HashCacheExtensions.SetAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.TimeSpan expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static UiPath.Caching.HashCacheExtensions.SetAsync(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, UiPath.Caching.HashCacheEntryOptions options, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static UiPath.Caching.HashCacheSyncExtensions.Contains(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool static UiPath.Caching.HashCacheSyncExtensions.ExpireTime(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.DateTimeOffset? @@ -169,17 +205,17 @@ static UiPath.Caching.HashCacheSyncExtensions.Get(this UiPath.Caching.IHashCa static UiPath.Caching.HashCacheSyncExtensions.GetCacheEntry(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> UiPath.Caching.ICacheEntry!>! static UiPath.Caching.HashCacheSyncExtensions.GetItem(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, string! field, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> T? static UiPath.Caching.HashCacheSyncExtensions.GetMetadata(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IDictionary? -static UiPath.Caching.HashCacheSyncExtensions.GetOrAdd(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IDictionary! +static UiPath.Caching.HashCacheSyncExtensions.GetOrAdd(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.DateTimeOffset expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IDictionary! static UiPath.Caching.HashCacheSyncExtensions.GetOrAdd(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IDictionary! -static UiPath.Caching.HashCacheSyncExtensions.GetOrAdd(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IDictionary! -static UiPath.Caching.HashCacheSyncExtensions.Refresh(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.HashCacheSyncExtensions.GetOrAdd(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Func!>! generator, System.TimeSpan expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IDictionary! +static UiPath.Caching.HashCacheSyncExtensions.Refresh(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.DateTimeOffset expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool static UiPath.Caching.HashCacheSyncExtensions.Refresh(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool -static UiPath.Caching.HashCacheSyncExtensions.Refresh(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.HashCacheSyncExtensions.Refresh(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.TimeSpan expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool static UiPath.Caching.HashCacheSyncExtensions.Refresh(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, UiPath.Caching.HashCacheEntryOptions options, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool static UiPath.Caching.HashCacheSyncExtensions.Remove(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool -static UiPath.Caching.HashCacheSyncExtensions.Set(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.HashCacheSyncExtensions.Set(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.DateTimeOffset expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool static UiPath.Caching.HashCacheSyncExtensions.Set(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool -static UiPath.Caching.HashCacheSyncExtensions.Set(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool +static UiPath.Caching.HashCacheSyncExtensions.Set(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, System.TimeSpan expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool static UiPath.Caching.HashCacheSyncExtensions.Set(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, UiPath.Caching.HashCacheEntryOptions options, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool static UiPath.Caching.HashCacheSyncExtensions.SetMetadata(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! metadata, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool static UiPath.Caching.HashCacheSyncExtensions.TimeToLive(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.TimeSpan? diff --git a/src/UiPath.Caching.Queue/ISetCache.cs b/src/UiPath.Caching.Queue/ISetCache.cs index af7c45f..62e4c05 100644 --- a/src/UiPath.Caching.Queue/ISetCache.cs +++ b/src/UiPath.Caching.Queue/ISetCache.cs @@ -6,6 +6,15 @@ namespace UiPath.Caching; /// insertion order and PopAsync removes a random member (Redis SPOP). Use the dedicated list /// caches when order matters. /// +/// +/// Expiration. The expiration parameter is not nullable. A caller with nothing to +/// say about lifetime calls the overload that has no expiration parameter and gets +/// , then the cache default; a caller that passes one +/// means it, so a duration that is not positive — or a deadline that has already passed — is +/// rejected with rather than silently ignored. See +/// . The no-op implementations in this package read no argument at all +/// and so enforce nothing. +/// public interface ISetCache : IDisposable { string Name { get; } @@ -21,10 +30,10 @@ public interface ISetCache : IDisposable ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, CachePolicy? policy, CancellationToken token = default); /// - ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default); + ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default); /// - ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default); + ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default); /// /// Removes and returns a random member of the set (Redis SPOP). The set is unordered, so this is diff --git a/src/UiPath.Caching.Queue/ISetCacheOfT.cs b/src/UiPath.Caching.Queue/ISetCacheOfT.cs index 875783b..a58a0bf 100644 --- a/src/UiPath.Caching.Queue/ISetCacheOfT.cs +++ b/src/UiPath.Caching.Queue/ISetCacheOfT.cs @@ -21,10 +21,10 @@ public interface ISetCache ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, CancellationToken token = default); /// - ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, TimeSpan? expiration, CancellationToken token = default); + ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, TimeSpan expiration, CancellationToken token = default); /// - ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, DateTimeOffset? expiration, CancellationToken token = default); + ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, DateTimeOffset expiration, CancellationToken token = default); /// /// Removes and returns a random member of the set (Redis SPOP). The set is unordered, so this is diff --git a/src/UiPath.Caching.Queue/MultilayerSetCache.cs b/src/UiPath.Caching.Queue/MultilayerSetCache.cs index 1265fc4..55767ef 100644 --- a/src/UiPath.Caching.Queue/MultilayerSetCache.cs +++ b/src/UiPath.Caching.Queue/MultilayerSetCache.cs @@ -58,12 +58,15 @@ public async ValueTask AddAsync(CacheKey cacheKey, T item, CachePolicy? } public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, CachePolicy? policy, CancellationToken token = default) => - AddAsync(cacheKey, items, expiration: (DateTimeOffset?)null, policy, token); + AddCoreAsync(cacheKey, items, expiration: null, policy, token); - public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) => - AddAsync(cacheKey, items, FromTtl(expiration), policy, token); + public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default) => + AddCoreAsync(cacheKey, items, DateTimeOffset.UtcNow.Add(CacheExpiration.ThrowIfNotPositive(expiration)), policy, token); - public async ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) + public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default) => + AddCoreAsync(cacheKey, items, CacheExpiration.ThrowIfNotFuture(expiration, DateTimeOffset.UtcNow), policy, token); + + private async ValueTask AddCoreAsync(CacheKey cacheKey, IEnumerable items, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token) { NotCacheableException.ThrowIfNotCacheable(); return await InternalAddAsync(cacheKey, Materialize(items), expiration, policy, token).ConfigureAwait(false); @@ -244,7 +247,11 @@ private async ValueTask InternalAddAsync(CacheKey cacheKey, IEnumerable { return await _memorySetCache.AddAsync(key, items, LocalWriteExpiration(expiration, policy), token).ConfigureAwait(false); } - var added = await _inner.AddAsync(cacheKey, items, expiration, policy, token).ConfigureAwait(false); + // null here is "no caller expiration": the inner cache resolves it from the policy, which is + // the overload that carries no expiration argument. + var added = expiration is { } deadline + ? await _inner.AddAsync(cacheKey, items, deadline, policy, token).ConfigureAwait(false) + : await _inner.AddAsync(cacheKey, items, policy, token).ConfigureAwait(false); await _memorySetCache.AddAsync(key, items, CancellationToken.None).ConfigureAwait(false); return added; } diff --git a/src/UiPath.Caching.Queue/NullSetCache.cs b/src/UiPath.Caching.Queue/NullSetCache.cs index c0a8728..d7efc0d 100644 --- a/src/UiPath.Caching.Queue/NullSetCache.cs +++ b/src/UiPath.Caching.Queue/NullSetCache.cs @@ -11,9 +11,9 @@ public sealed class NullSetCache : ISetCache public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, CachePolicy? policy, CancellationToken token = default) => ReturnZeroAsync(); - public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) => ReturnZeroAsync(); + public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default) => ReturnZeroAsync(); - public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) => ReturnZeroAsync(); + public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default) => ReturnZeroAsync(); public ValueTask PopAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default) { diff --git a/src/UiPath.Caching.Queue/PublicAPI.Shipped.txt b/src/UiPath.Caching.Queue/PublicAPI.Shipped.txt index 309ca31..9bf2c0c 100644 --- a/src/UiPath.Caching.Queue/PublicAPI.Shipped.txt +++ b/src/UiPath.Caching.Queue/PublicAPI.Shipped.txt @@ -16,9 +16,7 @@ UiPath.Caching.ISetCache.RemoveAsync(UiPath.Caching.CacheKey cacheKey, System UiPath.Caching.ISetCache.RemoveItemAsync(UiPath.Caching.CacheKey cacheKey, T item, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ISetCache.RemoveItemsAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ISetCache -UiPath.Caching.ISetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ISetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ISetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ISetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, T item, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ISetCache.ContainsAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ISetCache.ContainsItemAsync(UiPath.Caching.CacheKey cacheKey, T item, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask @@ -129,9 +127,7 @@ UiPath.Caching.RedisQueueCacheProvider.Enabled.get -> bool UiPath.Caching.RedisQueueCacheProvider.Name.get -> string! UiPath.Caching.RedisQueueCacheProvider.RedisQueueCacheProvider(Microsoft.Extensions.Options.IOptions! redisCacheOptions, Microsoft.Extensions.Options.IOptions! cacheOptions, Microsoft.Extensions.Options.IOptions! setCacheOptions, UiPath.Caching.Redis.IRedisConnector! redis, UiPath.Caching.ISerializerProxy! serializerProxy, UiPath.Caching.Policies.IResiliencePipelineProvider! resiliencePipelineProvider, UiPath.Caching.Telemetry.ICachingTelemetryProvider! cachingTelemetryProvider, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, UiPath.Caching.ICachePolicyFactory! policyFactory) -> void UiPath.Caching.SetCache -UiPath.Caching.SetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.SetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.SetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.SetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, T item, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.SetCache.ContainsAsync(UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.SetCache.ContainsItemAsync(UiPath.Caching.CacheKey cacheKey, T item, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask diff --git a/src/UiPath.Caching.Queue/PublicAPI.Unshipped.txt b/src/UiPath.Caching.Queue/PublicAPI.Unshipped.txt index 6b61b88..bc15121 100644 --- a/src/UiPath.Caching.Queue/PublicAPI.Unshipped.txt +++ b/src/UiPath.Caching.Queue/PublicAPI.Unshipped.txt @@ -1,37 +1,41 @@ #nullable enable -UiPath.Caching.ISetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.ISetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ISetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.DateTimeOffset expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ISetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.TimeSpan expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ISetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ISetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, T item, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ISetCache.MembersAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> UiPath.Caching.ISetCache.PopAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ISetCache.PopAsync(UiPath.Caching.CacheKey cacheKey, long count, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> -UiPath.Caching.NullSetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.NullSetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ISetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.DateTimeOffset expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.ISetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.TimeSpan expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullSetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.DateTimeOffset expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.NullSetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.TimeSpan expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.NullSetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.NullSetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, T item, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.NullSetCache.MembersAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> UiPath.Caching.NullSetCache.PopAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.NullSetCache.PopAsync(UiPath.Caching.CacheKey cacheKey, long count, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> -UiPath.Caching.Redis.RedisSetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -UiPath.Caching.Redis.RedisSetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.Redis.RedisSetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.DateTimeOffset expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.Redis.RedisSetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.TimeSpan expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.Redis.RedisSetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.Redis.RedisSetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, T item, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.Redis.RedisSetCache.MembersAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> UiPath.Caching.Redis.RedisSetCache.PopAsync(UiPath.Caching.CacheKey cacheKey, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.Redis.RedisSetCache.PopAsync(UiPath.Caching.CacheKey cacheKey, long count, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +UiPath.Caching.SetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.DateTimeOffset expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +UiPath.Caching.SetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.TimeSpan expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.SetCacheExtensions UiPath.Caching.SetCacheSyncExtensions -static UiPath.Caching.SetCacheExtensions.AddAsync(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.SetCacheExtensions.AddAsync(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.DateTimeOffset expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static UiPath.Caching.SetCacheExtensions.AddAsync(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask -static UiPath.Caching.SetCacheExtensions.AddAsync(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +static UiPath.Caching.SetCacheExtensions.AddAsync(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.TimeSpan expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static UiPath.Caching.SetCacheExtensions.AddAsync(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, T item, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static UiPath.Caching.SetCacheExtensions.MembersAsync(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> static UiPath.Caching.SetCacheExtensions.PopAsync(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static UiPath.Caching.SetCacheExtensions.PopAsync(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, long count, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> -static UiPath.Caching.SetCacheSyncExtensions.Add(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.DateTimeOffset? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> long +static UiPath.Caching.SetCacheSyncExtensions.Add(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.DateTimeOffset expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> long static UiPath.Caching.SetCacheSyncExtensions.Add(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> long -static UiPath.Caching.SetCacheSyncExtensions.Add(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.TimeSpan? expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> long +static UiPath.Caching.SetCacheSyncExtensions.Add(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.TimeSpan expiration, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> long static UiPath.Caching.SetCacheSyncExtensions.Add(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, T item, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool static UiPath.Caching.SetCacheSyncExtensions.Contains(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool static UiPath.Caching.SetCacheSyncExtensions.ContainsItem(this UiPath.Caching.ISetCache! cache, UiPath.Caching.CacheKey cacheKey, T item, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool diff --git a/src/UiPath.Caching.Queue/RedisSetCache.cs b/src/UiPath.Caching.Queue/RedisSetCache.cs index 8804097..74f1d36 100644 --- a/src/UiPath.Caching.Queue/RedisSetCache.cs +++ b/src/UiPath.Caching.Queue/RedisSetCache.cs @@ -41,27 +41,25 @@ public async ValueTask AddAsync(CacheKey cacheKey, T item, CachePolicy? { NotCacheableException.ThrowIfNotCacheable(); var value = _serializer.Serialize(item); - var added = await AddManyInnerAsync(cacheKey, [value], ResolveExpiration((DateTimeOffset?)null, policy), token).ConfigureAwait(false); + var added = await AddManyInnerAsync(cacheKey, [value], PolicyDeadline(policy), token).ConfigureAwait(false); return added > 0; } public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, CachePolicy? policy, CancellationToken token = default) => - AddAsync(cacheKey, items, expiration: (TimeSpan?)null, policy, token); + AddCoreAsync(cacheKey, items, PolicyDeadline(policy), token); - public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) - { - NotCacheableException.ThrowIfNotCacheable(); - ArgumentNullException.ThrowIfNull(items); - var values = items.Select(i => _serializer.Serialize(i)).ToArray(); - return AddManyInnerAsync(cacheKey, values, Clock.ToDateTimeOffset(ResolveExpiration(expiration, policy)), token); - } + public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default) => + AddCoreAsync(cacheKey, items, Clock.UtcNow.Add(CallerDuration(expiration)), token); + + public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default) => + AddCoreAsync(cacheKey, items, CallerDeadline(expiration), token); - public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) + private ValueTask AddCoreAsync(CacheKey cacheKey, IEnumerable items, DateTimeOffset expiration, CancellationToken token) { NotCacheableException.ThrowIfNotCacheable(); ArgumentNullException.ThrowIfNull(items); var values = items.Select(i => _serializer.Serialize(i)).ToArray(); - return AddManyInnerAsync(cacheKey, values, ResolveExpiration(expiration, policy), token); + return AddManyInnerAsync(cacheKey, values, expiration, token); } private async ValueTask AddManyInnerAsync(CacheKey cacheKey, RedisValue[] values, DateTimeOffset expiration, CancellationToken token) diff --git a/src/UiPath.Caching.Queue/SetCacheExtensions.cs b/src/UiPath.Caching.Queue/SetCacheExtensions.cs index 69cf8cb..a16b281 100644 --- a/src/UiPath.Caching.Queue/SetCacheExtensions.cs +++ b/src/UiPath.Caching.Queue/SetCacheExtensions.cs @@ -18,11 +18,11 @@ public static ValueTask AddAsync(this ISetCache cache, CacheKey cacheKe => cache.AddAsync(cacheKey, items, (CachePolicy?)null, token); /// - public static ValueTask AddAsync(this ISetCache cache, CacheKey cacheKey, IEnumerable items, TimeSpan? expiration, CancellationToken token = default) + public static ValueTask AddAsync(this ISetCache cache, CacheKey cacheKey, IEnumerable items, TimeSpan expiration, CancellationToken token = default) => cache.AddAsync(cacheKey, items, expiration, null, token); /// - public static ValueTask AddAsync(this ISetCache cache, CacheKey cacheKey, IEnumerable items, DateTimeOffset? expiration, CancellationToken token = default) + public static ValueTask AddAsync(this ISetCache cache, CacheKey cacheKey, IEnumerable items, DateTimeOffset expiration, CancellationToken token = default) => cache.AddAsync(cacheKey, items, expiration, null, token); /// diff --git a/src/UiPath.Caching.Queue/SetCacheOfT.cs b/src/UiPath.Caching.Queue/SetCacheOfT.cs index 2d7faf4..2ec49bf 100644 --- a/src/UiPath.Caching.Queue/SetCacheOfT.cs +++ b/src/UiPath.Caching.Queue/SetCacheOfT.cs @@ -24,10 +24,10 @@ public ValueTask AddAsync(CacheKey cacheKey, T item, CancellationToken tok public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, CancellationToken token = default) => _cache.AddAsync(GetCacheKey(cacheKey), items, policy: Policy, token: token); - public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, TimeSpan? expiration, CancellationToken token = default) => + public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, TimeSpan expiration, CancellationToken token = default) => _cache.AddAsync(GetCacheKey(cacheKey), items, expiration, Policy, token); - public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, DateTimeOffset? expiration, CancellationToken token = default) => + public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, DateTimeOffset expiration, CancellationToken token = default) => _cache.AddAsync(GetCacheKey(cacheKey), items, expiration, Policy, token); public ValueTask PopAsync(CacheKey cacheKey, CancellationToken token = default) => diff --git a/src/UiPath.Caching.Queue/SetCacheSyncExtensions.cs b/src/UiPath.Caching.Queue/SetCacheSyncExtensions.cs index 33b27b0..c1281c7 100644 --- a/src/UiPath.Caching.Queue/SetCacheSyncExtensions.cs +++ b/src/UiPath.Caching.Queue/SetCacheSyncExtensions.cs @@ -19,11 +19,11 @@ public static long Add(this ISetCache cache, CacheKey cacheKey, IEnumerabl => cache.AddAsync(cacheKey, items, token).AsTask().GetAwaiter().GetResult(); /// - public static long Add(this ISetCache cache, CacheKey cacheKey, IEnumerable items, TimeSpan? expiration, CancellationToken token = default) + public static long Add(this ISetCache cache, CacheKey cacheKey, IEnumerable items, TimeSpan expiration, CancellationToken token = default) => cache.AddAsync(cacheKey, items, expiration, token).AsTask().GetAwaiter().GetResult(); /// - public static long Add(this ISetCache cache, CacheKey cacheKey, IEnumerable items, DateTimeOffset? expiration, CancellationToken token = default) + public static long Add(this ISetCache cache, CacheKey cacheKey, IEnumerable items, DateTimeOffset expiration, CancellationToken token = default) => cache.AddAsync(cacheKey, items, expiration, token).AsTask().GetAwaiter().GetResult(); /// diff --git a/src/UiPath.Caching/Distributed/UiPathDistributedCache.cs b/src/UiPath.Caching/Distributed/UiPathDistributedCache.cs index 204098e..df9e76c 100644 --- a/src/UiPath.Caching/Distributed/UiPathDistributedCache.cs +++ b/src/UiPath.Caching/Distributed/UiPathDistributedCache.cs @@ -116,10 +116,19 @@ private ValueTask StoreAsync( IDictionary fields, DateTimeOffset now, TimeSpan? ttl, - CancellationToken token) => - ttl is null && _allowUnboundedEntries - ? _cache.SetAsync(cacheKey, fields, (DateTimeOffset?)DateTimeOffset.MaxValue, _policy, token) - : _cache.SetAsync(cacheKey, fields, ttl ?? Clamp(now, _defaultEntryExpiration), _policy, token); + CancellationToken token) + { + if (ttl is null && _allowUnboundedEntries) + { + return _cache.SetAsync(cacheKey, fields, DateTimeOffset.MaxValue, _policy, token); + } + + // Caller TTL, else this adapter's configured default. With neither, the provider's own + // default applies — which is what the overload carrying no expiration asks for. + return (ttl ?? Clamp(now, _defaultEntryExpiration)) is { } duration + ? _cache.SetAsync(cacheKey, fields, duration, _policy, token) + : _cache.SetAsync(cacheKey, fields, _policy, token); + } /// Composes the storage key by running the configured strategy over the validated caller key. private CacheKey Encode(string key) @@ -216,7 +225,7 @@ private async ValueTask SlideAsync( return; } - _ = await _cache.RefreshAsync(cacheKey, (DateTimeOffset?)target, _policy, token).ConfigureAwait(false); + _ = await _cache.RefreshAsync(cacheKey, target, _policy, token).ConfigureAwait(false); } /// Expiration metadata as written, decoded once. Null means the sentinel: that deadline was not set. diff --git a/src/UiPath.Caching/MultilayerCache.cs b/src/UiPath.Caching/MultilayerCache.cs index 0032988..3d0746a 100644 --- a/src/UiPath.Caching/MultilayerCache.cs +++ b/src/UiPath.Caching/MultilayerCache.cs @@ -85,30 +85,18 @@ public MultilayerCache( return GetOrAddInternalAsync(cacheKey, generator, writeExpiration, duration, policy, token); } - public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) + public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default) { ArgumentNullException.ThrowIfNull(generator); - policy ??= _defaultPolicy; - var duration = expiration ?? policy.DistributedExpiration; - return GetOrAddInternalAsync(cacheKey, generator, _clock.ToDateTimeOffset(ResolveWriteDuration(policy, expiration)), duration, policy, token); + var (writeExpiration, duration) = CallerWrite(expiration); + return GetOrAddInternalAsync(cacheKey, generator, writeExpiration, duration, policy ?? _defaultPolicy, token); } - public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) + public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default) { ArgumentNullException.ThrowIfNull(generator); - policy ??= _defaultPolicy; - TimeSpan? duration; - if (expiration.HasValue) - { - duration = expiration.Value - _clock.UtcNow; - if (duration is { } d && d <= TimeSpan.Zero) { duration = null; } - } - else - { - duration = policy.DistributedExpiration; - expiration = _clock.ToDateTimeOffset(ResolveWriteDuration(policy)); - } - return GetOrAddInternalAsync(cacheKey, generator, expiration, duration, policy, token); + var (writeExpiration, duration) = CallerWrite(expiration); + return GetOrAddInternalAsync(cacheKey, generator, writeExpiration, duration, policy ?? _defaultPolicy, token); } public ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, CachePolicy? policy, CancellationToken token = default) @@ -122,34 +110,22 @@ public MultilayerCache( return GetOrAddBatchInternalAsync(entries, generator, writeExpiration, duration, policy, token); } - public ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) + public ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default) where TState : notnull { ArgumentNullException.ThrowIfNull(entries); ArgumentNullException.ThrowIfNull(generator); - policy ??= _defaultPolicy; - var duration = expiration ?? policy.DistributedExpiration; - return GetOrAddBatchInternalAsync(entries, generator, _clock.ToDateTimeOffset(ResolveWriteDuration(policy, expiration)), duration, policy, token); + var (writeExpiration, duration) = CallerWrite(expiration); + return GetOrAddBatchInternalAsync(entries, generator, writeExpiration, duration, policy ?? _defaultPolicy, token); } - public ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) + public ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default) where TState : notnull { ArgumentNullException.ThrowIfNull(entries); ArgumentNullException.ThrowIfNull(generator); - policy ??= _defaultPolicy; - TimeSpan? duration; - if (expiration.HasValue) - { - duration = expiration.Value - _clock.UtcNow; - if (duration is { } d && d <= TimeSpan.Zero) { duration = null; } - } - else - { - duration = policy.DistributedExpiration; - expiration = _clock.ToDateTimeOffset(ResolveWriteDuration(policy)); - } - return GetOrAddBatchInternalAsync(entries, generator, expiration, duration, policy, token); + var (writeExpiration, duration) = CallerWrite(expiration); + return GetOrAddBatchInternalAsync(entries, generator, writeExpiration, duration, policy ?? _defaultPolicy, token); } private async ValueTask GetOrAddInternalAsync(CacheKey cacheKey, Func> generator, DateTimeOffset? expiration, TimeSpan? effectiveDuration, CachePolicy policy, CancellationToken token) @@ -634,20 +610,18 @@ private List> SelectEntriesToStore( public ValueTask SetAsync(CacheKey cacheKey, T? value, CachePolicy? policy, CancellationToken token = default) { policy ??= _defaultPolicy; - return SetAsync(cacheKey, value, ResolveWriteDuration(policy), policy, token); + return SetCoreAsync(cacheKey, value, PolicyDeadline(policy), policy, token); } - public ValueTask SetAsync(CacheKey cacheKey, T? value, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) - { - policy ??= _defaultPolicy; - return SetAsync(cacheKey, value, _clock.ToDateTimeOffset(ResolveWriteDuration(policy, expiration)), policy, token); - } + public ValueTask SetAsync(CacheKey cacheKey, T? value, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default) => + SetCoreAsync(cacheKey, value, CallerDeadline(expiration), policy ?? _defaultPolicy, token); + + public ValueTask SetAsync(CacheKey cacheKey, T? value, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default) => + SetCoreAsync(cacheKey, value, CallerDeadline(expiration), policy ?? _defaultPolicy, token); - public async ValueTask SetAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) + private async ValueTask SetCoreAsync(CacheKey cacheKey, T? value, DateTimeOffset expiration, CachePolicy policy, CancellationToken token) { NotCacheableException.ThrowIfNotCacheable(); - policy ??= _defaultPolicy; - expiration ??= _clock.ToDateTimeOffset(ResolveWriteDuration(policy)); var cacheEntryOptions = _entryBuilder.BuildEntryOptions(cacheKey, expiration, token); if (value is null && !_multiLayerCacheOptions.CacheNullValues) { @@ -669,18 +643,6 @@ public async ValueTask SetAsync(CacheKey cacheKey, T? value, DateTimeOf } - public ValueTask TryAddAsync(CacheKey cacheKey, T? value, CachePolicy? policy, CancellationToken token = default) - { - policy ??= _defaultPolicy; - return TryAddAsync(cacheKey, value, ResolveWriteDuration(policy), policy, token); - } - - public ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) - { - policy ??= _defaultPolicy; - return TryAddAsync(cacheKey, value, _clock.ToDateTimeOffset(ResolveWriteDuration(policy, expiration)), policy, token); - } - /// /// The L2 arbitrates and the L1 is populated only after a win — the reverse of SetAsync. /// L1 cannot arbitrate: a key absent locally may exist in the shared store, so a local probe @@ -688,11 +650,23 @@ public ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan? exp /// checks its connection first) rather than being gated on GetInnerCacheDisconnected, /// whose state also covers the broadcast transport — a dead topic must not stop a healthy Redis. /// - public async ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) + public ValueTask TryAddAsync(CacheKey cacheKey, T? value, CachePolicy? policy, CancellationToken token = default) { - NotCacheableException.ThrowIfNotCacheable(); policy ??= _defaultPolicy; - expiration ??= _clock.ToDateTimeOffset(ResolveWriteDuration(policy)); + return TryAddCoreAsync(cacheKey, value, PolicyDeadline(policy), policy, token); + } + + /// + public ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default) => + TryAddCoreAsync(cacheKey, value, CallerDeadline(expiration), policy ?? _defaultPolicy, token); + + /// + public ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default) => + TryAddCoreAsync(cacheKey, value, CallerDeadline(expiration), policy ?? _defaultPolicy, token); + + private async ValueTask TryAddCoreAsync(CacheKey cacheKey, T? value, DateTimeOffset expiration, CachePolicy policy, CancellationToken token) + { + NotCacheableException.ThrowIfNotCacheable(); var options = _entryBuilder.BuildEntryOptions(cacheKey, expiration, token); if (value is null && !_multiLayerCacheOptions.CacheNullValues) @@ -822,20 +796,18 @@ private async ValueTask LocalTryAddAsync(CacheEntryOptions options, T? public ValueTask SetAsync(KeyValuePair[] keyValues, CachePolicy? policy, CancellationToken token = default) { policy ??= _defaultPolicy; - return SetAsync(keyValues, ResolveWriteDuration(policy), policy, token); + return SetCoreAsync(keyValues, PolicyDeadline(policy), policy, token); } - public ValueTask SetAsync(KeyValuePair[] keyValues, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) - { - policy ??= _defaultPolicy; - return SetAsync(keyValues, _clock.ToDateTimeOffset(ResolveWriteDuration(policy, expiration)), policy, token); - } + public ValueTask SetAsync(KeyValuePair[] keyValues, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default) => + SetCoreAsync(keyValues, CallerDeadline(expiration), policy ?? _defaultPolicy, token); + + public ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default) => + SetCoreAsync(keyValues, CallerDeadline(expiration), policy ?? _defaultPolicy, token); - public async ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) + private async ValueTask SetCoreAsync(KeyValuePair[] keyValues, DateTimeOffset expiration, CachePolicy policy, CancellationToken token) { NotCacheableException.ThrowIfNotCacheable(); - policy ??= _defaultPolicy; - expiration ??= _clock.ToDateTimeOffset(ResolveWriteDuration(policy)); var removeEntries = new List(); var setEntries = new List>(); foreach (var keyValue in keyValues) @@ -904,20 +876,18 @@ public ValueTask RemoveAsync(CacheKey[] cacheKey, CancellationToken tok public ValueTask RefreshAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default) { policy ??= _defaultPolicy; - return RefreshAsync(cacheKey, ResolveWriteDuration(policy), policy, token); + return RefreshCoreAsync(cacheKey, PolicyDeadline(policy), policy, token); } - public ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) - { - policy ??= _defaultPolicy; - return RefreshAsync(cacheKey, _clock.ToDateTimeOffset(ResolveWriteDuration(policy, expiration)), policy, token); - } + public ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default) => + RefreshCoreAsync(cacheKey, CallerDeadline(expiration), policy ?? _defaultPolicy, token); - public async ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) + public ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default) => + RefreshCoreAsync(cacheKey, CallerDeadline(expiration), policy ?? _defaultPolicy, token); + + private async ValueTask RefreshCoreAsync(CacheKey cacheKey, DateTimeOffset expiration, CachePolicy policy, CancellationToken token) { NotCacheableException.ThrowIfNotCacheable(); - policy ??= _defaultPolicy; - expiration ??= _clock.ToDateTimeOffset(ResolveWriteDuration(policy)); var cacheEntryOptions = _entryBuilder.BuildEntryOptions(cacheKey, expiration, token); LogClearingCached(cacheEntryOptions.CacheKey); _memoryCache.Remove(cacheEntryOptions.CacheKey); @@ -1241,8 +1211,12 @@ private async ValueTask InternalSetAsync(CacheEntryValue[] cacheEntr return MemSet(policy.LocalExpirationDisconnected ?? _multiLayerCacheOptions.LocalMaxExpirationDisconnected); } - DateTimeOffset? batchExpiration = cacheEntries.Length > 0 ? cacheEntries[0].CacheEntry.Expiration : null; - var set = await _innerCache.SetAsync(cacheKeyValuePairs, batchExpiration, policy, token).ConfigureAwait(false); + // One deadline for the batch, taken from the first entry — every entry in a batch is + // built from the same resolved expiration. With no entries there is nothing to date, + // so the write inherits the policy. + var set = cacheEntries.Length > 0 + ? await _innerCache.SetAsync(cacheKeyValuePairs, cacheEntries[0].CacheEntry.Expiration, policy, token).ConfigureAwait(false) + : await _innerCache.SetAsync(cacheKeyValuePairs, policy, token).ConfigureAwait(false); return set && MemSet(policy.LocalExpiration ?? _multiLayerCacheOptions.LocalMaxExpiration); } catch (Exception ex) diff --git a/src/UiPath.Caching/MultilayerCacheBase.cs b/src/UiPath.Caching/MultilayerCacheBase.cs index 742ec38..c8b125b 100644 --- a/src/UiPath.Caching/MultilayerCacheBase.cs +++ b/src/UiPath.Caching/MultilayerCacheBase.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using UiPath.Caching.Config; using UiPath.Caching.Locking; using UiPath.Caching.Telemetry; @@ -192,6 +193,36 @@ private static TimeSpan NonNegativeOrFallback(TimeSpan? value, TimeSpan fallback return ApplyJitter(resolved, policy.JitterMaxDuration, _clock.UtcNow); } + /// + /// Validates a caller-supplied duration and pairs it with the deadline it implies. The write path + /// needs both: the deadline for the entry options, the duration for the L1 cap and the rehydrate + /// trigger. Jitter is deliberately not applied — a caller-supplied lifetime is honored exactly. + /// + private protected (DateTimeOffset Expiration, TimeSpan Duration) CallerWrite(TimeSpan expiration, [CallerArgumentExpression(nameof(expiration))] string? paramName = null) + { + var duration = CacheExpiration.ThrowIfNotPositive(expiration, paramName); + return (_clock.UtcNow.Add(duration), duration); + } + + /// + private protected (DateTimeOffset Expiration, TimeSpan Duration) CallerWrite(DateTimeOffset expiration, [CallerArgumentExpression(nameof(expiration))] string? paramName = null) + { + var now = _clock.UtcNow; + return (CacheExpiration.ThrowIfNotFuture(expiration, now, paramName), expiration - now); + } + + /// Write deadline for a call that carried no expiration: the policy's L2 TTL jittered, then the cache default. + private protected DateTimeOffset PolicyDeadline(CachePolicy policy) => + _clock.ToDateTimeOffset(ResolveWriteDuration(policy)); + + /// Write deadline for a caller-supplied duration, validated. + private protected DateTimeOffset CallerDeadline(TimeSpan expiration, [CallerArgumentExpression(nameof(expiration))] string? paramName = null) => + CallerWrite(expiration, paramName).Expiration; + + /// Write deadline for a caller-supplied deadline, validated. + private protected DateTimeOffset CallerDeadline(DateTimeOffset expiration, [CallerArgumentExpression(nameof(expiration))] string? paramName = null) => + CacheExpiration.ThrowIfNotFuture(expiration, _clock.UtcNow, paramName); + /// /// Acquires only the local lock for , for the callers to which it is a /// correctness requirement rather than a de-duplication optimization — the conditional add on a diff --git a/src/UiPath.Caching/MultilayerHashCache.cs b/src/UiPath.Caching/MultilayerHashCache.cs index 634837d..ec9f225 100644 --- a/src/UiPath.Caching/MultilayerHashCache.cs +++ b/src/UiPath.Caching/MultilayerHashCache.cs @@ -79,30 +79,18 @@ public MultilayerHashCache( return GetOrAddInternalAsync(cacheKey, generator, writeExpiration, duration, HashCacheSetOption.KeyReplace, policy, token); } - public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) + public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default) { ArgumentNullException.ThrowIfNull(generator); - policy ??= _defaultPolicy; - var duration = expiration ?? policy.DistributedExpiration; - return GetOrAddInternalAsync(cacheKey, generator, _clock.ToDateTimeOffset(ResolveWriteDuration(policy, expiration)), duration, HashCacheSetOption.KeyReplace, policy, token); + var (writeExpiration, duration) = CallerWrite(expiration); + return GetOrAddInternalAsync(cacheKey, generator, writeExpiration, duration, HashCacheSetOption.KeyReplace, policy ?? _defaultPolicy, token); } - public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) + public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default) { ArgumentNullException.ThrowIfNull(generator); - policy ??= _defaultPolicy; - TimeSpan? duration; - if (expiration.HasValue) - { - duration = expiration.Value - _clock.UtcNow; - if (duration is { } d && d <= TimeSpan.Zero) { duration = null; } - } - else - { - duration = policy.DistributedExpiration; - expiration = _clock.ToDateTimeOffset(ResolveWriteDuration(policy)); - } - return GetOrAddInternalAsync(cacheKey, generator, expiration, duration, HashCacheSetOption.KeyReplace, policy, token); + var (writeExpiration, duration) = CallerWrite(expiration); + return GetOrAddInternalAsync(cacheKey, generator, writeExpiration, duration, HashCacheSetOption.KeyReplace, policy ?? _defaultPolicy, token); } /// @@ -112,22 +100,11 @@ public MultilayerHashCache( /// _metadata_-as-empty-marker is present; we collapse that to for the /// caller, who can always iterate the result without a null check. /// - public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration, HashCacheSetOption? setOption, CachePolicy? policy, CancellationToken token = default) + public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset expiration, HashCacheSetOption? setOption, CachePolicy? policy, CancellationToken token = default) { ArgumentNullException.ThrowIfNull(generator); - policy ??= _defaultPolicy; - TimeSpan? duration; - if (expiration.HasValue) - { - duration = expiration.Value - _clock.UtcNow; - if (duration is { } d && d <= TimeSpan.Zero) { duration = null; } - } - else - { - duration = policy.DistributedExpiration; - expiration = _clock.ToDateTimeOffset(ResolveWriteDuration(policy)); - } - return GetOrAddInternalAsync(cacheKey, generator, expiration, duration, setOption ?? HashCacheSetOption.KeyReplace, policy, token); + var (writeExpiration, duration) = CallerWrite(expiration); + return GetOrAddInternalAsync(cacheKey, generator, writeExpiration, duration, setOption ?? HashCacheSetOption.KeyReplace, policy ?? _defaultPolicy, token); } private async ValueTask> GetOrAddInternalAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration, TimeSpan? effectiveDuration, HashCacheSetOption setOption, CachePolicy policy, CancellationToken token) @@ -210,20 +187,18 @@ private void TryHashRehydrate(CacheKey originalCacheKey, DateTimeOffset entry public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, CachePolicy? policy, CancellationToken token = default) { policy ??= _defaultPolicy; - return SetAsync(cacheKey, values, ResolveWriteDuration(policy), policy, token); + return SetCoreAsync(cacheKey, values, PolicyDeadline(policy), policy, token); } - public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) - { - policy ??= _defaultPolicy; - return SetAsync(cacheKey, values, _clock.ToDateTimeOffset(ResolveWriteDuration(policy, expiration)), policy, token); - } + public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default) => + SetCoreAsync(cacheKey, values, CallerDeadline(expiration), policy ?? _defaultPolicy, token); + + public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default) => + SetCoreAsync(cacheKey, values, CallerDeadline(expiration), policy ?? _defaultPolicy, token); - public async ValueTask SetAsync(CacheKey cacheKey, IDictionary values, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) + private async ValueTask SetCoreAsync(CacheKey cacheKey, IDictionary values, DateTimeOffset expiration, CachePolicy policy, CancellationToken token) { NotCacheableException.ThrowIfNotCacheable(); - policy ??= _defaultPolicy; - expiration ??= _clock.ToDateTimeOffset(ResolveWriteDuration(policy)); var options = _entryBuilder.BuildEntryOptions(cacheKey, expiration, token: token); if (IsNullOrEmpty(values) && !_multiLayerCacheOptions.CacheNullValues) { @@ -290,20 +265,14 @@ public ValueTask RemoveAsync(CacheKey cacheKey, CancellationToken token return RemoveAsync(_entryBuilder.BuildEntryOptions(cacheKey, default, token: token)); } - public ValueTask RefreshAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default) - { - policy ??= _defaultPolicy; - return RefreshAsync(cacheKey, ResolveWriteDuration(policy), policy, token); - } + public ValueTask RefreshAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default) => + RefreshAsync(cacheKey, new HashCacheEntryOptions(), policy, token); - public ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) - { - policy ??= _defaultPolicy; - return RefreshAsync(cacheKey, _clock.ToDateTimeOffset(ResolveWriteDuration(policy, expiration)), policy, token); - } + public ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default) => + RefreshAsync(cacheKey, new HashCacheEntryOptions(CallerDeadline(expiration)), policy, token); - public ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) => - RefreshAsync(cacheKey, new HashCacheEntryOptions(expiration), policy, token); + public ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default) => + RefreshAsync(cacheKey, new HashCacheEntryOptions(CallerDeadline(expiration)), policy, token); public async ValueTask RefreshAsync(CacheKey cacheKey, HashCacheEntryOptions options, CachePolicy? policy, CancellationToken token = default) { diff --git a/src/UiPath.Caching/PublicAPI.Shipped.txt b/src/UiPath.Caching/PublicAPI.Shipped.txt index 3d6b0c8..58e5a90 100644 --- a/src/UiPath.Caching/PublicAPI.Shipped.txt +++ b/src/UiPath.Caching/PublicAPI.Shipped.txt @@ -358,6 +358,8 @@ UiPath.Caching.InMemoryCacheProvider.Enabled.get -> bool UiPath.Caching.InMemoryCacheProvider.InMemoryCacheProvider(Microsoft.Extensions.Options.IOptions! optionsAccessor, Microsoft.Extensions.Options.IOptions! cacheOptionsAccessor, UiPath.Caching.IMemoryCacheFactory! memoryCacheFactory, UiPath.Caching.Broadcast.ICacheEventFactory! cacheEventFactory, UiPath.Caching.Broadcast.IChangeTokenFactory! changeTokenFactory, UiPath.Caching.Broadcast.ITopicFactory! topicFactory, UiPath.Caching.Telemetry.ICachingTelemetryProvider! cachingTelemetryProvider, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, UiPath.Caching.Locking.ILocalLock! localLock, UiPath.Caching.ICachePolicyFactory! policyFactory) -> void UiPath.Caching.InMemoryCacheProvider.Name.get -> string! UiPath.Caching.InMemoryRedisCacheOptions +UiPath.Caching.InMemoryRedisCacheOptions.BroadcastEnable.get -> bool +UiPath.Caching.InMemoryRedisCacheOptions.BroadcastEnable.set -> void UiPath.Caching.InMemoryRedisCacheOptions.CacheKeyStrategy.get -> UiPath.Caching.ICacheKeyStrategy? UiPath.Caching.InMemoryRedisCacheOptions.CacheKeyStrategy.set -> void UiPath.Caching.InMemoryRedisCacheOptions.CacheNullValues.get -> bool @@ -535,8 +537,6 @@ UiPath.Caching.Redis.RedisCacheBase.OnConnectionFailed -> System.EventHandler? UiPath.Caching.Redis.RedisCacheBase.OnConnectionRestored -> System.EventHandler? UiPath.Caching.Redis.RedisCacheBase.OnReconnected -> System.EventHandler? UiPath.Caching.Redis.RedisCacheBase.RedisCacheBase(UiPath.Caching.Redis.IRedisConnector! redis, UiPath.Caching.Telemetry.ICachingTelemetryProvider! telemetryProvider, UiPath.Caching.Redis.RedisCacheOptions! redisCacheOptions, UiPath.Caching.CacheOptions! cacheOptions, UiPath.Caching.ICachePolicyFactory! policyFactory) -> void -UiPath.Caching.Redis.RedisCacheBase.ResolveExpiration(System.DateTimeOffset? expiration, UiPath.Caching.CachePolicy? policy) -> System.DateTimeOffset -UiPath.Caching.Redis.RedisCacheBase.ResolveExpiration(System.TimeSpan? expiration, UiPath.Caching.CachePolicy? policy) -> System.TimeSpan? UiPath.Caching.Redis.RedisCacheBase.Telemetry.get -> UiPath.Caching.Telemetry.ICachingTelemetryProvider! UiPath.Caching.Redis.RedisCacheBase.TrackRead(UiPath.Caching.Telemetry.ITelemetryOperation! operation, bool hit, StackExchange.Redis.RedisKey key) -> void UiPath.Caching.Redis.RedisCacheOptions @@ -759,6 +759,7 @@ static UiPath.Caching.Config.ServiceCollectionExtensions.AddCaching(this Microso static UiPath.Caching.Config.ServiceCollectionExtensions.AddCaching(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static UiPath.Caching.Config.ServiceCollectionExtensions.AddCaching(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configure, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static UiPath.Caching.Config.ServiceCollectionExtensions.TryAddMemoryCacheFactory(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static UiPath.Caching.Config.ServiceCollectionExtensions.TryConfigure(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static UiPath.Caching.MemoryCacheExtensions.Monitor(this Microsoft.Extensions.Caching.Memory.IMemoryCache! cache, UiPath.Caching.ICacheOptions! cacheOptions, UiPath.Caching.Telemetry.ICachingTelemetryProvider! telemetryProvider, string! name) -> System.IDisposable! static UiPath.Caching.Metrics.GetReadTopicMetricName(string! topicName) -> string! static UiPath.Caching.Metrics.GetWriteTopicMetricName(string! topicName) -> string! @@ -819,6 +820,3 @@ virtual UiPath.Caching.Redis.RedisCacheBase.Dispose(bool disposing) -> void ~UiPath.Caching.Config.NullConfigurationSection.this[string key].set -> void ~static readonly UiPath.Caching.Config.NullConfiguration.Instance -> Microsoft.Extensions.Configuration.IConfiguration ~static readonly UiPath.Caching.Config.NullConfigurationSection.Instance -> Microsoft.Extensions.Configuration.IConfigurationSection -static UiPath.Caching.Config.ServiceCollectionExtensions.TryConfigure(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -UiPath.Caching.InMemoryRedisCacheOptions.BroadcastEnable.get -> bool -UiPath.Caching.InMemoryRedisCacheOptions.BroadcastEnable.set -> void diff --git a/src/UiPath.Caching/PublicAPI.Unshipped.txt b/src/UiPath.Caching/PublicAPI.Unshipped.txt index 00628a2..a6fb3b2 100644 --- a/src/UiPath.Caching/PublicAPI.Unshipped.txt +++ b/src/UiPath.Caching/PublicAPI.Unshipped.txt @@ -14,7 +14,13 @@ UiPath.Caching.Distributed.UiPathDistributedCacheOptions.RedisKeyDifferentiator. UiPath.Caching.Distributed.UiPathDistributedCacheOptions.RedisKeyStrategyFactory.get -> UiPath.Caching.Redis.IRedisKeyStrategyFactory? UiPath.Caching.Distributed.UiPathDistributedCacheOptions.RedisKeyStrategyFactory.set -> void UiPath.Caching.Distributed.UiPathDistributedCacheOptions.UiPathDistributedCacheOptions() -> void +UiPath.Caching.Redis.RedisCacheBase.CallerDeadline(System.DateTimeOffset expiration, string? paramName = null) -> System.DateTimeOffset +UiPath.Caching.Redis.RedisCacheBase.CallerDuration(System.DateTimeOffset expiration, string? paramName = null) -> System.TimeSpan +UiPath.Caching.Redis.RedisCacheBase.OptionsDeadline(System.DateTimeOffset? expireTime, System.TimeSpan? timeToLive, UiPath.Caching.CachePolicy? policy) -> System.DateTimeOffset +UiPath.Caching.Redis.RedisCacheBase.PolicyDeadline(UiPath.Caching.CachePolicy? policy) -> System.DateTimeOffset +UiPath.Caching.Redis.RedisCacheBase.PolicyDuration(UiPath.Caching.CachePolicy? policy) -> System.TimeSpan UiPath.Caching.Redis.RedisCacheOptions.AwaitRefresh.get -> bool UiPath.Caching.Redis.RedisCacheOptions.AwaitRefresh.set -> void const UiPath.Caching.Config.DistributedCacheCollectionExtensions.DistributedCacheServiceKey = "UiPath.Caching.Distributed" -> string! static UiPath.Caching.Config.DistributedCacheCollectionExtensions.AddDistributedCache(this UiPath.Caching.Config.ICachingBuilder! builder, string! providerName, System.Action? configure = null) -> UiPath.Caching.Config.ICachingBuilder! +static UiPath.Caching.Redis.RedisCacheBase.CallerDuration(System.TimeSpan expiration, string? paramName = null) -> System.TimeSpan diff --git a/src/UiPath.Caching/Redis/RedisCache.cs b/src/UiPath.Caching/Redis/RedisCache.cs index 0cabf6a..ff48b8c 100644 --- a/src/UiPath.Caching/Redis/RedisCache.cs +++ b/src/UiPath.Caching/Redis/RedisCache.cs @@ -72,19 +72,21 @@ public RedisCache( } public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, CachePolicy? policy, CancellationToken token = default) => - GetOrAddAsync(cacheKey, generator, expiration: (TimeSpan?)null, policy, token); + GetOrAddCoreAsync(cacheKey, generator, PolicyDuration(policy), policy, token); - public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) => - GetOrAddAsync(cacheKey, generator, expiration is { } d ? d.Subtract(Clock.UtcNow) : (TimeSpan?)null, policy, token); + public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default) => + GetOrAddCoreAsync(cacheKey, generator, CallerDuration(expiration), policy, token); - public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) + public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default) => + GetOrAddCoreAsync(cacheKey, generator, CallerDuration(expiration), policy, token); + + private ValueTask GetOrAddCoreAsync(CacheKey cacheKey, Func> generator, TimeSpan duration, CachePolicy? policy, CancellationToken token) { NotCacheableException.ThrowIfNotCacheable(); ArgumentNullException.ThrowIfNull(generator); var redisKey = ToRedisKey(cacheKey, token); - var effectiveExpiration = ResolveExpiration(expiration, policy); var wrappedGenerator = WrapWithFactoryTimeout(generator, (policy ?? DefaultPolicy)?.FactoryTimeout, cacheKey); - return GetOrAddInternalAsync(redisKey, wrappedGenerator, Clock.ToTimeSpan(effectiveExpiration), token); + return GetOrAddInternalAsync(redisKey, wrappedGenerator, duration, token); } /// @@ -97,7 +99,7 @@ public RedisCache( } /// - public ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) + public ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default) where TState : notnull { ArgumentNullException.ThrowIfNull(generator); @@ -106,7 +108,7 @@ public RedisCache( } /// - public ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) + public ValueTask[]> GetOrAddAsync(KeyValuePair[] entries, Func[]>> generator, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default) where TState : notnull { ArgumentNullException.ThrowIfNull(generator); @@ -143,16 +145,18 @@ public RedisCache( } public ValueTask RefreshAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default) => - RefreshAsync(cacheKey, expiration: (TimeSpan?)null, policy, token); + RefreshCoreAsync(cacheKey, PolicyDeadline(policy), token); + + public ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default) => + RefreshCoreAsync(cacheKey, Clock.UtcNow.Add(CallerDuration(expiration)), token); - public ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) => - RefreshAsync(cacheKey, Clock.ToDateTimeOffset(ResolveExpiration(expiration, policy)), policy, token); + public ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default) => + RefreshCoreAsync(cacheKey, CallerDeadline(expiration), token); - public async ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) + private async ValueTask RefreshCoreAsync(CacheKey cacheKey, DateTimeOffset expiration, CancellationToken token) { NotCacheableException.ThrowIfNotCacheable(); var redisKey = ToRedisKey(cacheKey, token); - expiration = ResolveExpiration(expiration, policy); LogRefreshingKey(redisKey, expiration); var ret = false; @@ -172,7 +176,7 @@ public async ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? ret = await _write.ExecuteAsync(async token => { token.ThrowIfCancellationRequested(); - return await Database.KeyExpireAsync(redisKey, expiration.Value.UtcDateTime, RefreshFlags).ConfigureAwait(false); + return await Database.KeyExpireAsync(redisKey, expiration.UtcDateTime, RefreshFlags).ConfigureAwait(false); }, default, token).ConfigureAwait(false); } operation.Stop(); @@ -202,57 +206,48 @@ public ValueTask RemoveAsync(CacheKey[] cacheKey, CancellationToken tok } public ValueTask SetAsync(CacheKey cacheKey, T? value, CachePolicy? policy, CancellationToken token = default) => - SetAsync(cacheKey, value, expiration: (TimeSpan?)null, policy, token); + SetCoreAsync(cacheKey, value, PolicyDuration(policy), token); - public ValueTask SetAsync(CacheKey cacheKey, T? value, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) - { - NotCacheableException.ThrowIfNotCacheable(); - var effective = ResolveExpiration(expiration, policy); - return SetInternalAsync(ToRedisKey(cacheKey, token), value, Clock.ToTimeSpan(effective), token); - } + public ValueTask SetAsync(CacheKey cacheKey, T? value, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default) => + SetCoreAsync(cacheKey, value, CallerDuration(expiration), token); - public ValueTask SetAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) - { - NotCacheableException.ThrowIfNotCacheable(); - var effective = ResolveExpiration(expiration, policy); - return SetInternalAsync(ToRedisKey(cacheKey, token), value, Clock.ToTimeSpan(effective), token); - } + public ValueTask SetAsync(CacheKey cacheKey, T? value, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default) => + SetCoreAsync(cacheKey, value, CallerDuration(expiration), token); - public ValueTask SetAsync(KeyValuePair[] keyValues, CachePolicy? policy, CancellationToken token = default) + private ValueTask SetCoreAsync(CacheKey cacheKey, T? value, TimeSpan duration, CancellationToken token) { NotCacheableException.ThrowIfNotCacheable(); - return SetAsync(keyValues, expiration: (TimeSpan?)null, policy, token); + return SetInternalAsync(ToRedisKey(cacheKey, token), value, duration, token); } - public ValueTask SetAsync(KeyValuePair[] keyValues, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) - { - NotCacheableException.ThrowIfNotCacheable(); - var effective = ResolveExpiration(expiration, policy); - return SetInternalAsync(keyValues, Clock.ToTimeSpan(effective), token); - } + public ValueTask SetAsync(KeyValuePair[] keyValues, CachePolicy? policy, CancellationToken token = default) => + SetCoreAsync(keyValues, PolicyDuration(policy), token); + + public ValueTask SetAsync(KeyValuePair[] keyValues, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default) => + SetCoreAsync(keyValues, CallerDuration(expiration), token); + + public ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default) => + SetCoreAsync(keyValues, CallerDuration(expiration), token); - public ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) + private ValueTask SetCoreAsync(KeyValuePair[] keyValues, TimeSpan duration, CancellationToken token) { NotCacheableException.ThrowIfNotCacheable(); - var effective = ResolveExpiration(expiration, policy); - return SetInternalAsync(keyValues, Clock.ToTimeSpan(effective), token); + return SetInternalAsync(keyValues, duration, token); } public ValueTask TryAddAsync(CacheKey cacheKey, T? value, CachePolicy? policy, CancellationToken token = default) => - TryAddAsync(cacheKey, value, expiration: (TimeSpan?)null, policy, token); + TryAddCoreAsync(cacheKey, value, PolicyDuration(policy), token); - public ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) - { - NotCacheableException.ThrowIfNotCacheable(); - var effective = ResolveExpiration(expiration, policy); - return TryAddInternalAsync(ToRedisKey(cacheKey, token), value, Clock.ToTimeSpan(effective), token); - } + public ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default) => + TryAddCoreAsync(cacheKey, value, CallerDuration(expiration), token); + + public ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default) => + TryAddCoreAsync(cacheKey, value, CallerDuration(expiration), token); - public ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) + private ValueTask TryAddCoreAsync(CacheKey cacheKey, T? value, TimeSpan duration, CancellationToken token) { NotCacheableException.ThrowIfNotCacheable(); - var effective = ResolveExpiration(expiration, policy); - return TryAddInternalAsync(ToRedisKey(cacheKey, token), value, Clock.ToTimeSpan(effective), token); + return TryAddInternalAsync(ToRedisKey(cacheKey, token), value, duration, token); } public async ValueTask ContainsAsync(CacheKey cacheKey, CancellationToken token = default) diff --git a/src/UiPath.Caching/Redis/RedisCacheBase.cs b/src/UiPath.Caching/Redis/RedisCacheBase.cs index 0154301..0710b57 100644 --- a/src/UiPath.Caching/Redis/RedisCacheBase.cs +++ b/src/UiPath.Caching/Redis/RedisCacheBase.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using UiPath.Caching.Config; using UiPath.Caching.Telemetry; @@ -53,11 +54,42 @@ protected void TrackRead(ITelemetryOperation operation, bool hit, RedisKey key) protected CacheClock Clock { get; } - protected TimeSpan? ResolveExpiration(TimeSpan? expiration, CachePolicy? policy) => - expiration ?? policy?.DistributedExpiration ?? DefaultExpiration; - - protected DateTimeOffset ResolveExpiration(DateTimeOffset? expiration, CachePolicy? policy) => - expiration ?? Clock.ToDateTimeOffset(policy?.DistributedExpiration ?? DefaultExpiration); + /// + /// Write duration for a call that carried no expiration: the policy's L2 TTL, then the + /// cache default, then for "no TTL". + /// + protected TimeSpan PolicyDuration(CachePolicy? policy) => + Clock.ToTimeSpan(policy?.DistributedExpiration ?? DefaultExpiration); + + /// + /// Write deadline for a call that carried no expiration, resolved the same way as + /// and yielding for "no TTL". + /// + protected DateTimeOffset PolicyDeadline(CachePolicy? policy) => + Clock.ToDateTimeOffset(policy?.DistributedExpiration ?? DefaultExpiration); + + /// + /// Write deadline carried by an entry-options object. keeps + /// its lifetime fields nullable — an options object is the one seam where null still + /// means "inherit" — so this resolves ExpireTime, then TimeToLive, then the policy + /// and cache defaults. + /// + protected DateTimeOffset OptionsDeadline(DateTimeOffset? expireTime, TimeSpan? timeToLive, CachePolicy? policy) => + expireTime.HasValue + ? Clock.ToDateTimeOffset(expireTime) + : Clock.ToDateTimeOffset(timeToLive ?? policy?.DistributedExpiration ?? DefaultExpiration); + + /// Validates a caller-supplied duration. + protected static TimeSpan CallerDuration(TimeSpan expiration, [CallerArgumentExpression(nameof(expiration))] string? paramName = null) => + CacheExpiration.ThrowIfNotPositive(expiration, paramName); + + /// Validates a caller-supplied deadline and turns it into a duration from the cache's now. + protected TimeSpan CallerDuration(DateTimeOffset expiration, [CallerArgumentExpression(nameof(expiration))] string? paramName = null) => + CacheExpiration.ToDuration(expiration, Clock.UtcNow, paramName); + + /// Validates a caller-supplied deadline. + protected DateTimeOffset CallerDeadline(DateTimeOffset expiration, [CallerArgumentExpression(nameof(expiration))] string? paramName = null) => + CacheExpiration.ThrowIfNotFuture(expiration, Clock.UtcNow, paramName); public event EventHandler? OnConnectionFailed { diff --git a/src/UiPath.Caching/Redis/RedisHashCache.cs b/src/UiPath.Caching/Redis/RedisHashCache.cs index 86dc1f8..795af39 100644 --- a/src/UiPath.Caching/Redis/RedisHashCache.cs +++ b/src/UiPath.Caching/Redis/RedisHashCache.cs @@ -72,15 +72,18 @@ public RedisHashCache( } public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, CachePolicy? policy, CancellationToken token = default) => - GetOrAddAsync(cacheKey, generator, expiration: (TimeSpan?)null, policy, token); + GetOrAddCoreAsync(cacheKey, generator, PolicyDeadline(policy), HashCacheSetOption.KeyReplace, policy, token); - public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) => - GetOrAddAsync(cacheKey, generator, Clock.ToDateTimeOffset(ResolveExpiration(expiration, policy)), HashCacheSetOption.KeyReplace, policy, token); + public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default) => + GetOrAddCoreAsync(cacheKey, generator, Clock.UtcNow.Add(CallerDuration(expiration)), HashCacheSetOption.KeyReplace, policy, token); - public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) => - GetOrAddAsync(cacheKey, generator, expiration, HashCacheSetOption.KeyReplace, policy, token); + public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default) => + GetOrAddCoreAsync(cacheKey, generator, CallerDeadline(expiration), HashCacheSetOption.KeyReplace, policy, token); - public async ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration, HashCacheSetOption? setOption, CachePolicy? policy, CancellationToken token = default) + public ValueTask> GetOrAddAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset expiration, HashCacheSetOption? setOption, CachePolicy? policy, CancellationToken token = default) => + GetOrAddCoreAsync(cacheKey, generator, CallerDeadline(expiration), setOption ?? HashCacheSetOption.KeyReplace, policy, token); + + private async ValueTask> GetOrAddCoreAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset effectiveExpiration, HashCacheSetOption setOption, CachePolicy? policy, CancellationToken token) { NotCacheableException.ThrowIfNotCacheable(); ArgumentNullException.ThrowIfNull(generator); @@ -91,17 +94,16 @@ public RedisHashCache( } LogCacheMissed(cacheKey); - var effectiveExpiration = ResolveExpiration(expiration, policy); var wrappedGenerator = WrapWithFactoryTimeout(generator, (policy ?? DefaultPolicy)?.FactoryTimeout, cacheKey); var ret = await wrappedGenerator(token).ConfigureAwait(false); if (ret.Count > 0) { - var options = new HashCacheEntryOptions(effectiveExpiration, default, default, setOption ?? HashCacheSetOption.KeyReplace); + var options = new HashCacheEntryOptions(effectiveExpiration, default, default, setOption); await SetAsync(cacheKey, ret, options, policy, token).ConfigureAwait(false); } else if (_cacheNullValues) { - var options = new HashCacheEntryOptions(effectiveExpiration, default, default, setOption ?? HashCacheSetOption.KeyReplace); + var options = new HashCacheEntryOptions(effectiveExpiration, default, default, setOption); await SetEmptyMarkerAsync(cacheKey, options, token).ConfigureAwait(false); } else @@ -225,16 +227,18 @@ public async ValueTask ContainsAsync(CacheKey cacheKey, CancellationTok } public ValueTask RefreshAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default) => - RefreshAsync(cacheKey, expiration: (TimeSpan?)null, policy, token); + RefreshCoreAsync(cacheKey, PolicyDeadline(policy), token); + + public ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default) => + RefreshCoreAsync(cacheKey, Clock.UtcNow.Add(CallerDuration(expiration)), token); - public ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) => - RefreshAsync(cacheKey, Clock.ToDateTimeOffset(ResolveExpiration(expiration, policy)), policy, token); + public ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default) => + RefreshCoreAsync(cacheKey, CallerDeadline(expiration), token); - public async ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) + private async ValueTask RefreshCoreAsync(CacheKey cacheKey, DateTimeOffset localExpiration, CancellationToken token) { NotCacheableException.ThrowIfNotCacheable(); var redisKey = ToRedisKey(cacheKey, token); - var localExpiration = ResolveExpiration(expiration, policy); LogRefreshingKey(redisKey, localExpiration); var ret = false; var operation = StartOperation(); @@ -270,10 +274,7 @@ public async ValueTask RefreshAsync(CacheKey cacheKey, HashCacheEntryOp { NotCacheableException.ThrowIfNotCacheable(); var redisKey = ToRedisKey(cacheKey, token); - var resolvedTtl = ResolveExpiration(options.TimeToLive, policy); - var expiration = options.ExpireTime.HasValue - ? Clock.ToDateTimeOffset(options.ExpireTime) - : Clock.ToDateTimeOffset(resolvedTtl); + var expiration = OptionsDeadline(options.ExpireTime, options.TimeToLive, policy); var now = Clock.UtcNow; var ret = false; var operation = StartOperation(); @@ -380,17 +381,19 @@ public async ValueTask RemoveAsync(CacheKey cacheKey, CancellationToken } public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, CachePolicy? policy, CancellationToken token = default) => - SetAsync(cacheKey, values, expiration: (TimeSpan?)null, policy, token); + SetCoreAsync(cacheKey, values, PolicyDeadline(policy), token); - public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) => - SetAsync(cacheKey, values, Clock.ToDateTimeOffset(ResolveExpiration(expiration, policy)), policy, token); + public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default) => + SetCoreAsync(cacheKey, values, Clock.UtcNow.Add(CallerDuration(expiration)), token); - public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) + public ValueTask SetAsync(CacheKey cacheKey, IDictionary values, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default) => + SetCoreAsync(cacheKey, values, CallerDeadline(expiration), token); + + private ValueTask SetCoreAsync(CacheKey cacheKey, IDictionary values, DateTimeOffset effective, CancellationToken token) { NotCacheableException.ThrowIfNotCacheable(); ValidateForWrite(values); var redisKey = ToRedisKey(cacheKey, token); - var effective = ResolveExpiration(expiration, policy); var hashEntries = new HashEntry[values.Count]; var i = 0; foreach (var kv in values) @@ -417,10 +420,7 @@ public ValueTask SetAsync(CacheKey cacheKey, IDictionary va entries[i] = new HashEntry(KnownFieldNames.MetadataKey, _serializer.Serialize(options.Metadata)); } - var resolvedTtl = ResolveExpiration(options.TimeToLive, policy); - var expiration = options.ExpireTime.HasValue - ? Clock.ToDateTimeOffset(options.ExpireTime) - : Clock.ToDateTimeOffset(resolvedTtl); + var expiration = OptionsDeadline(options.ExpireTime, options.TimeToLive, policy); var setOption = values.Count == 0 && _cacheNullValues ? HashCacheSetOption.KeyReplace : options.SetOption; return SetInnerAsync(redisKey, entries, setOption, expiration, token); diff --git a/tests/UiPath.Caching.Tests/CacheExpirationTests.cs b/tests/UiPath.Caching.Tests/CacheExpirationTests.cs new file mode 100644 index 0000000..aa2abbf --- /dev/null +++ b/tests/UiPath.Caching.Tests/CacheExpirationTests.cs @@ -0,0 +1,207 @@ +using Microsoft.Extensions.Logging.Abstractions; +using UiPath.Caching.Config; +using UiPath.Caching.Locking; +using UiPath.Caching.Telemetry; + +namespace UiPath.Caching.Tests; + +/// +/// The guard itself. covers it reaching the write surface. +/// +public class CacheExpirationTests +{ + [Theory] + [InlineData(0)] + [InlineData(-1)] + [InlineData(-3600)] + public void ThrowIfNotPositive_rejects_a_non_positive_duration(int seconds) + { + var act = () => CacheExpiration.ThrowIfNotPositive(TimeSpan.FromSeconds(seconds)); + + act.Should().Throw(); + } + + [Fact] + public void ThrowIfNotPositive_returns_the_value_it_accepts() + { + CacheExpiration.ThrowIfNotPositive(TimeSpan.FromTicks(1)).Should().Be(TimeSpan.FromTicks(1)); + CacheExpiration.ThrowIfNotPositive(TimeSpan.MaxValue).Should().Be(TimeSpan.MaxValue); + } + + [Fact] + public void ThrowIfNotPositive_names_the_parameter_it_was_given() + { + var expiration = TimeSpan.Zero; + + var act = () => CacheExpiration.ThrowIfNotPositive(expiration); + + act.Should().Throw().And.ParamName.Should().Be(nameof(expiration)); + } + + [Fact] + public void ThrowIfNotFuture_rejects_now_and_the_past() + { + var now = DateTimeOffset.UtcNow; + + var atNow = () => CacheExpiration.ThrowIfNotFuture(now, now); + var beforeNow = () => CacheExpiration.ThrowIfNotFuture(now.AddTicks(-1), now); + + atNow.Should().Throw(); + beforeNow.Should().Throw(); + } + + /// + /// is how the providers spell "no TTL", so the guard must + /// let it through rather than treating the sentinel as a bad argument. + /// + [Fact] + public void ThrowIfNotFuture_accepts_the_unbounded_sentinel() + { + var now = DateTimeOffset.UtcNow; + + CacheExpiration.ThrowIfNotFuture(DateTimeOffset.MaxValue, now).Should().Be(DateTimeOffset.MaxValue); + CacheExpiration.ThrowIfNotFuture(now.AddTicks(1), now).Should().Be(now.AddTicks(1)); + } + + [Fact] + public void ToDuration_measures_the_deadline_from_now() + { + var now = DateTimeOffset.UtcNow; + + CacheExpiration.ToDuration(now.AddMinutes(5), now).Should().Be(TimeSpan.FromMinutes(5)); + } + + [Fact] + public void ToDuration_rejects_a_deadline_that_has_passed() + { + var now = DateTimeOffset.UtcNow; + + var act = () => CacheExpiration.ToDuration(now.AddMinutes(-5), now); + + act.Should().Throw(); + } +} + +/// +/// The guard on the write surface, exercised through a real in-memory cache so what is covered is +/// the contract rather than one implementation's plumbing. Every write overload that takes an +/// expiration is here, because the point of dropping the nullable is that the rejection is uniform. +/// +public class CacheExpirationGuardTests +{ + private static CancellationToken Ct => TestContext.Current.CancellationToken; + + private static readonly TimeSpan[] NonPositive = [TimeSpan.Zero, TimeSpan.FromMinutes(-5)]; + + private static MultilayerCache CreateSut() + { + var options = new InMemoryCacheOptions(); + var cacheOptions = new CacheOptions { AppShortName = "test" }; + return new MultilayerCache( + KnownCacheProviderNames.InMemory, + NullCache.Instance, + new MemoryCacheFactory(null, NullLoggerFactory.Instance), + NullChangeTokenFactory.Instance, + NullTopicFactory.Instance, + NullCacheEventFactory.Instance, + NullTelemetryProvider.Instance, + options, + options, + cacheOptions, + localLock: new AsyncKeyedLocalLock(Options.Create(cacheOptions)), + distributedLock: NullDistributedLock.Instance, + policyFactory: NullCachePolicyFactory.Instance, + logger: NullLogger.Instance); + } + + private static DateTimeOffset Past => DateTimeOffset.UtcNow.AddMinutes(-5); + + private static async Task Rejects(Func write) + { + (await write.Should().ThrowAsync()).And.ParamName.Should().Be("expiration"); + } + + [Fact] + public async Task SetAsync_rejects_a_non_positive_duration() + { + using var sut = CreateSut(); + + foreach (var duration in NonPositive) + { + await Rejects(async () => await sut.SetAsync("k", "v", duration, policy: null, Ct)); + } + + (await sut.GetAsync("k", policy: null, token: Ct)).Should().BeNull(); + } + + [Fact] + public async Task SetAsync_rejects_a_deadline_that_has_passed() + { + using var sut = CreateSut(); + + await Rejects(async () => await sut.SetAsync("k", "v", Past, policy: null, Ct)); + + (await sut.GetAsync("k", policy: null, token: Ct)).Should().BeNull(); + } + + [Fact] + public async Task Batch_SetAsync_rejects_a_bad_expiration() + { + using var sut = CreateSut(); + KeyValuePair[] pairs = [new("k", "v")]; + + await Rejects(async () => await sut.SetAsync(pairs, TimeSpan.Zero, policy: null, Ct)); + await Rejects(async () => await sut.SetAsync(pairs, Past, policy: null, Ct)); + + (await sut.GetAsync("k", policy: null, token: Ct)).Should().BeNull(); + } + + [Fact] + public async Task RefreshAsync_rejects_a_bad_expiration() + { + using var sut = CreateSut(); + + await Rejects(async () => await sut.RefreshAsync("k", TimeSpan.Zero, policy: null, Ct)); + await Rejects(async () => await sut.RefreshAsync("k", Past, policy: null, Ct)); + } + + /// The generator must not run either — a bad lifetime is caught before any work. + [Fact] + public async Task GetOrAddAsync_rejects_a_bad_expiration_without_calling_the_generator() + { + using var sut = CreateSut(); + var called = false; + Func> generator = _ => + { + called = true; + return Task.FromResult("v"); + }; + + await Rejects(async () => await sut.GetOrAddAsync("k", generator, TimeSpan.Zero, policy: null, Ct)); + await Rejects(async () => await sut.GetOrAddAsync("k", generator, Past, policy: null, Ct)); + + called.Should().BeFalse(); + } + + [Fact] + public async Task Batch_GetOrAddAsync_rejects_a_bad_expiration() + { + using var sut = CreateSut(); + KeyValuePair[] entries = [new("k", "s")]; + + await Rejects(async () => await sut.GetOrAddAsync( + entries, (_, _) => Task.FromResult[]>([new("s", "v")]), TimeSpan.Zero, policy: null, Ct)); + await Rejects(async () => await sut.GetOrAddAsync( + entries, (_, _) => Task.FromResult[]>([new("s", "v")]), Past, policy: null, Ct)); + } + + /// The overload without an expiration is the supported way to ask for the policy default. + [Fact] + public async Task Omitting_the_expiration_still_writes() + { + using var sut = CreateSut(); + + (await sut.SetAsync("k", "v", policy: null, Ct)).Should().BeTrue(); + (await sut.GetAsync("k", policy: null, token: Ct)).Should().Be("v"); + } +} diff --git a/tests/UiPath.Caching.Tests/Distributed/DistributedCacheRedisIntegrationTests.cs b/tests/UiPath.Caching.Tests/Distributed/DistributedCacheRedisIntegrationTests.cs index 7f3280f..7934d90 100644 --- a/tests/UiPath.Caching.Tests/Distributed/DistributedCacheRedisIntegrationTests.cs +++ b/tests/UiPath.Caching.Tests/Distributed/DistributedCacheRedisIntegrationTests.cs @@ -162,14 +162,14 @@ public async Task Refresh_is_applied_and_reported_before_it_returns() await hash.SetAsync(cacheKey, new Dictionary { ["data"] = [1] }, TimeSpan.FromMinutes(5), null, token); - var applied = await hash.RefreshAsync(cacheKey, (DateTimeOffset?)DateTimeOffset.UtcNow.AddMinutes(30), null, token); + var applied = await hash.RefreshAsync(cacheKey, DateTimeOffset.UtcNow.AddMinutes(30), null, token); applied.Should().BeTrue("the reply is observed, so the result is meaningful"); (await database.KeyTimeToLiveAsync(redisKey)).Should() .BeGreaterThan(TimeSpan.FromMinutes(10), "no polling needed once the reply is awaited"); var absent = new CacheKey($"{UiPathDistributedCacheOptions.DefaultKeyPrefix}:{Unique()}", CacheKeyCasing.Sensitive); - (await hash.RefreshAsync(absent, (DateTimeOffset?)DateTimeOffset.UtcNow.AddMinutes(30), null, token)) + (await hash.RefreshAsync(absent, DateTimeOffset.UtcNow.AddMinutes(30), null, token)) .Should().BeFalse("a missing key is now distinguishable from a hit"); await hash.RemoveAsync(cacheKey, token); diff --git a/tests/UiPath.Caching.Tests/Distributed/UiPathDistributedCacheTests.cs b/tests/UiPath.Caching.Tests/Distributed/UiPathDistributedCacheTests.cs index 9bac865..d60df6d 100644 --- a/tests/UiPath.Caching.Tests/Distributed/UiPathDistributedCacheTests.cs +++ b/tests/UiPath.Caching.Tests/Distributed/UiPathDistributedCacheTests.cs @@ -105,7 +105,7 @@ await _inner.Received(1).GetAsync( Arg.Any(), Arg.Any(), Arg.Any()); await _inner.Received(1).SetAsync( Arg.Is(k => k.Name == expected), - Arg.Any>(), Arg.Any(), Arg.Any(), Arg.Any()); + Arg.Any>(), Arg.Any(), Arg.Any()); await _inner.Received(1).RemoveAsync( Arg.Is(k => k.Name == expected), Arg.Any()); } @@ -204,7 +204,7 @@ public async Task Failed_write_logs_the_key() new PrefixCacheKeyStrategy(UiPathDistributedCacheOptions.DefaultKeyPrefix), policy: null, logger, _clock); _inner.SetAsync(Arg.Any(), Arg.Any>(), - Arg.Any(), Arg.Any(), Arg.Any()).Returns(false); + Arg.Any(), Arg.Any(), Arg.Any()).Returns(false); await cache.SetAsync(key, Payload, new DistributedCacheEntryOptions(), TestContext.Current.CancellationToken); @@ -250,7 +250,7 @@ public async Task Get_returns_payload_and_slides_when_sliding_set() (await _cache.GetAsync("k", TestContext.Current.CancellationToken)).Should().Equal(Payload); await _inner.Received(1).RefreshAsync( - Arg.Any(), (DateTimeOffset?)Now.Add(sliding), Arg.Any(), Arg.Any()); + Arg.Any(), Now.Add(sliding), Arg.Any(), Arg.Any()); } [Fact] @@ -272,7 +272,7 @@ public async Task Get_slide_is_capped_by_absolute_expiration() await _cache.GetAsync("k", TestContext.Current.CancellationToken); await _inner.Received(1).RefreshAsync( - Arg.Any(), (DateTimeOffset?)absolute, Arg.Any(), Arg.Any()); + Arg.Any(), absolute, Arg.Any(), Arg.Any()); } [Fact] @@ -282,7 +282,7 @@ public async Task Get_without_sliding_does_not_refresh() (await _cache.GetAsync("k", TestContext.Current.CancellationToken)).Should().Equal(Payload); - await _inner.DidNotReceiveWithAnyArgs().RefreshAsync(default, (DateTimeOffset?)null, null, TestContext.Current.CancellationToken); + await _inner.DidNotReceiveWithAnyArgs().RefreshAsync(default, default(DateTimeOffset), null, TestContext.Current.CancellationToken); } [Fact] @@ -293,7 +293,7 @@ public async Task Absurd_sliding_window_clamps_instead_of_overflowing() (await _cache.GetAsync("k", TestContext.Current.CancellationToken)).Should().Equal(Payload); await _inner.Received(1).RefreshAsync( - Arg.Any(), (DateTimeOffset?)DateTimeOffset.MaxValue, Arg.Any(), Arg.Any()); + Arg.Any(), DateTimeOffset.MaxValue, Arg.Any(), Arg.Any()); } [Fact] @@ -312,7 +312,7 @@ public async Task Expired_absolute_entry_is_a_miss_and_is_not_removed() (await _cache.GetAsync("k", TestContext.Current.CancellationToken)).Should().BeNull(); await _inner.DidNotReceiveWithAnyArgs().RemoveAsync(default, TestContext.Current.CancellationToken); - await _inner.DidNotReceiveWithAnyArgs().RefreshAsync(default, (DateTimeOffset?)null, null, TestContext.Current.CancellationToken); + await _inner.DidNotReceiveWithAnyArgs().RefreshAsync(default, default(DateTimeOffset), null, TestContext.Current.CancellationToken); } @@ -323,7 +323,7 @@ public async Task Set_writes_payload_and_metadata_fields() IDictionary? written = null; TimeSpan? ttl = null; await _inner.SetAsync(Arg.Any(), Arg.Do>(v => written = v), - Arg.Do(t => ttl = t), Arg.Any(), Arg.Any()); + Arg.Do(t => ttl = t), Arg.Any(), Arg.Any()); await _cache.SetAsync("k", Payload, new DistributedCacheEntryOptions { SlidingExpiration = sliding }, TestContext.Current.CancellationToken); @@ -339,7 +339,7 @@ public async Task Set_with_both_uses_min_of_sliding_and_remaining_absolute() { TimeSpan? ttl = null; await _inner.SetAsync(Arg.Any(), Arg.Any>(), - Arg.Do(t => ttl = t), Arg.Any(), Arg.Any()); + Arg.Do(t => ttl = t), Arg.Any(), Arg.Any()); await _cache.SetAsync("k", Payload, new DistributedCacheEntryOptions { @@ -355,7 +355,7 @@ public async Task Set_with_relative_absolute_records_the_deadline() { IDictionary? written = null; await _inner.SetAsync(Arg.Any(), Arg.Do>(v => written = v), - Arg.Any(), Arg.Any(), Arg.Any()); + Arg.Any(), Arg.Any(), Arg.Any()); await _cache.SetAsync("k", Payload, new DistributedCacheEntryOptions { @@ -371,7 +371,7 @@ public async Task Set_with_no_expiration_uses_the_configured_default() { TimeSpan? ttl = null; await _inner.SetAsync(Arg.Any(), Arg.Any>(), - Arg.Do(t => ttl = t), Arg.Any(), Arg.Any()); + Arg.Do(t => ttl = t), Arg.Any(), Arg.Any()); var bounded = Build(new UiPathDistributedCacheOptions { DefaultEntryExpiration = TimeSpan.FromHours(2) }); await bounded.SetAsync("k", Payload, new DistributedCacheEntryOptions(), TestContext.Current.CancellationToken); @@ -382,13 +382,14 @@ await _inner.SetAsync(Arg.Any(), Arg.Any> [Fact] public async Task Set_with_no_expiration_and_no_default_defers_to_the_tier() { - TimeSpan? ttl = TimeSpan.FromDays(999); - await _inner.SetAsync(Arg.Any(), Arg.Any>(), - Arg.Do(t => ttl = t), Arg.Any(), Arg.Any()); - await _cache.SetAsync("k", Payload, new DistributedCacheEntryOptions(), TestContext.Current.CancellationToken); - ttl.Should().BeNull(); + // The write carries no expiration argument at all, which is the only way left to ask the + // tier for its own default. + await _inner.Received(1).SetAsync( + Arg.Any(), Arg.Any>(), Arg.Any(), Arg.Any()); + await _inner.DidNotReceive().SetAsync( + Arg.Any(), Arg.Any>(), Arg.Any(), Arg.Any(), Arg.Any()); } [Fact] @@ -412,7 +413,7 @@ await _inner.Received(1).GetAsync( Arg.Any(), Arg.Is(f => f != null && !f.Contains(DataField)), Arg.Any(), Arg.Any()); await _inner.Received(1).RefreshAsync( - Arg.Any(), (DateTimeOffset?)Now.Add(sliding), Arg.Any(), Arg.Any()); + Arg.Any(), Now.Add(sliding), Arg.Any(), Arg.Any()); } [Fact] @@ -440,7 +441,7 @@ await _inner.SetAsync(Arg.Any(), Arg.Do>( written.Should().NotBeNull(); written!.Should().ContainKey(SlidingExpirationField); written[DataField].Should().Equal(Payload); - await _inner.DidNotReceiveWithAnyArgs().RefreshAsync(default, (DateTimeOffset?)null, null, TestContext.Current.CancellationToken); + await _inner.DidNotReceiveWithAnyArgs().RefreshAsync(default, default(DateTimeOffset), null, TestContext.Current.CancellationToken); } @@ -456,7 +457,7 @@ public async Task Unbounded_entries_persist_instead_of_taking_a_default() { DateTimeOffset? expiration = null; await _inner.SetAsync(Arg.Any(), Arg.Any>(), - Arg.Do(e => expiration = e), Arg.Any(), Arg.Any()); + Arg.Do(e => expiration = e), Arg.Any(), Arg.Any()); var unbounded = Build(new UiPathDistributedCacheOptions { AllowUnboundedEntries = true }); await unbounded.SetAsync("k", Payload, new DistributedCacheEntryOptions(), TestContext.Current.CancellationToken); @@ -469,7 +470,7 @@ public async Task Absurd_sliding_write_clamps_the_ttl() { TimeSpan? ttl = null; await _inner.SetAsync(Arg.Any(), Arg.Any>(), - Arg.Do(t => ttl = t), Arg.Any(), Arg.Any()); + Arg.Do(t => ttl = t), Arg.Any(), Arg.Any()); await _cache.SetAsync("k", Payload, new DistributedCacheEntryOptions { SlidingExpiration = TimeSpan.MaxValue }, TestContext.Current.CancellationToken); @@ -501,7 +502,7 @@ public async Task Absurd_default_entry_expiration_is_clamped() { TimeSpan? ttl = null; await _inner.SetAsync(Arg.Any(), Arg.Any>(), - Arg.Do(t => ttl = t), Arg.Any(), Arg.Any()); + Arg.Do(t => ttl = t), Arg.Any(), Arg.Any()); var cache = Build(new UiPathDistributedCacheOptions { DefaultEntryExpiration = TimeSpan.MaxValue }); await cache.SetAsync("k", Payload, new DistributedCacheEntryOptions(), TestContext.Current.CancellationToken); @@ -629,7 +630,9 @@ public async Task Everything_a_write_produces_decodes_as_a_hit( var token = TestContext.Current.CancellationToken; IDictionary? written = null; await _inner.SetAsync(Arg.Any(), Arg.Do>(v => written = v), - Arg.Any(), Arg.Any(), Arg.Any()); + Arg.Any(), Arg.Any(), Arg.Any()); + await _inner.SetAsync(Arg.Any(), Arg.Do>(v => written = v), + Arg.Any(), Arg.Any()); await _cache.SetAsync("k", Payload, options, token); diff --git a/tests/UiPath.Caching.Tests/Fakes/DictionaryCache.cs b/tests/UiPath.Caching.Tests/Fakes/DictionaryCache.cs index 5325d78..3216fc9 100644 --- a/tests/UiPath.Caching.Tests/Fakes/DictionaryCache.cs +++ b/tests/UiPath.Caching.Tests/Fakes/DictionaryCache.cs @@ -67,25 +67,25 @@ public ValueTask SetAsync(KeyValuePair[] keyValues, Cache public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, CachePolicy? policy, CancellationToken token = default) => throw new NotSupportedException(); - public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) => + public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default) => throw new NotSupportedException(); - public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) => + public ValueTask GetOrAddAsync(CacheKey cacheKey, Func> generator, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default) => throw new NotSupportedException(); public ValueTask SetAsync(CacheKey cacheKey, T? value, CachePolicy? policy, CancellationToken token = default) => SetAsync([new KeyValuePair(cacheKey, value)], policy, token); - public ValueTask SetAsync(CacheKey cacheKey, T? value, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) => + public ValueTask SetAsync(CacheKey cacheKey, T? value, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default) => SetAsync([new KeyValuePair(cacheKey, value)], policy, token); - public ValueTask SetAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) => + public ValueTask SetAsync(CacheKey cacheKey, T? value, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default) => SetAsync([new KeyValuePair(cacheKey, value)], policy, token); - public ValueTask SetAsync(KeyValuePair[] keyValues, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) => + public ValueTask SetAsync(KeyValuePair[] keyValues, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default) => SetAsync(keyValues, policy, token); - public ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) => + public ValueTask SetAsync(KeyValuePair[] keyValues, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default) => SetAsync(keyValues, policy, token); // A real conditional add: the dictionary itself decides, so this fake can stand in for a store @@ -100,10 +100,10 @@ public ValueTask TryAddAsync(CacheKey cacheKey, T? value, CachePolicy? return ValueTask.FromResult(_store.TryAdd(cacheKey, value)); } - public ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) => + public ValueTask TryAddAsync(CacheKey cacheKey, T? value, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default) => TryAddAsync(cacheKey, value, policy, token); - public ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) => + public ValueTask TryAddAsync(CacheKey cacheKey, T? value, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default) => TryAddAsync(cacheKey, value, policy, token); public ValueTask RemoveAsync(CacheKey cacheKey, CancellationToken token = default) => @@ -117,9 +117,9 @@ public ValueTask RemoveAsync(CacheKey[] cacheKey, CancellationToken tok public ValueTask RefreshAsync(CacheKey cacheKey, CachePolicy? policy, CancellationToken token = default) => ValueTask.FromResult(true); - public ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan? expiration, CachePolicy? policy, CancellationToken token = default) => ValueTask.FromResult(true); + public ValueTask RefreshAsync(CacheKey cacheKey, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default) => ValueTask.FromResult(true); - public ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token = default) => ValueTask.FromResult(true); + public ValueTask RefreshAsync(CacheKey cacheKey, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default) => ValueTask.FromResult(true); public ValueTask ContainsAsync(CacheKey cacheKey, CancellationToken token = default) => ValueTask.FromResult(_store.ContainsKey(cacheKey)); diff --git a/tests/UiPath.Caching.Tests/InMemorySetCacheTests.cs b/tests/UiPath.Caching.Tests/InMemorySetCacheTests.cs index 1467d71..92ffa17 100644 --- a/tests/UiPath.Caching.Tests/InMemorySetCacheTests.cs +++ b/tests/UiPath.Caching.Tests/InMemorySetCacheTests.cs @@ -138,13 +138,13 @@ public async Task Remove_deletes_whole_set() } [Fact] - public async Task Add_with_past_expiration_stores_nothing() + public async Task Add_with_past_expiration_is_rejected() { var sut = CreateSut(); - var added = await sut.AddAsync("k", new[] { "a" }, TimeSpan.FromSeconds(-1), null, Ct); + var act = async () => await sut.AddAsync("k", new[] { "a" }, TimeSpan.FromSeconds(-1), null, Ct); - added.Should().Be(0); + (await act.Should().ThrowAsync()).And.ParamName.Should().Be("expiration"); (await sut.ContainsAsync("k", Ct)).Should().BeFalse(); } diff --git a/tests/UiPath.Caching.Tests/Locking/MultilayerCacheBatchGetOrAddLockTests.cs b/tests/UiPath.Caching.Tests/Locking/MultilayerCacheBatchGetOrAddLockTests.cs index 8898e72..00e68bf 100644 --- a/tests/UiPath.Caching.Tests/Locking/MultilayerCacheBatchGetOrAddLockTests.cs +++ b/tests/UiPath.Caching.Tests/Locking/MultilayerCacheBatchGetOrAddLockTests.cs @@ -237,14 +237,14 @@ public ValueTask InitializeAsync() _innerCache.GetCacheEntryAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns>(c => Entry(c.Arg())); - _innerCache.SetAsync(Arg.Any[]>(), Arg.Any(), Arg.Any(), Arg.Any()) + _innerCache.SetAsync(Arg.Any[]>(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(c => { foreach (var pair in c.Arg[]>()!) { _stored[pair.Key] = pair.Value; } return true; }); - _innerCache.SetAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _innerCache.SetAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(c => { _stored[c.Arg()] = c.ArgAt(1); diff --git a/tests/UiPath.Caching.Tests/Locking/MultilayerCacheGetOrAddLockTests.cs b/tests/UiPath.Caching.Tests/Locking/MultilayerCacheGetOrAddLockTests.cs index 8d51ba7..46756af 100644 --- a/tests/UiPath.Caching.Tests/Locking/MultilayerCacheGetOrAddLockTests.cs +++ b/tests/UiPath.Caching.Tests/Locking/MultilayerCacheGetOrAddLockTests.cs @@ -46,7 +46,7 @@ public async Task GetOrAddAsync_serializes_concurrent_generator_invocations_for_ string? storedValue = null; _innerCache.GetCacheEntryAsync((CacheKey)cacheKey, Arg.Any(), Arg.Any()) .Returns(_ => new TestCacheEntry { Value = storedValue }); - _innerCache.SetAsync((CacheKey)cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _innerCache.SetAsync((CacheKey)cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(c => { storedValue = c.Arg(); return true; }); var generatorCalls = 0; diff --git a/tests/UiPath.Caching.Tests/MemoryCacheSetterTests.cs b/tests/UiPath.Caching.Tests/MemoryCacheSetterTests.cs index 1d5cadf..123f537 100644 --- a/tests/UiPath.Caching.Tests/MemoryCacheSetterTests.cs +++ b/tests/UiPath.Caching.Tests/MemoryCacheSetterTests.cs @@ -127,7 +127,7 @@ public void RefreshMetadata_swallows_NewEntry_exception_and_emits_failure_event( .Returns(token); var cacheEntity = _fixture.Create(); - cacheEntity.NewEntry(Arg.Any(), Arg.Any?>()) + cacheEntity.NewEntry(Arg.Any(), Arg.Any?>()) .Returns(_ => throw new InvalidOperationException("simulated NewEntry failure")); var x = new InternalHashCacheEntryOptions() diff --git a/tests/UiPath.Caching.Tests/MultilayerCacheBatchGetOrAddTests.cs b/tests/UiPath.Caching.Tests/MultilayerCacheBatchGetOrAddTests.cs index ff236e8..e6ecf29 100644 --- a/tests/UiPath.Caching.Tests/MultilayerCacheBatchGetOrAddTests.cs +++ b/tests/UiPath.Caching.Tests/MultilayerCacheBatchGetOrAddTests.cs @@ -274,7 +274,7 @@ public async Task Expiration_overloads_flow_through_to_the_inner_write() _innerSetCalls.Should().HaveCount(2); await _innerCache.Received(1).SetAsync( Arg.Any[]>(), - Arg.Is(d => d.HasValue && d.Value == expiration), + Arg.Is(d => d == expiration), Arg.Any(), Arg.Any()); } @@ -345,7 +345,7 @@ public ValueTask InitializeAsync() : new TestCacheEntry { Value = null, Expiration = DateTimeOffset.MinValue })) .ToArray()); - _innerCache.SetAsync(Arg.Any[]>(), Arg.Any(), Arg.Any(), Arg.Any()) + _innerCache.SetAsync(Arg.Any[]>(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(c => { var pairs = c.Arg[]>()!; diff --git a/tests/UiPath.Caching.Tests/MultilayerCacheBatchRehydrateTests.cs b/tests/UiPath.Caching.Tests/MultilayerCacheBatchRehydrateTests.cs index 57101a4..d1631c5 100644 --- a/tests/UiPath.Caching.Tests/MultilayerCacheBatchRehydrateTests.cs +++ b/tests/UiPath.Caching.Tests/MultilayerCacheBatchRehydrateTests.cs @@ -450,7 +450,7 @@ public ValueTask InitializeAsync() : new TestCacheEntry { Value = null, Expiration = DateTimeOffset.MinValue })) .ToArray()); - _innerCache.SetAsync(Arg.Any[]>(), Arg.Any(), Arg.Any(), Arg.Any()) + _innerCache.SetAsync(Arg.Any[]>(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(c => { var pairs = c.Arg[]>()!; diff --git a/tests/UiPath.Caching.Tests/MultilayerCachePerNamePolicyWiringTests.cs b/tests/UiPath.Caching.Tests/MultilayerCachePerNamePolicyWiringTests.cs index 2b47d47..e621669 100644 --- a/tests/UiPath.Caching.Tests/MultilayerCachePerNamePolicyWiringTests.cs +++ b/tests/UiPath.Caching.Tests/MultilayerCachePerNamePolicyWiringTests.cs @@ -41,7 +41,7 @@ public async Task GetOrAdd_uses_policy_DistributedExpiration_when_caller_omits_e await _innerCache.Received(1).SetAsync( _cacheKey, "v", - Arg.Is(d => d.HasValue && d.Value - DateTimeOffset.UtcNow > policyTtl - TimeSpan.FromSeconds(5) && d.Value - DateTimeOffset.UtcNow < policyTtl + TimeSpan.FromSeconds(5)), + Arg.Is(d => d - DateTimeOffset.UtcNow > policyTtl - TimeSpan.FromSeconds(5) && d - DateTimeOffset.UtcNow < policyTtl + TimeSpan.FromSeconds(5)), Arg.Any(), Arg.Any()); } @@ -62,7 +62,7 @@ public async Task GetOrAdd_caller_expiration_beats_policy_DistributedExpiration( await _innerCache.Received(1).SetAsync( _cacheKey, "v", - Arg.Is(d => d.HasValue && d.Value - DateTimeOffset.UtcNow > callerTtl - TimeSpan.FromSeconds(5) && d.Value - DateTimeOffset.UtcNow < callerTtl + TimeSpan.FromSeconds(5)), + Arg.Is(d => d - DateTimeOffset.UtcNow > callerTtl - TimeSpan.FromSeconds(5) && d - DateTimeOffset.UtcNow < callerTtl + TimeSpan.FromSeconds(5)), Arg.Any(), Arg.Any()); } @@ -87,7 +87,7 @@ public async Task GetOrAdd_picks_up_DefaultCachePolicy_when_caller_passes_null_p await _innerCache.Received(1).SetAsync( _cacheKey, "v", - Arg.Is(d => d.HasValue && d.Value - DateTimeOffset.UtcNow > defaultTtl - TimeSpan.FromSeconds(5) && d.Value - DateTimeOffset.UtcNow < defaultTtl + TimeSpan.FromSeconds(5)), + Arg.Is(d => d - DateTimeOffset.UtcNow > defaultTtl - TimeSpan.FromSeconds(5) && d - DateTimeOffset.UtcNow < defaultTtl + TimeSpan.FromSeconds(5)), Arg.Any(), Arg.Any()); } @@ -111,7 +111,7 @@ public async Task GetOrAdd_falls_back_to_cache_options_DefaultExpiration_when_po await _innerCache.Received(1).SetAsync( _cacheKey, "v", - Arg.Is(d => d.HasValue && d.Value - DateTimeOffset.UtcNow > optionsTtl - TimeSpan.FromSeconds(5) && d.Value - DateTimeOffset.UtcNow < optionsTtl + TimeSpan.FromSeconds(5)), + Arg.Is(d => d - DateTimeOffset.UtcNow > optionsTtl - TimeSpan.FromSeconds(5) && d - DateTimeOffset.UtcNow < optionsTtl + TimeSpan.FromSeconds(5)), Arg.Any(), Arg.Any()); } @@ -125,7 +125,7 @@ public async Task SetAsync_uses_DefaultCachePolicy_DistributedExpiration_when_ca _fixture.Inject(new CacheOptions { DefaultCachePolicy = defaultPolicy, AppShortName = "test" }); _sut = null; - _innerCache.SetAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _innerCache.SetAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(true); _topic.PublishAsync(Arg.Any(), Arg.Any()) .Returns(_ => true); @@ -135,7 +135,7 @@ public async Task SetAsync_uses_DefaultCachePolicy_DistributedExpiration_when_ca await _innerCache.Received(1).SetAsync( _cacheKey, "v", - Arg.Is(d => d.HasValue && d.Value - DateTimeOffset.UtcNow > defaultTtl - TimeSpan.FromSeconds(5) && d.Value - DateTimeOffset.UtcNow < defaultTtl + TimeSpan.FromSeconds(5)), + Arg.Is(d => d - DateTimeOffset.UtcNow > defaultTtl - TimeSpan.FromSeconds(5) && d - DateTimeOffset.UtcNow < defaultTtl + TimeSpan.FromSeconds(5)), Arg.Any(), Arg.Any()); } @@ -144,7 +144,7 @@ await _innerCache.Received(1).SetAsync( public async Task SetAsync_falls_back_to_cache_options_DefaultExpiration_when_policy_omits_TTL() { // Default policy with no DistributedExpiration → SetAsync uses _multiLayerCacheOptions.DefaultExpiration. - _innerCache.SetAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _innerCache.SetAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(true); _topic.PublishAsync(Arg.Any(), Arg.Any()) .Returns(_ => true); @@ -155,7 +155,7 @@ public async Task SetAsync_falls_back_to_cache_options_DefaultExpiration_when_po await _innerCache.Received(1).SetAsync( _cacheKey, "v", - Arg.Is(d => d.HasValue && d.Value - DateTimeOffset.UtcNow > optionsTtl - TimeSpan.FromSeconds(5) && d.Value - DateTimeOffset.UtcNow < optionsTtl + TimeSpan.FromSeconds(5)), + Arg.Is(d => d - DateTimeOffset.UtcNow > optionsTtl - TimeSpan.FromSeconds(5) && d - DateTimeOffset.UtcNow < optionsTtl + TimeSpan.FromSeconds(5)), Arg.Any(), Arg.Any()); } @@ -216,7 +216,7 @@ public async Task RefreshAsync_uses_DefaultCachePolicy_DistributedExpiration_whe _fixture.Inject(new CacheOptions { DefaultCachePolicy = defaultPolicy, AppShortName = "test" }); _sut = null; - _innerCache.RefreshAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any()) + _innerCache.RefreshAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any()) .Returns(true); _topic.PublishAsync(Arg.Any(), Arg.Any()) .Returns(_ => true); @@ -225,9 +225,8 @@ public async Task RefreshAsync_uses_DefaultCachePolicy_DistributedExpiration_whe await _innerCache.Received(1).RefreshAsync( _cacheKey, - Arg.Is(d => d.HasValue - && d.Value - DateTimeOffset.UtcNow > policyTtl - TimeSpan.FromSeconds(5) - && d.Value - DateTimeOffset.UtcNow < policyTtl + TimeSpan.FromSeconds(5)), Arg.Any(), Arg.Any()); + Arg.Is(d => d - DateTimeOffset.UtcNow > policyTtl - TimeSpan.FromSeconds(5) + && d - DateTimeOffset.UtcNow < policyTtl + TimeSpan.FromSeconds(5)), Arg.Any(), Arg.Any()); } [Fact] @@ -240,7 +239,7 @@ public async Task SetAsync_jitters_policy_derived_expiration_within_max() _fixture.Inject(new CacheOptions { DefaultCachePolicy = defaultPolicy, AppShortName = "test" }); _sut = null; - _innerCache.SetAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _innerCache.SetAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(true); _topic.PublishAsync(Arg.Any(), Arg.Any()) .Returns(_ => true); @@ -250,9 +249,8 @@ public async Task SetAsync_jitters_policy_derived_expiration_within_max() await _innerCache.Received(1).SetAsync( _cacheKey, "v", - Arg.Is(d => d.HasValue - && d.Value - DateTimeOffset.UtcNow >= baseTtl - TimeSpan.FromSeconds(5) - && d.Value - DateTimeOffset.UtcNow <= baseTtl + maxJitter + TimeSpan.FromSeconds(5)), + Arg.Is(d => d - DateTimeOffset.UtcNow >= baseTtl - TimeSpan.FromSeconds(5) + && d - DateTimeOffset.UtcNow <= baseTtl + maxJitter + TimeSpan.FromSeconds(5)), Arg.Any(), Arg.Any()); } @@ -267,7 +265,7 @@ public async Task SetAsync_honors_caller_explicit_expiration_without_jitter() var callerTtl = TimeSpan.FromMinutes(2); - _innerCache.SetAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _innerCache.SetAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(true); _topic.PublishAsync(Arg.Any(), Arg.Any()) .Returns(_ => true); @@ -277,9 +275,8 @@ public async Task SetAsync_honors_caller_explicit_expiration_without_jitter() await _innerCache.Received(1).SetAsync( _cacheKey, "v", - Arg.Is(d => d.HasValue - && d.Value - DateTimeOffset.UtcNow > callerTtl - TimeSpan.FromSeconds(5) - && d.Value - DateTimeOffset.UtcNow < callerTtl + TimeSpan.FromSeconds(5)), + Arg.Is(d => d - DateTimeOffset.UtcNow > callerTtl - TimeSpan.FromSeconds(5) + && d - DateTimeOffset.UtcNow < callerTtl + TimeSpan.FromSeconds(5)), Arg.Any(), Arg.Any()); } @@ -304,9 +301,8 @@ public async Task GetOrAdd_jitters_policy_derived_expiration_within_max() await _innerCache.Received(1).SetAsync( _cacheKey, "v", - Arg.Is(d => d.HasValue - && d.Value - DateTimeOffset.UtcNow >= baseTtl - TimeSpan.FromSeconds(5) - && d.Value - DateTimeOffset.UtcNow <= baseTtl + maxJitter + TimeSpan.FromSeconds(5)), + Arg.Is(d => d - DateTimeOffset.UtcNow >= baseTtl - TimeSpan.FromSeconds(5) + && d - DateTimeOffset.UtcNow <= baseTtl + maxJitter + TimeSpan.FromSeconds(5)), Arg.Any(), Arg.Any()); } @@ -320,19 +316,18 @@ public async Task SetAsync_jitters_options_DefaultExpiration_when_policy_Distrib _fixture.Inject(new CacheOptions { DefaultCachePolicy = defaultPolicy, AppShortName = "test" }); _sut = null; - _innerCache.SetAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _innerCache.SetAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(true); _topic.PublishAsync(Arg.Any(), Arg.Any()) .Returns(_ => true); - await Sut.SetAsync(_cacheKey, "v", (TimeSpan?)null, policy: null, TestContext.Current.CancellationToken); + await Sut.SetAsync(_cacheKey, "v", policy: null, TestContext.Current.CancellationToken); await _innerCache.Received(1).SetAsync( _cacheKey, "v", - Arg.Is(d => d.HasValue - && d.Value - DateTimeOffset.UtcNow >= optionsTtl - TimeSpan.FromSeconds(5) - && d.Value - DateTimeOffset.UtcNow <= optionsTtl + maxJitter + TimeSpan.FromSeconds(5)), + Arg.Is(d => d - DateTimeOffset.UtcNow >= optionsTtl - TimeSpan.FromSeconds(5) + && d - DateTimeOffset.UtcNow <= optionsTtl + maxJitter + TimeSpan.FromSeconds(5)), Arg.Any(), Arg.Any()); } @@ -347,19 +342,18 @@ public async Task SetAsync_DateTimeOffset_overload_jitters_when_caller_passes_nu _fixture.Inject(new CacheOptions { DefaultCachePolicy = defaultPolicy, AppShortName = "test" }); _sut = null; - _innerCache.SetAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _innerCache.SetAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(true); _topic.PublishAsync(Arg.Any(), Arg.Any()) .Returns(_ => true); - await Sut.SetAsync(_cacheKey, "v", (DateTimeOffset?)null, policy: null, TestContext.Current.CancellationToken); + await Sut.SetAsync(_cacheKey, "v", policy: null, TestContext.Current.CancellationToken); await _innerCache.Received(1).SetAsync( _cacheKey, "v", - Arg.Is(d => d.HasValue - && d.Value - DateTimeOffset.UtcNow >= baseTtl - TimeSpan.FromSeconds(5) - && d.Value - DateTimeOffset.UtcNow <= baseTtl + maxJitter + TimeSpan.FromSeconds(5)), + Arg.Is(d => d - DateTimeOffset.UtcNow >= baseTtl - TimeSpan.FromSeconds(5) + && d - DateTimeOffset.UtcNow <= baseTtl + maxJitter + TimeSpan.FromSeconds(5)), Arg.Any(), Arg.Any()); } @@ -374,7 +368,7 @@ public async Task SetAsync_bulk_KeyValuePair_overload_jitters_policy_derived_exp _fixture.Inject(new CacheOptions { DefaultCachePolicy = defaultPolicy, AppShortName = "test" }); _sut = null; - _innerCache.SetAsync(Arg.Any[]>(), Arg.Any(), Arg.Any(), Arg.Any()) + _innerCache.SetAsync(Arg.Any[]>(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(true); _topic.PublishAsync(Arg.Any(), Arg.Any()) .Returns(_ => true); @@ -384,9 +378,8 @@ public async Task SetAsync_bulk_KeyValuePair_overload_jitters_policy_derived_exp await _innerCache.Received(1).SetAsync( Arg.Any[]>(), - Arg.Is(d => d.HasValue - && d.Value - DateTimeOffset.UtcNow >= baseTtl - TimeSpan.FromSeconds(5) - && d.Value - DateTimeOffset.UtcNow <= baseTtl + maxJitter + TimeSpan.FromSeconds(5)), + Arg.Is(d => d - DateTimeOffset.UtcNow >= baseTtl - TimeSpan.FromSeconds(5) + && d - DateTimeOffset.UtcNow <= baseTtl + maxJitter + TimeSpan.FromSeconds(5)), Arg.Any(), Arg.Any()); } @@ -404,7 +397,7 @@ public async Task SetAsync_jitter_actually_varies_across_calls() var ttls = new List(); _innerCache.SetAsync(_cacheKey, Arg.Any(), - Arg.Do(d => { if (d.HasValue) { ttls.Add(d.Value - DateTimeOffset.UtcNow); } }), + Arg.Do(d => ttls.Add(d - DateTimeOffset.UtcNow)), Arg.Any(), Arg.Any()) .Returns(true); _topic.PublishAsync(Arg.Any(), Arg.Any()) @@ -428,7 +421,7 @@ public async Task SetAsync_clamps_jitter_when_base_plus_jitter_would_overflow_Da _fixture.Inject(new CacheOptions { DefaultCachePolicy = defaultPolicy, AppShortName = "test" }); _sut = null; - _innerCache.SetAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _innerCache.SetAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(true); _topic.PublishAsync(Arg.Any(), Arg.Any()) .Returns(_ => true); @@ -441,7 +434,7 @@ await act.Should().NotThrowAsync( await _innerCache.Received(1).SetAsync( _cacheKey, "v", - Arg.Is(d => d.HasValue && d.Value <= DateTimeOffset.MaxValue), + Arg.Is(d => d <= DateTimeOffset.MaxValue), Arg.Any(), Arg.Any()); } diff --git a/tests/UiPath.Caching.Tests/MultilayerCacheRehydrateTests.cs b/tests/UiPath.Caching.Tests/MultilayerCacheRehydrateTests.cs index 6d0d08c..1256417 100644 --- a/tests/UiPath.Caching.Tests/MultilayerCacheRehydrateTests.cs +++ b/tests/UiPath.Caching.Tests/MultilayerCacheRehydrateTests.cs @@ -102,13 +102,13 @@ public async Task Rehydrate_writes_value_back_through_inner_cache_on_success() var acquiredLock = Substitute.For(); _distributedLock.TryAcquireAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(acquiredLock); - _innerCache.SetAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _innerCache.SetAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(true); await Sut.GetOrAddAsync(_cacheKey, generator, RehydratePolicy(), token); await WaitForAsync(() => _innerCache.ReceivedCalls().Any(c => c.GetMethodInfo().Name == nameof(ICache.SetAsync)), TimeSpan.FromSeconds(5), token); - await _innerCache.Received(1).SetAsync(_cacheKey, "rehydrated", Arg.Any(), Arg.Any(), Arg.Any()); + await _innerCache.Received(1).SetAsync(_cacheKey, "rehydrated", Arg.Any(), Arg.Any(), Arg.Any()); await acquiredLock.Received(1).DisposeAsync(); } @@ -135,7 +135,7 @@ public async Task Rehydrate_skipped_when_distributed_lock_unavailable() await WaitForAsync(() => _distributedLock.ReceivedCalls().Any(), TimeSpan.FromSeconds(5), token); await Task.Delay(50, TestContext.Current.CancellationToken); generatorCalls.Should().Be(0); - await _innerCache.DidNotReceive().SetAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + await _innerCache.DidNotReceive().SetAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); } [Fact] @@ -157,7 +157,7 @@ public async Task Rehydrate_does_not_release_lock_on_generator_failure() await WaitForAsync(() => _distributedLock.ReceivedCalls().Any(), TimeSpan.FromSeconds(5), token); await Task.Delay(100, TestContext.Current.CancellationToken); await acquiredLock.DidNotReceive().DisposeAsync(); - await _innerCache.DidNotReceive().SetAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + await _innerCache.DidNotReceive().SetAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); } [Fact] @@ -199,7 +199,7 @@ public async Task Rehydrate_holds_lock_when_inner_write_returns_false() _distributedLock.TryAcquireAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(acquiredLock); // Inner cache returns false from SetAsync — rehydrate must NOT report success. - _innerCache.SetAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _innerCache.SetAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(false); await Sut.GetOrAddAsync(_cacheKey, generator, RehydratePolicy(), token); @@ -254,7 +254,7 @@ public async Task Rehydrate_publishes_broadcast_so_other_nodes_invalidate_their_ var acquiredLock = Substitute.For(); _distributedLock.TryAcquireAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(acquiredLock); - _innerCache.SetAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _innerCache.SetAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(true); await Sut.GetOrAddAsync(_cacheKey, generator, RehydratePolicy(), token); @@ -307,7 +307,7 @@ public async Task Rehydrate_with_factory_returning_null_preserves_original_entry var acquiredLock = Substitute.For(); _distributedLock.TryAcquireAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(acquiredLock); - _innerCache.SetAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _innerCache.SetAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(true); await Sut.GetOrAddAsync(_cacheKey, generator, RehydratePolicy(), token); @@ -316,7 +316,7 @@ public async Task Rehydrate_with_factory_returning_null_preserves_original_entry await _innerCache.Received(1).SetAsync( _cacheKey, null, - Arg.Is(d => d.HasValue && Math.Abs((d.Value - originalDeadline).TotalSeconds) < 1), + Arg.Is(d => Math.Abs((d - originalDeadline).TotalSeconds) < 1), Arg.Any(), Arg.Any()); } diff --git a/tests/UiPath.Caching.Tests/MultilayerCacheTests.cs b/tests/UiPath.Caching.Tests/MultilayerCacheTests.cs index bb2b40d..d42244d 100644 --- a/tests/UiPath.Caching.Tests/MultilayerCacheTests.cs +++ b/tests/UiPath.Caching.Tests/MultilayerCacheTests.cs @@ -421,7 +421,7 @@ public async Task GetOrAdd_data_from_generator_default() var actual = await Sut.GetOrAddAsync(_cacheKey, generator, _fixture.Create(), token: testContextAccessor.Current.CancellationToken); generatorWasCalled.Should().BeTrue(); _memoryCache.Received(0).CreateEntry(_innerCacheKey); - await _innerCache.Received(0).SetAsync(_innerCacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + await _innerCache.Received(0).SetAsync(_innerCacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); actual.Should().BeNull(); } @@ -517,7 +517,7 @@ public async Task Multi_set_keeps_null_entries_in_set_path_when_CacheNullValues_ { _options.CacheNullValues = true; _sut = null; - _innerCache.SetAsync(Arg.Any[]>(), Arg.Any(), Arg.Any(), Arg.Any()) + _innerCache.SetAsync(Arg.Any[]>(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(true); var pairs = new KeyValuePair[] @@ -530,7 +530,7 @@ public async Task Multi_set_keeps_null_entries_in_set_path_when_CacheNullValues_ await _innerCache.DidNotReceive().RemoveAsync(Arg.Any(), Arg.Any()); await _innerCache.Received(1).SetAsync( Arg.Is[]>(p => p != null && p.Length == 2 && p.Any(kv => kv.Value == null)), - Arg.Any(), Arg.Any(), Arg.Any()); + Arg.Any(), Arg.Any(), Arg.Any()); } [Fact] @@ -538,7 +538,7 @@ public async Task Multi_set_forwards_caller_expiration_to_inner_cache() { _options.CacheNullValues = true; _sut = null; - _innerCache.SetAsync(Arg.Any[]>(), Arg.Any(), Arg.Any(), Arg.Any()) + _innerCache.SetAsync(Arg.Any[]>(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(true); var ttl = TimeSpan.FromMinutes(7); @@ -551,7 +551,7 @@ public async Task Multi_set_forwards_caller_expiration_to_inner_cache() await _innerCache.Received(1).SetAsync( Arg.Any[]>(), - Arg.Is(exp => exp.HasValue && exp.Value > _clock.UtcNow), Arg.Any(), Arg.Any()); + Arg.Is(exp => exp > _clock.UtcNow), Arg.Any(), Arg.Any()); } [Fact] @@ -595,7 +595,7 @@ public async Task Set_value_inner_cache_throw_exception() var actual = await Sut.SetAsync(_cacheKey, value, _fixture.Create(), token: testContextAccessor.Current.CancellationToken); actual.Should().BeFalse(); - actual = await Sut.SetAsync(_cacheKey, value, _fixture.Create(), token: testContextAccessor.Current.CancellationToken); + actual = await Sut.SetAsync(_cacheKey, value, DateTimeOffset.UtcNow.AddMinutes(5), token: testContextAccessor.Current.CancellationToken); actual.Should().BeFalse(); } @@ -912,7 +912,7 @@ public async Task Refresh_value_default_expiration() [Fact] public async Task Refresh_value_TimeSpan() { - var expiration = _fixture.Create(); + var expiration = TimeSpan.FromMinutes(5); await Sut.RefreshAsync(_cacheKey, expiration, token: testContextAccessor.Current.CancellationToken); _memoryCache.Received(1).Remove(_innerCacheKey); await _innerCache.Received(1).RefreshAsync(_innerCacheKey, Arg.Any(), Arg.Any(), Arg.Any()); @@ -934,7 +934,7 @@ public async Task Refresh_value_DateTimeOffset() [InlineData(true)] public async Task Refresh_inner_cache_exception_timespan(bool eventFired) { - var expiration = _fixture.Create(); + var expiration = TimeSpan.FromMinutes(5); _innerCache.RefreshAsync(_innerCacheKey, Arg.Any(), Arg.Any(), Arg.Any()) .ThrowsAsync(new Exception()); _topic.PublishAsync(Arg.Any(), Arg.Any()) @@ -1029,7 +1029,7 @@ public async Task Read_ExpireTime_For_Key() }; _changeTokenFactory.Create(Arg.Any(), Arg.Any>(), Arg.Any(), Arg.Any()) .Returns(token); - _innerCache.SetAsync(_innerCacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _innerCache.SetAsync(_innerCacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(true); var expiration = _clock.UtcNow.AddYears(1); @@ -1055,7 +1055,7 @@ public async Task Read_ExpireTimeToLive_For_Key() _changeTokenFactory.Create(Arg.Any(), Arg.Any>(), Arg.Any(), Arg.Any()) .Returns(_ => token); - _innerCache.SetAsync(_innerCacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _innerCache.SetAsync(_innerCacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(true); _topic.PublishAsync(Arg.Any(), Arg.Any()) .Returns(_ => true); @@ -1077,7 +1077,7 @@ public async Task When_no_inner_cache_expire_time_use_max() _innerCache.GetCacheEntryAsync(_innerCacheKey, Arg.Any(), Arg.Any()) .Returns(new TestCacheEntry { Value = expected, Expiration = DateTimeOffset.MaxValue }); _options.DefaultExpiration = null; - _ = await Sut.GetOrAddAsync(_cacheKey, generator, expiration: default(DateTimeOffset?), token: testContextAccessor.Current.CancellationToken); + _ = await Sut.GetOrAddAsync(_cacheKey, generator, token: testContextAccessor.Current.CancellationToken); cacheEntry.AbsoluteExpiration.Should().Be(DateTimeOffset.MaxValue); } @@ -1338,7 +1338,7 @@ public async Task Multi_SetAsync_calls_inner_cache_based_on_innerCacheDisconnect _topicProvider.IsConnected.Returns(isConnected); _topic.PublishAsync(Arg.Any(), Arg.Any()) .Returns(_ => true); - _innerCache.SetAsync(Arg.Any[]>(), Arg.Any(), Arg.Any(), Arg.Any()) + _innerCache.SetAsync(Arg.Any[]>(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(true); var actual = await Sut.SetAsync(new KeyValuePair[] { new(_cacheKey, value), new(_multiKey, value) }, _fixture.Create(), policy: null, token: testContextAccessor.Current.CancellationToken); @@ -1348,12 +1348,12 @@ public async Task Multi_SetAsync_calls_inner_cache_based_on_innerCacheDisconnect if (innerCacheDisconnected) { - await _innerCache.DidNotReceive().SetAsync(Arg.Any[]>(), Arg.Any(), Arg.Any(), Arg.Any()); + await _innerCache.DidNotReceive().SetAsync(Arg.Any[]>(), Arg.Any(), Arg.Any(), Arg.Any()); await _topic.DidNotReceive().PublishAsync(Arg.Any(), Arg.Any()); } else { - await _innerCache.Received(1).SetAsync(Arg.Any[]>(), Arg.Any(), Arg.Any(), Arg.Any()); + await _innerCache.Received(1).SetAsync(Arg.Any[]>(), Arg.Any(), Arg.Any(), Arg.Any()); await _topic.Received(2).PublishAsync(Arg.Any(), Arg.Any()); } } diff --git a/tests/UiPath.Caching.Tests/MultilayerCacheTryAddTests.cs b/tests/UiPath.Caching.Tests/MultilayerCacheTryAddTests.cs index 29b4605..ba17002 100644 --- a/tests/UiPath.Caching.Tests/MultilayerCacheTryAddTests.cs +++ b/tests/UiPath.Caching.Tests/MultilayerCacheTryAddTests.cs @@ -42,14 +42,14 @@ public class MultilayerCacheTryAddTests(ITestContextAccessor testContextAccessor public async Task TryAdd_delegates_the_decision_to_the_inner_cache() { var value = _fixture.Create(); - _innerCache.TryAddAsync(_cacheKey, value, Arg.Any(), Arg.Any(), Arg.Any()) + _innerCache.TryAddAsync(_cacheKey, value, Arg.Any(), Arg.Any(), Arg.Any()) .Returns(true); _topic.PublishAsync(Arg.Any(), Arg.Any()).Returns(_ => true); var added = await Sut.TryAddAsync(_cacheKey, value, policy: null, token: Ct); added.Should().BeTrue(); - await _innerCache.Received(1).TryAddAsync(_cacheKey, value, Arg.Any(), Arg.Any(), Arg.Any()); + await _innerCache.Received(1).TryAddAsync(_cacheKey, value, Arg.Any(), Arg.Any(), Arg.Any()); _memoryCache.Received(1).CreateEntry(_cacheKey); } @@ -64,20 +64,20 @@ public async Task TryAdd_never_probes_the_local_tier_to_decide() x[1] = new TestCacheEntry { Value = _fixture.Create() }; return true; }); - _innerCache.TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _innerCache.TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(true); _topic.PublishAsync(Arg.Any(), Arg.Any()).Returns(_ => true); var added = await Sut.TryAddAsync(_cacheKey, _fixture.Create(), policy: null, token: Ct); added.Should().BeTrue("the inner cache said the key was free, and it is the only authority"); - await _innerCache.Received(1).TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + await _innerCache.Received(1).TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); } [Fact] public async Task TryAdd_leaves_both_tiers_untouched_when_the_inner_cache_reports_a_loss() { - _innerCache.TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _innerCache.TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(false); var added = await Sut.TryAddAsync(_cacheKey, _fixture.Create(), policy: null, token: Ct); @@ -90,7 +90,7 @@ public async Task TryAdd_leaves_both_tiers_untouched_when_the_inner_cache_report [Fact] public async Task TryAdd_broadcasts_after_a_win_so_peers_drop_stale_local_copies() { - _innerCache.TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _innerCache.TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(true); _topic.PublishAsync(Arg.Any(), Arg.Any()).Returns(_ => true); @@ -102,7 +102,7 @@ public async Task TryAdd_broadcasts_after_a_win_so_peers_drop_stale_local_copies [Fact] public async Task TryAdd_still_reports_the_win_when_the_broadcast_fails() { - _innerCache.TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _innerCache.TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(true); _topic.PublishAsync(Arg.Any(), Arg.Any()) .ThrowsAsync(new InvalidOperationException("broadcast down")); @@ -115,7 +115,7 @@ public async Task TryAdd_still_reports_the_win_when_the_broadcast_fails() [Fact] public async Task TryAdd_fails_closed_when_the_inner_cache_throws() { - _innerCache.TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _innerCache.TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .ThrowsAsync(new InvalidOperationException("redis down")); var added = await Sut.TryAddAsync(_cacheKey, _fixture.Create(), policy: null, token: Ct); @@ -127,7 +127,7 @@ public async Task TryAdd_fails_closed_when_the_inner_cache_throws() [Fact] public async Task TryAdd_surfaces_an_inner_cache_that_cannot_arbitrate_at_all() { - _innerCache.TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _innerCache.TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .ThrowsAsync(new NotSupportedException("no NX here")); var act = async () => await Sut.TryAddAsync(_cacheKey, _fixture.Create(), policy: null, token: Ct); @@ -136,23 +136,31 @@ public async Task TryAdd_surfaces_an_inner_cache_that_cannot_arbitrate_at_all() _memoryCache.DidNotReceive().CreateEntry(_cacheKey); } + /// + /// A deadline that has passed used to be a silent no-op returning false. The expiration is no + /// longer nullable, so there is nothing left for such a value to mean and it is rejected at the + /// boundary instead of being confused with "somebody else holds the key". + /// [Fact] - public async Task TryAdd_claims_nothing_for_an_expiration_that_has_already_passed() + public async Task TryAdd_rejects_an_expiration_that_has_already_passed() { - var added = await Sut.TryAddAsync(_cacheKey, _fixture.Create(), DateTimeOffset.UtcNow.AddMinutes(-5), token: Ct); + var act = async () => await Sut.TryAddAsync(_cacheKey, _fixture.Create(), DateTimeOffset.UtcNow.AddMinutes(-5), token: Ct); - added.Should().BeFalse(); - await _innerCache.DidNotReceive().TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + (await act.Should().ThrowAsync()).And.ParamName.Should().Be("expiration"); + await _innerCache.DidNotReceive().TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); _memoryCache.DidNotReceive().CreateEntry(_cacheKey); } - [Fact] - public async Task TryAdd_claims_nothing_for_a_zero_expiration() + /// + [Theory] + [InlineData(0)] + [InlineData(-5)] + public async Task TryAdd_rejects_a_non_positive_expiration(int minutes) { - var added = await Sut.TryAddAsync(_cacheKey, _fixture.Create(), TimeSpan.Zero, token: Ct); + var act = async () => await Sut.TryAddAsync(_cacheKey, _fixture.Create(), TimeSpan.FromMinutes(minutes), token: Ct); - added.Should().BeFalse(); - await _innerCache.DidNotReceive().TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + (await act.Should().ThrowAsync()).And.ParamName.Should().Be("expiration"); + await _innerCache.DidNotReceive().TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); } [Fact] @@ -160,7 +168,7 @@ public async Task A_broadcast_that_reports_not_published_still_stands_and_is_log { // CacheSetAsync signals an ordinary publish failure with false rather than throwing, so the // catch alone would let it pass unreported. - _innerCache.TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _innerCache.TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(true); _topic.PublishAsync(Arg.Any(), Arg.Any()).Returns(_ => false); @@ -180,7 +188,7 @@ public async Task A_failed_broadcast_still_populates_the_local_tier() { // The broadcast and the L1 write are independent best-effort steps after the win; a dead // topic must not cost the winning node its local copy. - _innerCache.TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _innerCache.TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(true); _topic.PublishAsync(Arg.Any(), Arg.Any()) .ThrowsAsync(new InvalidOperationException("broadcast down")); @@ -196,7 +204,7 @@ public async Task TryAdd_fails_closed_when_the_inner_cache_cannot_claim_the_key( { _options.UseLocalOnlyWhenDisconnected = true; _options.ConnectionMonitorEnabled = true; - _innerCache.TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _innerCache.TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(false); _sut = null; @@ -212,14 +220,14 @@ public async Task A_disconnected_broadcast_transport_does_not_stop_a_healthy_L2_ _options.UseLocalOnlyWhenDisconnected = true; _options.ConnectionMonitorEnabled = true; _topicProvider.IsConnected.Returns(false); - _innerCache.TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _innerCache.TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(true); _sut = null; var added = await Sut.TryAddAsync(_cacheKey, _fixture.Create(), policy: null, token: Ct); added.Should().BeTrue("the aggregate connection state covers the topic too, and broadcast is best-effort after a win"); - await _innerCache.Received(1).TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + await _innerCache.Received(1).TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); } [Fact] @@ -231,7 +239,7 @@ public async Task TryAdd_surfaces_a_cancellation_raised_by_the_inner_cache() // CA2012: NSubstitute intercepts the call and Returns only uses the ValueTask as its // receiver — it is never awaited, so there is no single-consumption hazard here. #pragma warning disable CA2012 - _innerCache.TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _innerCache.TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns>(_ => { cts.Cancel(); @@ -243,7 +251,7 @@ public async Task TryAdd_surfaces_a_cancellation_raised_by_the_inner_cache() await act.Should().ThrowAsync( "reporting false would say someone else owns the key, which InMemoryRedis must not claim any more than Redis does"); - await _innerCache.Received(1).TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + await _innerCache.Received(1).TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); } [Fact] @@ -258,7 +266,7 @@ public async Task TryAdd_of_a_null_value_never_deletes_and_never_reaches_the_inn // SetAsync removes the key in this case; a conditional add must not. _memoryCache.DidNotReceive().Remove(_cacheKey); await _innerCache.DidNotReceive().RemoveAsync(_cacheKey, Arg.Any()); - await _innerCache.DidNotReceive().TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + await _innerCache.DidNotReceive().TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); } [Fact] @@ -266,21 +274,21 @@ public async Task TryAdd_of_a_null_value_reaches_the_inner_cache_when_CacheNullV { _options.CacheNullValues = true; _sut = null; - _innerCache.TryAddAsync(_cacheKey, default, Arg.Any(), Arg.Any(), Arg.Any()) + _innerCache.TryAddAsync(_cacheKey, default, Arg.Any(), Arg.Any(), Arg.Any()) .Returns(true); _topic.PublishAsync(Arg.Any(), Arg.Any()).Returns(_ => true); var added = await Sut.TryAddAsync(_cacheKey, default(string), policy: null, token: Ct); added.Should().BeTrue(); - await _innerCache.Received(1).TryAddAsync(_cacheKey, default, Arg.Any(), Arg.Any(), Arg.Any()); + await _innerCache.Received(1).TryAddAsync(_cacheKey, default, Arg.Any(), Arg.Any(), Arg.Any()); } [Fact] public async Task TryAdd_forwards_the_caller_expiration_to_the_inner_cache() { var ttl = TimeSpan.FromMinutes(7); - _innerCache.TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _innerCache.TryAddAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(true); _topic.PublishAsync(Arg.Any(), Arg.Any()).Returns(_ => true); @@ -289,7 +297,7 @@ public async Task TryAdd_forwards_the_caller_expiration_to_the_inner_cache() await _innerCache.Received(1).TryAddAsync( _cacheKey, Arg.Any(), - Arg.Is(e => e.HasValue && e.Value > DateTimeOffset.UtcNow), + Arg.Is(e => e > DateTimeOffset.UtcNow), Arg.Any(), Arg.Any()); } @@ -551,13 +559,14 @@ public async Task A_non_positive_local_retention_claims_nothing(int minutes) } [Fact] - public async Task An_expiration_that_has_already_passed_claims_nothing() + public async Task An_expiration_that_has_already_passed_is_rejected() { using var sut = CreateSut(); var past = DateTimeOffset.UtcNow.AddMinutes(-5); - (await sut.TryAddAsync("k", "first", past, token: Ct)).Should().BeFalse(); - (await sut.TryAddAsync("k", "second", past, token: Ct)).Should().BeFalse(); + var act = async () => await sut.TryAddAsync("k", "first", past, token: Ct); + + (await act.Should().ThrowAsync()).And.ParamName.Should().Be("expiration"); (await sut.GetAsync("k", policy: null, token: Ct)).Should().BeNull(); } diff --git a/tests/UiPath.Caching.Tests/MultilayerHashCachePerNameJitterTests.cs b/tests/UiPath.Caching.Tests/MultilayerHashCachePerNameJitterTests.cs index 88471fc..8172a55 100644 --- a/tests/UiPath.Caching.Tests/MultilayerHashCachePerNameJitterTests.cs +++ b/tests/UiPath.Caching.Tests/MultilayerHashCachePerNameJitterTests.cs @@ -99,7 +99,7 @@ public async Task SetAsync_jitters_options_DefaultExpiration_when_policy_Distrib .Returns(_ => true); var values = new Dictionary { ["f"] = "v" }; - await Sut.SetAsync(_cacheKey, values, (TimeSpan?)null, policy: null, token); + await Sut.SetAsync(_cacheKey, values, policy: null, token); await _innerCache.Received(1).SetAsync( _cacheKey, @@ -127,7 +127,7 @@ public async Task RefreshAsync_TimeSpan_overload_jitters_policy_derived_expirati _topic.PublishAsync(Arg.Any(), Arg.Any()) .Returns(_ => true); - await Sut.RefreshAsync(_cacheKey, (TimeSpan?)null, policy: null, token); + await Sut.RefreshAsync(_cacheKey, policy: null, token); await _innerCache.Received(1).RefreshAsync( _cacheKey, diff --git a/tests/UiPath.Caching.Tests/MultilayerHashCacheTests.cs b/tests/UiPath.Caching.Tests/MultilayerHashCacheTests.cs index 9a76ead..50648f3 100644 --- a/tests/UiPath.Caching.Tests/MultilayerHashCacheTests.cs +++ b/tests/UiPath.Caching.Tests/MultilayerHashCacheTests.cs @@ -211,7 +211,7 @@ public async Task GetOrAdd_data_from_inner_cache_HashCacheSetOption(HashCacheSet _innerCache.GetCacheEntryAsync(_innerCacheKey, Arg.Any(), Arg.Any()) .Returns(expectedCacheEntry); - var actual = await Sut.GetOrAddAsync(_cacheKey, generator, expiration: _fixture.Create(), setOption: hashCacheSetOption, token: testContextAccessor.Current.CancellationToken); + var actual = await Sut.GetOrAddAsync(_cacheKey, generator, expiration: DateTimeOffset.UtcNow.AddMinutes(5), setOption: hashCacheSetOption, token: testContextAccessor.Current.CancellationToken); generatorWasCalled.Should().BeFalse(); actual.Should().BeEquivalentTo(expected); } @@ -234,7 +234,7 @@ public async Task GetOrAdd_data_from_inner_cache_datetime() _innerCache.GetCacheEntryAsync(_innerCacheKey, Arg.Any(), Arg.Any()) .Returns(expectedCacheEntry); - var actual = await Sut.GetOrAddAsync(_cacheKey, generator, _fixture.Create(), (CachePolicy?)null, testContextAccessor.Current.CancellationToken); + var actual = await Sut.GetOrAddAsync(_cacheKey, generator, DateTimeOffset.UtcNow.AddMinutes(5), (CachePolicy?)null, testContextAccessor.Current.CancellationToken); generatorWasCalled.Should().BeFalse(); actual.Should().BeEquivalentTo(expected); } @@ -519,7 +519,7 @@ public async Task Set_value_inner_cache_throw_exception() var actual = await Sut.SetAsync(_cacheKey, expected, _fixture.Create(), token: testContextAccessor.Current.CancellationToken); actual.Should().BeFalse(); - actual = await Sut.SetAsync(_cacheKey, expected, _fixture.Create(), token: testContextAccessor.Current.CancellationToken); + actual = await Sut.SetAsync(_cacheKey, expected, DateTimeOffset.UtcNow.AddMinutes(5), token: testContextAccessor.Current.CancellationToken); actual.Should().BeFalse(); } @@ -781,7 +781,7 @@ public async Task Remove_evict_token_non_active() [Fact] public async Task Refresh_value_TimeSpan() { - var expiration = _fixture.Create(); + var expiration = TimeSpan.FromMinutes(5); await Sut.RefreshAsync(_cacheKey, expiration, token: testContextAccessor.Current.CancellationToken); _memoryCache.Received(1).Remove(_innerCacheKey); await _innerCache.Received(1).RefreshAsync(_innerCacheKey, Arg.Any(), Arg.Any(), Arg.Any()); @@ -812,7 +812,7 @@ public async Task Refresh_value_DateTimeOffset() [InlineData(true)] public async Task Refresh_inner_cache_exception_timespan(bool eventFired) { - var expiration = _fixture.Create(); + var expiration = TimeSpan.FromMinutes(5); _innerCache.RefreshAsync(_innerCacheKey, Arg.Any(), Arg.Any(), Arg.Any()) .ThrowsAsync(new Exception()); _topic.PublishAsync(Arg.Any(), Arg.Any()) @@ -1109,7 +1109,7 @@ public async Task When_inner_cache_returns_max_expiration_local_uses_max() .Returns(cacheEntry); _options.DefaultExpiration = null; - _ = await Sut.GetOrAddAsync(_cacheKey, generator, default(DateTimeOffset?), (CachePolicy?)null, testContextAccessor.Current.CancellationToken); + _ = await Sut.GetOrAddAsync(_cacheKey, generator, (CachePolicy?)null, testContextAccessor.Current.CancellationToken); cacheEntry.AbsoluteExpiration.Should().Be(DateTimeOffset.MaxValue); } diff --git a/tests/UiPath.Caching.Tests/MultilayerSetCacheTests.cs b/tests/UiPath.Caching.Tests/MultilayerSetCacheTests.cs index 58f5fd8..7b70eb0 100644 --- a/tests/UiPath.Caching.Tests/MultilayerSetCacheTests.cs +++ b/tests/UiPath.Caching.Tests/MultilayerSetCacheTests.cs @@ -72,7 +72,7 @@ public async Task Add_many_writes_through_into_the_local_snapshot() var (sut, l2) = CreateSut(); SetupMembers(l2, "a"); // The batch overloads normalize into the DateTimeOffset one before hitting L2. - l2.AddAsync(default, default!, default(DateTimeOffset?), default, Ct).ReturnsForAnyArgs(_ => 2L); + l2.AddAsync(default, default!, default(DateTimeOffset), default, Ct).ReturnsForAnyArgs(_ => 2L); await sut.MembersAsync("k", token: Ct); // primes L1 await sut.AddAsync("k", (IEnumerable)new[] { "b", "c" }, (CachePolicy?)null, Ct); diff --git a/tests/UiPath.Caching.Tests/NullCacheConditionalAddTests.cs b/tests/UiPath.Caching.Tests/NullCacheConditionalAddTests.cs index 17af5fe..960e679 100644 --- a/tests/UiPath.Caching.Tests/NullCacheConditionalAddTests.cs +++ b/tests/UiPath.Caching.Tests/NullCacheConditionalAddTests.cs @@ -15,15 +15,14 @@ public async Task TryAdd_reports_not_added_for_every_caller() (await sut.TryAddAsync("k", "second", policy: null, token: Ct)).Should().BeFalse(); } - [Theory] - [InlineData(null)] - [InlineData(5)] - public async Task TryAdd_reports_not_added_whatever_the_expiration(int? minutes) + [Fact] + public async Task TryAdd_reports_not_added_whatever_the_expiration() { - TimeSpan? ttl = minutes is { } m ? TimeSpan.FromMinutes(m) : null; + var ttl = TimeSpan.FromMinutes(5); + (await NullCache.Instance.TryAddAsync("k", "v", token: Ct)).Should().BeFalse(); (await NullCache.Instance.TryAddAsync("k", "v", ttl, token: Ct)).Should().BeFalse(); - (await NullCache.Instance.TryAddAsync("k", "v", ttl.HasValue ? DateTimeOffset.UtcNow.Add(ttl.Value) : null, token: Ct)).Should().BeFalse(); + (await NullCache.Instance.TryAddAsync("k", "v", DateTimeOffset.UtcNow.Add(ttl), token: Ct)).Should().BeFalse(); } [Fact] diff --git a/tests/UiPath.Caching.Tests/Redis/RedisCacheTests.cs b/tests/UiPath.Caching.Tests/Redis/RedisCacheTests.cs index 0c02fe1..03a29b2 100644 --- a/tests/UiPath.Caching.Tests/Redis/RedisCacheTests.cs +++ b/tests/UiPath.Caching.Tests/Redis/RedisCacheTests.cs @@ -676,11 +676,11 @@ public async Task Refresh_no_expiration_no_default(Type expirationType) _cacheOptions.DefaultExpiration = null; if (expirationType == typeof(TimeSpan)) { - await Sut.RefreshAsync(_cacheKey, default(TimeSpan?), token: testContextAccessor.Current.CancellationToken); + await Sut.RefreshAsync(_cacheKey, policy: null, token: testContextAccessor.Current.CancellationToken); } else if (expirationType == typeof(DateTimeOffset)) { - await Sut.RefreshAsync(_cacheKey, default(DateTimeOffset?), token: testContextAccessor.Current.CancellationToken); + await Sut.RefreshAsync(_cacheKey, policy: null, token: testContextAccessor.Current.CancellationToken); } else { @@ -701,7 +701,7 @@ public async Task GetOrAdd_default_key_expiration() actualExpiration = ci.Arg(); return _fixture.Create(); }); - await Sut.GetOrAddAsync(_cacheKey, _ => Task.FromResult(_fixture.Create()), expiration: default(DateTimeOffset?), token: testContextAccessor.Current.CancellationToken); + await Sut.GetOrAddAsync(_cacheKey, _ => Task.FromResult(_fixture.Create()), token: testContextAccessor.Current.CancellationToken); var expectedExpiration = _clock.UtcNow.Add(_cacheOptions.DefaultExpiration!.Value).Subtract(_clock.UtcNow); await _database.Received(1).StringSetAsync(_redisKey, Arg.Any(), Arg.Is(t => expectedExpiration == t), When.Always, CommandFlags.DemandMaster); @@ -834,9 +834,9 @@ public async Task GetOrAdd_key_expiration_no_default_expiration() actualExpiration = ci.Arg(); return _fixture.Create(); }); - DateTimeOffset? expiration = _fixture.Create(); - await Sut.GetOrAddAsync(_cacheKey, _ => Task.FromResult(_fixture.Create()), expiration: expiration, token: testContextAccessor.Current.CancellationToken); - var expectedExpiration = expiration.Value.Subtract(_clock.UtcNow); + var expiration = _clock.UtcNow.AddMinutes(5); + await Sut.GetOrAddAsync(_cacheKey, _ => Task.FromResult(_fixture.Create()), expiration: expiration, policy: null, token: testContextAccessor.Current.CancellationToken); + var expectedExpiration = expiration.Subtract(_clock.UtcNow); await _database.Received(1).StringSetAsync(_redisKey, Arg.Any(), Arg.Is(t => expectedExpiration == t), When.Always, CommandFlags.DemandMaster); } @@ -845,17 +845,9 @@ public async Task GetOrAdd_key_expiration_no_default_expiration() [InlineData(5)] public async Task Refresh_redis_exception(int? expirationMinutes) { - TimeSpan? expiration = null; - TimeSpan expectedExpiration; - if (expirationMinutes.HasValue) - { - expiration = TimeSpan.FromMinutes(expirationMinutes.Value); - expectedExpiration = expiration.Value; - } - else - { - expectedExpiration = _cacheOptions.DefaultExpiration ?? TimeSpan.MinValue; - } + var expectedExpiration = expirationMinutes.HasValue + ? TimeSpan.FromMinutes(expirationMinutes.Value) + : _cacheOptions.DefaultExpiration ?? TimeSpan.MinValue; DateTime? actualExpiration = default; _database.KeyExpireAsync(_redisKey, Arg.Any(), CommandFlags.DemandMaster| CommandFlags.FireAndForget) @@ -864,7 +856,11 @@ public async Task Refresh_redis_exception(int? expirationMinutes) actualExpiration = ci.Arg(); return new RedisException("test"); }); - await Sut.RefreshAsync(_cacheKey, expiration, token: testContextAccessor.Current.CancellationToken); + // No expiration minutes means the caller omits the argument entirely, which is now the + // only way to ask for the policy default. + await (expirationMinutes.HasValue + ? Sut.RefreshAsync(_cacheKey, TimeSpan.FromMinutes(expirationMinutes.Value), policy: null, testContextAccessor.Current.CancellationToken) + : Sut.RefreshAsync(_cacheKey, policy: null, testContextAccessor.Current.CancellationToken)); actualExpiration.GetValueOrDefault().Subtract(_now.UtcDateTime).Should().BeCloseTo(expectedExpiration, TimeSpan.FromSeconds(10)); } @@ -1234,16 +1230,23 @@ public async Task SetAsync_deletes_key_when_CacheNullValues_false_and_value_is_n await _database.DidNotReceive().StringSetAsync(_redisKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); } + /// + /// The write used to accept a deadline in the past and turn it into a delete. Expiration is no + /// longer nullable, so a past deadline is a bad argument rather than a shorthand, and it is + /// rejected before the key is touched at all. (SetInternalAsync still guards a + /// non-positive resolved duration — that path is now only reachable from a misconfigured + /// provider default, not from a caller.) + /// [Fact] - public async Task SetAsync_null_with_past_expiration_deletes_even_when_CacheNullValues_true() + public async Task SetAsync_rejects_a_past_expiration_even_when_CacheNullValues_true() { _cacheOptions.CacheNullValues = true; var pastExpiration = _clock.UtcNow.AddMinutes(-5); - var ok = await Sut.SetAsync(_cacheKey, value: null, pastExpiration, token: testContextAccessor.Current.CancellationToken); + var act = async () => await Sut.SetAsync(_cacheKey, value: null, pastExpiration, token: testContextAccessor.Current.CancellationToken); - ok.Should().BeTrue(); - await _database.Received().KeyDeleteAsync(_redisKey, Arg.Any()); + (await act.Should().ThrowAsync()).And.ParamName.Should().Be("expiration"); + await _database.DidNotReceive().KeyDeleteAsync(_redisKey, Arg.Any()); await _database.DidNotReceive().StringSetAsync(_redisKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); } @@ -1392,7 +1395,7 @@ private async Task Set_works_as_expected(Type expirationType) } else if (expirationType == typeof(DateTimeOffset)) { - actualResponse = await Sut.SetAsync(_cacheKey, value, _fixture.Create(), token: testContextAccessor.Current.CancellationToken); + actualResponse = await Sut.SetAsync(_cacheKey, value, DateTimeOffset.UtcNow.AddMinutes(5), token: testContextAccessor.Current.CancellationToken); } else { @@ -1420,7 +1423,7 @@ private async Task Multi_set_works_as_expected(Type expirationType) } else if (expirationType == typeof(DateTimeOffset)) { - actualResponse = await Sut.SetAsync(new KeyValuePair[] { new(_cacheKey, value), new(_multiKey, value) }, _fixture.Create(), policy: null, token: testContextAccessor.Current.CancellationToken); + actualResponse = await Sut.SetAsync(new KeyValuePair[] { new(_cacheKey, value), new(_multiKey, value) }, DateTimeOffset.UtcNow.AddMinutes(5), policy: null, token: testContextAccessor.Current.CancellationToken); } else { diff --git a/tests/UiPath.Caching.Tests/Redis/RedisCacheTryAddTests.cs b/tests/UiPath.Caching.Tests/Redis/RedisCacheTryAddTests.cs index 012edeb..3d82bc7 100644 --- a/tests/UiPath.Caching.Tests/Redis/RedisCacheTryAddTests.cs +++ b/tests/UiPath.Caching.Tests/Redis/RedisCacheTryAddTests.cs @@ -241,18 +241,23 @@ public async Task A_redis_failure_that_is_not_a_cancellation_still_reports_not_a added.Should().BeFalse(); } + /// + /// A non-positive lifetime used to return false, which is the same answer as "the key already + /// exists". With expiration non-nullable there is no third state to lean on, so the argument is + /// rejected rather than answered. + /// [Theory] [InlineData(0)] [InlineData(-5)] - public async Task TryAdd_claims_nothing_for_a_non_positive_expiration(int minutes) + public async Task TryAdd_rejects_a_non_positive_expiration(int minutes) { - var added = await Sut.TryAddAsync( + var act = async () => await Sut.TryAddAsync( _cacheKey, _fixture.Create(), TimeSpan.FromMinutes(minutes), token: testContextAccessor.Current.CancellationToken); - added.Should().BeFalse(); + (await act.Should().ThrowAsync()).And.ParamName.Should().Be("expiration"); await _database.DidNotReceive().StringSetAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); } diff --git a/tests/UiPath.Caching.Tests/Redis/RedisHashCacheTests.cs b/tests/UiPath.Caching.Tests/Redis/RedisHashCacheTests.cs index f462ad5..55d525f 100644 --- a/tests/UiPath.Caching.Tests/Redis/RedisHashCacheTests.cs +++ b/tests/UiPath.Caching.Tests/Redis/RedisHashCacheTests.cs @@ -300,7 +300,7 @@ public async Task GetOrAdd_generator_not_called() return Task.FromResult(fields.ToDictionary(k => k, k => _fixture.Create()) as IDictionary); }; - var actual = await Sut.GetOrAddAsync(_cacheKey, generator, _fixture.Create(), token: testContextAccessor.Current.CancellationToken); + var actual = await Sut.GetOrAddAsync(_cacheKey, generator, TimeSpan.FromMinutes(5), token: testContextAccessor.Current.CancellationToken); actual.Should().BeEquivalentTo(expected); generatorCalled.Should().BeFalse(); } @@ -348,7 +348,7 @@ public async Task GetOrAdd_generator_called() return Task.FromResult(expected); }; _transaction.ExecuteAsync(Arg.Any()).Returns(true); - var actual = await Sut.GetOrAddAsync(_cacheKey, generator, _fixture.Create(), token: testContextAccessor.Current.CancellationToken); + var actual = await Sut.GetOrAddAsync(_cacheKey, generator, TimeSpan.FromMinutes(5), token: testContextAccessor.Current.CancellationToken); actual.Should().BeEquivalentTo(expected); _database.Received(1).CreateTransaction(); await _transaction.Received(1).HashSetAsync(_redisKey, Arg.Any(), CommandFlags.DemandMaster); @@ -401,7 +401,7 @@ public async Task GetOrAdd_generator_called_empty_result() generatorCalled = true; return Task.FromResult(expected); }; - var actual = await Sut.GetOrAddAsync(_cacheKey, generator, _fixture.Create(), token: testContextAccessor.Current.CancellationToken); + var actual = await Sut.GetOrAddAsync(_cacheKey, generator, TimeSpan.FromMinutes(5), token: testContextAccessor.Current.CancellationToken); actual.Should().BeEquivalentTo(expected); _database.Received(0).CreateTransaction(); await _transaction.Received(0).HashSetAsync(_redisKey, Arg.Any(), CommandFlags.DemandMaster); @@ -423,7 +423,7 @@ public async Task GetOrAdd_returns_empty_dict_without_invoking_generator_when_on return Task.FromResult>(new Dictionary { ["fresh"] = "v" }); }; - var actual = await Sut.GetOrAddAsync(_cacheKey, generator, _fixture.Create(), token: testContextAccessor.Current.CancellationToken); + var actual = await Sut.GetOrAddAsync(_cacheKey, generator, TimeSpan.FromMinutes(5), token: testContextAccessor.Current.CancellationToken); actual.Should().BeEmpty(); generatorCalled.Should().BeFalse(); @@ -497,7 +497,7 @@ public async Task GetOrAdd_empty_result_with_CacheNullValues_writes_metadata_mar _transaction.HashSetAsync(_redisKey, Arg.Do(h => captured = h), Arg.Any()) .Returns(Task.CompletedTask); - var actual = await Sut.GetOrAddAsync(_cacheKey, _ => Task.FromResult(generated), _fixture.Create(), token: testContextAccessor.Current.CancellationToken); + var actual = await Sut.GetOrAddAsync(_cacheKey, _ => Task.FromResult(generated), TimeSpan.FromMinutes(5), token: testContextAccessor.Current.CancellationToken); actual.Should().BeEmpty(); captured.Should().NotBeNull(); @@ -515,7 +515,7 @@ public async Task GetOrAdd_empty_result_without_CacheNullValues_deletes_key() _database.HashGetAllAsync(_redisKey, CommandFlags.PreferReplica) .Returns(_ => Array.Empty()); - var actual = await Sut.GetOrAddAsync(_cacheKey, _ => Task.FromResult(generated), _fixture.Create(), token: testContextAccessor.Current.CancellationToken); + var actual = await Sut.GetOrAddAsync(_cacheKey, _ => Task.FromResult(generated), TimeSpan.FromMinutes(5), token: testContextAccessor.Current.CancellationToken); actual.Should().BeEmpty(); await _database.Received().KeyDeleteAsync(_redisKey, Arg.Any()); @@ -598,7 +598,7 @@ public async Task GetOrAdd_legacy_nonempty_metadata_only_hash_runs_generator_whe return Task.FromResult>(new Dictionary { ["fresh"] = "v" }); }; - await Sut.GetOrAddAsync(_cacheKey, generator, _fixture.Create(), token: testContextAccessor.Current.CancellationToken); + await Sut.GetOrAddAsync(_cacheKey, generator, TimeSpan.FromMinutes(5), token: testContextAccessor.Current.CancellationToken); generatorCalled.Should().BeTrue("only Length==0 _metadata_ is the cached-empty sentinel in the GetOrAdd probe path; legacy non-empty _metadata_-only hashes must remain misses"); } @@ -618,7 +618,7 @@ public async Task GetOrAdd_marker_only_hash_runs_generator_when_CacheNullValues_ return Task.FromResult>(generated); }; - var actual = await Sut.GetOrAddAsync(_cacheKey, generator, _fixture.Create(), token: testContextAccessor.Current.CancellationToken); + var actual = await Sut.GetOrAddAsync(_cacheKey, generator, TimeSpan.FromMinutes(5), token: testContextAccessor.Current.CancellationToken); generatorCalled.Should().BeTrue(); actual.Should().BeEquivalentTo(generated); @@ -682,8 +682,9 @@ public async Task GetOrAdd_empty_result_forces_KeyReplace_even_when_caller_passe await Sut.GetOrAddAsync( _cacheKey, _ => Task.FromResult(generated), - expiration: (DateTimeOffset?)_now.AddMinutes(5), + expiration: _now.AddMinutes(5), setOption: HashCacheSetOption.HashReplace, + policy: null, token: testContextAccessor.Current.CancellationToken); await _transaction.Received(1).KeyDeleteAsync(_redisKey); @@ -852,7 +853,6 @@ public async Task Contains_redis_exception() [InlineData(3)] public async Task Refresh_works_as_expected(int? expirationMinutes) { - TimeSpan? expiration = expirationMinutes.HasValue ? TimeSpan.FromMinutes(expirationMinutes.Value) : null; var fieldsCalled = false; DateTime actualExpiration = default; _database.KeyExpireAsync(_redisKey, Arg.Any(), CommandFlags.DemandMaster | CommandFlags.FireAndForget) @@ -863,7 +863,11 @@ public async Task Refresh_works_as_expected(int? expirationMinutes) return _fixture.Create(); }); - await Sut.RefreshAsync(_cacheKey, expiration, token: testContextAccessor.Current.CancellationToken); + // No expiration minutes means the caller omits the argument entirely, which is now the + // only way to ask for the policy default. + await (expirationMinutes.HasValue + ? Sut.RefreshAsync(_cacheKey, TimeSpan.FromMinutes(expirationMinutes.Value), policy: null, testContextAccessor.Current.CancellationToken) + : Sut.RefreshAsync(_cacheKey, policy: null, testContextAccessor.Current.CancellationToken)); fieldsCalled.Should().BeTrue(); _logger.ReceivedCalls().Should().Contain(c => c.GetMethodInfo().Name == "Log" && (LogLevel)c.GetArguments()[0]! == LogLevel.Trace); var expectedTime = expirationMinutes.HasValue @@ -897,7 +901,7 @@ public async Task Refresh_redis_exception_timespan() _database.KeyExpireAsync(_redisKey, Arg.Any(), CommandFlags.DemandMaster | CommandFlags.FireAndForget) .ThrowsAsync(); - var actual = await Sut.RefreshAsync(_cacheKey, _fixture.Create(), token: testContextAccessor.Current.CancellationToken); + var actual = await Sut.RefreshAsync(_cacheKey, TimeSpan.FromMinutes(5), token: testContextAccessor.Current.CancellationToken); actual.Should().BeFalse(); } @@ -907,7 +911,7 @@ public async Task Refresh_redis_exception_datetime() _database.KeyExpireAsync(_redisKey, Arg.Any(), CommandFlags.DemandMaster | CommandFlags.FireAndForget) .ThrowsAsync(); - var actual = await Sut.RefreshAsync(_cacheKey, _fixture.Create(), token: testContextAccessor.Current.CancellationToken); + var actual = await Sut.RefreshAsync(_cacheKey, _clock.UtcNow.AddMinutes(5), token: testContextAccessor.Current.CancellationToken); actual.Should().BeFalse(); } @@ -1140,7 +1144,7 @@ public async Task Set_empty_values() return expected; }); - var actual = await Sut.SetAsync(_cacheKey, values, _fixture.Create(), token: testContextAccessor.Current.CancellationToken); + var actual = await Sut.SetAsync(_cacheKey, values, TimeSpan.FromMinutes(5), token: testContextAccessor.Current.CancellationToken); actionCalled.Should().BeTrue(); actual.Should().Be(expected); _database.Received(0).CreateTransaction(); @@ -1174,7 +1178,7 @@ public async Task Set_redis_exception() var entries = _fixture.CreateMany().ToArray(); IDictionary values = entries.ToDictionary(k => k.Name.ToString(), k => (string?)k.Value); _transaction.ExecuteAsync(Arg.Any()).ThrowsAsync(); - var actual = await Sut.SetAsync(_cacheKey, values, _fixture.Create(), token: testContextAccessor.Current.CancellationToken); + var actual = await Sut.SetAsync(_cacheKey, values, TimeSpan.FromMinutes(5), token: testContextAccessor.Current.CancellationToken); actual.Should().BeFalse(); _database.Received(1).CreateTransaction(); } From f07a41558a3d15c72d021903bf14fbead34172cc Mon Sep 17 00:00:00 2001 From: Cosmin Staicu Date: Thu, 3 Sep 2026 14:22:20 +0300 Subject: [PATCH 9/9] feat(cache)!: floor the resolved expiration at one hour so omission cannot mean "forever" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `CachePolicy.DefaultDistributedExpiration` (1 hour) is the new floor under `CachePolicy.DistributedExpiration` and the providers' `DefaultExpiration`, applied on every write that carries no caller expiration. The whole chain was nullable with nothing underneath it, so `DefaultExpiration = null` — set in code or bound from configuration — wrote entries with no TTL into shared storage, and `HardcodedDefaults` in `MultilayerCacheBase` already established the pattern for exactly this problem on the lock fields. The 1 hour that four options classes each declared as a property initializer now comes from that one constant. Unbounded entries stay available and now have to be asked for: configure a lifetime of `TimeSpan.MaxValue`, which is already what the providers read as "no TTL" (`SET` with none, `PERSIST` on refresh). `CacheClock` saturates a duration that would run past the representable range to `DateTimeOffset.MaxValue` rather than throwing, so `TimeSpan.MaxValue` works on the deadline path too. The floor sits on the write paths only — `MultilayerCacheBase.ResolveWriteDuration`, `RedisCacheBase.PolicyDuration` / `PolicyDeadline` / `OptionsDeadline`, and `MultilayerSetCache.LocalWriteExpiration` for the memory-only set cache, which extends neither base. It is deliberately not merged into the resolved default policy: `CacheClock` is built from that policy and also materializes the expiration a *read* found, so a key with genuinely no TTL in Redis has to keep reporting `DateTimeOffset.MaxValue` instead of a fabricated `now + default`. `GetCacheEntry_returns_max_value_when_remote_has_no_ttl_and_no_default_v7` catches that, and did when the floor was briefly in the policy. `ResolveWriteDuration` returns `TimeSpan` rather than `TimeSpan?` as a result. `AddDistributedCache` loses its "would store entries without an expiration" registration throw: that state no longer exists. The non-positive check stays, since zero or negative is a value someone configured rather than one they left unset, and `AllowUnboundedEntries` keeps its meaning as the adapter's way to honor `IDistributedCache`'s "until removed" literally. Signed-off-by: Cosmin Staicu Review follow-up, three defects in the unbounded and rehydrate paths: Turning a resolved duration into a deadline is now one helper, CacheExpiration.AddSaturating, rather than a bare Add repeated per call site. The memory-only set tier threw ArgumentOutOfRangeException the moment TimeSpan.MaxValue became the documented way to ask for unbounded, and the rehydrate write had the same latent overflow for a caller-supplied DateTimeOffset.MaxValue. Jitter no longer consumes the sentinel. It clamps to the remaining DateTime range, so an unbounded lifetime came out just under DateTimeOffset.MaxValue and the key went back on the EXPIRE path instead of PERSIST. Jitter spreads an expiry that is coming; an unbounded entry has none. GetOrAddAsync resolves the lifetime once and feeds both the write deadline and the rehydrate threshold. They were separate chains and the floor reached only the first, so an entry written under the default expired after an hour while proactive rehydration -- still bottoming out at null -- never fired. Resolving once also stops a jittered policy from computing two different lifetimes for one write. The set-cache path takes the clock instead of reading the ambient one. MultilayerSetCache and MemorySetCache resolved every expiration against DateTimeOffset.UtcNow, so they ignored a configured ISystemClock and could not be tested deterministically -- the outlier next to MultilayerCacheBase and RedisCacheBase, which both hold a CacheClock. Both are internal, so plumbing one through costs no public API; InMemoryQueueCacheOptions and InMemoryRedisQueueCacheOptions gain the Clock knob the other options classes already have. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_011QeZZPMo8bFYz98AuPZU2V Signed-off-by: Cosmin Staicu --- CHANGELOG.md | 40 +++++++++++ docs/reference/interfaces.md | 5 +- docs/reference/settings.md | 14 ++-- .../CacheExpiration.cs | 20 ++++++ .../Config/CachePolicy.cs | 20 ++++++ .../PublicAPI.Unshipped.txt | 2 + .../InMemoryQueueCacheOptions.cs | 16 ++++- .../InMemoryQueueCacheProvider.cs | 3 +- .../InMemoryRedisQueueCacheOptions.cs | 8 +++ .../InMemoryRedisQueueCacheProvider.cs | 3 +- src/UiPath.Caching.Queue/MemorySetCache.cs | 5 +- .../MultilayerSetCache.cs | 25 ++++--- .../PublicAPI.Unshipped.txt | 4 ++ src/UiPath.Caching/CacheClock.cs | 14 +++- .../DistributedCacheCollectionExtensions.cs | 33 +++++---- .../UiPathDistributedCacheOptions.cs | 9 ++- src/UiPath.Caching/InMemoryCacheOptions.cs | 2 +- .../InMemoryRedisCacheOptions.cs | 2 +- src/UiPath.Caching/MultilayerCache.cs | 30 ++++---- src/UiPath.Caching/MultilayerCacheBase.cs | 30 +++++--- src/UiPath.Caching/MultilayerHashCache.cs | 15 ++-- src/UiPath.Caching/PublicAPI.Shipped.txt | 1 - src/UiPath.Caching/PublicAPI.Unshipped.txt | 1 + src/UiPath.Caching/Redis/RedisCacheBase.cs | 15 ++-- src/UiPath.Caching/Redis/RedisCacheOptions.cs | 2 +- .../CacheExpirationTests.cs | 34 +++++++++ .../DistributedCacheRegistrationTests.cs | 13 +++- .../InMemorySetCacheTests.cs | 42 ++++++++++- ...MultilayerCachePerNamePolicyWiringTests.cs | 69 +++++++++++++++++++ .../MultilayerCacheRehydrateTests.cs | 44 ++++++++++++ .../Redis/RedisCacheTests.cs | 45 +++++++----- .../Redis/RedisHashCacheTests.cs | 8 +-- 32 files changed, 461 insertions(+), 113 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eee5d46..acffea3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -101,6 +101,46 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ### Changed +- **BREAKING:** an unset expiration no longer means "keep this forever". `CachePolicy` gained + `DefaultDistributedExpiration` (1 hour), applied as the floor under + `CachePolicy.DistributedExpiration` and the providers' `DefaultExpiration` on every write that + carries no caller expiration. Previously the whole chain was nullable with nothing underneath it, + so a provider whose `DefaultExpiration` was set to `null` — in code or bound from configuration — + wrote entries with no TTL into shared storage. The 1 hour that four options classes each declared + as a property initializer now comes from that one constant, so the value is stated once. + Unbounded entries are still available and now have to be asked for: configure a lifetime of + `TimeSpan.MaxValue`, which is what the providers already read as "no TTL" (`SET` with no TTL, + `PERSIST` on refresh). `CacheExpiration.AddSaturating` (new) turns a resolved duration into a + deadline, saturating at `DateTimeOffset.MaxValue` rather than overflowing, and every path that + converts one now goes through it — `CacheClock`, the memory-only set tier, and the rehydrate + write — so `TimeSpan.MaxValue` reaches the providers as the sentinel they read as "no TTL". + Jitter leaves that sentinel alone: jitter spreads an expiry that is coming, and clamping an + unbounded lifetime would put the key back on the `EXPIRE` path. + + The floor is on the **write** paths only — `MultilayerCacheBase.ResolveWriteDuration`, + `RedisCacheBase.PolicyDuration` / `PolicyDeadline` / `OptionsDeadline` — deliberately not in the + resolved default policy that `CacheClock` is built from, because that same clock materializes the + expiration a *read* found: a key that genuinely has no TTL in Redis must keep reporting + `DateTimeOffset.MaxValue` rather than a fabricated `now + default`. + `MultilayerCacheBase.ResolveWriteDuration` returns `TimeSpan` rather than `TimeSpan?` as a result. + + `GetOrAddAsync` resolves the lifetime **once** and uses it for both the write deadline and the + rehydrate threshold. These were two separate chains, and the floor would otherwise have applied to + only one of them: entries written under the default would expire after an hour while proactive + rehydration, still bottoming out at `null`, never fired. Resolving once also stops a jittered + policy from computing two different lifetimes for the same write. Rehydration is skipped for an + unbounded lifetime, which has no deadline to pre-empt. +- **`InMemoryQueueCacheOptions.Clock` / `InMemoryRedisQueueCacheOptions.Clock`** join the other + options classes' `Clock` knob. The set-cache path resolved every expiration against + `DateTimeOffset.UtcNow` directly, so it ignored a configured clock and could not be tested + deterministically; `MultilayerSetCache` and `MemorySetCache` now take the clock and read "now" + from it, matching `MultilayerCacheBase` and `RedisCacheBase`. +- **BREAKING:** `AddDistributedCache` no longer fails at registration when no bounded default + resolves, because that state no longer exists — an unset default now resolves to the floor. The + `"would store entries without an expiration"` throw is removed; the non-positive check stays, + since a value someone configured as zero or negative is still a real misconfiguration. + `UiPathDistributedCacheOptions.AllowUnboundedEntries` keeps its meaning and is now the only way + to reach an unbounded entry through that adapter without naming a lifetime. - **BREAKING:** the per-call `expiration` is no longer nullable. Every write on `ICache`, `ICache`, `IHashCache`, `IHashCache`, `ISetCache`, `ISetCache` and their extension surfaces takes `TimeSpan` / `DateTimeOffset` instead of `TimeSpan?` / `DateTimeOffset?`. The diff --git a/docs/reference/interfaces.md b/docs/reference/interfaces.md index ed74e26..d435571 100644 --- a/docs/reference/interfaces.md +++ b/docs/reference/interfaces.md @@ -189,7 +189,10 @@ That leaves one resolution chain with no redundant state in it: | passes `expiration` | exactly that value, no jitter | | omits `expiration` | `CachePolicy.DistributedExpiration`, jittered by `CachePolicy.JitterMaxDuration` | | omits it, policy has no TTL | the provider's `DefaultExpiration`, jittered | -| omits it, nothing configured | unbounded — `TimeSpan.MaxValue` / `DateTimeOffset.MaxValue`, which the providers store as "no TTL" | +| omits it, nothing configured | `CachePolicy.DefaultDistributedExpiration` — **1 hour**, jittered | +| omits it, a lifetime is configured as `TimeSpan.MaxValue` | unbounded — stored as `DateTimeOffset.MaxValue`, which the providers read as "no TTL" | + +The last two rows are the point: **omission never means "keep this forever"**. Every level of the chain is nullable-meaning-*inherit*, and the floor under all of them is a bounded hour, so a provider whose `DefaultExpiration` was left unset — or bound to `null` from configuration — writes an entry that expires rather than one that accumulates in shared storage. Unbounded is still available; it has to be asked for, by configuring a lifetime of `TimeSpan.MaxValue`. The one exception is the `IDistributedCache` adapter, whose `AllowUnboundedEntries` exists to honor that contract's "until removed" literally. Because the argument can no longer be `null`, there is nothing left for a meaningless value to mean, so it is rejected rather than absorbed: a duration that is not strictly positive, or a deadline at or before the cache's current time, raises `ArgumentOutOfRangeException` with `ParamName` `"expiration"` and nothing is written. `TimeSpan.MaxValue` and `DateTimeOffset.MaxValue` stay valid — they are how the providers spell "no TTL". `CacheExpiration` holds the guard if you need it in your own implementation. The no-op caches (`NullCache`, `NullHashCache`, `NullSetCache`) read no argument at all and so enforce nothing; they keep degrading to "caching is off, carry on". diff --git a/docs/reference/settings.md b/docs/reference/settings.md index ae27232..88d4acf 100644 --- a/docs/reference/settings.md +++ b/docs/reference/settings.md @@ -139,7 +139,7 @@ Per-topic overrides: add entries to `Topics[]` under `Broadcast:RedisPubSub`. Ea | Property | Type | Default | Scope | Notes | |---|---|---|---|---| | `Enabled` | `bool` | `true` | Per-provider | Enable/disable this two-tier (L1 in-memory + L2 Redis) cache provider. | -| `DefaultExpiration` | `TimeSpan?` | `01:00:00` | Per-provider | Default TTL when no per-call or per-policy expiration is set. | +| `DefaultExpiration` | `TimeSpan?` | `01:00:00` | Per-provider | Default TTL when no per-call or per-policy expiration is set. `null` means *inherit*, which resolves to `CachePolicy.DefaultDistributedExpiration` (1 h) — it does **not** mean "never expire". For unbounded entries set `TimeSpan.MaxValue`. | | `Timeout` | `TimeSpan` | `00:00:01` | Per-provider | Max wait for a cache operation before giving up and falling through. | | `TrackStatistics` | `bool` | `true` | Per-provider | Emit hit/miss/eviction counters via the telemetry provider. | | `StatisticsFlushInterval` | `TimeSpan` | `00:01:00` | Per-provider | How often statistics are flushed to the telemetry sink. | @@ -168,7 +168,7 @@ Per-topic overrides: add entries to `Topics[]` under `Broadcast:RedisPubSub`. Ea | Property | Type | Default | Scope | Notes | |---|---|---|---|---| | `Enabled` | `bool` | `true` | Per-provider | Enable/disable the standalone Redis cache provider. | -| `DefaultExpiration` | `TimeSpan?` | `01:00:00` | Per-provider | Default TTL when no per-call or per-policy expiration is set. | +| `DefaultExpiration` | `TimeSpan?` | `01:00:00` | Per-provider | Default TTL when no per-call or per-policy expiration is set. `null` means *inherit*, which resolves to `CachePolicy.DefaultDistributedExpiration` (1 h) — it does **not** mean "never expire". For unbounded entries set `TimeSpan.MaxValue`. | | `KeyPrefix` | `string` | `""` | Per-provider | Prefix prepended to every Redis key before `AppShortName` and the cache key segments. | | `Timeout` | `TimeSpan` | `00:00:01` | Per-provider | Max wait for a cache operation before giving up and falling through. | | `ConnectionMonitorEnabled` | `bool?` | `null` | Per-provider | `null` = inherit from `CacheOptions.ConnectionMonitorEnabled`. | @@ -185,13 +185,13 @@ Per-topic overrides: add entries to `Topics[]` under `Broadcast:RedisPubSub`. Ea | Property | Type | Default | Scope | Notes | |---|---|---|---|---| | `Enabled` | `bool` | `true` | Per-provider | Enable/disable the in-memory-only cache provider. | -| `DefaultExpiration` | `TimeSpan?` | `01:00:00` | Per-provider | Default TTL when no per-call or per-policy expiration is set. | +| `DefaultExpiration` | `TimeSpan?` | `01:00:00` | Per-provider | Default TTL when no per-call or per-policy expiration is set. `null` means *inherit*, which resolves to `CachePolicy.DefaultDistributedExpiration` (1 h) — it does **not** mean "never expire". For unbounded entries set `TimeSpan.MaxValue`. | | `Timeout` | `TimeSpan` | `00:00:01` | Per-provider | Max wait for a cache operation before giving up. | | `TrackStatistics` | `bool` | `true` | Per-provider | Emit hit/miss/eviction counters via the telemetry provider. | | `StatisticsFlushInterval` | `TimeSpan` | `00:01:00` | Per-provider | How often statistics are flushed to the telemetry sink. | | `BroadcastEnable` | `bool` | `false` | Per-provider | Enable broadcast invalidation for this in-memory cache instance. | | `Topic` | `string?` | `null` | Per-provider | Topic name for invalidation broadcasts; `null` = use `CacheOptions.DefaultTopic`. | -| `LocalMaxExpiration` | `TimeSpan?` | `01:00:00` | Per-provider | Cap on in-memory TTL; `null` = no cap (falls back to `DefaultExpiration`). | +| `LocalMaxExpiration` | `TimeSpan?` | `01:00:00` | Per-provider | Cap on in-memory TTL; `null` = no cap (falls back to the resolved `DefaultExpiration`). | | `ConnectionMonitorEnabled` | `bool?` | `null` | Per-provider | Inert for this provider (no Redis connection); present to satisfy `IMultilayerCacheOptions`. | | `CacheNullValues` | `bool` | `false` | Per-provider | Persist `null`/empty factory returns as sentinels. | | `ConnectionMonitorPeriod` | `TimeSpan?` | `00:00:05` | Per-provider | Inert for this provider; present to satisfy `IMultilayerCacheOptions`. | @@ -221,8 +221,8 @@ extension rather than bound from configuration. | `RedisKeyDifferentiator` | `string?` | `null` | Per registration | Fills the slot after `AppShortName` that the application's caches fill with a `RedisTypePrefixes` value. Null uses `DefaultRedisKeyDifferentiator` (`"dh"`). Inert on the `InMemory` tier; a value matching a `RedisTypePrefixes` value is rejected at registration. Prefixes belonging to packages layered on top of `UiPath.Caching` are **not** checked — it cannot see them without depending on them — so avoid those too: `UiPath.Caching.Queue`'s set cache uses `"se"`. | | `RedisKeyStrategyFactory` | `IRedisKeyStrategyFactory?` | `null` | Per registration | Builds the Redis key, receiving `RedisKeyDifferentiator`. Null inherits the application's `RedisCacheOptions.RedisKeyStrategyFactory`, keeping its `AppShortName`, separator and sharding conventions. Code-only seam. | | `PolicyName` | `string?` | `null` | Per registration | Named `CachePolicy` applied to the adapter's operations; an unregistered name fails fast at startup. | -| `DefaultEntryExpiration` | `TimeSpan?` | `null` | Per registration | Expiration used when the caller supplies none. `IDistributedCache` treats absent expiration as "until removed"; unless `AllowUnboundedEntries` is set that is mapped to this value, falling back to the backing tier's `DefaultExpiration`. | -| `AllowUnboundedEntries` | `bool` | `false` | Per registration | Honor "no expiration" literally. Off by default: registration fails when no bounded default can be resolved, so shared storage cannot accumulate keys that never expire. | +| `DefaultEntryExpiration` | `TimeSpan?` | `null` | Per registration | Expiration used when the caller supplies none. `IDistributedCache` treats absent expiration as "until removed"; unless `AllowUnboundedEntries` is set that is mapped to this value, falling back to the backing tier's `DefaultExpiration` and then to `CachePolicy.DefaultDistributedExpiration`. | +| `AllowUnboundedEntries` | `bool` | `false` | Per registration | Honor "no expiration" literally. Off by default, and now the only way to reach an unbounded entry through this adapter without naming a lifetime — an unset default resolves to `CachePolicy.DefaultDistributedExpiration` rather than to "until removed". | Entries are stored as a Redis hash (`data`, `absexp`, `sldexp`) in a keyspace disjoint from the application's own caches, so `Refresh` reads only the expiration metadata. Keys are always @@ -300,7 +300,7 @@ Entries are keyed by string under `Caching:Policies`. `ICache` and `IHashCach |---|---|---|---|---| | `LocalExpiration` | `TimeSpan?` | `null` | Per-policy | L1 (in-memory) TTL cap for this policy; `null` = inherit from provider `LocalMaxExpiration`. Effective L1 TTL is `min(entry.Expiration, LocalExpiration)`. | | `LocalExpirationDisconnected` | `TimeSpan?` | `null` | Per-policy | L1 TTL cap when L2 is disconnected; `null` = inherit from provider `LocalMaxExpirationDisconnected`. | -| `DistributedExpiration` | `TimeSpan?` | `null` | Per-policy | L2 (Redis) entry lifetime; `null` = use provider `DefaultExpiration`. Per-call expiration arguments still take precedence. | +| `DistributedExpiration` | `TimeSpan?` | `null` | Per-policy | L2 (Redis) entry lifetime; `null` = use provider `DefaultExpiration`, and under that `CachePolicy.DefaultDistributedExpiration` (1 h). Per-call expiration arguments still take precedence. Set `TimeSpan.MaxValue` for unbounded. | | `FactoryTimeout` | `TimeSpan?` | `null` | Per-policy | Max time allowed for the value factory before it is abandoned; `null` = no timeout. | | `JitterMaxDuration` | `TimeSpan?` | `null` | Per-policy | Max random duration added to the L2 TTL at write time (uniform in `[0, JitterMaxDuration)`); `null` or `00:00:00` disables jitter. Caller-supplied expiration is honored exactly (no jitter). | | `RehydrateEnabled` | `bool?` | `null` | Per-policy | Master switch for proactive background refresh; `null` = inherit (default off). | diff --git a/src/UiPath.Caching.Abstractions/CacheExpiration.cs b/src/UiPath.Caching.Abstractions/CacheExpiration.cs index 7a5b905..333c602 100644 --- a/src/UiPath.Caching.Abstractions/CacheExpiration.cs +++ b/src/UiPath.Caching.Abstractions/CacheExpiration.cs @@ -55,6 +55,26 @@ public static DateTimeOffset ThrowIfNotFuture(DateTimeOffset expiration, DateTim return expiration; } + /// + /// Adds to , saturating at + /// instead of throwing. + /// + /// + /// is how a configured lifetime spells "no TTL", so every place + /// that turns a resolved duration into a deadline has to land on the sentinel the providers read + /// as "no TTL" rather than overflow on the way there. + /// + public static DateTimeOffset AddSaturating(DateTimeOffset now, TimeSpan duration) + { + // Add advances the wall-clock DateTime, and the result has to keep the UTC instant + // representable too, so the headroom is whichever of the two runs out first. They differ + // whenever the offset is not zero. + var headroom = Math.Min( + DateTime.MaxValue.Ticks - now.DateTime.Ticks, + DateTime.MaxValue.Ticks - now.UtcDateTime.Ticks); + return duration.Ticks > headroom ? DateTimeOffset.MaxValue : now.Add(duration); + } + /// Validates a caller deadline against and returns it as a duration from . /// is at or before . public static TimeSpan ToDuration(DateTimeOffset expiration, DateTimeOffset now, [CallerArgumentExpression(nameof(expiration))] string? paramName = null) => diff --git a/src/UiPath.Caching.Abstractions/Config/CachePolicy.cs b/src/UiPath.Caching.Abstractions/Config/CachePolicy.cs index 6840e47..d4c9182 100644 --- a/src/UiPath.Caching.Abstractions/Config/CachePolicy.cs +++ b/src/UiPath.Caching.Abstractions/Config/CachePolicy.cs @@ -9,6 +9,26 @@ namespace UiPath.Caching; /// public sealed class CachePolicy { + /// + /// The L2 lifetime a write inherits when nothing else supplies one: the floor under + /// and the providers' DefaultExpiration, applied by + /// the write-side resolution so every write carries a bounded lifetime. + /// + /// + /// It exists so that "nobody configured a TTL" cannot mean "keep this forever". An unbounded + /// entry in shared storage has to be asked for, by setting a lifetime of + /// — which is what the providers already read as "no TTL" — + /// rather than by leaving a nullable unset. Per-call and per-policy values still win; this only + /// answers the case where the whole chain came back empty. + /// + /// It is deliberately not merged into the resolved default policy alongside the other + /// hardcoded defaults. That policy also builds the clock a read materializes an + /// expiration with, and a key that genuinely has no TTL in storage has to keep reporting + /// rather than a fabricated now + default. + /// + /// + public static readonly TimeSpan DefaultDistributedExpiration = TimeSpan.FromHours(1); + /// /// Per-policy L1 (in-memory tier) cap. Applied at SetAsync / GetOrAddAsync write /// time when the L2 is connected, falling back to diff --git a/src/UiPath.Caching.Abstractions/PublicAPI.Unshipped.txt b/src/UiPath.Caching.Abstractions/PublicAPI.Unshipped.txt index cd96e0c..0a83a8b 100644 --- a/src/UiPath.Caching.Abstractions/PublicAPI.Unshipped.txt +++ b/src/UiPath.Caching.Abstractions/PublicAPI.Unshipped.txt @@ -131,6 +131,7 @@ UiPath.Caching.SystemJsonByteSerializerProxy.Serialize(object? value) -> byte[]? UiPath.Caching.SystemJsonByteSerializerProxy.SystemJsonByteSerializerProxy(System.Text.Json.JsonSerializerOptions? options = null) -> void UiPath.Caching.SystemJsonByteSerializerProxy.TryDeserialize(object? value, out T? result) -> bool UiPath.Caching.SystemJsonByteSerializerProxy.TryDeserialize(string? value, out T? result) -> bool +static UiPath.Caching.CacheExpiration.AddSaturating(System.DateTimeOffset now, System.TimeSpan duration) -> System.DateTimeOffset static UiPath.Caching.CacheExpiration.ThrowIfNotFuture(System.DateTimeOffset expiration, System.DateTimeOffset now, string? paramName = null) -> System.DateTimeOffset static UiPath.Caching.CacheExpiration.ThrowIfNotPositive(System.TimeSpan expiration, string? paramName = null) -> System.TimeSpan static UiPath.Caching.CacheExpiration.ToDuration(System.DateTimeOffset expiration, System.DateTimeOffset now, string? paramName = null) -> System.TimeSpan @@ -219,3 +220,4 @@ static UiPath.Caching.HashCacheSyncExtensions.Set(this UiPath.Caching.IHashCa static UiPath.Caching.HashCacheSyncExtensions.Set(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! values, UiPath.Caching.HashCacheEntryOptions options, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool static UiPath.Caching.HashCacheSyncExtensions.SetMetadata(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IDictionary! metadata, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> bool static UiPath.Caching.HashCacheSyncExtensions.TimeToLive(this UiPath.Caching.IHashCache! cache, UiPath.Caching.CacheKey cacheKey, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.TimeSpan? +static readonly UiPath.Caching.CachePolicy.DefaultDistributedExpiration -> System.TimeSpan diff --git a/src/UiPath.Caching.Queue/InMemoryQueueCacheOptions.cs b/src/UiPath.Caching.Queue/InMemoryQueueCacheOptions.cs index c3469ed..9249b7e 100644 --- a/src/UiPath.Caching.Queue/InMemoryQueueCacheOptions.cs +++ b/src/UiPath.Caching.Queue/InMemoryQueueCacheOptions.cs @@ -1,3 +1,5 @@ +using Microsoft.Extensions.Internal; + namespace UiPath.Caching; /// @@ -8,15 +10,23 @@ namespace UiPath.Caching; /// public sealed class InMemoryQueueCacheOptions : IMemoryCacheOptions { + /// + /// Clock the set cache reads "now" from when it resolves expirations. + /// uses the system clock. Mirrors . + /// + public ISystemClock? Clock { get; set; } + /// Indicates whether the in-memory set cache is enabled. public bool Enabled { get; set; } = true; /// /// Default whole-set lifetime applied when no explicit expiration or - /// expiration is supplied. means the set never expires. Every add - /// re-applies the resolved expiration, matching . + /// expiration is supplied. means "inherit", which resolves to + /// ; to keep a set forever, set + /// . Every add re-applies the resolved expiration, matching + /// . /// - public TimeSpan? DefaultExpiration { get; set; } = TimeSpan.FromHours(1); + public TimeSpan? DefaultExpiration { get; set; } = CachePolicy.DefaultDistributedExpiration; /// public bool TrackStatistics { get; set; } = true; diff --git a/src/UiPath.Caching.Queue/InMemoryQueueCacheProvider.cs b/src/UiPath.Caching.Queue/InMemoryQueueCacheProvider.cs index 0efb06b..4c68424 100644 --- a/src/UiPath.Caching.Queue/InMemoryQueueCacheProvider.cs +++ b/src/UiPath.Caching.Queue/InMemoryQueueCacheProvider.cs @@ -55,5 +55,6 @@ private MultilayerSetCache BuildSetCache() => _options, _localLock, localMaxExpiration: null, - defaultExpiration: _options.DefaultExpiration); + defaultExpiration: _options.DefaultExpiration, + clock: _options.Clock); } diff --git a/src/UiPath.Caching.Queue/InMemoryRedisQueueCacheOptions.cs b/src/UiPath.Caching.Queue/InMemoryRedisQueueCacheOptions.cs index 44cafa4..f93313a 100644 --- a/src/UiPath.Caching.Queue/InMemoryRedisQueueCacheOptions.cs +++ b/src/UiPath.Caching.Queue/InMemoryRedisQueueCacheOptions.cs @@ -1,3 +1,5 @@ +using Microsoft.Extensions.Internal; + namespace UiPath.Caching; /// @@ -8,6 +10,12 @@ namespace UiPath.Caching; /// public sealed class InMemoryRedisQueueCacheOptions : IMemoryCacheOptions { + /// + /// Clock the set cache reads "now" from when it resolves expirations. + /// uses the system clock. Mirrors . + /// + public ISystemClock? Clock { get; set; } + /// Indicates whether the multilayer set cache is enabled. public bool Enabled { get; set; } = true; diff --git a/src/UiPath.Caching.Queue/InMemoryRedisQueueCacheProvider.cs b/src/UiPath.Caching.Queue/InMemoryRedisQueueCacheProvider.cs index b7ba577..648212a 100644 --- a/src/UiPath.Caching.Queue/InMemoryRedisQueueCacheProvider.cs +++ b/src/UiPath.Caching.Queue/InMemoryRedisQueueCacheProvider.cs @@ -62,5 +62,6 @@ private MultilayerSetCache BuildSetCache() => _options.ConnectionMonitorEnabled, _options.ConnectionMonitorPeriod, _options.UseLocalOnlyWhenDisconnected, - _options.LocalMaxExpirationDisconnected); + _options.LocalMaxExpirationDisconnected, + clock: _options.Clock); } diff --git a/src/UiPath.Caching.Queue/MemorySetCache.cs b/src/UiPath.Caching.Queue/MemorySetCache.cs index 0f3b380..91731ec 100644 --- a/src/UiPath.Caching.Queue/MemorySetCache.cs +++ b/src/UiPath.Caching.Queue/MemorySetCache.cs @@ -8,7 +8,8 @@ internal sealed class MemorySetCache( IMemoryCache memoryCache, ISerializerProxy serializer, ILocalLock localLock, - IMemoryCacheOptions memoryCacheOptions) + IMemoryCacheOptions memoryCacheOptions, + CacheClock clock) { private readonly bool _trackSize = memoryCacheOptions.SizeLimit.HasValue; private readonly string _localLockKeyPrefix = cacheName + ":"; @@ -86,7 +87,7 @@ public async ValueTask AddAsync(string key, IEnumerable items, DateT } using (await localLock.AcquireAsync(_localLockKeyPrefix + key, token).ConfigureAwait(false)) { - if (expiration.HasValue && expiration.Value <= DateTimeOffset.UtcNow) + if (expiration.HasValue && expiration.Value <= clock.UtcNow) { memoryCache.Remove(key); return 0; diff --git a/src/UiPath.Caching.Queue/MultilayerSetCache.cs b/src/UiPath.Caching.Queue/MultilayerSetCache.cs index 55767ef..51b87c0 100644 --- a/src/UiPath.Caching.Queue/MultilayerSetCache.cs +++ b/src/UiPath.Caching.Queue/MultilayerSetCache.cs @@ -1,3 +1,4 @@ +using Microsoft.Extensions.Internal; using UiPath.Caching.Locking; namespace UiPath.Caching; @@ -13,6 +14,7 @@ internal sealed class MultilayerSetCache : ISetCache private readonly bool _useLocalOnlyWhenDisconnected; private readonly TimeSpan? _localMaxExpirationDisconnected; private readonly TimeSpan? _defaultExpiration; + private readonly CacheClock _clock; public MultilayerSetCache( string name, @@ -26,7 +28,8 @@ public MultilayerSetCache( TimeSpan? connectionMonitorPeriod = null, bool useLocalOnlyWhenDisconnected = false, TimeSpan? localMaxExpirationDisconnected = null, - TimeSpan? defaultExpiration = null) + TimeSpan? defaultExpiration = null, + ISystemClock? clock = null) { ArgumentNullException.ThrowIfNull(inner); ArgumentNullException.ThrowIfNull(memoryCacheFactory); @@ -35,8 +38,9 @@ public MultilayerSetCache( ArgumentNullException.ThrowIfNull(localLock); _name = name; _inner = inner; + _clock = new CacheClock(clock); _memoryCache = memoryCacheFactory.Get(memoryOptions); - _memorySetCache = new MemorySetCache(name, _memoryCache, serializer, localLock, memoryOptions); + _memorySetCache = new MemorySetCache(name, _memoryCache, serializer, localLock, memoryOptions, _clock); _localMaxExpiration = localMaxExpiration; _connectionState = connectionMonitorEnabled ? GetConnectionMonitor(inner, connectionMonitorPeriod) : NullConnectionStateMonitor.Instance; _useLocalOnlyWhenDisconnected = useLocalOnlyWhenDisconnected && connectionMonitorEnabled; @@ -61,10 +65,10 @@ public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, Cach AddCoreAsync(cacheKey, items, expiration: null, policy, token); public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, TimeSpan expiration, CachePolicy? policy, CancellationToken token = default) => - AddCoreAsync(cacheKey, items, DateTimeOffset.UtcNow.Add(CacheExpiration.ThrowIfNotPositive(expiration)), policy, token); + AddCoreAsync(cacheKey, items, CacheExpiration.AddSaturating(_clock.UtcNow, CacheExpiration.ThrowIfNotPositive(expiration)), policy, token); public ValueTask AddAsync(CacheKey cacheKey, IEnumerable items, DateTimeOffset expiration, CachePolicy? policy, CancellationToken token = default) => - AddCoreAsync(cacheKey, items, CacheExpiration.ThrowIfNotFuture(expiration, DateTimeOffset.UtcNow), policy, token); + AddCoreAsync(cacheKey, items, CacheExpiration.ThrowIfNotFuture(expiration, _clock.UtcNow), policy, token); private async ValueTask AddCoreAsync(CacheKey cacheKey, IEnumerable items, DateTimeOffset? expiration, CachePolicy? policy, CancellationToken token) { @@ -224,7 +228,10 @@ public void Dispose() private DateTimeOffset? LocalWriteExpiration(DateTimeOffset? requested, CachePolicy? policy) { - requested ??= FromTtl(policy?.DistributedExpiration ?? (_inner is NullSetCache ? _defaultExpiration : null)); + // With a Redis inner the L2 resolves the lifetime and this only caps L1; memory-only, this + // is the whole answer, so the floor applies here too rather than leaving the set unbounded. + requested ??= FromTtl(policy?.DistributedExpiration + ?? (_inner is NullSetCache ? _defaultExpiration ?? CachePolicy.DefaultDistributedExpiration : null)); return _inner is NullSetCache ? requested : DisconnectedExpiration(requested); } @@ -262,12 +269,12 @@ private async ValueTask InternalAddAsync(CacheKey cacheKey, IEnumerable { return requested; } - var cap = DateTimeOffset.UtcNow.Add(_localMaxExpirationDisconnected.Value); + var cap = CacheExpiration.AddSaturating(_clock.UtcNow, _localMaxExpirationDisconnected.Value); return requested.HasValue && requested.Value < cap ? requested.Value : cap; } - private static DateTimeOffset? FromTtl(TimeSpan? ttl) => - ttl.HasValue ? DateTimeOffset.UtcNow.Add(ttl.Value) : null; + private DateTimeOffset? FromTtl(TimeSpan? ttl) => + ttl.HasValue ? CacheExpiration.AddSaturating(_clock.UtcNow, ttl.Value) : null; private static IEnumerable Materialize(IEnumerable items) { @@ -276,7 +283,7 @@ private static IEnumerable Materialize(IEnumerable items) } private DateTimeOffset? LocalExpiration() => - _localMaxExpiration.HasValue ? DateTimeOffset.UtcNow.Add(_localMaxExpiration.Value) : null; + _localMaxExpiration.HasValue ? CacheExpiration.AddSaturating(_clock.UtcNow, _localMaxExpiration.Value) : null; private static string Key(CacheKey cacheKey, CancellationToken token) { diff --git a/src/UiPath.Caching.Queue/PublicAPI.Unshipped.txt b/src/UiPath.Caching.Queue/PublicAPI.Unshipped.txt index bc15121..8c17cec 100644 --- a/src/UiPath.Caching.Queue/PublicAPI.Unshipped.txt +++ b/src/UiPath.Caching.Queue/PublicAPI.Unshipped.txt @@ -1,4 +1,8 @@ #nullable enable +UiPath.Caching.InMemoryQueueCacheOptions.Clock.get -> Microsoft.Extensions.Internal.ISystemClock? +UiPath.Caching.InMemoryQueueCacheOptions.Clock.set -> void +UiPath.Caching.InMemoryRedisQueueCacheOptions.Clock.get -> Microsoft.Extensions.Internal.ISystemClock? +UiPath.Caching.InMemoryRedisQueueCacheOptions.Clock.set -> void UiPath.Caching.ISetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.DateTimeOffset expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ISetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, System.TimeSpan expiration, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask UiPath.Caching.ISetCache.AddAsync(UiPath.Caching.CacheKey cacheKey, System.Collections.Generic.IEnumerable! items, UiPath.Caching.CachePolicy? policy, System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask diff --git a/src/UiPath.Caching/CacheClock.cs b/src/UiPath.Caching/CacheClock.cs index b841771..a9ee364 100644 --- a/src/UiPath.Caching/CacheClock.cs +++ b/src/UiPath.Caching/CacheClock.cs @@ -20,14 +20,22 @@ public DateTimeOffset ToDateTimeOffset(TimeSpan? timeSpan) { if (_defaultExpiration.HasValue) { - return _clock.UtcNow.Add(timeSpan ?? _defaultExpiration.Value); + return AddSaturating(timeSpan ?? _defaultExpiration.Value); } - return timeSpan.HasValue ? _clock.UtcNow.Add(timeSpan.Value) : DateTimeOffset.MaxValue; + return timeSpan.HasValue ? AddSaturating(timeSpan.Value) : DateTimeOffset.MaxValue; } public DateTimeOffset ToDateTimeOffset(DateTimeOffset? dateTimeOffset) => - dateTimeOffset ?? (_defaultExpiration.HasValue ? _clock.UtcNow.Add(_defaultExpiration.Value) : DateTimeOffset.MaxValue); + dateTimeOffset ?? (_defaultExpiration.HasValue ? AddSaturating(_defaultExpiration.Value) : DateTimeOffset.MaxValue); + + /// + /// is how a configured lifetime spells "no TTL", so a duration + /// that would run past the representable range lands on — + /// which the providers already read as "no TTL" — rather than throwing. + /// + private DateTimeOffset AddSaturating(TimeSpan duration) => + CacheExpiration.AddSaturating(_clock.UtcNow, duration); public TimeSpan ToTimeSpan(DateTimeOffset? dateTimeOffset) { diff --git a/src/UiPath.Caching/Config/DistributedCacheCollectionExtensions.cs b/src/UiPath.Caching/Config/DistributedCacheCollectionExtensions.cs index e88c631..a27f291 100644 --- a/src/UiPath.Caching/Config/DistributedCacheCollectionExtensions.cs +++ b/src/UiPath.Caching/Config/DistributedCacheCollectionExtensions.cs @@ -195,12 +195,14 @@ private static ICacheProvider CreateProvider(IServiceProvider sp, string provide } /// - /// Writes without a caller expiration take a default TTL; reject configurations where none resolves, or - /// resolves non-positive, and unbounded entries are not allowed. The fallback is resolved in the order the + /// Writes without a caller expiration take a default TTL; reject configurations where one resolves + /// non-positive, so such a write would expire on arrival. The fallback is resolved in the order the /// backing cache applies it, which differs by case: a named policy passed per-operation beats the tier /// default (), but with no named policy the cache builds its default from /// the tier default as the primary and the factory default as the fallback - /// (), so the tier default wins there. + /// (), so the tier default wins there — and + /// under both, which is why "nothing configured" + /// is no longer a rejectable state. /// private static void EnsureBoundedWrites(IServiceProvider sp, string providerName, UiPathDistributedCacheOptions options) { @@ -227,22 +229,19 @@ private static void EnsureBoundedWrites(IServiceProvider sp, string providerName KnownCacheProviderNames.InMemoryRedis => sp.GetRequiredService>().Value.DefaultExpiration, _ => sp.GetRequiredService>().Value.DefaultExpiration, }; - var fallback = ResolvePolicy(sp, options) is { } named - ? named.DistributedExpiration ?? tierDefault - : tierDefault ?? sp.GetService()?.Default?.DistributedExpiration; - - if (fallback is { } resolved) - { - if (resolved <= TimeSpan.Zero) - { - throw new InvalidOperationException( - $"AddDistributedCache('{providerName}') resolved {resolved} as the expiration for writes without a caller expiration, so they would expire immediately. Correct the provider's DefaultExpiration or the policy's DistributedExpiration, or set UiPathDistributedCacheOptions.DefaultEntryExpiration."); - } - } - else + // Mirrors the cache's own chain, floor included. An unset default no longer resolves to + // "no TTL", so there is nothing left to reject on that account — only a value someone + // actually configured non-positive. + var resolved = ResolvePolicy(sp, options) is { } named + ? named.DistributedExpiration ?? tierDefault ?? CachePolicy.DefaultDistributedExpiration + : tierDefault + ?? sp.GetService()?.Default?.DistributedExpiration + ?? CachePolicy.DefaultDistributedExpiration; + + if (resolved <= TimeSpan.Zero) { throw new InvalidOperationException( - $"AddDistributedCache('{providerName}') would store entries without an expiration: the provider's DefaultExpiration is null and no cache policy supplies DistributedExpiration. Set UiPathDistributedCacheOptions.DefaultEntryExpiration, configure the provider's DefaultExpiration, or set AllowUnboundedEntries."); + $"AddDistributedCache('{providerName}') resolved {resolved} as the expiration for writes without a caller expiration, so they would expire immediately. Correct the provider's DefaultExpiration or the policy's DistributedExpiration, or set UiPathDistributedCacheOptions.DefaultEntryExpiration."); } } diff --git a/src/UiPath.Caching/Distributed/UiPathDistributedCacheOptions.cs b/src/UiPath.Caching/Distributed/UiPathDistributedCacheOptions.cs index 9b5caf5..a73eb0b 100644 --- a/src/UiPath.Caching/Distributed/UiPathDistributedCacheOptions.cs +++ b/src/UiPath.Caching/Distributed/UiPathDistributedCacheOptions.cs @@ -44,13 +44,16 @@ public class UiPathDistributedCacheOptions /// Expiration applied when the caller supplies none. treats absent /// expiration as "until removed"; unless is set, that is mapped /// to this value so shared storage cannot accumulate unbounded keys. Null falls back to the backing - /// tier's default expiration. + /// tier's default expiration, and under that to + /// . /// public TimeSpan? DefaultEntryExpiration { get; set; } /// - /// Honor "no expiration" literally instead of substituting a bounded default. Off by default: - /// registration fails when no bounded default can be resolved. + /// Honor "no expiration" literally instead of substituting a bounded default. Off by default, and + /// now the only way to reach an unbounded entry through this adapter without naming a lifetime: a + /// default left unset resolves to rather + /// than to "until removed". /// public bool AllowUnboundedEntries { get; set; } } diff --git a/src/UiPath.Caching/InMemoryCacheOptions.cs b/src/UiPath.Caching/InMemoryCacheOptions.cs index 5103fe3..5d23abb 100644 --- a/src/UiPath.Caching/InMemoryCacheOptions.cs +++ b/src/UiPath.Caching/InMemoryCacheOptions.cs @@ -8,7 +8,7 @@ public class InMemoryCacheOptions : IMultilayerCacheOptions, IMemoryCacheOptions { public bool Enabled { get; set; } = true; - public TimeSpan? DefaultExpiration { get; set; } = TimeSpan.FromHours(1); + public TimeSpan? DefaultExpiration { get; set; } = CachePolicy.DefaultDistributedExpiration; public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(1); diff --git a/src/UiPath.Caching/InMemoryRedisCacheOptions.cs b/src/UiPath.Caching/InMemoryRedisCacheOptions.cs index cc777bc..ee05244 100644 --- a/src/UiPath.Caching/InMemoryRedisCacheOptions.cs +++ b/src/UiPath.Caching/InMemoryRedisCacheOptions.cs @@ -8,7 +8,7 @@ public class InMemoryRedisCacheOptions : IMultilayerCacheOptions, IMemoryCacheOp { public bool Enabled { get; set; } = true; - public TimeSpan? DefaultExpiration { get; set; } = TimeSpan.FromHours(1); + public TimeSpan? DefaultExpiration { get; set; } = CachePolicy.DefaultDistributedExpiration; public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(1); diff --git a/src/UiPath.Caching/MultilayerCache.cs b/src/UiPath.Caching/MultilayerCache.cs index 3d0746a..ddcceb0 100644 --- a/src/UiPath.Caching/MultilayerCache.cs +++ b/src/UiPath.Caching/MultilayerCache.cs @@ -80,8 +80,10 @@ public MultilayerCache( { ArgumentNullException.ThrowIfNull(generator); policy ??= _defaultPolicy; - var duration = policy.DistributedExpiration; - var writeExpiration = _clock.ToDateTimeOffset(ResolveWriteDuration(policy)); + // One resolution feeds both the write deadline and the rehydrate threshold: resolving twice + // would jitter twice, and would leave rehydration on the unfloored chain. + var duration = ResolveWriteDuration(policy); + var writeExpiration = _clock.ToDateTimeOffset(duration); return GetOrAddInternalAsync(cacheKey, generator, writeExpiration, duration, policy, token); } @@ -105,8 +107,10 @@ public MultilayerCache( ArgumentNullException.ThrowIfNull(entries); ArgumentNullException.ThrowIfNull(generator); policy ??= _defaultPolicy; - var duration = policy.DistributedExpiration; - var writeExpiration = _clock.ToDateTimeOffset(ResolveWriteDuration(policy)); + // One resolution feeds both the write deadline and the rehydrate threshold: resolving twice + // would jitter twice, and would leave rehydration on the unfloored chain. + var duration = ResolveWriteDuration(policy); + var writeExpiration = _clock.ToDateTimeOffset(duration); return GetOrAddBatchInternalAsync(entries, generator, writeExpiration, duration, policy, token); } @@ -128,7 +132,7 @@ public MultilayerCache( return GetOrAddBatchInternalAsync(entries, generator, writeExpiration, duration, policy ?? _defaultPolicy, token); } - private async ValueTask GetOrAddInternalAsync(CacheKey cacheKey, Func> generator, DateTimeOffset? expiration, TimeSpan? effectiveDuration, CachePolicy policy, CancellationToken token) + private async ValueTask GetOrAddInternalAsync(CacheKey cacheKey, Func> generator, DateTimeOffset? expiration, TimeSpan effectiveDuration, CachePolicy policy, CancellationToken token) { NotCacheableException.ThrowIfNotCacheable(); var cacheEntryOptions = _entryBuilder.BuildEntryOptions(cacheKey, expiration, token); @@ -150,7 +154,7 @@ public MultilayerCache( return result.Value; } - private void TryRehydrate(CacheKey originalCacheKey, DateTimeOffset entryExpiration, T? currentValue, Func> generator, CachePolicy policy, TimeSpan? effectiveDuration) + private void TryRehydrate(CacheKey originalCacheKey, DateTimeOffset entryExpiration, T? currentValue, Func> generator, CachePolicy policy, TimeSpan duration) { if (policy.RehydrateEnabled != true || policy.Rehydrate is null) { @@ -160,8 +164,7 @@ private void TryRehydrate(CacheKey originalCacheKey, DateTimeOffset entryExpi { return; } - var resolvedDuration = effectiveDuration ?? policy.DistributedExpiration ?? _multiLayerCacheOptions.DefaultExpiration; - if (resolvedDuration is not { } duration || duration <= TimeSpan.Zero) + if (duration <= TimeSpan.Zero || duration == TimeSpan.MaxValue) { return; } @@ -181,7 +184,7 @@ private void TryRehydrate(CacheKey originalCacheKey, DateTimeOffset entryExpi // Factory transitions to null: preserve the original deadline so the null doesn't get a fresh TTL window. var rehydrateExpiration = newValue is null ? entryExpiration - : _clock.UtcNow.Add(duration); + : CacheExpiration.AddSaturating(_clock.UtcNow, duration); var rehydrateOptions = _entryBuilder.BuildEntryOptions(originalCacheKey, rehydrateExpiration, ct); var innerCacheDisconnected = GetInnerCacheDisconnected(); var fired = innerCacheDisconnected || await _eventPublisher.CacheSetAsync(rehydrateOptions).ConfigureAwait(false); @@ -201,15 +204,14 @@ private void TryRehydrateBatch( List<(CacheKey CallerKey, TState State, CacheEntryOptions Options, DateTimeOffset Expiration, T? Value)> hits, Func[]>> generator, CachePolicy policy, - TimeSpan? effectiveDuration) + TimeSpan duration) where TState : notnull { if (policy.RehydrateEnabled != true || policy.Rehydrate is null) { return; } - var resolvedDuration = effectiveDuration ?? policy.DistributedExpiration ?? _multiLayerCacheOptions.DefaultExpiration; - if (resolvedDuration is not { } duration || duration <= TimeSpan.Zero) + if (duration <= TimeSpan.Zero || duration == TimeSpan.MaxValue) { return; } @@ -296,7 +298,7 @@ private static TState[] MapReservedKeysToStates(CacheKey[] reservedKeys, var requested = new HashSet(rehydrateStates); var seen = new HashSet(rehydrateStates.Length); - var freshExpiration = _clock.UtcNow.Add(duration); + var freshExpiration = CacheExpiration.AddSaturating(_clock.UtcNow, duration); var groups = new Dictionary Entry, CacheKey CallerKey)>>(); foreach (var pair in produced ?? []) { @@ -375,7 +377,7 @@ private async ValueTask PublishCacheSetEventsAsync(List<(CacheEntryValu KeyValuePair[] entries, Func[]>> generator, DateTimeOffset? expiration, - TimeSpan? effectiveDuration, + TimeSpan effectiveDuration, CachePolicy policy, CancellationToken token) where TState : notnull diff --git a/src/UiPath.Caching/MultilayerCacheBase.cs b/src/UiPath.Caching/MultilayerCacheBase.cs index c8b125b..26225cf 100644 --- a/src/UiPath.Caching/MultilayerCacheBase.cs +++ b/src/UiPath.Caching/MultilayerCacheBase.cs @@ -34,6 +34,11 @@ public abstract class MultilayerCacheBase : IDisposable // Hardcoded fallback values for the lock fields. Merged in as the lowest-priority policy // (after provider-specific + user DefaultCachePolicy) so every cache instance has a fully // resolved Lock — every field non-null — by the time validation runs. + // + // DistributedExpiration is deliberately NOT floored here: this policy feeds CacheClock, which + // also materializes the expiration a READ found, and a key that genuinely has no TTL must keep + // reporting DateTimeOffset.MaxValue rather than a fabricated now+default. The floor belongs on + // the write paths only — see ResolveWriteDuration. private static readonly CachePolicy HardcodedDefaults = new() { Lock = new LockProfile @@ -165,7 +170,9 @@ private static TimeSpan NonNegativeOrFallback(TimeSpan? value, TimeSpan fallback protected static TimeSpan? ApplyJitter(TimeSpan? duration, TimeSpan? maxJitter, DateTimeOffset utcNow) { - if (duration is not { } d || d <= TimeSpan.Zero || maxJitter is not { } max || max <= TimeSpan.Zero) + // TimeSpan.MaxValue means "no TTL", and jitter exists to spread an expiry that never comes. + // Jittering it would clamp it below the sentinel and put the entry back on the EXPIRE path. + if (duration is not { } d || d <= TimeSpan.Zero || d == TimeSpan.MaxValue || maxJitter is not { } max || max <= TimeSpan.Zero) { return duration; } @@ -180,17 +187,24 @@ private static TimeSpan NonNegativeOrFallback(TimeSpan? value, TimeSpan fallback /// /// Resolves the L2 write duration, applying jitter only when the caller did not pass an explicit /// expiration. Resolves the full fallback chain (policy.DistributedExpiration → - /// IMultilayerCacheOptions.DefaultExpiration) BEFORE jittering so the options-default path - /// is jittered too — not just the policy path. + /// IMultilayerCacheOptions.DefaultExpiration → + /// ) BEFORE jittering so the options-default + /// path is jittered too — not just the policy path. The last step is what stops "nobody + /// configured a lifetime" from meaning "keep this forever"; unbounded has to be asked for, by + /// configuring . /// - protected TimeSpan? ResolveWriteDuration(CachePolicy policy, TimeSpan? callerExpiration = null) + protected TimeSpan ResolveWriteDuration(CachePolicy policy, TimeSpan? callerExpiration = null) { - if (callerExpiration is not null) + if (callerExpiration is { } caller) { - return callerExpiration; + return caller; } - var resolved = policy.DistributedExpiration ?? _multiLayerCacheOptions.DefaultExpiration; - return ApplyJitter(resolved, policy.JitterMaxDuration, _clock.UtcNow); + var resolved = policy.DistributedExpiration + ?? _multiLayerCacheOptions.DefaultExpiration + ?? CachePolicy.DefaultDistributedExpiration; + // ApplyJitter is nullable in, nullable out; it only returns null for a null input, so the + // coalesce is the compiler's price for a resolved value rather than a real branch. + return ApplyJitter(resolved, policy.JitterMaxDuration, _clock.UtcNow) ?? resolved; } /// diff --git a/src/UiPath.Caching/MultilayerHashCache.cs b/src/UiPath.Caching/MultilayerHashCache.cs index ec9f225..bce806e 100644 --- a/src/UiPath.Caching/MultilayerHashCache.cs +++ b/src/UiPath.Caching/MultilayerHashCache.cs @@ -74,8 +74,10 @@ public MultilayerHashCache( { ArgumentNullException.ThrowIfNull(generator); policy ??= _defaultPolicy; - var duration = policy.DistributedExpiration; - var writeExpiration = _clock.ToDateTimeOffset(ResolveWriteDuration(policy)); + // One resolution feeds both the write deadline and the rehydrate threshold: resolving twice + // would jitter twice, and would leave rehydration on the unfloored chain. + var duration = ResolveWriteDuration(policy); + var writeExpiration = _clock.ToDateTimeOffset(duration); return GetOrAddInternalAsync(cacheKey, generator, writeExpiration, duration, HashCacheSetOption.KeyReplace, policy, token); } @@ -107,7 +109,7 @@ public MultilayerHashCache( return GetOrAddInternalAsync(cacheKey, generator, writeExpiration, duration, setOption ?? HashCacheSetOption.KeyReplace, policy ?? _defaultPolicy, token); } - private async ValueTask> GetOrAddInternalAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration, TimeSpan? effectiveDuration, HashCacheSetOption setOption, CachePolicy policy, CancellationToken token) + private async ValueTask> GetOrAddInternalAsync(CacheKey cacheKey, Func>> generator, DateTimeOffset? expiration, TimeSpan effectiveDuration, HashCacheSetOption setOption, CachePolicy policy, CancellationToken token) { NotCacheableException.ThrowIfNotCacheable(); var cacheEntryOptions = _entryBuilder.BuildEntryOptions(cacheKey, expiration, setOption, token); @@ -128,7 +130,7 @@ public MultilayerHashCache( return result.Value ?? Empty(); } - private void TryHashRehydrate(CacheKey originalCacheKey, DateTimeOffset entryExpiration, IDictionary? currentValue, Func>> generator, CachePolicy policy, TimeSpan? effectiveDuration) + private void TryHashRehydrate(CacheKey originalCacheKey, DateTimeOffset entryExpiration, IDictionary? currentValue, Func>> generator, CachePolicy policy, TimeSpan duration) { if (policy.RehydrateEnabled != true || policy.Rehydrate is null) { @@ -138,8 +140,7 @@ private void TryHashRehydrate(CacheKey originalCacheKey, DateTimeOffset entry { return; } - var resolvedDuration = effectiveDuration ?? policy.DistributedExpiration ?? _multiLayerCacheOptions.DefaultExpiration; - if (resolvedDuration is not { } duration || duration <= TimeSpan.Zero) + if (duration <= TimeSpan.Zero || duration == TimeSpan.MaxValue) { return; } @@ -159,7 +160,7 @@ private void TryHashRehydrate(CacheKey originalCacheKey, DateTimeOffset entry // Factory transitions to empty: preserve the original deadline so the marker doesn't get a fresh TTL window. var rehydrateExpiration = IsNullOrEmpty(newValue) ? entryExpiration - : _clock.UtcNow.Add(duration); + : CacheExpiration.AddSaturating(_clock.UtcNow, duration); var rehydrateOptions = _entryBuilder.BuildEntryOptions(originalCacheKey, rehydrateExpiration, HashCacheSetOption.KeyReplace, ct); var innerCacheDisconnected = GetInnerCacheDisconnected(); var fired = innerCacheDisconnected || await _eventPublisher.CacheSetAsync(rehydrateOptions).ConfigureAwait(false); diff --git a/src/UiPath.Caching/PublicAPI.Shipped.txt b/src/UiPath.Caching/PublicAPI.Shipped.txt index 58e5a90..2d19ccd 100644 --- a/src/UiPath.Caching/PublicAPI.Shipped.txt +++ b/src/UiPath.Caching/PublicAPI.Shipped.txt @@ -442,7 +442,6 @@ UiPath.Caching.MultilayerCacheBase.GetInnerCacheDisconnected() -> bool UiPath.Caching.MultilayerCacheBase.InvokeFactoryAsync(UiPath.Caching.CacheKey cacheKey, System.Func!>! factory, System.TimeSpan? factoryTimeout, System.Threading.CancellationToken token) -> System.Threading.Tasks.Task! UiPath.Caching.MultilayerCacheBase.MultilayerCacheBase(string! cacheName, object! innerCache, UiPath.Caching.IMemoryCacheFactory! memoryCacheFactory, UiPath.Caching.Broadcast.ITopicFactory! topicFactory, UiPath.Caching.Broadcast.ICacheEventFactory! cacheEventFactory, UiPath.Caching.Telemetry.ICachingTelemetryProvider! telemetryProvider, UiPath.Caching.IMultilayerCacheOptions! multiLayerCacheOptions, UiPath.Caching.IMemoryCacheOptions! memoryOptions, UiPath.Caching.CacheOptions! cacheOptions, UiPath.Caching.Locking.ILocalLock! localLock, UiPath.Caching.Locking.IDistributedLock! distributedLock, UiPath.Caching.ICachePolicyFactory! policyFactory, Microsoft.Extensions.Logging.ILogger! logger) -> void UiPath.Caching.MultilayerCacheBase.Name.get -> string! -UiPath.Caching.MultilayerCacheBase.ResolveWriteDuration(UiPath.Caching.CachePolicy! policy, System.TimeSpan? callerExpiration = null) -> System.TimeSpan? UiPath.Caching.MultilayerCacheBase.RunUnderLocksAsync(UiPath.Caching.CacheKey cacheKey, System.Func>! readCachedAsync, System.Func! isHit, System.Func>! runGeneratorAndStoreAsync, System.Threading.CancellationToken token, UiPath.Caching.LockProfile? policyLock = null) -> System.Threading.Tasks.ValueTask UiPath.Caching.MultilayerCacheBase.Telemetry.get -> UiPath.Caching.Telemetry.ICachingTelemetryProvider! UiPath.Caching.PrefixCacheKeyStrategy diff --git a/src/UiPath.Caching/PublicAPI.Unshipped.txt b/src/UiPath.Caching/PublicAPI.Unshipped.txt index a6fb3b2..93aa4cf 100644 --- a/src/UiPath.Caching/PublicAPI.Unshipped.txt +++ b/src/UiPath.Caching/PublicAPI.Unshipped.txt @@ -14,6 +14,7 @@ UiPath.Caching.Distributed.UiPathDistributedCacheOptions.RedisKeyDifferentiator. UiPath.Caching.Distributed.UiPathDistributedCacheOptions.RedisKeyStrategyFactory.get -> UiPath.Caching.Redis.IRedisKeyStrategyFactory? UiPath.Caching.Distributed.UiPathDistributedCacheOptions.RedisKeyStrategyFactory.set -> void UiPath.Caching.Distributed.UiPathDistributedCacheOptions.UiPathDistributedCacheOptions() -> void +UiPath.Caching.MultilayerCacheBase.ResolveWriteDuration(UiPath.Caching.CachePolicy! policy, System.TimeSpan? callerExpiration = null) -> System.TimeSpan UiPath.Caching.Redis.RedisCacheBase.CallerDeadline(System.DateTimeOffset expiration, string? paramName = null) -> System.DateTimeOffset UiPath.Caching.Redis.RedisCacheBase.CallerDuration(System.DateTimeOffset expiration, string? paramName = null) -> System.TimeSpan UiPath.Caching.Redis.RedisCacheBase.OptionsDeadline(System.DateTimeOffset? expireTime, System.TimeSpan? timeToLive, UiPath.Caching.CachePolicy? policy) -> System.DateTimeOffset diff --git a/src/UiPath.Caching/Redis/RedisCacheBase.cs b/src/UiPath.Caching/Redis/RedisCacheBase.cs index 0710b57..4a7a83d 100644 --- a/src/UiPath.Caching/Redis/RedisCacheBase.cs +++ b/src/UiPath.Caching/Redis/RedisCacheBase.cs @@ -56,28 +56,29 @@ protected void TrackRead(ITelemetryOperation operation, bool hit, RedisKey key) /// /// Write duration for a call that carried no expiration: the policy's L2 TTL, then the - /// cache default, then for "no TTL". + /// cache default, then . Never unbounded + /// by omission — a lifetime of has to be configured to get that. /// protected TimeSpan PolicyDuration(CachePolicy? policy) => - Clock.ToTimeSpan(policy?.DistributedExpiration ?? DefaultExpiration); + policy?.DistributedExpiration ?? DefaultExpiration ?? CachePolicy.DefaultDistributedExpiration; /// /// Write deadline for a call that carried no expiration, resolved the same way as - /// and yielding for "no TTL". + /// . /// protected DateTimeOffset PolicyDeadline(CachePolicy? policy) => - Clock.ToDateTimeOffset(policy?.DistributedExpiration ?? DefaultExpiration); + Clock.ToDateTimeOffset(PolicyDuration(policy)); /// /// Write deadline carried by an entry-options object. keeps /// its lifetime fields nullable — an options object is the one seam where null still - /// means "inherit" — so this resolves ExpireTime, then TimeToLive, then the policy - /// and cache defaults. + /// means "inherit" — so this resolves ExpireTime, then TimeToLive, then the same + /// chain as . /// protected DateTimeOffset OptionsDeadline(DateTimeOffset? expireTime, TimeSpan? timeToLive, CachePolicy? policy) => expireTime.HasValue ? Clock.ToDateTimeOffset(expireTime) - : Clock.ToDateTimeOffset(timeToLive ?? policy?.DistributedExpiration ?? DefaultExpiration); + : Clock.ToDateTimeOffset(timeToLive ?? PolicyDuration(policy)); /// Validates a caller-supplied duration. protected static TimeSpan CallerDuration(TimeSpan expiration, [CallerArgumentExpression(nameof(expiration))] string? paramName = null) => diff --git a/src/UiPath.Caching/Redis/RedisCacheOptions.cs b/src/UiPath.Caching/Redis/RedisCacheOptions.cs index 63c052b..1d9b975 100644 --- a/src/UiPath.Caching/Redis/RedisCacheOptions.cs +++ b/src/UiPath.Caching/Redis/RedisCacheOptions.cs @@ -4,7 +4,7 @@ public class RedisCacheOptions : ICacheOptions { public bool Enabled { get; set; } = true; - public TimeSpan? DefaultExpiration { get; set; } = TimeSpan.FromHours(1); + public TimeSpan? DefaultExpiration { get; set; } = CachePolicy.DefaultDistributedExpiration; public string KeyPrefix { get; set; } = string.Empty; diff --git a/tests/UiPath.Caching.Tests/CacheExpirationTests.cs b/tests/UiPath.Caching.Tests/CacheExpirationTests.cs index aa2abbf..cee4736 100644 --- a/tests/UiPath.Caching.Tests/CacheExpirationTests.cs +++ b/tests/UiPath.Caching.Tests/CacheExpirationTests.cs @@ -10,6 +10,40 @@ namespace UiPath.Caching.Tests; /// public class CacheExpirationTests { + [Fact] + public void AddSaturating_lands_on_MaxValue_instead_of_overflowing() + { + CacheExpiration.AddSaturating(DateTimeOffset.UtcNow, TimeSpan.MaxValue) + .Should().Be(DateTimeOffset.MaxValue); + } + + /// + /// Add advances the wall-clock DateTime, so a positive offset leaves less room than the UTC + /// instant alone suggests. Measuring only the UTC headroom let this through to a throwing Add. + /// + [Theory] + [InlineData(5)] + [InlineData(-5)] + [InlineData(0)] + public void AddSaturating_saturates_near_the_boundary_whatever_the_offset(int offsetHours) + { + var offset = TimeSpan.FromHours(offsetHours); + var now = new DateTimeOffset(DateTime.MaxValue.AddHours(-10), TimeSpan.Zero).ToOffset(offset); + + var act = () => CacheExpiration.AddSaturating(now, TimeSpan.FromHours(9)); + + act.Should().NotThrow().Which.Should().BeOnOrBefore(DateTimeOffset.MaxValue); + } + + [Fact] + public void AddSaturating_adds_normally_when_the_result_fits() + { + var now = new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.FromHours(5)); + + CacheExpiration.AddSaturating(now, TimeSpan.FromHours(2)) + .Should().Be(now.AddHours(2)); + } + [Theory] [InlineData(0)] [InlineData(-1)] diff --git a/tests/UiPath.Caching.Tests/Config/DistributedCacheRegistrationTests.cs b/tests/UiPath.Caching.Tests/Config/DistributedCacheRegistrationTests.cs index aa5cef4..e78d5e4 100644 --- a/tests/UiPath.Caching.Tests/Config/DistributedCacheRegistrationTests.cs +++ b/tests/UiPath.Caching.Tests/Config/DistributedCacheRegistrationTests.cs @@ -34,8 +34,14 @@ public void InMemory_tier_works_without_AddMemory() cache.Get("k").Should().Equal(1); } + /// + /// This used to fail fast: a null provider default meant the adapter would store entries with no + /// TTL at all. It resolves to now, so + /// there is nothing left to reject and registration succeeds. Unbounded is still available, but + /// through AllowUnboundedEntries rather than by leaving a nullable unset. + /// [Fact] - public void Null_default_expiration_without_policy_fails_fast() + public void Null_default_expiration_without_policy_is_bounded_by_the_library_default() { var services = new ServiceCollection(); services.AddCaching(b => @@ -45,9 +51,10 @@ public void Null_default_expiration_without_policy_fails_fast() }); using var provider = services.BuildServiceProvider(); - var act = () => provider.GetRequiredService(); + var cache = provider.GetRequiredService(); + cache.Set("k", [1], new DistributedCacheEntryOptions()); - act.Should().Throw().WithMessage("*DefaultExpiration*DistributedExpiration*"); + cache.Get("k").Should().Equal(1); } [Fact] diff --git a/tests/UiPath.Caching.Tests/InMemorySetCacheTests.cs b/tests/UiPath.Caching.Tests/InMemorySetCacheTests.cs index 92ffa17..c810f25 100644 --- a/tests/UiPath.Caching.Tests/InMemorySetCacheTests.cs +++ b/tests/UiPath.Caching.Tests/InMemorySetCacheTests.cs @@ -1,3 +1,4 @@ +using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging.Abstractions; using UiPath.Caching.Locking; @@ -14,11 +15,17 @@ private static MultilayerSetCache CreateSut(InMemoryQueueCacheOptions? options = options ??= new InMemoryQueueCacheOptions(); return new MultilayerSetCache( KnownCacheProviderNames.InMemory, NullSetCache.Instance, - new MemoryCacheFactory(null, NullLoggerFactory.Instance), + new MemoryCacheFactory(options.Clock, NullLoggerFactory.Instance), new SystemJsonSerializerProxy(), options, NullLocalLock.Instance, localMaxExpiration: null, - defaultExpiration: options.DefaultExpiration); + defaultExpiration: options.DefaultExpiration, + clock: options.Clock); + } + + private sealed class FakeClock(DateTimeOffset now) : ISystemClock + { + public DateTimeOffset UtcNow { get; } = now; } // Casts to IEnumerable so the call binds to the IEnumerable AddAsync overload rather @@ -29,6 +36,37 @@ private static ValueTask AddMany(MultilayerSetCache sut, CacheKey key, par [Fact] public void Name_is_InMemory() => CreateSut().Name.Should().Be("InMemory"); + /// + /// Memory-only, this tier resolves the whole lifetime, so the configured spelling of unbounded + /// has to survive the conversion to a deadline instead of overflowing on the way. + /// + /// + /// Every expiration decision on this path reads the configured clock, not the ambient one. The + /// deadline here is in the past by wall-clock time and in the future by the cache's own time, + /// so it is only accepted if both the validation and the L1 store agree to use the latter. + /// + [Fact] + public async Task Expirations_are_resolved_against_the_configured_clock() + { + var now = new DateTimeOffset(2020, 1, 1, 0, 0, 0, TimeSpan.Zero); + var sut = CreateSut(new InMemoryQueueCacheOptions { Clock = new FakeClock(now) }); + + var added = await sut.AddAsync("k", (IEnumerable)["a"], now.AddDays(1), (CachePolicy?)null, Ct); + + added.Should().Be(1); + (await sut.MembersAsync("k", token: Ct)).Should().BeEquivalentTo(new[] { "a" }); + } + + [Fact] + public async Task Add_with_an_unbounded_default_expiration_stores_the_item() + { + var sut = CreateSut(new InMemoryQueueCacheOptions { DefaultExpiration = TimeSpan.MaxValue }); + + (await sut.AddAsync("k", "a", token: Ct)).Should().BeTrue(); + + (await sut.MembersAsync("k", token: Ct)).Should().BeEquivalentTo(new[] { "a" }); + } + [Fact] public async Task Add_single_deduplicates() { diff --git a/tests/UiPath.Caching.Tests/MultilayerCachePerNamePolicyWiringTests.cs b/tests/UiPath.Caching.Tests/MultilayerCachePerNamePolicyWiringTests.cs index e621669..f4ecfb1 100644 --- a/tests/UiPath.Caching.Tests/MultilayerCachePerNamePolicyWiringTests.cs +++ b/tests/UiPath.Caching.Tests/MultilayerCachePerNamePolicyWiringTests.cs @@ -116,6 +116,75 @@ await _innerCache.Received(1).SetAsync( Arg.Any()); } + /// + /// Nothing in the chain supplies a lifetime, so the write takes + /// rather than being stored unbounded. + /// + [Fact] + public async Task SetAsync_falls_back_to_the_library_default_when_nothing_configures_a_TTL() + { + _options.DefaultExpiration = null; + _fixture.Inject(new CacheOptions { AppShortName = "test" }); + _sut = null; + + _innerCache.SetAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(true); + _topic.PublishAsync(Arg.Any(), Arg.Any()) + .Returns(_ => true); + + await Sut.SetAsync(_cacheKey, "v", (CachePolicy?)null, TestContext.Current.CancellationToken); + + var floor = CachePolicy.DefaultDistributedExpiration; + await _innerCache.Received(1).SetAsync( + _cacheKey, + "v", + Arg.Is(d => d - DateTimeOffset.UtcNow > floor - TimeSpan.FromSeconds(5) && d - DateTimeOffset.UtcNow < floor + TimeSpan.FromSeconds(5)), + Arg.Any(), + Arg.Any()); + } + + /// An unbounded lifetime is still reachable, by configuring it rather than omitting it. + [Fact] + public async Task SetAsync_stores_unbounded_when_the_default_is_configured_TimeSpan_MaxValue() + { + _options.DefaultExpiration = TimeSpan.MaxValue; + _fixture.Inject(new CacheOptions { AppShortName = "test" }); + _sut = null; + + _innerCache.SetAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(true); + _topic.PublishAsync(Arg.Any(), Arg.Any()) + .Returns(_ => true); + + await Sut.SetAsync(_cacheKey, "v", (CachePolicy?)null, TestContext.Current.CancellationToken); + + await _innerCache.Received(1).SetAsync( + _cacheKey, "v", DateTimeOffset.MaxValue, Arg.Any(), Arg.Any()); + } + + /// + /// Jitter spreads an expiry that is coming; an unbounded entry has none. Jittering the sentinel + /// would clamp it under and put the key back on EXPIRE. + /// + [Fact] + public async Task SetAsync_keeps_an_unbounded_default_unbounded_when_the_policy_jitters() + { + _options.DefaultExpiration = TimeSpan.MaxValue; + _fixture.Inject(new CacheOptions { AppShortName = "test" }); + _sut = null; + + _innerCache.SetAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(true); + _topic.PublishAsync(Arg.Any(), Arg.Any()) + .Returns(_ => true); + + var jittered = new CachePolicy { JitterMaxDuration = TimeSpan.FromMinutes(5) }; + await Sut.SetAsync(_cacheKey, "v", jittered, TestContext.Current.CancellationToken); + + await _innerCache.Received(1).SetAsync( + _cacheKey, "v", DateTimeOffset.MaxValue, Arg.Any(), Arg.Any()); + } + [Fact] public async Task SetAsync_uses_DefaultCachePolicy_DistributedExpiration_when_caller_omits_expiration() { diff --git a/tests/UiPath.Caching.Tests/MultilayerCacheRehydrateTests.cs b/tests/UiPath.Caching.Tests/MultilayerCacheRehydrateTests.cs index 1256417..189da2b 100644 --- a/tests/UiPath.Caching.Tests/MultilayerCacheRehydrateTests.cs +++ b/tests/UiPath.Caching.Tests/MultilayerCacheRehydrateTests.cs @@ -89,6 +89,50 @@ public async Task Hit_past_threshold_acquires_distributed_lock_and_invokes_gener generatorTcs.TrySetResult("rehydrated"); } + /// + /// One resolution feeds both the write deadline and the rehydrate threshold. They used to be + /// separate chains: the write took the floor while the threshold bottomed out at null, so an + /// entry written under the library default expired after an hour and was never rehydrated. + /// + [Fact] + public async Task Hit_past_threshold_rehydrates_when_only_the_library_default_supplies_the_duration() + { + var token = testContextAccessor.Current.CancellationToken; + _options.DefaultExpiration = null; + var policy = new CachePolicy + { + RehydrateEnabled = true, + Rehydrate = new RehydrateOptions + { + Threshold = 0.75, + BaseCooldown = TimeSpan.FromSeconds(1), + MaxCooldown = TimeSpan.FromMinutes(5), + TimeoutFraction = 0.5, + Name = "test-profile", + }, + }; + // Five minutes left of the one-hour default is inside the last quarter, so past the threshold. + var aged = new TestCacheEntry { Value = "cached", Expiration = DateTimeOffset.UtcNow.AddMinutes(5) }; + _innerCache.GetCacheEntryAsync(_cacheKey, Arg.Any(), Arg.Any()).Returns(aged); + + var generatorCalls = 0; + Func> generator = _ => + { + Interlocked.Increment(ref generatorCalls); + return Task.FromResult("rehydrated"); + }; + var acquiredLock = Substitute.For(); + _distributedLock.TryAcquireAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(acquiredLock); + _innerCache.SetAsync(_cacheKey, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(true); + + var result = await Sut.GetOrAddAsync(_cacheKey, generator, policy, token); + + result.Should().Be("cached"); + await WaitForAsync(() => Volatile.Read(ref generatorCalls) > 0, TimeSpan.FromSeconds(5), token); + } + [Fact] public async Task Rehydrate_writes_value_back_through_inner_cache_on_success() { diff --git a/tests/UiPath.Caching.Tests/Redis/RedisCacheTests.cs b/tests/UiPath.Caching.Tests/Redis/RedisCacheTests.cs index 03a29b2..292a39d 100644 --- a/tests/UiPath.Caching.Tests/Redis/RedisCacheTests.cs +++ b/tests/UiPath.Caching.Tests/Redis/RedisCacheTests.cs @@ -667,25 +667,36 @@ public async Task Refresh_no_expiration_works_as_expected() actualOffset.Should().Be(expected); } - [Theory] - [InlineData(typeof(TimeSpan))] - [InlineData(typeof(DateTimeOffset))] - [InlineData(typeof(object))] - public async Task Refresh_no_expiration_no_default(Type expirationType) + /// + /// An unset provider default no longer reads as "no TTL": the write inherits + /// , so the key gets a deadline instead of + /// being persisted. + /// + [Fact] + public async Task Refresh_with_no_expiration_and_no_default_takes_the_library_default() { _cacheOptions.DefaultExpiration = null; - if (expirationType == typeof(TimeSpan)) - { - await Sut.RefreshAsync(_cacheKey, policy: null, token: testContextAccessor.Current.CancellationToken); - } - else if (expirationType == typeof(DateTimeOffset)) - { - await Sut.RefreshAsync(_cacheKey, policy: null, token: testContextAccessor.Current.CancellationToken); - } - else - { - await Sut.RefreshAsync(_cacheKey, policy: null, token: testContextAccessor.Current.CancellationToken); - } + DateTime? actualExpiration = default; + _database.KeyExpireAsync(_redisKey, Arg.Any(), Arg.Any()) + .Returns(ci => + { + actualExpiration = ci.Arg(); + return true; + }); + + await Sut.RefreshAsync(_cacheKey, policy: null, token: testContextAccessor.Current.CancellationToken); + + await _database.DidNotReceive().KeyPersistAsync(Arg.Any(), Arg.Any()); + actualExpiration.Should().Be(_now.Add(CachePolicy.DefaultDistributedExpiration).UtcDateTime); + } + + /// Unbounded is still reachable — it just has to be asked for. + [Fact] + public async Task Refresh_persists_the_key_when_the_default_is_configured_unbounded() + { + _cacheOptions.DefaultExpiration = TimeSpan.MaxValue; + + await Sut.RefreshAsync(_cacheKey, policy: null, token: testContextAccessor.Current.CancellationToken); await _database.DidNotReceive().KeyExpireAsync(Arg.Any(), Arg.Any(), Arg.Any()); await _database.Received(1).KeyPersistAsync(Arg.Any(), Arg.Any()); diff --git a/tests/UiPath.Caching.Tests/Redis/RedisHashCacheTests.cs b/tests/UiPath.Caching.Tests/Redis/RedisHashCacheTests.cs index 55d525f..da22a43 100644 --- a/tests/UiPath.Caching.Tests/Redis/RedisHashCacheTests.cs +++ b/tests/UiPath.Caching.Tests/Redis/RedisHashCacheTests.cs @@ -965,9 +965,9 @@ public async Task Refresh_HashCacheEntryOptions_no_extended_props() } [Fact] - public async Task Refresh_HashCacheEntryOptions_with_extended_props_no_default_expiration() + public async Task Refresh_HashCacheEntryOptions_with_extended_props_unbounded_default() { - _redisCacheOptions.DefaultExpiration = null; + _redisCacheOptions.DefaultExpiration = TimeSpan.MaxValue; var metadata = _fixture.Create>(); var options = new HashCacheEntryOptions(default, default, metadata); var actual = await Sut.RefreshAsync(_cacheKey, options, token: testContextAccessor.Current.CancellationToken); @@ -981,7 +981,7 @@ public async Task Refresh_HashCacheEntryOptions_with_extended_props_no_default_e [Fact] public async Task Refresh_HashCacheEntryOptions_transaction_fail() { - _redisCacheOptions.DefaultExpiration = null; + _redisCacheOptions.DefaultExpiration = TimeSpan.MaxValue; var metadata = _fixture.Create>(); var options = new HashCacheEntryOptions(default, default, metadata); _transaction.ExecuteAsync(Arg.Any()).Returns(false); @@ -996,7 +996,7 @@ public async Task Refresh_HashCacheEntryOptions_transaction_fail() [Fact] public async Task Refresh_HashCacheEntryOptions_transaction_exception() { - _redisCacheOptions.DefaultExpiration = null; + _redisCacheOptions.DefaultExpiration = TimeSpan.MaxValue; var metadata = _fixture.Create>(); var options = new HashCacheEntryOptions(default, default, metadata); _transaction.ExecuteAsync(Arg.Any()).ThrowsAsync(new Exception());