#### [Abblix\.Utils](https://www.abblix.com/en/docs/api/abblix-utils 'index')
### [Abblix\.Utils](https://www.abblix.com/en/docs/api/abblix-utils/Abblix.Utils 'Abblix\.Utils')

## DistributedCacheExtensions Class

Extension methods for [Microsoft\.Extensions\.Caching\.Distributed\.IDistributedCache](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache 'Microsoft\.Extensions\.Caching\.Distributed\.IDistributedCache') providing atomic operations\.

```csharp
public static class DistributedCacheExtensions
```

Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') → DistributedCacheExtensions
### Methods

## DistributedCacheExtensions\.TryAddAsync\(this IDistributedCache, string, TimeSpan, CancellationToken\) Method {#Abblix.Utils.DistributedCacheExtensions.TryAddAsync(thisMicrosoft.Extensions.Caching.Distributed.IDistributedCache,string,System.TimeSpan,System.Threading.CancellationToken)}

Marks a key as present unless it already is, telling a first call from a repeat: the
add\-if\-absent primitive replay caches and other "seen before?" checks are built on\.

```csharp
public static System.Threading.Tasks.Task<bool> TryAddAsync(this Microsoft.Extensions.Caching.Distributed.IDistributedCache cache, string key, System.TimeSpan timeToLive, System.Threading.CancellationToken cancellationToken=default(System.Threading.CancellationToken));
```
#### Parameters

###### `cache` [Microsoft\.Extensions\.Caching\.Distributed\.IDistributedCache](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache 'Microsoft\.Extensions\.Caching\.Distributed\.IDistributedCache') {#Abblix.Utils.DistributedCacheExtensions.TryAddAsync(thisMicrosoft.Extensions.Caching.Distributed.IDistributedCache,string,System.TimeSpan,System.Threading.CancellationToken).cache}

The distributed cache instance\.

###### `key` [System\.String](https://learn.microsoft.com/en-us/dotnet/api/system.string 'System\.String') {#Abblix.Utils.DistributedCacheExtensions.TryAddAsync(thisMicrosoft.Extensions.Caching.Distributed.IDistributedCache,string,System.TimeSpan,System.Threading.CancellationToken).key}

The key whose first sighting is being recorded\.

###### `timeToLive` [System\.TimeSpan](https://learn.microsoft.com/en-us/dotnet/api/system.timespan 'System\.TimeSpan') {#Abblix.Utils.DistributedCacheExtensions.TryAddAsync(thisMicrosoft.Extensions.Caching.Distributed.IDistributedCache,string,System.TimeSpan,System.Threading.CancellationToken).timeToLive}

How long the sighting is remembered\.

###### `cancellationToken` [System\.Threading\.CancellationToken](https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtoken 'System\.Threading\.CancellationToken') {#Abblix.Utils.DistributedCacheExtensions.TryAddAsync(thisMicrosoft.Extensions.Caching.Distributed.IDistributedCache,string,System.TimeSpan,System.Threading.CancellationToken).cancellationToken}

Optional cancellation token to cancel the operation\.

#### Returns
[System\.Threading\.Tasks\.Task&lt;](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1 'System\.Threading\.Tasks\.Task\`1')[System\.Boolean](https://learn.microsoft.com/en-us/dotnet/api/system.boolean 'System\.Boolean')[&gt;](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1 'System\.Threading\.Tasks\.Task\`1')  
A task containing true when the key was absent and is now marked; false when it was
already present\.

### Remarks

<strong>Not atomic:</strong>[Microsoft\.Extensions\.Caching\.Distributed\.IDistributedCache](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache 'Microsoft\.Extensions\.Caching\.Distributed\.IDistributedCache') exposes only Get + Set, no
            compare-and-set primitive, so two concurrent callers of the same key can both observe a miss
            before either writes and both hear "new". The race window is bounded by the cache round-trip,
            which makes the duplicate-detection guarantee probabilistic rather than strict. Callers whose
            domain needs strict exactly-once use a backend-aware primitive instead - for replay prevention
            that is a `ReplayCacheBase` over the store's own conditional write (Redis
            `SET ... NX PX`, SQL `INSERT ... ON CONFLICT DO NOTHING`).

