diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md
index 758e0a30f..d912f0842 100644
--- a/docs/ReleaseNotes.md
+++ b/docs/ReleaseNotes.md
@@ -6,9 +6,10 @@ Current package versions:
| ------------ | ----------------- | ----- |
| [](https://www.nuget.org/packages/StackExchange.Redis/) | [](https://www.nuget.org/packages/StackExchange.Redis/) | [](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) |
-## Unreleased
+## Unreleased (2.9.xxx)
-- nothing yet
+- Add `HGETDEL`, `HGETEX` and `HSETEX` support ([#2863 by atakavci](https://github.com/StackExchange/StackExchange.Redis/pull/2863))
+- Fix key-prefix omission in `SetIntersectionLength` and `SortedSet{Combine[WithScores]|IntersectionLength}` ([#2863 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2863))
## 2.8.58
diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs
index 34e1eb296..52f0b134d 100644
--- a/src/StackExchange.Redis/Enums/RedisCommand.cs
+++ b/src/StackExchange.Redis/Enums/RedisCommand.cs
@@ -70,6 +70,8 @@ internal enum RedisCommand
HEXPIREAT,
HEXPIRETIME,
HGET,
+ HGETEX,
+ HGETDEL,
HGETALL,
HINCRBY,
HINCRBYFLOAT,
@@ -85,6 +87,7 @@ internal enum RedisCommand
HRANDFIELD,
HSCAN,
HSET,
+ HSETEX,
HSETNX,
HSTRLEN,
HVALS,
@@ -294,6 +297,8 @@ internal static bool IsPrimaryOnly(this RedisCommand command)
case RedisCommand.HDEL:
case RedisCommand.HEXPIRE:
case RedisCommand.HEXPIREAT:
+ case RedisCommand.HGETDEL:
+ case RedisCommand.HGETEX:
case RedisCommand.HINCRBY:
case RedisCommand.HINCRBYFLOAT:
case RedisCommand.HMSET:
@@ -301,6 +306,7 @@ internal static bool IsPrimaryOnly(this RedisCommand command)
case RedisCommand.HPEXPIRE:
case RedisCommand.HPEXPIREAT:
case RedisCommand.HSET:
+ case RedisCommand.HSETEX:
case RedisCommand.HSETNX:
case RedisCommand.INCR:
case RedisCommand.INCRBY:
diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs
index c37d3ddb0..37fb6e32b 100644
--- a/src/StackExchange.Redis/Interfaces/IDatabase.cs
+++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs
@@ -516,6 +516,149 @@ public interface IDatabase : IRedis, IDatabaseAsync
///
RedisValue[] HashGet(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None);
+ ///
+ /// Returns the value associated with field in the hash stored at key.
+ ///
+ /// The key of the hash.
+ /// The field in the hash to get.
+ /// The flags to use for this operation.
+ /// The value associated with field, or when field is not present in the hash or key does not exist.
+ ///
+ RedisValue HashFieldGetAndDelete(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Returns the value associated with field in the hash stored at key.
+ ///
+ /// The key of the hash.
+ /// The field in the hash to get.
+ /// The flags to use for this operation.
+ /// The value associated with field, or when field is not present in the hash or key does not exist.
+ ///
+ Lease? HashFieldGetLeaseAndDelete(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Returns the values associated with the specified fields in the hash stored at key.
+ /// For every field that does not exist in the hash, a value is returned.
+ /// Because non-existing keys are treated as empty hashes, running HMGET against a non-existing key will return a list of values.
+ ///
+ /// The key of the hash.
+ /// The fields in the hash to get.
+ /// The flags to use for this operation.
+ /// List of values associated with the given fields, in the same order as they are requested.
+ ///
+ RedisValue[] HashFieldGetAndDelete(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Gets the value of the specified hash field and sets its expiration time.
+ ///
+ /// The key of the hash.
+ /// The field in the hash to get and set the expiration for.
+ /// The expiration time to set.
+ /// If true, the expiration will be removed. And 'expiry' parameter is ignored.
+ /// The flags to use for this operation.
+ /// The value of the specified hash field.
+ RedisValue HashFieldGetAndSetExpiry(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Gets the value of the specified hash field and sets its expiration time.
+ ///
+ /// The key of the hash.
+ /// The field in the hash to get and set the expiration for.
+ /// The exact date and time to set the expiration to.
+ /// The flags to use for this operation.
+ /// The value of the specified hash field.
+ RedisValue HashFieldGetAndSetExpiry(RedisKey key, RedisValue hashField, DateTime expiry, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Gets the value of the specified hash field and sets its expiration time, returning a lease.
+ ///
+ /// The key of the hash.
+ /// The field in the hash to get and set the expiration for.
+ /// The expiration time to set.
+ /// If true, the expiration will be removed. And 'expiry' parameter is ignored.
+ /// The flags to use for this operation.
+ /// The value of the specified hash field as a lease.
+ Lease? HashFieldGetLeaseAndSetExpiry(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Gets the value of the specified hash field and sets its expiration time, returning a lease.
+ ///
+ /// The key of the hash.
+ /// The field in the hash to get and set the expiration for.
+ /// The exact date and time to set the expiration to.
+ /// The flags to use for this operation.
+ /// The value of the specified hash field as a lease.
+ Lease? HashFieldGetLeaseAndSetExpiry(RedisKey key, RedisValue hashField, DateTime expiry, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Gets the values of the specified hash fields and sets their expiration times.
+ ///
+ /// The key of the hash.
+ /// The fields in the hash to get and set the expiration for.
+ /// The expiration time to set.
+ /// If true, the expiration will be removed. And 'expiry' parameter is ignored.
+ /// The flags to use for this operation.
+ /// The values of the specified hash fields.
+ RedisValue[] HashFieldGetAndSetExpiry(RedisKey key, RedisValue[] hashFields, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Gets the values of the specified hash fields and sets their expiration times.
+ ///
+ /// The key of the hash.
+ /// The fields in the hash to get and set the expiration for.
+ /// The exact date and time to set the expiration to.
+ /// The flags to use for this operation.
+ /// The values of the specified hash fields.
+ RedisValue[] HashFieldGetAndSetExpiry(RedisKey key, RedisValue[] hashFields, DateTime expiry, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Sets the value of the specified hash field and sets its expiration time.
+ ///
+ /// The key of the hash.
+ /// The field in the hash to set and set the expiration for.
+ /// The value in the hash to set and set the expiration for.
+ /// The expiration time to set.
+ /// Whether to maintain the existing field's TTL (KEEPTTL flag).
+ /// Which conditions to set the value under (defaults to always).
+ /// The flags to use for this operation.
+ /// 0 if no fields were set, 1 if all the fields were set.
+ RedisValue HashFieldSetAndSetExpiry(RedisKey key, RedisValue field, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Sets the value of the specified hash field and sets its expiration time.
+ ///
+ /// The key of the hash.
+ /// The field in the hash to set and set the expiration for.
+ /// The value in the hash to set and set the expiration for.
+ /// The exact date and time to set the expiration to.
+ /// Which conditions to set the value under (defaults to always).
+ /// The flags to use for this operation.
+ /// 0 if no fields were set, 1 if all the fields were set.
+ RedisValue HashFieldSetAndSetExpiry(RedisKey key, RedisValue field, RedisValue value, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Sets the values of the specified hash fields and sets their expiration times.
+ ///
+ /// The key of the hash.
+ /// The fields in the hash to set and set the expiration for.
+ /// The expiration time to set.
+ /// Whether to maintain the existing fields' TTL (KEEPTTL flag).
+ /// Which conditions to set the values under (defaults to always).
+ /// The flags to use for this operation.
+ /// 0 if no fields were set, 1 if all the fields were set.
+ RedisValue HashFieldSetAndSetExpiry(RedisKey key, HashEntry[] hashFields, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Sets the values of the specified hash fields and sets their expiration times.
+ ///
+ /// The key of the hash.
+ /// The fields in the hash to set and set the expiration for.
+ /// The exact date and time to set the expiration to.
+ /// Which conditions to set the values under (defaults to always).
+ /// The flags to use for this operation.
+ /// 0 if no fields were set, 1 if all the fields were set.
+ RedisValue HashFieldSetAndSetExpiry(RedisKey key, HashEntry[] hashFields, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None);
+
///
/// Returns all fields and values of the hash stored at key.
///
diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs
index 4873c1069..b88570790 100644
--- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs
+++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs
@@ -84,6 +84,45 @@ public interface IDatabaseAsync : IRedisAsync
///
Task HashExistsAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None);
+ ///
+ Task HashFieldGetAndDeleteAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None);
+
+ ///
+ Task?> HashFieldGetLeaseAndDeleteAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None);
+
+ ///
+ Task HashFieldGetAndDeleteAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None);
+
+ ///
+ Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None);
+
+ ///
+ Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue hashField, DateTime expiry, CommandFlags flags = CommandFlags.None);
+
+ ///
+ Task?> HashFieldGetLeaseAndSetExpiryAsync(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None);
+
+ ///
+ Task?> HashFieldGetLeaseAndSetExpiryAsync(RedisKey key, RedisValue hashField, DateTime expiry, CommandFlags flags = CommandFlags.None);
+
+ ///
+ Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue[] hashFields, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None);
+
+ ///
+ Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue[] hashFields, DateTime expiry, CommandFlags flags = CommandFlags.None);
+
+ ///
+ Task HashFieldSetAndSetExpiryAsync(RedisKey key, RedisValue field, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None);
+
+ ///
+ Task HashFieldSetAndSetExpiryAsync(RedisKey key, RedisValue field, RedisValue value, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None);
+
+ ///
+ Task HashFieldSetAndSetExpiryAsync(RedisKey key, HashEntry[] hashFields, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None);
+
+ ///
+ Task HashFieldSetAndSetExpiryAsync(RedisKey key, HashEntry[] hashFields, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None);
+
///
Task HashFieldExpireAsync(RedisKey key, RedisValue[] hashFields, TimeSpan expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None);
diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs
index 331d23ea7..06c5359eb 100644
--- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs
+++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs
@@ -84,6 +84,45 @@ public Task HashDeleteAsync(RedisKey key, RedisValue hashField, CommandFla
public Task HashExistsAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) =>
Inner.HashExistsAsync(ToInner(key), hashField, flags);
+ public Task HashFieldGetAndDeleteAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) =>
+ Inner.HashFieldGetAndDeleteAsync(ToInner(key), hashField, flags);
+
+ public Task?> HashFieldGetLeaseAndDeleteAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) =>
+ Inner.HashFieldGetLeaseAndDeleteAsync(ToInner(key), hashField, flags);
+
+ public Task HashFieldGetAndDeleteAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) =>
+ Inner.HashFieldGetAndDeleteAsync(ToInner(key), hashFields, flags);
+
+ public Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) =>
+ Inner.HashFieldGetAndSetExpiryAsync(ToInner(key), hashField, expiry, persist, flags);
+
+ public Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue hashField, DateTime expiry, CommandFlags flags = CommandFlags.None) =>
+ Inner.HashFieldGetAndSetExpiryAsync(ToInner(key), hashField, expiry, flags);
+
+ public Task?> HashFieldGetLeaseAndSetExpiryAsync(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) =>
+ Inner.HashFieldGetLeaseAndSetExpiryAsync(ToInner(key), hashField, expiry, persist, flags);
+
+ public Task?> HashFieldGetLeaseAndSetExpiryAsync(RedisKey key, RedisValue hashField, DateTime expiry, CommandFlags flags = CommandFlags.None) =>
+ Inner.HashFieldGetLeaseAndSetExpiryAsync(ToInner(key), hashField, expiry, flags);
+
+ public Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue[] hashFields, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) =>
+ Inner.HashFieldGetAndSetExpiryAsync(ToInner(key), hashFields, expiry, persist, flags);
+
+ public Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue[] hashFields, DateTime expiry, CommandFlags flags = CommandFlags.None) =>
+ Inner.HashFieldGetAndSetExpiryAsync(ToInner(key), hashFields, expiry, flags);
+
+ public Task HashFieldSetAndSetExpiryAsync(RedisKey key, RedisValue field, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) =>
+ Inner.HashFieldSetAndSetExpiryAsync(ToInner(key), field, value, expiry, keepTtl, when, flags);
+
+ public Task HashFieldSetAndSetExpiryAsync(RedisKey key, RedisValue field, RedisValue value, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None) =>
+ Inner.HashFieldSetAndSetExpiryAsync(ToInner(key), field, value, expiry, when, flags);
+
+ public Task HashFieldSetAndSetExpiryAsync(RedisKey key, HashEntry[] hashFields, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) =>
+ Inner.HashFieldSetAndSetExpiryAsync(ToInner(key), hashFields, expiry, keepTtl, when, flags);
+
+ public Task HashFieldSetAndSetExpiryAsync(RedisKey key, HashEntry[] hashFields, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None) =>
+ Inner.HashFieldSetAndSetExpiryAsync(ToInner(key), hashFields, expiry, when, flags);
+
public Task HashFieldExpireAsync(RedisKey key, RedisValue[] hashFields, TimeSpan expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) =>
Inner.HashFieldExpireAsync(ToInner(key), hashFields, expiry, when, flags);
@@ -394,7 +433,7 @@ public Task SetContainsAsync(RedisKey key, RedisValue[] values, CommandF
Inner.SetContainsAsync(ToInner(key), values, flags);
public Task SetIntersectionLengthAsync(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) =>
- Inner.SetIntersectionLengthAsync(keys, limit, flags);
+ Inner.SetIntersectionLengthAsync(ToInner(keys), limit, flags);
public Task SetLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) =>
Inner.SetLengthAsync(ToInner(key), flags);
@@ -450,10 +489,10 @@ public Task SortedSetAddAsync(RedisKey key, RedisValue member, double scor
public Task SortedSetAddAsync(RedisKey key, RedisValue member, double score, SortedSetWhen updateWhen = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) =>
Inner.SortedSetAddAsync(ToInner(key), member, score, updateWhen, flags);
public Task SortedSetCombineAsync(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) =>
- Inner.SortedSetCombineAsync(operation, keys, weights, aggregate, flags);
+ Inner.SortedSetCombineAsync(operation, ToInner(keys), weights, aggregate, flags);
public Task SortedSetCombineWithScoresAsync(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) =>
- Inner.SortedSetCombineWithScoresAsync(operation, keys, weights, aggregate, flags);
+ Inner.SortedSetCombineWithScoresAsync(operation, ToInner(keys), weights, aggregate, flags);
public Task SortedSetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) =>
Inner.SortedSetCombineAndStoreAsync(operation, ToInner(destination), ToInner(keys), weights, aggregate, flags);
@@ -468,7 +507,7 @@ public Task SortedSetIncrementAsync(RedisKey key, RedisValue member, dou
Inner.SortedSetIncrementAsync(ToInner(key), member, value, flags);
public Task SortedSetIntersectionLengthAsync(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) =>
- Inner.SortedSetIntersectionLengthAsync(keys, limit, flags);
+ Inner.SortedSetIntersectionLengthAsync(ToInner(keys), limit, flags);
public Task SortedSetLengthAsync(RedisKey key, double min = -1.0 / 0.0, double max = 1.0 / 0.0, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) =>
Inner.SortedSetLengthAsync(ToInner(key), min, max, exclude, flags);
diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs
index 18406ba9f..48df1e4db 100644
--- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs
+++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs
@@ -81,6 +81,45 @@ public bool HashDelete(RedisKey key, RedisValue hashField, CommandFlags flags =
public bool HashExists(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) =>
Inner.HashExists(ToInner(key), hashField, flags);
+ public RedisValue HashFieldGetAndDelete(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) =>
+ Inner.HashFieldGetAndDelete(ToInner(key), hashField, flags);
+
+ public Lease? HashFieldGetLeaseAndDelete(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) =>
+ Inner.HashFieldGetLeaseAndDelete(ToInner(key), hashField, flags);
+
+ public RedisValue[] HashFieldGetAndDelete(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) =>
+ Inner.HashFieldGetAndDelete(ToInner(key), hashFields, flags);
+
+ public RedisValue HashFieldGetAndSetExpiry(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) =>
+ Inner.HashFieldGetAndSetExpiry(ToInner(key), hashField, expiry, persist, flags);
+
+ public RedisValue HashFieldGetAndSetExpiry(RedisKey key, RedisValue hashField, DateTime expiry, CommandFlags flags = CommandFlags.None) =>
+ Inner.HashFieldGetAndSetExpiry(ToInner(key), hashField, expiry, flags);
+
+ public Lease? HashFieldGetLeaseAndSetExpiry(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) =>
+ Inner.HashFieldGetLeaseAndSetExpiry(ToInner(key), hashField, expiry, persist, flags);
+
+ public Lease? HashFieldGetLeaseAndSetExpiry(RedisKey key, RedisValue hashField, DateTime expiry, CommandFlags flags = CommandFlags.None) =>
+ Inner.HashFieldGetLeaseAndSetExpiry(ToInner(key), hashField, expiry, flags);
+
+ public RedisValue[] HashFieldGetAndSetExpiry(RedisKey key, RedisValue[] hashFields, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) =>
+ Inner.HashFieldGetAndSetExpiry(ToInner(key), hashFields, expiry, persist, flags);
+
+ public RedisValue[] HashFieldGetAndSetExpiry(RedisKey key, RedisValue[] hashFields, DateTime expiry, CommandFlags flags = CommandFlags.None) =>
+ Inner.HashFieldGetAndSetExpiry(ToInner(key), hashFields, expiry, flags);
+
+ public RedisValue HashFieldSetAndSetExpiry(RedisKey key, RedisValue field, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) =>
+ Inner.HashFieldSetAndSetExpiry(ToInner(key), field, value, expiry, keepTtl, when, flags);
+
+ public RedisValue HashFieldSetAndSetExpiry(RedisKey key, RedisValue field, RedisValue value, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None) =>
+ Inner.HashFieldSetAndSetExpiry(ToInner(key), field, value, expiry, when, flags);
+
+ public RedisValue HashFieldSetAndSetExpiry(RedisKey key, HashEntry[] hashFields, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) =>
+ Inner.HashFieldSetAndSetExpiry(ToInner(key), hashFields, expiry, keepTtl, when, flags);
+
+ public RedisValue HashFieldSetAndSetExpiry(RedisKey key, HashEntry[] hashFields, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None) =>
+ Inner.HashFieldSetAndSetExpiry(ToInner(key), hashFields, expiry, when, flags);
+
public ExpireResult[] HashFieldExpire(RedisKey key, RedisValue[] hashFields, TimeSpan expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) =>
Inner.HashFieldExpire(ToInner(key), hashFields, expiry, when, flags);
@@ -381,7 +420,7 @@ public bool[] SetContains(RedisKey key, RedisValue[] values, CommandFlags flags
Inner.SetContains(ToInner(key), values, flags);
public long SetIntersectionLength(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) =>
- Inner.SetIntersectionLength(keys, limit, flags);
+ Inner.SetIntersectionLength(ToInner(keys), limit, flags);
public long SetLength(RedisKey key, CommandFlags flags = CommandFlags.None) =>
Inner.SetLength(ToInner(key), flags);
@@ -435,10 +474,10 @@ public bool SortedSetAdd(RedisKey key, RedisValue member, double score, SortedSe
Inner.SortedSetAdd(ToInner(key), member, score, when, flags);
public RedisValue[] SortedSetCombine(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) =>
- Inner.SortedSetCombine(operation, keys, weights, aggregate, flags);
+ Inner.SortedSetCombine(operation, ToInner(keys), weights, aggregate, flags);
public SortedSetEntry[] SortedSetCombineWithScores(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) =>
- Inner.SortedSetCombineWithScores(operation, keys, weights, aggregate, flags);
+ Inner.SortedSetCombineWithScores(operation, ToInner(keys), weights, aggregate, flags);
public long SortedSetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) =>
Inner.SortedSetCombineAndStore(operation, ToInner(destination), ToInner(keys), weights, aggregate, flags);
@@ -453,7 +492,7 @@ public double SortedSetIncrement(RedisKey key, RedisValue member, double value,
Inner.SortedSetIncrement(ToInner(key), member, value, flags);
public long SortedSetIntersectionLength(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) =>
- Inner.SortedSetIntersectionLength(keys, limit, flags);
+ Inner.SortedSetIntersectionLength(ToInner(keys), limit, flags);
public long SortedSetLength(RedisKey key, double min = -1.0 / 0.0, double max = 1.0 / 0.0, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) =>
Inner.SortedSetLength(ToInner(key), min, max, exclude, flags);
diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs
index 9472b6db0..cd3d29947 100644
--- a/src/StackExchange.Redis/Message.cs
+++ b/src/StackExchange.Redis/Message.cs
@@ -310,6 +310,9 @@ public static Message Create(int db, CommandFlags flags, RedisCommand command, i
public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3, in RedisValue value4) =>
new CommandValueValueValueValueValueMessage(db, flags, command, value0, value1, value2, value3, value4);
+ public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value0, in RedisValue value1, in RedisValue[] values) =>
+ new CommandKeyValueValueValuesMessage(db, flags, command, key, value0, value1, values);
+
public static Message Create(
int db,
CommandFlags flags,
@@ -1180,6 +1183,36 @@ protected override void WriteImpl(PhysicalConnection physical)
public override int ArgCount => values.Length + 1;
}
+ private sealed class CommandKeyValueValueValuesMessage : CommandKeyBase
+ {
+ private readonly RedisValue value0;
+ private readonly RedisValue value1;
+ private readonly RedisValue[] values;
+ public CommandKeyValueValueValuesMessage(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value0, in RedisValue value1, RedisValue[] values) : base(db, flags, command, key)
+ {
+ for (int i = 0; i < values.Length; i++)
+ {
+ values[i].AssertNotNull();
+ }
+
+ value0.AssertNotNull();
+ value1.AssertNotNull();
+ this.value0 = value0;
+ this.value1 = value1;
+ this.values = values;
+ }
+
+ protected override void WriteImpl(PhysicalConnection physical)
+ {
+ physical.WriteHeader(Command, values.Length + 3);
+ physical.Write(Key);
+ physical.WriteBulkString(value0);
+ physical.WriteBulkString(value1);
+ for (int i = 0; i < values.Length; i++) physical.WriteBulkString(values[i]);
+ }
+ public override int ArgCount => values.Length + 3;
+ }
+
private sealed class CommandKeyValueValueMessage : CommandKeyBase
{
private readonly RedisValue value0, value1;
diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt
index 43b35ba58..4a77208aa 100644
--- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt
+++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt
@@ -1924,4 +1924,31 @@ StackExchange.Redis.StreamTrimMode.KeepReferences = 0 -> StackExchange.Redis.Str
StackExchange.Redis.StreamTrimResult
StackExchange.Redis.StreamTrimResult.Deleted = 1 -> StackExchange.Redis.StreamTrimResult
StackExchange.Redis.StreamTrimResult.NotDeleted = 2 -> StackExchange.Redis.StreamTrimResult
-StackExchange.Redis.StreamTrimResult.NotFound = -1 -> StackExchange.Redis.StreamTrimResult
\ No newline at end of file
+StackExchange.Redis.StreamTrimResult.NotFound = -1 -> StackExchange.Redis.StreamTrimResult
+StackExchange.Redis.IDatabase.HashFieldGetAndDelete(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue
+StackExchange.Redis.IDatabase.HashFieldGetAndDelete(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]!
+StackExchange.Redis.IDatabase.HashFieldGetAndSetExpiry(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, System.DateTime expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue
+StackExchange.Redis.IDatabase.HashFieldGetAndSetExpiry(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, System.TimeSpan? expiry = null, bool persist = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue
+StackExchange.Redis.IDatabase.HashFieldGetAndSetExpiry(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, System.DateTime expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]!
+StackExchange.Redis.IDatabase.HashFieldGetAndSetExpiry(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, System.TimeSpan? expiry = null, bool persist = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]!
+StackExchange.Redis.IDatabase.HashFieldGetLeaseAndDelete(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease?
+StackExchange.Redis.IDatabase.HashFieldGetLeaseAndSetExpiry(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, System.DateTime expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease?
+StackExchange.Redis.IDatabase.HashFieldGetLeaseAndSetExpiry(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, System.TimeSpan? expiry = null, bool persist = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease?
+StackExchange.Redis.IDatabase.HashFieldSetAndSetExpiry(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue field, StackExchange.Redis.RedisValue value, System.DateTime expiry, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue
+StackExchange.Redis.IDatabase.HashFieldSetAndSetExpiry(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue field, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue
+StackExchange.Redis.IDatabase.HashFieldSetAndSetExpiry(StackExchange.Redis.RedisKey key, StackExchange.Redis.HashEntry[]! hashFields, System.DateTime expiry, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue
+StackExchange.Redis.IDatabase.HashFieldSetAndSetExpiry(StackExchange.Redis.RedisKey key, StackExchange.Redis.HashEntry[]! hashFields, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue
+StackExchange.Redis.IDatabaseAsync.HashFieldGetAndDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+StackExchange.Redis.IDatabaseAsync.HashFieldGetAndDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+StackExchange.Redis.IDatabaseAsync.HashFieldGetAndSetExpiryAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, System.DateTime expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+StackExchange.Redis.IDatabaseAsync.HashFieldGetAndSetExpiryAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, System.TimeSpan? expiry = null, bool persist = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+StackExchange.Redis.IDatabaseAsync.HashFieldGetAndSetExpiryAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, System.DateTime expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+StackExchange.Redis.IDatabaseAsync.HashFieldGetAndSetExpiryAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, System.TimeSpan? expiry = null, bool persist = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+StackExchange.Redis.IDatabaseAsync.HashFieldGetLeaseAndDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>!
+StackExchange.Redis.IDatabaseAsync.HashFieldGetLeaseAndSetExpiryAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, System.DateTime expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>!
+StackExchange.Redis.IDatabaseAsync.HashFieldGetLeaseAndSetExpiryAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, System.TimeSpan? expiry = null, bool persist = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>!
+StackExchange.Redis.IDatabaseAsync.HashFieldSetAndSetExpiryAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue field, StackExchange.Redis.RedisValue value, System.DateTime expiry, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+StackExchange.Redis.IDatabaseAsync.HashFieldSetAndSetExpiryAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue field, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+StackExchange.Redis.IDatabaseAsync.HashFieldSetAndSetExpiryAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.HashEntry[]! hashFields, System.DateTime expiry, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+StackExchange.Redis.IDatabaseAsync.HashFieldSetAndSetExpiryAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.HashEntry[]! hashFields, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+
diff --git a/src/StackExchange.Redis/RedisDatabase.ExpiryToken.cs b/src/StackExchange.Redis/RedisDatabase.ExpiryToken.cs
new file mode 100644
index 000000000..42cfdcb18
--- /dev/null
+++ b/src/StackExchange.Redis/RedisDatabase.ExpiryToken.cs
@@ -0,0 +1,76 @@
+using System;
+
+namespace StackExchange.Redis;
+
+internal partial class RedisDatabase
+{
+ ///
+ /// Parses, validates and represents, for example: "EX 10", "KEEPTTL" or "".
+ ///
+ internal readonly struct ExpiryToken
+ {
+ private static readonly ExpiryToken s_Persist = new(RedisLiterals.PERSIST), s_KeepTtl = new(RedisLiterals.KEEPTTL), s_Null = new(RedisValue.Null);
+
+ public RedisValue Operand { get; }
+ public long Value { get; }
+ public int Tokens => Value == long.MinValue ? (Operand.IsNull ? 0 : 1) : 2;
+ public bool HasValue => Value != long.MinValue;
+ public bool HasOperand => !Operand.IsNull;
+
+ public static ExpiryToken Persist(TimeSpan? expiry, bool persist)
+ {
+ if (expiry.HasValue)
+ {
+ if (persist) throw new ArgumentException("Cannot specify both expiry and persist", nameof(persist));
+ return new(expiry.GetValueOrDefault()); // EX 10
+ }
+
+ return persist ? s_Persist : s_Null; // PERSIST (or nothing)
+ }
+
+ public static ExpiryToken KeepTtl(TimeSpan? expiry, bool keepTtl)
+ {
+ if (expiry.HasValue)
+ {
+ if (keepTtl) throw new ArgumentException("Cannot specify both expiry and keepTtl", nameof(keepTtl));
+ return new(expiry.GetValueOrDefault()); // EX 10
+ }
+
+ return keepTtl ? s_KeepTtl : s_Null; // KEEPTTL (or nothing)
+ }
+
+ private ExpiryToken(RedisValue operand, long value = long.MinValue)
+ {
+ Operand = operand;
+ Value = value;
+ }
+
+ public ExpiryToken(TimeSpan expiry)
+ {
+ long milliseconds = expiry.Ticks / TimeSpan.TicksPerMillisecond;
+ var useSeconds = milliseconds % 1000 == 0;
+
+ Operand = useSeconds ? RedisLiterals.EX : RedisLiterals.PX;
+ Value = useSeconds ? (milliseconds / 1000) : milliseconds;
+ }
+
+ public ExpiryToken(DateTime expiry)
+ {
+ long milliseconds = GetUnixTimeMilliseconds(expiry);
+ var useSeconds = milliseconds % 1000 == 0;
+
+ Operand = useSeconds ? RedisLiterals.EXAT : RedisLiterals.PXAT;
+ Value = useSeconds ? (milliseconds / 1000) : milliseconds;
+ }
+
+ public override string ToString() => Tokens switch
+ {
+ 2 => $"{Operand} {Value}",
+ 1 => Operand.ToString(),
+ _ => "",
+ };
+
+ public override int GetHashCode() => throw new NotSupportedException();
+ public override bool Equals(object? obj) => throw new NotSupportedException();
+ }
+}
diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs
index 651d1b4a3..7b23b0773 100644
--- a/src/StackExchange.Redis/RedisDatabase.cs
+++ b/src/StackExchange.Redis/RedisDatabase.cs
@@ -8,7 +8,7 @@
namespace StackExchange.Redis
{
- internal class RedisDatabase : RedisBase, IDatabase
+ internal partial class RedisDatabase : RedisBase, IDatabase
{
internal RedisDatabase(ConnectionMultiplexer multiplexer, int db, object? asyncState)
: base(multiplexer, asyncState)
@@ -396,7 +396,7 @@ public ExpireResult[] HashFieldExpire(RedisKey key, RedisValue[] hashFields, Tim
public ExpireResult[] HashFieldExpire(RedisKey key, RedisValue[] hashFields, DateTime expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None)
{
- long milliseconds = GetMillisecondsUntil(expiry);
+ long milliseconds = GetUnixTimeMilliseconds(expiry);
return HashFieldExpireExecute(key, milliseconds, when, PickExpireAtCommandByPrecision, SyncCustomArrExecutor>, ResultProcessor.ExpireResultArray, flags, hashFields);
}
@@ -408,7 +408,7 @@ public Task HashFieldExpireAsync(RedisKey key, RedisValue[] hash
public Task HashFieldExpireAsync(RedisKey key, RedisValue[] hashFields, DateTime expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None)
{
- long milliseconds = GetMillisecondsUntil(expiry);
+ long milliseconds = GetUnixTimeMilliseconds(expiry);
return HashFieldExpireExecute(key, milliseconds, when, PickExpireAtCommandByPrecision, AsyncCustomArrExecutor>, ResultProcessor.ExpireResultArray, flags, hashFields);
}
@@ -447,6 +447,300 @@ private T HashFieldExecute(RedisCommand cmd, RedisKey key, Custom
private Task AsyncCustomArrExecutor(Message msg, TProcessor processor) where TProcessor : ResultProcessor => ExecuteAsync(msg, processor)!;
+ public RedisValue HashFieldGetAndDelete(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(Database, flags, RedisCommand.HGETDEL, key, RedisLiterals.FIELDS, 1, hashField);
+ return ExecuteSync(msg, ResultProcessor.RedisValueFromArray);
+ }
+
+ public Lease? HashFieldGetLeaseAndDelete(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(Database, flags, RedisCommand.HGETDEL, key, RedisLiterals.FIELDS, 1, hashField);
+ return ExecuteSync(msg, ResultProcessor.LeaseFromArray);
+ }
+
+ public RedisValue[] HashFieldGetAndDelete(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None)
+ {
+ if (hashFields == null) throw new ArgumentNullException(nameof(hashFields));
+ if (hashFields.Length == 0) return Array.Empty();
+ var msg = Message.Create(Database, flags, RedisCommand.HGETDEL, key, RedisLiterals.FIELDS, hashFields.Length, hashFields);
+ return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty());
+ }
+
+ public Task HashFieldGetAndDeleteAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(Database, flags, RedisCommand.HGETDEL, key, RedisLiterals.FIELDS, 1, hashField);
+ return ExecuteAsync(msg, ResultProcessor.RedisValueFromArray);
+ }
+
+ public Task?> HashFieldGetLeaseAndDeleteAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(Database, flags, RedisCommand.HGETDEL, key, RedisLiterals.FIELDS, 1, hashField);
+ return ExecuteAsync(msg, ResultProcessor.LeaseFromArray);
+ }
+
+ public Task HashFieldGetAndDeleteAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None)
+ {
+ if (hashFields == null) throw new ArgumentNullException(nameof(hashFields));
+ if (hashFields.Length == 0) return CompletedTask.FromDefault(Array.Empty(), asyncState);
+ var msg = Message.Create(Database, flags, RedisCommand.HGETDEL, key, RedisLiterals.FIELDS, hashFields.Length, hashFields);
+ return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty());
+ }
+
+ private Message HashFieldGetAndSetExpiryMessage(in RedisKey key, in RedisValue hashField, ExpiryToken expiry, CommandFlags flags) =>
+ expiry.Tokens switch
+ {
+ // expiry, for example EX 10
+ 2 => Message.Create(Database, flags, RedisCommand.HGETEX, key, expiry.Operand, expiry.Value, RedisLiterals.FIELDS, 1, hashField),
+ // keyword only, for example PERSIST
+ 1 => Message.Create(Database, flags, RedisCommand.HGETEX, key, expiry.Operand, RedisLiterals.FIELDS, 1, hashField),
+ // default case when neither expiry nor persist are set
+ _ => Message.Create(Database, flags, RedisCommand.HGETEX, key, RedisLiterals.FIELDS, 1, hashField),
+ };
+
+ private Message HashFieldGetAndSetExpiryMessage(in RedisKey key, RedisValue[] hashFields, ExpiryToken expiry, CommandFlags flags)
+ {
+ if (hashFields is null) throw new ArgumentNullException(nameof(hashFields));
+ if (hashFields.Length == 1)
+ {
+ return HashFieldGetAndSetExpiryMessage(key, in hashFields[0], expiry, flags);
+ }
+
+ // precision, time, FIELDS, hashFields.Length
+ int extraTokens = expiry.Tokens + 2;
+
+ RedisValue[] values = new RedisValue[expiry.Tokens + 2 + hashFields.Length];
+
+ int index = 0;
+ // add PERSIST or expiry values
+ switch (expiry.Tokens)
+ {
+ case 2:
+ values[index++] = expiry.Operand;
+ values[index++] = expiry.Value;
+ break;
+ case 1:
+ values[index++] = expiry.Operand;
+ break;
+ }
+ // add the fields
+ values[index++] = RedisLiterals.FIELDS;
+ values[index++] = hashFields.Length;
+ // check we've added everything we expected to
+ Debug.Assert(index == extraTokens + hashFields.Length);
+
+ // Add hash fields to the array
+ hashFields.AsSpan().CopyTo(values.AsSpan(index));
+
+ return Message.Create(Database, flags, RedisCommand.HGETEX, key, values);
+ }
+
+ public RedisValue HashFieldGetAndSetExpiry(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = HashFieldGetAndSetExpiryMessage(key, hashField, ExpiryToken.Persist(expiry, persist), flags);
+ return ExecuteSync(msg, ResultProcessor.RedisValueFromArray);
+ }
+
+ public RedisValue HashFieldGetAndSetExpiry(RedisKey key, RedisValue hashField, DateTime expiry, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = HashFieldGetAndSetExpiryMessage(key, hashField, new(expiry), flags);
+ return ExecuteSync(msg, ResultProcessor.RedisValueFromArray);
+ }
+
+ public Lease? HashFieldGetLeaseAndSetExpiry(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = HashFieldGetAndSetExpiryMessage(key, hashField, ExpiryToken.Persist(expiry, persist), flags);
+ return ExecuteSync(msg, ResultProcessor.LeaseFromArray);
+ }
+
+ public Lease? HashFieldGetLeaseAndSetExpiry(RedisKey key, RedisValue hashField, DateTime expiry, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = HashFieldGetAndSetExpiryMessage(key, hashField, new(expiry), flags);
+ return ExecuteSync(msg, ResultProcessor.LeaseFromArray);
+ }
+
+ public RedisValue[] HashFieldGetAndSetExpiry(RedisKey key, RedisValue[] hashFields, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None)
+ {
+ if (hashFields == null) throw new ArgumentNullException(nameof(hashFields));
+ if (hashFields.Length == 0) return Array.Empty();
+ var msg = HashFieldGetAndSetExpiryMessage(key, hashFields, ExpiryToken.Persist(expiry, persist), flags);
+ return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty());
+ }
+
+ public RedisValue[] HashFieldGetAndSetExpiry(RedisKey key, RedisValue[] hashFields, DateTime expiry, CommandFlags flags = CommandFlags.None)
+ {
+ if (hashFields == null) throw new ArgumentNullException(nameof(hashFields));
+ if (hashFields.Length == 0) return Array.Empty();
+ var msg = HashFieldGetAndSetExpiryMessage(key, hashFields, new(expiry), flags);
+ return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty());
+ }
+
+ public Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = HashFieldGetAndSetExpiryMessage(key, hashField, ExpiryToken.Persist(expiry, persist), flags);
+ return ExecuteAsync(msg, ResultProcessor.RedisValueFromArray);
+ }
+
+ public Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue hashField, DateTime expiry, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = HashFieldGetAndSetExpiryMessage(key, hashField, new(expiry), flags);
+ return ExecuteAsync(msg, ResultProcessor.RedisValueFromArray);
+ }
+
+ public Task?> HashFieldGetLeaseAndSetExpiryAsync(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = HashFieldGetAndSetExpiryMessage(key, hashField, ExpiryToken.Persist(expiry, persist), flags);
+ return ExecuteAsync(msg, ResultProcessor.LeaseFromArray);
+ }
+
+ public Task?> HashFieldGetLeaseAndSetExpiryAsync(RedisKey key, RedisValue hashField, DateTime expiry, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = HashFieldGetAndSetExpiryMessage(key, hashField, new(expiry), flags);
+ return ExecuteAsync(msg, ResultProcessor.LeaseFromArray);
+ }
+
+ public Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue[] hashFields, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None)
+ {
+ if (hashFields == null) throw new ArgumentNullException(nameof(hashFields));
+ if (hashFields.Length == 0) return CompletedTask.FromDefault(Array.Empty(), asyncState);
+ var msg = HashFieldGetAndSetExpiryMessage(key, hashFields, ExpiryToken.Persist(expiry, persist), flags);
+ return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty());
+ }
+
+ public Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue[] hashFields, DateTime expiry, CommandFlags flags = CommandFlags.None)
+ {
+ if (hashFields == null) throw new ArgumentNullException(nameof(hashFields));
+ if (hashFields.Length == 0) return CompletedTask.FromDefault(Array.Empty(), asyncState);
+ var msg = HashFieldGetAndSetExpiryMessage(key, hashFields, new(expiry), flags);
+ return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty());
+ }
+
+ private Message HashFieldSetAndSetExpiryMessage(in RedisKey key, in RedisValue field, in RedisValue value, ExpiryToken expiry, When when, CommandFlags flags)
+ {
+ if (when == When.Always)
+ {
+ return expiry.Tokens switch
+ {
+ 2 => Message.Create(Database, flags, RedisCommand.HSETEX, key, expiry.Operand, expiry.Value, RedisLiterals.FIELDS, 1, field, value),
+ 1 => Message.Create(Database, flags, RedisCommand.HSETEX, key, expiry.Operand, RedisLiterals.FIELDS, 1, field, value),
+ _ => Message.Create(Database, flags, RedisCommand.HSETEX, key, RedisLiterals.FIELDS, 1, field, value),
+ };
+ }
+ else
+ {
+ // we need an extra token
+ var existance = when switch
+ {
+ When.Exists => RedisLiterals.FXX,
+ When.NotExists => RedisLiterals.FNX,
+ _ => throw new ArgumentOutOfRangeException(nameof(when)),
+ };
+
+ return expiry.Tokens switch
+ {
+ 2 => Message.Create(Database, flags, RedisCommand.HSETEX, key, existance, expiry.Operand, expiry.Value, RedisLiterals.FIELDS, 1, field, value),
+ 1 => Message.Create(Database, flags, RedisCommand.HSETEX, key, existance, expiry.Operand, RedisLiterals.FIELDS, 1, field, value),
+ _ => Message.Create(Database, flags, RedisCommand.HSETEX, key, existance, RedisLiterals.FIELDS, 1, field, value),
+ };
+ }
+ }
+
+ private Message HashFieldSetAndSetExpiryMessage(in RedisKey key, HashEntry[] hashFields, ExpiryToken expiry, When when, CommandFlags flags)
+ {
+ if (hashFields.Length == 1)
+ {
+ var field = hashFields[0];
+ return HashFieldSetAndSetExpiryMessage(key, field.Name, field.Value, expiry, when, flags);
+ }
+ // Determine the base array size
+ var extraTokens = expiry.Tokens + (when == When.Always ? 2 : 3); // [FXX|FNX] {expiry} FIELDS {length}
+ RedisValue[] values = new RedisValue[(hashFields.Length * 2) + extraTokens];
+
+ int index = 0;
+ switch (when)
+ {
+ case When.Always:
+ break;
+ case When.Exists:
+ values[index++] = RedisLiterals.FXX;
+ break;
+ case When.NotExists:
+ values[index++] = RedisLiterals.FNX;
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(when));
+ }
+ switch (expiry.Tokens)
+ {
+ case 2:
+ values[index++] = expiry.Operand;
+ values[index++] = expiry.Value;
+ break;
+ case 1:
+ values[index++] = expiry.Operand;
+ break;
+ }
+ values[index++] = RedisLiterals.FIELDS;
+ values[index++] = hashFields.Length;
+ for (int i = 0; i < hashFields.Length; i++)
+ {
+ values[index++] = hashFields[i].name;
+ values[index++] = hashFields[i].value;
+ }
+ Debug.Assert(index == values.Length);
+ return Message.Create(Database, flags, RedisCommand.HSETEX, key, values);
+ }
+
+ public RedisValue HashFieldSetAndSetExpiry(RedisKey key, RedisValue field, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = HashFieldSetAndSetExpiryMessage(key, field, value, ExpiryToken.KeepTtl(expiry, keepTtl), when, flags);
+ return ExecuteSync(msg, ResultProcessor.RedisValue);
+ }
+
+ public RedisValue HashFieldSetAndSetExpiry(RedisKey key, RedisValue field, RedisValue value, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = HashFieldSetAndSetExpiryMessage(key, field, value, new(expiry), when, flags);
+ return ExecuteSync(msg, ResultProcessor.RedisValue);
+ }
+
+ public RedisValue HashFieldSetAndSetExpiry(RedisKey key, HashEntry[] hashFields, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None)
+ {
+ if (hashFields == null) throw new ArgumentNullException(nameof(hashFields));
+ var msg = HashFieldSetAndSetExpiryMessage(key, hashFields, ExpiryToken.KeepTtl(expiry, keepTtl), when, flags);
+ return ExecuteSync(msg, ResultProcessor.RedisValue);
+ }
+ public RedisValue HashFieldSetAndSetExpiry(RedisKey key, HashEntry[] hashFields, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None)
+ {
+ if (hashFields == null) throw new ArgumentNullException(nameof(hashFields));
+ var msg = HashFieldSetAndSetExpiryMessage(key, hashFields, new(expiry), when, flags);
+ return ExecuteSync(msg, ResultProcessor.RedisValue);
+ }
+
+ public Task HashFieldSetAndSetExpiryAsync(RedisKey key, RedisValue field, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = HashFieldSetAndSetExpiryMessage(key, field, value, ExpiryToken.KeepTtl(expiry, keepTtl), when, flags);
+ return ExecuteAsync(msg, ResultProcessor.RedisValue);
+ }
+
+ public Task HashFieldSetAndSetExpiryAsync(RedisKey key, RedisValue field, RedisValue value, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = HashFieldSetAndSetExpiryMessage(key, field, value, new(expiry), when, flags);
+ return ExecuteAsync(msg, ResultProcessor.RedisValue);
+ }
+
+ public Task HashFieldSetAndSetExpiryAsync(RedisKey key, HashEntry[] hashFields, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None)
+ {
+ if (hashFields == null) throw new ArgumentNullException(nameof(hashFields));
+ var msg = HashFieldSetAndSetExpiryMessage(key, hashFields, ExpiryToken.KeepTtl(expiry, keepTtl), when, flags);
+ return ExecuteAsync(msg, ResultProcessor.RedisValue);
+ }
+ public Task HashFieldSetAndSetExpiryAsync(RedisKey key, HashEntry[] hashFields, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None)
+ {
+ if (hashFields == null) throw new ArgumentNullException(nameof(hashFields));
+ var msg = HashFieldSetAndSetExpiryMessage(key, hashFields, new(expiry), when, flags);
+ return ExecuteAsync(msg, ResultProcessor.RedisValue);
+ }
+
public long[] HashFieldGetExpireDateTime(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) =>
HashFieldExecute(RedisCommand.HPEXPIRETIME, key, SyncCustomArrExecutor>, ResultProcessor.Int64Array, flags, hashFields);
@@ -3474,7 +3768,7 @@ public Task StringSetRangeAsync(RedisKey key, long offset, RedisValu
return ExecuteAsync(msg, ResultProcessor.RedisValue);
}
- private long GetMillisecondsUntil(DateTime when) => when.Kind switch
+ private static long GetUnixTimeMilliseconds(DateTime when) => when.Kind switch
{
DateTimeKind.Local or DateTimeKind.Utc => (when.ToUniversalTime() - RedisBase.UnixEpoch).Ticks / TimeSpan.TicksPerMillisecond,
_ => throw new ArgumentException("Expiry time must be either Utc or Local", nameof(when)),
@@ -3518,7 +3812,7 @@ private Message GetExpiryMessage(in RedisKey key, CommandFlags flags, DateTime?
};
}
- long milliseconds = GetMillisecondsUntil(expiry.Value);
+ long milliseconds = GetUnixTimeMilliseconds(expiry.Value);
return GetExpiryMessage(key, RedisCommand.PEXPIREAT, RedisCommand.EXPIREAT, milliseconds, when, flags, out server);
}
@@ -4677,7 +4971,7 @@ private Message GetStringBitOperationMessage(Bitwise operation, RedisKey destina
private Message GetStringGetExMessage(in RedisKey key, DateTime expiry, CommandFlags flags = CommandFlags.None) => expiry == DateTime.MaxValue
? Message.Create(Database, flags, RedisCommand.GETEX, key, RedisLiterals.PERSIST)
- : Message.Create(Database, flags, RedisCommand.GETEX, key, RedisLiterals.PXAT, GetMillisecondsUntil(expiry));
+ : Message.Create(Database, flags, RedisCommand.GETEX, key, RedisLiterals.PXAT, GetUnixTimeMilliseconds(expiry));
private Message GetStringGetWithExpiryMessage(RedisKey key, CommandFlags flags, out ResultProcessor processor, out ServerEndPoint? server)
{
diff --git a/src/StackExchange.Redis/RedisFeatures.cs b/src/StackExchange.Redis/RedisFeatures.cs
index 06a44e643..87bcbf20c 100644
--- a/src/StackExchange.Redis/RedisFeatures.cs
+++ b/src/StackExchange.Redis/RedisFeatures.cs
@@ -45,7 +45,9 @@ namespace StackExchange.Redis
v7_2_0_rc1 = new Version(7, 1, 240), // 7.2 RC1 is version 7.1.240
v7_4_0_rc1 = new Version(7, 3, 240), // 7.4 RC1 is version 7.3.240
v7_4_0_rc2 = new Version(7, 3, 241), // 7.4 RC2 is version 7.3.241
+ v8_0_0_M04 = new Version(7, 9, 227), // 8.0 M04 is version 7.9.227
v8_2_0_rc1 = new Version(8, 1, 240); // 8.2 RC1 is version 8.1.240
+
#pragma warning restore SA1310 // Field names should not contain underscore
#pragma warning restore SA1311 // Static readonly fields should begin with upper-case letter
diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs
index 29937c0dd..46a64cc88 100644
--- a/src/StackExchange.Redis/RedisLiterals.cs
+++ b/src/StackExchange.Redis/RedisLiterals.cs
@@ -84,7 +84,9 @@ public static readonly RedisValue
FIELDS = "FIELDS",
FILTERBY = "FILTERBY",
FLUSH = "FLUSH",
+ FNX = "FNX",
FREQ = "FREQ",
+ FXX = "FXX",
GET = "GET",
GETKEYS = "GETKEYS",
GETNAME = "GETNAME",
diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs
index 67dd73173..982ee565d 100644
--- a/src/StackExchange.Redis/ResultProcessor.cs
+++ b/src/StackExchange.Redis/ResultProcessor.cs
@@ -88,9 +88,15 @@ public static readonly ResultProcessor
public static readonly ResultProcessor
RedisValue = new RedisValueProcessor();
+ public static readonly ResultProcessor
+ RedisValueFromArray = new RedisValueFromArrayProcessor();
+
public static readonly ResultProcessor>
Lease = new LeaseProcessor();
+ public static readonly ResultProcessor>
+ LeaseFromArray = new LeaseFromArrayProcessor();
+
public static readonly ResultProcessor
RedisValueArray = new RedisValueArrayProcessor();
@@ -1904,6 +1910,25 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes
}
}
+ private sealed class RedisValueFromArrayProcessor : ResultProcessor
+ {
+ protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result)
+ {
+ switch (result.Resp2TypeBulkString)
+ {
+ case ResultType.Array:
+ var items = result.GetItems();
+ if (items.Length == 1)
+ { // treat an array of 1 like a single reply
+ SetResult(message, items[0].AsRedisValue());
+ return true;
+ }
+ break;
+ }
+ return false;
+ }
+ }
+
private sealed class RoleProcessor : ResultProcessor
{
protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result)
@@ -2049,6 +2074,25 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes
}
}
+ private sealed class LeaseFromArrayProcessor : ResultProcessor>
+ {
+ protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result)
+ {
+ switch (result.Resp2TypeBulkString)
+ {
+ case ResultType.Array:
+ var items = result.GetItems();
+ if (items.Length == 1)
+ { // treat an array of 1 like a single reply
+ SetResult(message, items[0].AsLease()!);
+ return true;
+ }
+ break;
+ }
+ return false;
+ }
+ }
+
private sealed class ScriptResultProcessor : ResultProcessor
{
public override bool SetResult(PhysicalConnection connection, Message message, in RawResult result)
diff --git a/tests/StackExchange.Redis.Tests/CancellationTests.cs b/tests/StackExchange.Redis.Tests/CancellationTests.cs
index f9f955eb8..a512743f9 100644
--- a/tests/StackExchange.Redis.Tests/CancellationTests.cs
+++ b/tests/StackExchange.Redis.Tests/CancellationTests.cs
@@ -12,6 +12,10 @@ public class CancellationTests(ITestOutputHelper output, SharedConnectionFixture
[Fact]
public async Task WithCancellation_CancelledToken_ThrowsOperationCanceledException()
{
+#if NETFRAMEWORK
+ Skip.UnlessLongRunning(); // unpredictable on netfx due to weak WaitAsync impl
+#endif
+
await using var conn = Create();
var db = conn.GetDatabase();
@@ -156,6 +160,8 @@ public async Task CancellationDuringOperation_Async_CancelsGracefully(CancelStra
[Fact]
public async Task ScanCancellable()
{
+ Skip.UnlessLongRunning(); // because of CLIENT PAUSE impact to unrelated tests
+
using var conn = Create();
var db = conn.GetDatabase();
var server = conn.GetServer(conn.GetEndPoints()[0]);
@@ -182,7 +188,7 @@ public async Task ScanCancellable()
var taken = watch.ElapsedMilliseconds;
// Expected if cancellation happens during operation
Log($"Cancelled after {taken}ms");
- Assert.True(taken < ConnectionPauseMilliseconds / 2, "Should have cancelled much sooner");
+ Assert.True(taken < (ConnectionPauseMilliseconds * 3) / 4, $"Should have cancelled sooner; took {taken}ms");
Assert.Equal(cts.Token, oce.CancellationToken);
}
}
diff --git a/tests/StackExchange.Redis.Tests/ExpiryTokenTests.cs b/tests/StackExchange.Redis.Tests/ExpiryTokenTests.cs
new file mode 100644
index 000000000..3f0d39f28
--- /dev/null
+++ b/tests/StackExchange.Redis.Tests/ExpiryTokenTests.cs
@@ -0,0 +1,117 @@
+using System;
+using Xunit;
+using static StackExchange.Redis.RedisDatabase;
+using static StackExchange.Redis.RedisDatabase.ExpiryToken;
+namespace StackExchange.Redis.Tests;
+
+public class ExpiryTokenTests // pure tests, no DB
+{
+ [Fact]
+ public void Persist_Seconds()
+ {
+ TimeSpan? time = TimeSpan.FromMilliseconds(5000);
+ var ex = Persist(time, false);
+ Assert.Equal(2, ex.Tokens);
+ Assert.Equal("EX 5", ex.ToString());
+ }
+
+ [Fact]
+ public void Persist_Milliseconds()
+ {
+ TimeSpan? time = TimeSpan.FromMilliseconds(5001);
+ var ex = Persist(time, false);
+ Assert.Equal(2, ex.Tokens);
+ Assert.Equal("PX 5001", ex.ToString());
+ }
+
+ [Fact]
+ public void Persist_None_False()
+ {
+ TimeSpan? time = null;
+ var ex = Persist(time, false);
+ Assert.Equal(0, ex.Tokens);
+ Assert.Equal("", ex.ToString());
+ }
+
+ [Fact]
+ public void Persist_None_True()
+ {
+ TimeSpan? time = null;
+ var ex = Persist(time, true);
+ Assert.Equal(1, ex.Tokens);
+ Assert.Equal("PERSIST", ex.ToString());
+ }
+
+ [Fact]
+ public void Persist_Both()
+ {
+ TimeSpan? time = TimeSpan.FromMilliseconds(5000);
+ var ex = Assert.Throws(() => Persist(time, true));
+ Assert.Equal("persist", ex.ParamName);
+ Assert.StartsWith("Cannot specify both expiry and persist", ex.Message);
+ }
+
+ [Fact]
+ public void KeepTtl_Seconds()
+ {
+ TimeSpan? time = TimeSpan.FromMilliseconds(5000);
+ var ex = KeepTtl(time, false);
+ Assert.Equal(2, ex.Tokens);
+ Assert.Equal("EX 5", ex.ToString());
+ }
+
+ [Fact]
+ public void KeepTtl_Milliseconds()
+ {
+ TimeSpan? time = TimeSpan.FromMilliseconds(5001);
+ var ex = KeepTtl(time, false);
+ Assert.Equal(2, ex.Tokens);
+ Assert.Equal("PX 5001", ex.ToString());
+ }
+
+ [Fact]
+ public void KeepTtl_None_False()
+ {
+ TimeSpan? time = null;
+ var ex = KeepTtl(time, false);
+ Assert.Equal(0, ex.Tokens);
+ Assert.Equal("", ex.ToString());
+ }
+
+ [Fact]
+ public void KeepTtl_None_True()
+ {
+ TimeSpan? time = null;
+ var ex = KeepTtl(time, true);
+ Assert.Equal(1, ex.Tokens);
+ Assert.Equal("KEEPTTL", ex.ToString());
+ }
+
+ [Fact]
+ public void KeepTtl_Both()
+ {
+ TimeSpan? time = TimeSpan.FromMilliseconds(5000);
+ var ex = Assert.Throws(() => KeepTtl(time, true));
+ Assert.Equal("keepTtl", ex.ParamName);
+ Assert.StartsWith("Cannot specify both expiry and keepTtl", ex.Message);
+ }
+
+ [Fact]
+ public void DateTime_Seconds()
+ {
+ var when = new DateTime(2025, 7, 23, 10, 4, 14, DateTimeKind.Utc);
+ var ex = new ExpiryToken(when);
+ Assert.Equal(2, ex.Tokens);
+ Assert.Equal("EXAT 1753265054", ex.ToString());
+ }
+
+ [Fact]
+ public void DateTime_Milliseconds()
+ {
+ var when = new DateTime(2025, 7, 23, 10, 4, 14, DateTimeKind.Utc);
+ when = when.AddMilliseconds(14);
+ var ex = new ExpiryToken(when);
+ Assert.Equal(2, ex.Tokens);
+ Assert.Equal("PXAT 1753265054014", ex.ToString());
+ }
+}
diff --git a/tests/StackExchange.Redis.Tests/HashFieldTests.cs b/tests/StackExchange.Redis.Tests/HashFieldTests.cs
index 3d1cb0c6e..2bb98eb85 100644
--- a/tests/StackExchange.Redis.Tests/HashFieldTests.cs
+++ b/tests/StackExchange.Redis.Tests/HashFieldTests.cs
@@ -18,6 +18,8 @@ public class HashFieldTests(ITestOutputHelper output, SharedConnectionFixture fi
private readonly RedisValue[] fields = ["f1", "f2"];
+ private readonly RedisValue[] values = [1, 2];
+
[Fact]
public void HashFieldExpire()
{
@@ -297,4 +299,273 @@ public void HashFieldPersistNoField()
var fieldsResult = db.HashFieldPersist(hashKey, ["notExistingField1", "notExistingField2"]);
Assert.Equal([PersistResult.NoSuchField, PersistResult.NoSuchField], fieldsResult);
}
+
+ [Fact]
+ public void HashFieldGetAndSetExpiry()
+ {
+ using var conn = Create(require: RedisFeatures.v8_0_0_M04);
+ var db = conn.GetDatabase();
+ var hashKey = Me();
+
+ // testing with timespan
+ db.HashSet(hashKey, entries);
+ var fieldResult = db.HashFieldGetAndSetExpiry(hashKey, "f1", TimeSpan.FromHours(1));
+ Assert.Equal(1, fieldResult);
+ var fieldTtl = db.HashFieldGetTimeToLive(hashKey, new RedisValue[] { "f1" })[0];
+ Assert.InRange(fieldTtl, TimeSpan.FromMinutes(59).TotalMilliseconds, TimeSpan.FromHours(1).TotalMilliseconds);
+
+ // testing with datetime
+ db.HashSet(hashKey, entries);
+ fieldResult = db.HashFieldGetAndSetExpiry(hashKey, "f1", DateTime.Now.AddMinutes(120));
+ Assert.Equal(1, fieldResult);
+ fieldTtl = db.HashFieldGetTimeToLive(hashKey, new RedisValue[] { "f1" })[0];
+ Assert.InRange(fieldTtl, TimeSpan.FromMinutes(119).TotalMilliseconds, TimeSpan.FromHours(2).TotalMilliseconds);
+
+ // testing persist
+ fieldResult = db.HashFieldGetAndSetExpiry(hashKey, "f1", persist: true);
+ Assert.Equal(1, fieldResult);
+ fieldTtl = db.HashFieldGetTimeToLive(hashKey, new RedisValue[] { "f1" })[0];
+ Assert.Equal(-1, fieldTtl);
+
+ // testing multiple fields with timespan
+ db.HashSet(hashKey, entries);
+ var fieldResults = db.HashFieldGetAndSetExpiry(hashKey, fields, TimeSpan.FromHours(1));
+ Assert.Equal(values, fieldResults);
+ var fieldTtls = db.HashFieldGetTimeToLive(hashKey, fields);
+ Assert.InRange(fieldTtls[0], TimeSpan.FromMinutes(59).TotalMilliseconds, TimeSpan.FromHours(1).TotalMilliseconds);
+ Assert.InRange(fieldTtls[1], TimeSpan.FromMinutes(59).TotalMilliseconds, TimeSpan.FromHours(1).TotalMilliseconds);
+
+ // testing multiple fields with datetime
+ db.HashSet(hashKey, entries);
+ fieldResults = db.HashFieldGetAndSetExpiry(hashKey, fields, DateTime.Now.AddMinutes(120));
+ Assert.Equal(values, fieldResults);
+ fieldTtls = db.HashFieldGetTimeToLive(hashKey, fields);
+ Assert.InRange(fieldTtls[0], TimeSpan.FromMinutes(119).TotalMilliseconds, TimeSpan.FromHours(2).TotalMilliseconds);
+ Assert.InRange(fieldTtls[1], TimeSpan.FromMinutes(119).TotalMilliseconds, TimeSpan.FromHours(2).TotalMilliseconds);
+
+ // testing multiple fields with persist
+ fieldResults = db.HashFieldGetAndSetExpiry(hashKey, fields, persist: true);
+ Assert.Equal(values, fieldResults);
+ fieldTtls = db.HashFieldGetTimeToLive(hashKey, fields);
+ Assert.Equal(new long[] { -1, -1 }, fieldTtls);
+ }
+
+ [Fact]
+ public async Task HashFieldGetAndSetExpiryAsync()
+ {
+ await using var conn = Create(require: RedisFeatures.v8_0_0_M04);
+ var db = conn.GetDatabase();
+ var hashKey = Me();
+
+ // testing with timespan
+ db.HashSet(hashKey, entries);
+ var fieldResult = await db.HashFieldGetAndSetExpiryAsync(hashKey, "f1", TimeSpan.FromHours(1));
+ Assert.Equal(1, fieldResult);
+ var fieldTtl = db.HashFieldGetTimeToLive(hashKey, new RedisValue[] { "f1" })[0];
+ Assert.InRange(fieldTtl, TimeSpan.FromMinutes(59).TotalMilliseconds, TimeSpan.FromHours(1).TotalMilliseconds);
+
+ // testing with datetime
+ db.HashSet(hashKey, entries);
+ fieldResult = await db.HashFieldGetAndSetExpiryAsync(hashKey, "f1", DateTime.Now.AddMinutes(120));
+ Assert.Equal(1, fieldResult);
+ fieldTtl = db.HashFieldGetTimeToLive(hashKey, new RedisValue[] { "f1" })[0];
+ Assert.InRange(fieldTtl, TimeSpan.FromMinutes(119).TotalMilliseconds, TimeSpan.FromHours(2).TotalMilliseconds);
+
+ // testing persist
+ fieldResult = await db.HashFieldGetAndSetExpiryAsync(hashKey, "f1", persist: true);
+ Assert.Equal(1, fieldResult);
+ fieldTtl = db.HashFieldGetTimeToLive(hashKey, new RedisValue[] { "f1" })[0];
+ Assert.Equal(-1, fieldTtl);
+
+ // testing multiple fields with timespan
+ db.HashSet(hashKey, entries);
+ var fieldResults = await db.HashFieldGetAndSetExpiryAsync(hashKey, fields, TimeSpan.FromHours(1));
+ Assert.Equal(values, fieldResults);
+ var fieldTtls = db.HashFieldGetTimeToLive(hashKey, fields);
+ Assert.InRange(fieldTtls[0], TimeSpan.FromMinutes(59).TotalMilliseconds, TimeSpan.FromHours(1).TotalMilliseconds);
+ Assert.InRange(fieldTtls[1], TimeSpan.FromMinutes(59).TotalMilliseconds, TimeSpan.FromHours(1).TotalMilliseconds);
+
+ // testing multiple fields with datetime
+ db.HashSet(hashKey, entries);
+ fieldResults = await db.HashFieldGetAndSetExpiryAsync(hashKey, fields, DateTime.Now.AddMinutes(120));
+ Assert.Equal(values, fieldResults);
+ fieldTtls = db.HashFieldGetTimeToLive(hashKey, fields);
+ Assert.InRange(fieldTtls[0], TimeSpan.FromMinutes(119).TotalMilliseconds, TimeSpan.FromHours(2).TotalMilliseconds);
+ Assert.InRange(fieldTtls[1], TimeSpan.FromMinutes(119).TotalMilliseconds, TimeSpan.FromHours(2).TotalMilliseconds);
+
+ // testing multiple fields with persist
+ fieldResults = await db.HashFieldGetAndSetExpiryAsync(hashKey, fields, persist: true);
+ Assert.Equal(values, fieldResults);
+ fieldTtls = db.HashFieldGetTimeToLive(hashKey, fields);
+ Assert.Equal(new long[] { -1, -1 }, fieldTtls);
+ }
+
+ [Fact]
+ public void HashFieldSetAndSetExpiry()
+ {
+ using var conn = Create(require: RedisFeatures.v8_0_0_M04);
+ var db = conn.GetDatabase();
+ var hashKey = Me();
+
+ // testing with timespan
+ var result = db.HashFieldSetAndSetExpiry(hashKey, "f1", 1, TimeSpan.FromHours(1));
+ Assert.Equal(1, result);
+ var fieldTtl = db.HashFieldGetTimeToLive(hashKey, new RedisValue[] { "f1" })[0];
+ Assert.InRange(fieldTtl, TimeSpan.FromMinutes(59).TotalMilliseconds, TimeSpan.FromHours(1).TotalMilliseconds);
+
+ // testing with datetime
+ result = db.HashFieldSetAndSetExpiry(hashKey, "f1", 1, DateTime.Now.AddMinutes(120));
+ Assert.Equal(1, result);
+ fieldTtl = db.HashFieldGetTimeToLive(hashKey, new RedisValue[] { "f1" })[0];
+ Assert.InRange(fieldTtl, TimeSpan.FromMinutes(119).TotalMilliseconds, TimeSpan.FromHours(2).TotalMilliseconds);
+
+ // testing with keepttl
+ result = db.HashFieldSetAndSetExpiry(hashKey, "f1", 1, keepTtl: true);
+ Assert.Equal(1, result);
+ fieldTtl = db.HashFieldGetTimeToLive(hashKey, new RedisValue[] { "f1" })[0];
+ Assert.InRange(fieldTtl, TimeSpan.FromMinutes(119).TotalMilliseconds, TimeSpan.FromHours(2).TotalMilliseconds);
+
+ // testing multiple fields with timespan
+ result = db.HashFieldSetAndSetExpiry(hashKey, entries, TimeSpan.FromHours(1));
+ Assert.Equal(1, result);
+ var fieldTtls = db.HashFieldGetTimeToLive(hashKey, fields);
+ Assert.InRange(fieldTtls[0], TimeSpan.FromMinutes(59).TotalMilliseconds, TimeSpan.FromHours(1).TotalMilliseconds);
+ Assert.InRange(fieldTtls[1], TimeSpan.FromMinutes(59).TotalMilliseconds, TimeSpan.FromHours(1).TotalMilliseconds);
+
+ // testing multiple fields with datetime
+ result = db.HashFieldSetAndSetExpiry(hashKey, entries, DateTime.Now.AddMinutes(120));
+ Assert.Equal(1, result);
+ fieldTtls = db.HashFieldGetTimeToLive(hashKey, fields);
+ Assert.InRange(fieldTtls[0], TimeSpan.FromMinutes(119).TotalMilliseconds, TimeSpan.FromHours(2).TotalMilliseconds);
+ Assert.InRange(fieldTtls[1], TimeSpan.FromMinutes(119).TotalMilliseconds, TimeSpan.FromHours(2).TotalMilliseconds);
+
+ // testing multiple fields with keepttl
+ result = db.HashFieldSetAndSetExpiry(hashKey, entries, keepTtl: true);
+ Assert.Equal(1, result);
+ fieldTtls = db.HashFieldGetTimeToLive(hashKey, fields);
+ Assert.InRange(fieldTtls[0], TimeSpan.FromMinutes(119).TotalMilliseconds, TimeSpan.FromHours(2).TotalMilliseconds);
+ Assert.InRange(fieldTtls[1], TimeSpan.FromMinutes(119).TotalMilliseconds, TimeSpan.FromHours(2).TotalMilliseconds);
+
+ // testing with ExpireWhen.Exists
+ db.KeyDelete(hashKey);
+ result = db.HashFieldSetAndSetExpiry(hashKey, "f1", 1, TimeSpan.FromHours(1), when: When.Exists);
+ Assert.Equal(0, result); // should not set because it doesnt exist
+
+ // testing with ExpireWhen.NotExists
+ result = db.HashFieldSetAndSetExpiry(hashKey, "f1", 1, TimeSpan.FromHours(1), when: When.NotExists);
+ Assert.Equal(1, result); // should set because it doesnt exist
+ fieldTtl = db.HashFieldGetTimeToLive(hashKey, new RedisValue[] { "f1" })[0];
+ Assert.InRange(fieldTtl, TimeSpan.FromMinutes(59).TotalMilliseconds, TimeSpan.FromHours(1).TotalMilliseconds);
+
+ // testing with ExpireWhen.GreaterThanCurrentExpiry
+ result = db.HashFieldSetAndSetExpiry(hashKey, "f1", -1, keepTtl: true, when: When.Exists);
+ Assert.Equal(1, result); // should set because it exists
+ fieldTtl = db.HashFieldGetTimeToLive(hashKey, new RedisValue[] { "f1" })[0];
+ Assert.InRange(fieldTtl, TimeSpan.FromMinutes(59).TotalMilliseconds, TimeSpan.FromHours(1).TotalMilliseconds);
+ }
+
+ [Fact]
+ public async Task HashFieldSetAndSetExpiryAsync()
+ {
+ await using var conn = Create(require: RedisFeatures.v8_0_0_M04);
+ var db = conn.GetDatabase();
+ var hashKey = Me();
+
+ // testing with timespan
+ var result = await db.HashFieldSetAndSetExpiryAsync(hashKey, "f1", 1, TimeSpan.FromHours(1));
+ Assert.Equal(1, result);
+ var fieldTtl = db.HashFieldGetTimeToLive(hashKey, new RedisValue[] { "f1" })[0];
+ Assert.InRange(fieldTtl, TimeSpan.FromMinutes(59).TotalMilliseconds, TimeSpan.FromHours(1).TotalMilliseconds);
+
+ // testing with datetime
+ result = await db.HashFieldSetAndSetExpiryAsync(hashKey, "f1", 1, DateTime.Now.AddMinutes(120));
+ Assert.Equal(1, result);
+ fieldTtl = db.HashFieldGetTimeToLive(hashKey, new RedisValue[] { "f1" })[0];
+ Assert.InRange(fieldTtl, TimeSpan.FromMinutes(119).TotalMilliseconds, TimeSpan.FromHours(2).TotalMilliseconds);
+
+ // testing with keepttl
+ result = await db.HashFieldSetAndSetExpiryAsync(hashKey, "f1", 1, keepTtl: true);
+ Assert.Equal(1, result);
+ fieldTtl = db.HashFieldGetTimeToLive(hashKey, new RedisValue[] { "f1" })[0];
+ Assert.InRange(fieldTtl, TimeSpan.FromMinutes(119).TotalMilliseconds, TimeSpan.FromHours(2).TotalMilliseconds);
+
+ // testing multiple fields with timespan
+ result = await db.HashFieldSetAndSetExpiryAsync(hashKey, entries, TimeSpan.FromHours(1));
+ Assert.Equal(1, result);
+ var fieldTtls = db.HashFieldGetTimeToLive(hashKey, fields);
+ Assert.InRange(fieldTtls[0], TimeSpan.FromMinutes(59).TotalMilliseconds, TimeSpan.FromHours(1).TotalMilliseconds);
+ Assert.InRange(fieldTtls[1], TimeSpan.FromMinutes(59).TotalMilliseconds, TimeSpan.FromHours(1).TotalMilliseconds);
+
+ // testing multiple fields with datetime
+ result = await db.HashFieldSetAndSetExpiryAsync(hashKey, entries, DateTime.Now.AddMinutes(120));
+ Assert.Equal(1, result);
+ fieldTtls = db.HashFieldGetTimeToLive(hashKey, fields);
+ Assert.InRange(fieldTtls[0], TimeSpan.FromMinutes(119).TotalMilliseconds, TimeSpan.FromHours(2).TotalMilliseconds);
+ Assert.InRange(fieldTtls[1], TimeSpan.FromMinutes(119).TotalMilliseconds, TimeSpan.FromHours(2).TotalMilliseconds);
+
+ // testing multiple fields with keepttl
+ result = await db.HashFieldSetAndSetExpiryAsync(hashKey, entries, keepTtl: true);
+ Assert.Equal(1, result);
+ fieldTtls = db.HashFieldGetTimeToLive(hashKey, fields);
+ Assert.InRange(fieldTtls[0], TimeSpan.FromMinutes(119).TotalMilliseconds, TimeSpan.FromHours(2).TotalMilliseconds);
+ Assert.InRange(fieldTtls[1], TimeSpan.FromMinutes(119).TotalMilliseconds, TimeSpan.FromHours(2).TotalMilliseconds);
+
+ // testing with ExpireWhen.Exists
+ db.KeyDelete(hashKey);
+ result = await db.HashFieldSetAndSetExpiryAsync(hashKey, "f1", 1, TimeSpan.FromHours(1), when: When.Exists);
+ Assert.Equal(0, result); // should not set because it doesnt exist
+
+ // testing with ExpireWhen.NotExists
+ result = await db.HashFieldSetAndSetExpiryAsync(hashKey, "f1", 1, TimeSpan.FromHours(1), when: When.NotExists);
+ Assert.Equal(1, result); // should set because it doesnt exist
+ fieldTtl = db.HashFieldGetTimeToLive(hashKey, new RedisValue[] { "f1" })[0];
+ Assert.InRange(fieldTtl, TimeSpan.FromMinutes(59).TotalMilliseconds, TimeSpan.FromHours(1).TotalMilliseconds);
+
+ // testing with ExpireWhen.GreaterThanCurrentExpiry
+ result = await db.HashFieldSetAndSetExpiryAsync(hashKey, "f1", -1, keepTtl: true, when: When.Exists);
+ Assert.Equal(1, result); // should set because it exists
+ fieldTtl = db.HashFieldGetTimeToLive(hashKey, new RedisValue[] { "f1" })[0];
+ Assert.InRange(fieldTtl, TimeSpan.FromMinutes(59).TotalMilliseconds, TimeSpan.FromHours(1).TotalMilliseconds);
+ }
+ [Fact]
+ public void HashFieldGetAndDelete()
+ {
+ using var conn = Create(require: RedisFeatures.v8_0_0_M04);
+ var db = conn.GetDatabase();
+ var hashKey = Me();
+
+ // single field
+ db.HashSet(hashKey, entries);
+ var fieldResult = db.HashFieldGetAndDelete(hashKey, "f1");
+ Assert.Equal(1, fieldResult);
+ Assert.False(db.HashExists(hashKey, "f1"));
+
+ // multiple fields
+ db.HashSet(hashKey, entries);
+ var fieldResults = db.HashFieldGetAndDelete(hashKey, fields);
+ Assert.Equal(values, fieldResults);
+ Assert.False(db.HashExists(hashKey, "f1"));
+ Assert.False(db.HashExists(hashKey, "f2"));
+ }
+
+ [Fact]
+ public async Task HashFieldGetAndDeleteAsync()
+ {
+ await using var conn = Create(require: RedisFeatures.v8_0_0_M04);
+ var db = conn.GetDatabase();
+ var hashKey = Me();
+
+ // single field
+ db.HashSet(hashKey, entries);
+ var fieldResult = await db.HashFieldGetAndDeleteAsync(hashKey, "f1");
+ Assert.Equal(1, fieldResult);
+ Assert.False(db.HashExists(hashKey, "f1"));
+
+ // multiple fields
+ db.HashSet(hashKey, entries);
+ var fieldResults = await db.HashFieldGetAndDeleteAsync(hashKey, fields);
+ Assert.Equal(values, fieldResults);
+ Assert.False(db.HashExists(hashKey, "f1"));
+ Assert.False(db.HashExists(hashKey, "f2"));
+ }
}
diff --git a/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs b/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs
index 612ca182b..571961eb0 100644
--- a/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs
+++ b/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using System.Linq.Expressions;
using System.Net;
using System.Text;
@@ -18,6 +19,14 @@ public sealed class KeyPrefixedDatabaseTests
private readonly IDatabase mock;
private readonly IDatabase prefixed;
+ internal static RedisKey[] IsKeys(params RedisKey[] expected) => IsRaw(expected);
+ internal static RedisValue[] IsValues(params RedisValue[] expected) => IsRaw(expected);
+ private static T[] IsRaw(T[] expected)
+ {
+ Expression> lambda = actual => actual.Length == expected.Length && expected.SequenceEqual(actual);
+ return Arg.Is(lambda);
+ }
+
public KeyPrefixedDatabaseTests()
{
mock = Substitute.For();
@@ -237,10 +246,8 @@ public void HyperLogLogMerge_1()
[Fact]
public void HyperLogLogMerge_2()
{
- RedisKey[] keys = ["a", "b"];
- Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b";
- prefixed.HyperLogLogMerge("destination", keys, CommandFlags.None);
- mock.Received().HyperLogLogMerge("prefix:destination", Arg.Is(valid), CommandFlags.None);
+ prefixed.HyperLogLogMerge("destination", ["a", "b"], CommandFlags.None);
+ mock.Received().HyperLogLogMerge("prefix:destination", IsKeys(["prefix:a", "prefix:b"]), CommandFlags.None);
}
[Fact]
@@ -267,10 +274,8 @@ public void KeyDelete_1()
[Fact]
public void KeyDelete_2()
{
- RedisKey[] keys = ["a", "b"];
- Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b";
- prefixed.KeyDelete(keys, CommandFlags.None);
- mock.Received().KeyDelete(Arg.Is(valid), CommandFlags.None);
+ prefixed.KeyDelete(["a", "b"], CommandFlags.None);
+ mock.Received().KeyDelete(IsKeys(["prefix:a", "prefix:b"]), CommandFlags.None);
}
[Fact]
@@ -594,9 +599,8 @@ public void ScriptEvaluate_1()
byte[] hash = Array.Empty();
RedisValue[] values = Array.Empty();
RedisKey[] keys = ["a", "b"];
- Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b";
prefixed.ScriptEvaluate(hash, keys, values, CommandFlags.None);
- mock.Received().ScriptEvaluate(hash, Arg.Is(valid), values, CommandFlags.None);
+ mock.Received().ScriptEvaluate(hash, IsKeys(["prefix:a", "prefix:b"]), values, CommandFlags.None);
}
[Fact]
@@ -604,9 +608,8 @@ public void ScriptEvaluate_2()
{
RedisValue[] values = Array.Empty();
RedisKey[] keys = ["a", "b"];
- Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b";
prefixed.ScriptEvaluate(script: "script", keys: keys, values: values, flags: CommandFlags.None);
- mock.Received().ScriptEvaluate(script: "script", keys: Arg.Is(valid), values: values, flags: CommandFlags.None);
+ mock.Received().ScriptEvaluate(script: "script", keys: IsKeys(["prefix:a", "prefix:b"]), values: values, flags: CommandFlags.None);
}
[Fact]
@@ -635,9 +638,8 @@ public void SetCombine_1()
public void SetCombine_2()
{
RedisKey[] keys = ["a", "b"];
- Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b";
prefixed.SetCombine(SetOperation.Intersect, keys, CommandFlags.None);
- mock.Received().SetCombine(SetOperation.Intersect, Arg.Is(valid), CommandFlags.None);
+ mock.Received().SetCombine(SetOperation.Intersect, IsKeys(["prefix:a", "prefix:b"]), CommandFlags.None);
}
[Fact]
@@ -651,9 +653,8 @@ public void SetCombineAndStore_1()
public void SetCombineAndStore_2()
{
RedisKey[] keys = ["a", "b"];
- Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b";
prefixed.SetCombineAndStore(SetOperation.Intersect, "destination", keys, CommandFlags.None);
- mock.Received().SetCombineAndStore(SetOperation.Intersect, "prefix:destination", Arg.Is(valid), CommandFlags.None);
+ mock.Received().SetCombineAndStore(SetOperation.Intersect, "prefix:destination", IsKeys(["prefix:a", "prefix:b"]), CommandFlags.None);
}
[Fact]
@@ -674,9 +675,8 @@ public void SetContains_2()
[Fact]
public void SetIntersectionLength()
{
- var keys = new RedisKey[] { "key1", "key2" };
- prefixed.SetIntersectionLength(keys);
- mock.Received().SetIntersectionLength(keys, 0, CommandFlags.None);
+ prefixed.SetIntersectionLength(["key1", "key2"]);
+ mock.Received().SetIntersectionLength(IsKeys(["prefix:key1", "prefix:key2"]), 0, CommandFlags.None);
}
[Fact]
@@ -764,26 +764,24 @@ public void SetScan_Full()
public void Sort()
{
RedisValue[] get = ["a", "#"];
- Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "#";
prefixed.Sort("key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", get, CommandFlags.None);
prefixed.Sort("key", 123, 456, Order.Descending, SortType.Alphabetic, "by", get, CommandFlags.None);
- mock.Received().Sort("prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", Arg.Is(valid), CommandFlags.None);
- mock.Received().Sort("prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "prefix:by", Arg.Is(valid), CommandFlags.None);
+ mock.Received().Sort("prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", IsValues(["prefix:a", "#"]), CommandFlags.None);
+ mock.Received().Sort("prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "prefix:by", IsValues(["prefix:a", "#"]), CommandFlags.None);
}
[Fact]
public void SortAndStore()
{
RedisValue[] get = ["a", "#"];
- Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "#";
prefixed.SortAndStore("destination", "key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", get, CommandFlags.None);
prefixed.SortAndStore("destination", "key", 123, 456, Order.Descending, SortType.Alphabetic, "by", get, CommandFlags.None);
- mock.Received().SortAndStore("prefix:destination", "prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", Arg.Is(valid), CommandFlags.None);
- mock.Received().SortAndStore("prefix:destination", "prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "prefix:by", Arg.Is(valid), CommandFlags.None);
+ mock.Received().SortAndStore("prefix:destination", "prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", IsValues(["prefix:a", "#"]), CommandFlags.None);
+ mock.Received().SortAndStore("prefix:destination", "prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "prefix:by", IsValues(["prefix:a", "#"]), CommandFlags.None);
}
[Fact]
@@ -813,16 +811,15 @@ public void SortedSetAdd_3()
public void SortedSetCombine()
{
RedisKey[] keys = ["a", "b"];
- prefixed.SortedSetCombine(SetOperation.Intersect, keys);
- mock.Received().SortedSetCombine(SetOperation.Intersect, keys, null, Aggregate.Sum, CommandFlags.None);
+ prefixed.SortedSetCombine(SetOperation.Intersect, ["a", "b"]);
+ mock.Received().SortedSetCombine(SetOperation.Intersect, IsKeys(["prefix:a", "prefix:b"]), null, Aggregate.Sum, CommandFlags.None);
}
[Fact]
public void SortedSetCombineWithScores()
{
- RedisKey[] keys = ["a", "b"];
- prefixed.SortedSetCombineWithScores(SetOperation.Intersect, keys);
- mock.Received().SortedSetCombineWithScores(SetOperation.Intersect, keys, null, Aggregate.Sum, CommandFlags.None);
+ prefixed.SortedSetCombineWithScores(SetOperation.Intersect, ["a", "b"]);
+ mock.Received().SortedSetCombineWithScores(SetOperation.Intersect, IsKeys("prefix:a", "prefix:b"), null, Aggregate.Sum, CommandFlags.None);
}
[Fact]
@@ -836,9 +833,8 @@ public void SortedSetCombineAndStore_1()
public void SortedSetCombineAndStore_2()
{
RedisKey[] keys = ["a", "b"];
- Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b";
prefixed.SetCombineAndStore(SetOperation.Intersect, "destination", keys, CommandFlags.None);
- mock.Received().SetCombineAndStore(SetOperation.Intersect, "prefix:destination", Arg.Is(valid), CommandFlags.None);
+ mock.Received().SetCombineAndStore(SetOperation.Intersect, "prefix:destination", IsKeys("prefix:a", "prefix:b"), CommandFlags.None);
}
[Fact]
@@ -858,9 +854,8 @@ public void SortedSetIncrement()
[Fact]
public void SortedSetIntersectionLength()
{
- RedisKey[] keys = ["a", "b"];
- prefixed.SortedSetIntersectionLength(keys, 1, CommandFlags.None);
- mock.Received().SortedSetIntersectionLength(keys, 1, CommandFlags.None);
+ prefixed.SortedSetIntersectionLength(["a", "b"], 1, CommandFlags.None);
+ mock.Received().SortedSetIntersectionLength(IsKeys("prefix:a", "prefix:b"), 1, CommandFlags.None);
}
[Fact]
@@ -1255,45 +1250,40 @@ public void StringBitOperation_1()
public void StringBitOperation_2()
{
RedisKey[] keys = ["a", "b"];
- Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b";
prefixed.StringBitOperation(Bitwise.Xor, "destination", keys, CommandFlags.None);
- mock.Received().StringBitOperation(Bitwise.Xor, "prefix:destination", Arg.Is(valid), CommandFlags.None);
+ mock.Received().StringBitOperation(Bitwise.Xor, "prefix:destination", IsKeys("prefix:a", "prefix:b"), CommandFlags.None);
}
[Fact]
public void StringBitOperation_Diff()
{
RedisKey[] keys = ["x", "y1", "y2"];
- Expression> valid = _ => _.Length == 3 && _[0] == "prefix:x" && _[1] == "prefix:y1" && _[2] == "prefix:y2";
prefixed.StringBitOperation(Bitwise.Diff, "destination", keys, CommandFlags.None);
- mock.Received().StringBitOperation(Bitwise.Diff, "prefix:destination", Arg.Is(valid), CommandFlags.None);
+ mock.Received().StringBitOperation(Bitwise.Diff, "prefix:destination", IsKeys("prefix:x", "prefix:y1", "prefix:y2"), CommandFlags.None);
}
[Fact]
public void StringBitOperation_Diff1()
{
RedisKey[] keys = ["x", "y1", "y2"];
- Expression> valid = _ => _.Length == 3 && _[0] == "prefix:x" && _[1] == "prefix:y1" && _[2] == "prefix:y2";
prefixed.StringBitOperation(Bitwise.Diff1, "destination", keys, CommandFlags.None);
- mock.Received().StringBitOperation(Bitwise.Diff1, "prefix:destination", Arg.Is(valid), CommandFlags.None);
+ mock.Received().StringBitOperation(Bitwise.Diff1, "prefix:destination", IsKeys("prefix:x", "prefix:y1", "prefix:y2"), CommandFlags.None);
}
[Fact]
public void StringBitOperation_AndOr()
{
RedisKey[] keys = ["x", "y1", "y2"];
- Expression> valid = _ => _.Length == 3 && _[0] == "prefix:x" && _[1] == "prefix:y1" && _[2] == "prefix:y2";
prefixed.StringBitOperation(Bitwise.AndOr, "destination", keys, CommandFlags.None);
- mock.Received().StringBitOperation(Bitwise.AndOr, "prefix:destination", Arg.Is(valid), CommandFlags.None);
+ mock.Received().StringBitOperation(Bitwise.AndOr, "prefix:destination", IsKeys("prefix:x", "prefix:y1", "prefix:y2"), CommandFlags.None);
}
[Fact]
public void StringBitOperation_One()
{
RedisKey[] keys = ["a", "b", "c"];
- Expression> valid = _ => _.Length == 3 && _[0] == "prefix:a" && _[1] == "prefix:b" && _[2] == "prefix:c";
prefixed.StringBitOperation(Bitwise.One, "destination", keys, CommandFlags.None);
- mock.Received().StringBitOperation(Bitwise.One, "prefix:destination", Arg.Is(valid), CommandFlags.None);
+ mock.Received().StringBitOperation(Bitwise.One, "prefix:destination", IsKeys("prefix:a", "prefix:b", "prefix:c"), CommandFlags.None);
}
[Fact]
@@ -1335,9 +1325,8 @@ public void StringGet_1()
public void StringGet_2()
{
RedisKey[] keys = ["a", "b"];
- Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b";
prefixed.StringGet(keys, CommandFlags.None);
- mock.Received().StringGet(Arg.Is(valid), CommandFlags.None);
+ mock.Received().StringGet(IsKeys("prefix:a", "prefix:b"), CommandFlags.None);
}
[Fact]
@@ -1442,4 +1431,364 @@ public void StringSetRange()
prefixed.StringSetRange("key", 123, "value", CommandFlags.None);
mock.Received().StringSetRange("prefix:key", 123, "value", CommandFlags.None);
}
+
+ [Fact]
+ public void Execute_1()
+ {
+ prefixed.Execute("CUSTOM", "arg1", (RedisKey)"arg2");
+ mock.Received().Execute("CUSTOM", Arg.Is