The entry stores an opaque marker; only the key's presence carries meaning. The requested
time-to-live is floored to a small positive minimum so a value the cache would reject or
discard immediately still records the sighting.

## DistributedCacheExtensions\.TryGetAndRemoveAsync\(this IDistributedCache, string, Nullable\<TimeSpan\>, CancellationToken\) Method {#Abblix.Utils.DistributedCacheExtensions.TryGetAndRemoveAsync(thisMicrosoft.Extensions.Caching.Distributed.IDistributedCache,string,System.Nullable_System.TimeSpan_,System.Threading.CancellationToken)}

Reads a value and removes it, both under one hold of the per\-key gate, for a cache with no native
primitive of its own\. This is NOT the equivalent of Redis GETDEL\. The gate holds out other
REDEMPTIONS of the key and nothing else, so a plain write landing between the read and the removal
is destroyed by this caller while it is handed the earlier bytes \- and that is only one of the ways
the two differ\. AT MOST one caller is handed the value when nothing writes the key, and none may be\.
Read the remarks before relying on it\.

```csharp
public static System.Threading.Tasks.Task<byte[]?> TryGetAndRemoveAsync(this Microsoft.Extensions.Caching.Distributed.IDistributedCache cache, string key, System.Nullable<System.TimeSpan> lockTimeout=null, System.Threading.CancellationToken cancellationToken=default(System.Threading.CancellationToken));
```
#### Parameters

###### `cache` [Microsoft\.Extensions\.Caching\.Distributed\.IDistributedCache](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache 'Microsoft\.Extensions\.Caching\.Distributed\.IDistributedCache') {#Abblix.Utils.DistributedCacheExtensions.TryGetAndRemoveAsync(thisMicrosoft.Extensions.Caching.Distributed.IDistributedCache,string,System.Nullable_System.TimeSpan_,System.Threading.CancellationToken).cache}

The distributed cache instance\.

###### `key` [System\.String](https://learn.microsoft.com/en-us/dotnet/api/system.string 'System\.String') {#Abblix.Utils.DistributedCacheExtensions.TryGetAndRemoveAsync(thisMicrosoft.Extensions.Caching.Distributed.IDistributedCache,string,System.Nullable_System.TimeSpan_,System.Threading.CancellationToken).key}

The key of the value to retrieve and remove\.

###### `lockTimeout` [System\.Nullable&lt;](https://learn.microsoft.com/en-us/dotnet/api/system.nullable-1 'System\.Nullable\`1')[System\.TimeSpan](https://learn.microsoft.com/en-us/dotnet/api/system.timespan 'System\.TimeSpan')[&gt;](https://learn.microsoft.com/en-us/dotnet/api/system.nullable-1 'System\.Nullable\`1') {#Abblix.Utils.DistributedCacheExtensions.TryGetAndRemoveAsync(thisMicrosoft.Extensions.Caching.Distributed.IDistributedCache,string,System.Nullable_System.TimeSpan_,System.Threading.CancellationToken).lockTimeout}

Duration after which the lock expires, 5 seconds if null\.

###### `cancellationToken` [System\.Threading\.CancellationToken](https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtoken 'System\.Threading\.CancellationToken') {#Abblix.Utils.DistributedCacheExtensions.TryGetAndRemoveAsync(thisMicrosoft.Extensions.Caching.Distributed.IDistributedCache,string,System.Nullable_System.TimeSpan_,System.Threading.CancellationToken).cancellationToken}

Optional cancellation token to cancel the operation\.

#### Returns
[System\.Threading\.Tasks\.Task&lt;](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1 'System\.Threading\.Tasks\.Task\`1')[System\.Byte](https://learn.microsoft.com/en-us/dotnet/api/system.byte 'System\.Byte')[\[\]](https://learn.microsoft.com/en-us/dotnet/api/system.array 'System\.Array')[&gt;](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1 'System\.Threading\.Tasks\.Task\`1')  
A task that completes when the operation finishes, containing the value read under this caller's
own hold of the gate, when its claim was still in the store afterwards\. Null otherwise, which does
NOT mean somebody else took it: see [TryRemoveAsync\(this IDistributedCache, string, Nullable&lt;TimeSpan&gt;, CancellationToken\)](https://www.abblix.com/en/docs/api/abblix-utils/Abblix.Utils.DistributedCacheExtensions#Abblix.Utils.DistributedCacheExtensions.TryRemoveAsync(thisMicrosoft.Extensions.Caching.Distributed.IDistributedCache,string,System.Nullable_System.TimeSpan_,System.Threading.CancellationToken) 'Abblix\.Utils\.DistributedCacheExtensions\.TryRemoveAsync\(this Microsoft\.Extensions\.Caching\.Distributed\.IDistributedCache, string, System\.Nullable\<System\.TimeSpan\>, System\.Threading\.CancellationToken\)'), whose remarks carry the
condition; this method adds nothing to it beyond returning the value\. What the placement of the
read does and does not settle is in the remarks\.

### Remarks

<strong>Atomicity Protocol:</strong>
1. Step 1:
2. Step 2:
3. Step 3:

<strong>How it provides atomicity:</strong> the removal reports itself under the same condition
            [TryRemoveAsync\(this IDistributedCache, string, Nullable&lt;TimeSpan&gt;, CancellationToken\)](https://www.abblix.com/en/docs/api/abblix-utils/Abblix.Utils.DistributedCacheExtensions#Abblix.Utils.DistributedCacheExtensions.TryRemoveAsync(thisMicrosoft.Extensions.Caching.Distributed.IDistributedCache,string,System.Nullable_System.TimeSpan_,System.Threading.CancellationToken) 'Abblix\.Utils\.DistributedCacheExtensions\.TryRemoveAsync\(this Microsoft\.Extensions\.Caching\.Distributed\.IDistributedCache, string, System\.Nullable\<System\.TimeSpan\>, System\.Threading\.CancellationToken\)') states, and the read is inside the same hold of the gate rather than
            in front of it.

<strong>What the read's placement buys, and what it does not.</strong> The bytes handed back are
            the bytes at the key when this caller got IN - not the ones there before it waited, and that wait
            is as long as another caller's whole redemption. What is still open is narrower and real: a writer
            that takes no gate can land between the read and the removal, so this caller destroys that write
            and is handed the earlier bytes. Nothing in this class closes THAT, and the SERIALIZATION survives
            no second process at all - the lock protocol still admits at most one winner across nodes, but
            nothing there holds the read and the removal together. A store whose own primitive returns the
            removed value closes both, and issue 435 tracks it.

<strong>Lock timeout:</strong> the claim auto-expires after the specified timeout (5 seconds by
            default), so a process that crashes mid-protocol does not leave the key claimed forever. It also
            means a caller slower than the timeout loses its own claim: see [TryRemoveAsync\(this IDistributedCache, string, Nullable&lt;TimeSpan&gt;, CancellationToken\)](https://www.abblix.com/en/docs/api/abblix-utils/Abblix.Utils.DistributedCacheExtensions#Abblix.Utils.DistributedCacheExtensions.TryRemoveAsync(thisMicrosoft.Extensions.Caching.Distributed.IDistributedCache,string,System.Nullable_System.TimeSpan_,System.Threading.CancellationToken) 'Abblix\.Utils\.DistributedCacheExtensions\.TryRemoveAsync\(this Microsoft\.Extensions\.Caching\.Distributed\.IDistributedCache, string, System\.Nullable\<System\.TimeSpan\>, System\.Threading\.CancellationToken\)').

<strong>Performance:</strong> up to six cache operations - one read, and up to five in the removal
            protocol - so it has higher latency than a store's own atomic primitive, and works with any
            IDistributedCache in exchange. All of them inside the gate, so a contended key serializes for the
            whole of that rather than for the removal alone.

## DistributedCacheExtensions\.TryRemoveAsync\(this IDistributedCache, string, Nullable\<TimeSpan\>, CancellationToken\) Method {#Abblix.Utils.DistributedCacheExtensions.TryRemoveAsync(thisMicrosoft.Extensions.Caching.Distributed.IDistributedCache,string,System.Nullable_System.TimeSpan_,System.Threading.CancellationToken)}

Atomically attempts to remove a value from the distributed cache\.
Uses a lock\-based protocol to ensure atomic removal semantics, preventing race conditions
where multiple threads attempt to remove the same key concurrently\.

```csharp
public static System.Threading.Tasks.Task<bool> TryRemoveAsync(this Microsoft.Extensions.Caching.Distributed.IDistributedCache cache, string key, System.Nullable<System.TimeSpan> lockTimeout=null, System.Threading.CancellationToken cancellationToken=default(System.Threading.CancellationToken));
```
#### Parameters

###### `cache` [Microsoft\.Extensions\.Caching\.Distributed\.IDistributedCache](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.caching.distributed.idistributedcache 'Microsoft\.Extensions\.Caching\.Distributed\.IDistributedCache') {#Abblix.Utils.DistributedCacheExtensions.TryRemoveAsync(thisMicrosoft.Extensions.Caching.Distributed.IDistributedCache,string,System.Nullable_System.TimeSpan_,System.Threading.CancellationToken).cache}

The distributed cache instance\.

###### `key` [System\.String](https://learn.microsoft.com/en-us/dotnet/api/system.string 'System\.String') {#Abblix.Utils.DistributedCacheExtensions.TryRemoveAsync(thisMicrosoft.Extensions.Caching.Distributed.IDistributedCache,string,System.Nullable_System.TimeSpan_,System.Threading.CancellationToken).key}

The key of the value to remove\.

###### `lockTimeout` [System\.Nullable&lt;](https://learn.microsoft.com/en-us/dotnet/api/system.nullable-1 'System\.Nullable\`1')[System\.TimeSpan](https://learn.microsoft.com/en-us/dotnet/api/system.timespan 'System\.TimeSpan')[&gt;](https://learn.microsoft.com/en-us/dotnet/api/system.nullable-1 'System\.Nullable\`1') {#Abblix.Utils.DistributedCacheExtensions.TryRemoveAsync(thisMicrosoft.Extensions.Caching.Distributed.IDistributedCache,string,System.Nullable_System.TimeSpan_,System.Threading.CancellationToken).lockTimeout}

Duration after which the lock expires, 5 seconds if null\.

###### `cancellationToken` [System\.Threading\.CancellationToken](https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtoken 'System\.Threading\.CancellationToken') {#Abblix.Utils.DistributedCacheExtensions.TryRemoveAsync(thisMicrosoft.Extensions.Caching.Distributed.IDistributedCache,string,System.Nullable_System.TimeSpan_,System.Threading.CancellationToken).cancellationToken}

Optional cancellation token to cancel the operation\.

#### Returns
[System\.Threading\.Tasks\.Task&lt;](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1 'System\.Threading\.Tasks\.Task\`1')[System\.Boolean](https://learn.microsoft.com/en-us/dotnet/api/system.boolean 'System\.Boolean')[&gt;](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1 'System\.Threading\.Tasks\.Task\`1')  
A task that completes when the operation finishes, containing true when the value was removed by
this caller AND its own lock token was still in the store afterwards\. False otherwise \- which covers
the key not being there, another caller having taken it, and the case where the value is gone and
nobody can be told they took it\. That last one does not need a second node: see the remarks\. A
store fault after the removal raises rather than returning, and loses the value the same way\.

### Remarks

<strong>Atomicity Protocol:</strong>
1. Step 1:
2. Step 2:
3. Step 3:
4. Step 4:
5. Step 5:

<strong>How it provides atomicity:</strong> a caller reports the removal as its own only when its
            own claim is still in the store at the end of the protocol. What that condition does and does not
            buy is spelled out below.

<strong>Use Case:</strong> This method is useful when you need to atomically remove a value without
            retrieving it (unlike [TryGetAndRemoveAsync\(this IDistributedCache, string, Nullable&lt;TimeSpan&gt;, CancellationToken\)](https://www.abblix.com/en/docs/api/abblix-utils/Abblix.Utils.DistributedCacheExtensions#Abblix.Utils.DistributedCacheExtensions.TryGetAndRemoveAsync(thisMicrosoft.Extensions.Caching.Distributed.IDistributedCache,string,System.Nullable_System.TimeSpan_,System.Threading.CancellationToken) 'Abblix\.Utils\.DistributedCacheExtensions\.TryGetAndRemoveAsync\(this Microsoft\.Extensions\.Caching\.Distributed\.IDistributedCache, string, System\.Nullable\<System\.TimeSpan\>, System\.Threading\.CancellationToken\)')). For example, in the Device Authorization
            Grant flow when a user denies authorization, you only need confirmation that the request was removed,
            not the request data itself.

<strong>What the protocol decides:</strong> a caller is told it took the value only when the
            protocol runs to the end AND finds its own lock token still in the store.

That is the whole contract, and it is deliberately not followed by a count of what can go wrong.
Such a list is not closable - the token can be overwritten, it can expire while a cache call
stalls, and the store calls after the removal can fail, and there is no argument that those are
all. What the tests carry instead, each dying when its fact stops holding:
`TryRemoveAsync_TheLockExpiresMidProtocol_OneCallerAloneLosesTheValue` for a removal with
nobody told, on one node with no competitor, and
`TryRemoveAsync_TheStoreFaultsAfterTheRemoval_TheValueIsGoneAndNobodyIsTold` for the same
outcome reached by a fault, where the caller gets an exception rather than an answer at all.

<strong>What the check does NOT give you.</strong> It does not make this a take-once: the gate
            serializes callers within one process, so a competitor cannot overwrite another's token HERE, and
            nothing about that survives a second node - see below.

<strong>Across processes even the overwrite reopens.</strong> Two nodes redeeming the same key at
            the same moment can end with the value removed and neither told it took it, because the lock
            protocol is
            assembled from `Get`, `Set` and `Remove` as three separate operations and there is a
            window between any two of them. A take-once needs one indivisible read-modify-write, and this
            interface exposes none: no compare-and-swap, no set-if-absent, no delete-returning-value.

<strong>A deployment on several nodes that cannot afford that</strong> supplies its own storage and
            reaches for the primitive its store already has. No new API is needed for that:
            `IDeviceAuthorizationStorage`, `IBackChannelRequestStorage` and `IEntityStorage` are
            public and registered with `TryAddSingleton`, so a host's own registration wins. Which
            primitive to reach for depends on the store, which is why it cannot be chosen here:
- Redis 6.2 and later: `GETDEL key`, one command, which returns the value to exactly one
                caller and deletes it. Earlier versions get the same effect from a two-line `EVAL` script,
                since Redis runs a script indivisibly. Both are for a storage of your own: they read a STRING,
                and the `IDistributedCache` implementation for Redis keeps each entry as a hash whose value
                sits in a field, so pointed at these keys they answer `WRONGTYPE`.
- PostgreSQL: `DELETE FROM ... WHERE key = $1 RETURNING value`. The row lock picks the
                winner, and at the default isolation level the loser returns no rows; under REPEATABLE READ or
                SERIALIZABLE it fails to serialize instead, which is the same answer through an exception.
- SQL Server: `DELETE ... OUTPUT deleted.value`, the same shape.
- Oracle: `DELETE ... RETURNING value INTO :out`. Oracle documents the RETURNING INTO
                clause as belonging to DELETE among others, and for a DELETE it yields the pre-deletion value.

<strong>Lock timeout:</strong> Locks auto-expire after the specified timeout (default 5 seconds)
            to prevent orphaned locks if a process crashes between writing the lock token and cleaning it up.
