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: | ------------ | ----------------- | ----- | | [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis MyGet](https://img.shields.io/myget/stackoverflow/vpre/StackExchange.Redis.svg)](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(args => args.Length == 2 && args[0].Equals("arg1") && args[1].Equals((RedisKey)"prefix:arg2")), CommandFlags.None); + } + + [Fact] + public void Execute_2() + { + var args = new List { "arg1", (RedisKey)"arg2" }; + prefixed.Execute("CUSTOM", args, CommandFlags.None); + mock.Received().Execute("CUSTOM", Arg.Is>(a => a.Count == 2 && a.ElementAt(0).Equals("arg1") && a.ElementAt(1).Equals((RedisKey)"prefix:arg2"))!, CommandFlags.None); + } + + [Fact] + public void GeoAdd_1() + { + prefixed.GeoAdd("key", 1.23, 4.56, "member", CommandFlags.None); + mock.Received().GeoAdd("prefix:key", 1.23, 4.56, "member", CommandFlags.None); + } + + [Fact] + public void GeoAdd_2() + { + var geoEntry = new GeoEntry(1.23, 4.56, "member"); + prefixed.GeoAdd("key", geoEntry, CommandFlags.None); + mock.Received().GeoAdd("prefix:key", geoEntry, CommandFlags.None); + } + + [Fact] + public void GeoAdd_3() + { + var geoEntries = new GeoEntry[] { new GeoEntry(1.23, 4.56, "member1") }; + prefixed.GeoAdd("key", geoEntries, CommandFlags.None); + mock.Received().GeoAdd("prefix:key", geoEntries, CommandFlags.None); + } + + [Fact] + public void GeoRemove() + { + prefixed.GeoRemove("key", "member", CommandFlags.None); + mock.Received().GeoRemove("prefix:key", "member", CommandFlags.None); + } + + [Fact] + public void GeoDistance() + { + prefixed.GeoDistance("key", "member1", "member2", GeoUnit.Meters, CommandFlags.None); + mock.Received().GeoDistance("prefix:key", "member1", "member2", GeoUnit.Meters, CommandFlags.None); + } + + [Fact] + public void GeoHash_1() + { + prefixed.GeoHash("key", "member", CommandFlags.None); + mock.Received().GeoHash("prefix:key", "member", CommandFlags.None); + } + + [Fact] + public void GeoHash_2() + { + var members = new RedisValue[] { "member1", "member2" }; + prefixed.GeoHash("key", members, CommandFlags.None); + mock.Received().GeoHash("prefix:key", members, CommandFlags.None); + } + + [Fact] + public void GeoPosition_1() + { + prefixed.GeoPosition("key", "member", CommandFlags.None); + mock.Received().GeoPosition("prefix:key", "member", CommandFlags.None); + } + + [Fact] + public void GeoPosition_2() + { + var members = new RedisValue[] { "member1", "member2" }; + prefixed.GeoPosition("key", members, CommandFlags.None); + mock.Received().GeoPosition("prefix:key", members, CommandFlags.None); + } + + [Fact] + public void GeoRadius_1() + { + prefixed.GeoRadius("key", "member", 100, GeoUnit.Meters, 10, Order.Ascending, GeoRadiusOptions.Default, CommandFlags.None); + mock.Received().GeoRadius("prefix:key", "member", 100, GeoUnit.Meters, 10, Order.Ascending, GeoRadiusOptions.Default, CommandFlags.None); + } + + [Fact] + public void GeoRadius_2() + { + prefixed.GeoRadius("key", 1.23, 4.56, 100, GeoUnit.Meters, 10, Order.Ascending, GeoRadiusOptions.Default, CommandFlags.None); + mock.Received().GeoRadius("prefix:key", 1.23, 4.56, 100, GeoUnit.Meters, 10, Order.Ascending, GeoRadiusOptions.Default, CommandFlags.None); + } + + [Fact] + public void GeoSearch_1() + { + var shape = new GeoSearchCircle(100, GeoUnit.Meters); + prefixed.GeoSearch("key", "member", shape, 10, true, Order.Ascending, GeoRadiusOptions.Default, CommandFlags.None); + mock.Received().GeoSearch("prefix:key", "member", shape, 10, true, Order.Ascending, GeoRadiusOptions.Default, CommandFlags.None); + } + + [Fact] + public void GeoSearch_2() + { + var shape = new GeoSearchCircle(100, GeoUnit.Meters); + prefixed.GeoSearch("key", 1.23, 4.56, shape, 10, true, Order.Ascending, GeoRadiusOptions.Default, CommandFlags.None); + mock.Received().GeoSearch("prefix:key", 1.23, 4.56, shape, 10, true, Order.Ascending, GeoRadiusOptions.Default, CommandFlags.None); + } + + [Fact] + public void GeoSearchAndStore_1() + { + var shape = new GeoSearchCircle(100, GeoUnit.Meters); + prefixed.GeoSearchAndStore("source", "destination", "member", shape, 10, true, Order.Ascending, false, CommandFlags.None); + mock.Received().GeoSearchAndStore("prefix:source", "prefix:destination", "member", shape, 10, true, Order.Ascending, false, CommandFlags.None); + } + + [Fact] + public void GeoSearchAndStore_2() + { + var shape = new GeoSearchCircle(100, GeoUnit.Meters); + prefixed.GeoSearchAndStore("source", "destination", 1.23, 4.56, shape, 10, true, Order.Ascending, false, CommandFlags.None); + mock.Received().GeoSearchAndStore("prefix:source", "prefix:destination", 1.23, 4.56, shape, 10, true, Order.Ascending, false, CommandFlags.None); + } + + [Fact] + public void HashFieldExpire_1() + { + var hashFields = new RedisValue[] { "field1", "field2" }; + var expiry = TimeSpan.FromSeconds(60); + prefixed.HashFieldExpire("key", hashFields, expiry, ExpireWhen.Always, CommandFlags.None); + mock.Received().HashFieldExpire("prefix:key", hashFields, expiry, ExpireWhen.Always, CommandFlags.None); + } + + [Fact] + public void HashFieldExpire_2() + { + var hashFields = new RedisValue[] { "field1", "field2" }; + var expiry = DateTime.Now.AddMinutes(1); + prefixed.HashFieldExpire("key", hashFields, expiry, ExpireWhen.Always, CommandFlags.None); + mock.Received().HashFieldExpire("prefix:key", hashFields, expiry, ExpireWhen.Always, CommandFlags.None); + } + + [Fact] + public void HashFieldGetExpireDateTime() + { + var hashFields = new RedisValue[] { "field1", "field2" }; + prefixed.HashFieldGetExpireDateTime("key", hashFields, CommandFlags.None); + mock.Received().HashFieldGetExpireDateTime("prefix:key", hashFields, CommandFlags.None); + } + + [Fact] + public void HashFieldPersist() + { + var hashFields = new RedisValue[] { "field1", "field2" }; + prefixed.HashFieldPersist("key", hashFields, CommandFlags.None); + mock.Received().HashFieldPersist("prefix:key", hashFields, CommandFlags.None); + } + + [Fact] + public void HashFieldGetTimeToLive() + { + var hashFields = new RedisValue[] { "field1", "field2" }; + prefixed.HashFieldGetTimeToLive("key", hashFields, CommandFlags.None); + mock.Received().HashFieldGetTimeToLive("prefix:key", hashFields, CommandFlags.None); + } + + [Fact] + public void HashGetLease() + { + prefixed.HashGetLease("key", "field", CommandFlags.None); + mock.Received().HashGetLease("prefix:key", "field", CommandFlags.None); + } + + [Fact] + public void HashFieldGetAndDelete_1() + { + prefixed.HashFieldGetAndDelete("key", "field", CommandFlags.None); + mock.Received().HashFieldGetAndDelete("prefix:key", "field", CommandFlags.None); + } + + [Fact] + public void HashFieldGetAndDelete_2() + { + var hashFields = new RedisValue[] { "field1", "field2" }; + prefixed.HashFieldGetAndDelete("key", hashFields, CommandFlags.None); + mock.Received().HashFieldGetAndDelete("prefix:key", hashFields, CommandFlags.None); + } + + [Fact] + public void HashFieldGetLeaseAndDelete() + { + prefixed.HashFieldGetLeaseAndDelete("key", "field", CommandFlags.None); + mock.Received().HashFieldGetLeaseAndDelete("prefix:key", "field", CommandFlags.None); + } + + [Fact] + public void HashFieldGetAndSetExpiry_1() + { + var expiry = TimeSpan.FromMinutes(5); + prefixed.HashFieldGetAndSetExpiry("key", "field", expiry, false, CommandFlags.None); + mock.Received().HashFieldGetAndSetExpiry("prefix:key", "field", expiry, false, CommandFlags.None); + } + + [Fact] + public void HashFieldGetAndSetExpiry_2() + { + var expiry = DateTime.Now.AddMinutes(5); + prefixed.HashFieldGetAndSetExpiry("key", "field", expiry, CommandFlags.None); + mock.Received().HashFieldGetAndSetExpiry("prefix:key", "field", expiry, CommandFlags.None); + } + + [Fact] + public void HashFieldGetLeaseAndSetExpiry_1() + { + var expiry = TimeSpan.FromMinutes(5); + prefixed.HashFieldGetLeaseAndSetExpiry("key", "field", expiry, false, CommandFlags.None); + mock.Received().HashFieldGetLeaseAndSetExpiry("prefix:key", "field", expiry, false, CommandFlags.None); + } + + [Fact] + public void HashFieldGetLeaseAndSetExpiry_2() + { + var expiry = DateTime.Now.AddMinutes(5); + prefixed.HashFieldGetLeaseAndSetExpiry("key", "field", expiry, CommandFlags.None); + mock.Received().HashFieldGetLeaseAndSetExpiry("prefix:key", "field", expiry, CommandFlags.None); + } + [Fact] + public void StringGetLease() + { + prefixed.StringGetLease("key", CommandFlags.None); + mock.Received().StringGetLease("prefix:key", CommandFlags.None); + } + + [Fact] + public void StringGetSetExpiry_1() + { + var expiry = TimeSpan.FromMinutes(5); + prefixed.StringGetSetExpiry("key", expiry, CommandFlags.None); + mock.Received().StringGetSetExpiry("prefix:key", expiry, CommandFlags.None); + } + + [Fact] + public void StringGetSetExpiry_2() + { + var expiry = DateTime.Now.AddMinutes(5); + prefixed.StringGetSetExpiry("key", expiry, CommandFlags.None); + mock.Received().StringGetSetExpiry("prefix:key", expiry, CommandFlags.None); + } + + [Fact] + public void StringSetAndGet_1() + { + var expiry = TimeSpan.FromMinutes(5); + prefixed.StringSetAndGet("key", "value", expiry, When.Always, CommandFlags.None); + mock.Received().StringSetAndGet("prefix:key", "value", expiry, When.Always, CommandFlags.None); + } + + [Fact] + public void StringSetAndGet_2() + { + var expiry = TimeSpan.FromMinutes(5); + prefixed.StringSetAndGet("key", "value", expiry, false, When.Always, CommandFlags.None); + mock.Received().StringSetAndGet("prefix:key", "value", expiry, false, When.Always, CommandFlags.None); + } + [Fact] + public void StringLongestCommonSubsequence() + { + prefixed.StringLongestCommonSubsequence("key1", "key2", CommandFlags.None); + mock.Received().StringLongestCommonSubsequence("prefix:key1", "prefix:key2", CommandFlags.None); + } + + [Fact] + public void StringLongestCommonSubsequenceLength() + { + prefixed.StringLongestCommonSubsequenceLength("key1", "key2", CommandFlags.None); + mock.Received().StringLongestCommonSubsequenceLength("prefix:key1", "prefix:key2", CommandFlags.None); + } + + [Fact] + public void StringLongestCommonSubsequenceWithMatches() + { + prefixed.StringLongestCommonSubsequenceWithMatches("key1", "key2", 5, CommandFlags.None); + mock.Received().StringLongestCommonSubsequenceWithMatches("prefix:key1", "prefix:key2", 5, CommandFlags.None); + } + [Fact] + public void IsConnected() + { + prefixed.IsConnected("key", CommandFlags.None); + mock.Received().IsConnected("prefix:key", CommandFlags.None); + } + [Fact] + public void StreamAdd_WithTrimMode_1() + { + prefixed.StreamAdd("key", "field", "value", "*", 1000, false, 100, StreamTrimMode.KeepReferences, CommandFlags.None); + mock.Received().StreamAdd("prefix:key", "field", "value", "*", 1000, false, 100, StreamTrimMode.KeepReferences, CommandFlags.None); + } + + [Fact] + public void StreamAdd_WithTrimMode_2() + { + var fields = new NameValueEntry[] { new NameValueEntry("field", "value") }; + prefixed.StreamAdd("key", fields, "*", 1000, false, 100, StreamTrimMode.KeepReferences, CommandFlags.None); + mock.Received().StreamAdd("prefix:key", fields, "*", 1000, false, 100, StreamTrimMode.KeepReferences, CommandFlags.None); + } + + [Fact] + public void StreamTrim_WithMode() + { + prefixed.StreamTrim("key", 1000, false, 100, StreamTrimMode.KeepReferences, CommandFlags.None); + mock.Received().StreamTrim("prefix:key", 1000, false, 100, StreamTrimMode.KeepReferences, CommandFlags.None); + } + + [Fact] + public void StreamTrimByMinId_WithMode() + { + prefixed.StreamTrimByMinId("key", "1111111111", false, 100, StreamTrimMode.KeepReferences, CommandFlags.None); + mock.Received().StreamTrimByMinId("prefix:key", "1111111111", false, 100, StreamTrimMode.KeepReferences, CommandFlags.None); + } + + [Fact] + public void StreamReadGroup_WithNoAck_1() + { + prefixed.StreamReadGroup("key", "group", "consumer", "0-0", 10, true, CommandFlags.None); + mock.Received().StreamReadGroup("prefix:key", "group", "consumer", "0-0", 10, true, CommandFlags.None); + } + + [Fact] + public void StreamReadGroup_WithNoAck_2() + { + var streamPositions = new StreamPosition[] { new StreamPosition("key", "0-0") }; + prefixed.StreamReadGroup(streamPositions, "group", "consumer", 10, true, CommandFlags.None); + mock.Received().StreamReadGroup(streamPositions, "group", "consumer", 10, true, CommandFlags.None); + } + + [Fact] + public void StreamTrim_Simple() + { + prefixed.StreamTrim("key", 1000, true, CommandFlags.None); + mock.Received().StreamTrim("prefix:key", 1000, true, CommandFlags.None); + } + + [Fact] + public void StreamReadGroup_Simple_1() + { + prefixed.StreamReadGroup("key", "group", "consumer", "0-0", 10, CommandFlags.None); + mock.Received().StreamReadGroup("prefix:key", "group", "consumer", "0-0", 10, CommandFlags.None); + } + + [Fact] + public void StreamReadGroup_Simple_2() + { + var streamPositions = new StreamPosition[] { new StreamPosition("key", "0-0") }; + prefixed.StreamReadGroup(streamPositions, "group", "consumer", 10, CommandFlags.None); + mock.Received().StreamReadGroup(streamPositions, "group", "consumer", 10, CommandFlags.None); + } } diff --git a/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs b/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs index b8cf9a4b9..cbef3d9e7 100644 --- a/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Linq.Expressions; using System.Net; using System.Text; @@ -7,6 +8,7 @@ using NSubstitute; using StackExchange.Redis.KeyspaceIsolation; using Xunit; +using static StackExchange.Redis.Tests.KeyPrefixedDatabaseTests; // for IsKeys etc namespace StackExchange.Redis.Tests { @@ -177,9 +179,8 @@ public async Task HyperLogLogMergeAsync_1() public async Task HyperLogLogMergeAsync_2() { RedisKey[] keys = ["a", "b"]; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; await prefixed.HyperLogLogMergeAsync("destination", keys, CommandFlags.None); - await mock.Received().HyperLogLogMergeAsync("prefix:destination", Arg.Is(valid), CommandFlags.None); + await mock.Received().HyperLogLogMergeAsync("prefix:destination", IsKeys("prefix:a", "prefix:b"), CommandFlags.None); } [Fact] @@ -214,9 +215,8 @@ public async Task KeyDeleteAsync_1() public async Task KeyDeleteAsync_2() { RedisKey[] keys = ["a", "b"]; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; await prefixed.KeyDeleteAsync(keys, CommandFlags.None); - await mock.Received().KeyDeleteAsync(Arg.Is(valid), CommandFlags.None); + await mock.Received().KeyDeleteAsync(IsKeys("prefix:a", "prefix:b"), CommandFlags.None); } [Fact] @@ -538,9 +538,8 @@ public async Task ScriptEvaluateAsync_1() byte[] hash = Array.Empty(); RedisValue[] values = Array.Empty(); RedisKey[] keys = ["a", "b"]; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; await prefixed.ScriptEvaluateAsync(hash, keys, values, CommandFlags.None); - await mock.Received().ScriptEvaluateAsync(hash, Arg.Is(valid), values, CommandFlags.None); + await mock.Received().ScriptEvaluateAsync(hash, IsKeys("prefix:a", "prefix:b"), values, CommandFlags.None); } [Fact] @@ -548,9 +547,8 @@ public async Task ScriptEvaluateAsync_2() { RedisValue[] values = Array.Empty(); RedisKey[] keys = ["a", "b"]; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; await prefixed.ScriptEvaluateAsync("script", keys, values, CommandFlags.None); - await mock.Received().ScriptEvaluateAsync(script: "script", keys: Arg.Is(valid), values: values, flags: CommandFlags.None); + await mock.Received().ScriptEvaluateAsync(script: "script", keys: IsKeys("prefix:a", "prefix:b"), values: values, flags: CommandFlags.None); } [Fact] @@ -579,9 +577,8 @@ public async Task SetCombineAndStoreAsync_1() public async Task SetCombineAndStoreAsync_2() { RedisKey[] keys = ["a", "b"]; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; await prefixed.SetCombineAndStoreAsync(SetOperation.Intersect, "destination", keys, CommandFlags.None); - await mock.Received().SetCombineAndStoreAsync(SetOperation.Intersect, "prefix:destination", Arg.Is(valid), CommandFlags.None); + await mock.Received().SetCombineAndStoreAsync(SetOperation.Intersect, "prefix:destination", IsKeys("prefix:a", "prefix:b"), CommandFlags.None); } [Fact] @@ -595,9 +592,8 @@ public async Task SetCombineAsync_1() public async Task SetCombineAsync_2() { RedisKey[] keys = ["a", "b"]; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; await prefixed.SetCombineAsync(SetOperation.Intersect, keys, CommandFlags.None); - await mock.Received().SetCombineAsync(SetOperation.Intersect, Arg.Is(valid), CommandFlags.None); + await mock.Received().SetCombineAsync(SetOperation.Intersect, IsKeys("prefix:a", "prefix:b"), CommandFlags.None); } [Fact] @@ -618,9 +614,8 @@ public async Task SetContainsAsync_2() [Fact] public async Task SetIntersectionLengthAsync() { - var keys = new RedisKey[] { "key1", "key2" }; - await prefixed.SetIntersectionLengthAsync(keys); - await mock.Received().SetIntersectionLengthAsync(keys, 0, CommandFlags.None); + await prefixed.SetIntersectionLengthAsync(["key1", "key2"]); + await mock.Received().SetIntersectionLengthAsync(IsKeys("prefix:key1", "prefix:key2"), 0, CommandFlags.None); } [Fact] @@ -694,26 +689,24 @@ public async Task SetRemoveAsync_2() public async Task SortAndStoreAsync() { RedisValue[] get = ["a", "#"]; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "#"; await prefixed.SortAndStoreAsync("destination", "key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", get, CommandFlags.None); await prefixed.SortAndStoreAsync("destination", "key", 123, 456, Order.Descending, SortType.Alphabetic, "by", get, CommandFlags.None); - await mock.Received().SortAndStoreAsync("prefix:destination", "prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", Arg.Is(valid), CommandFlags.None); - await mock.Received().SortAndStoreAsync("prefix:destination", "prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "prefix:by", Arg.Is(valid), CommandFlags.None); + await mock.Received().SortAndStoreAsync("prefix:destination", "prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", IsValues("prefix:a", "#"), CommandFlags.None); + await mock.Received().SortAndStoreAsync("prefix:destination", "prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "prefix:by", IsValues("prefix:a", "#"), CommandFlags.None); } [Fact] public async Task SortAsync() { RedisValue[] get = ["a", "#"]; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "#"; await prefixed.SortAsync("key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", get, CommandFlags.None); await prefixed.SortAsync("key", 123, 456, Order.Descending, SortType.Alphabetic, "by", get, CommandFlags.None); - await mock.Received().SortAsync("prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", Arg.Is(valid), CommandFlags.None); - await mock.Received().SortAsync("prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "prefix:by", Arg.Is(valid), CommandFlags.None); + await mock.Received().SortAsync("prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", IsValues("prefix:a", "#"), CommandFlags.None); + await mock.Received().SortAsync("prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "prefix:by", IsValues("prefix:a", "#"), CommandFlags.None); } [Fact] @@ -742,17 +735,15 @@ public async Task SortedSetAddAsync_3() [Fact] public async Task SortedSetCombineAsync() { - RedisKey[] keys = ["a", "b"]; - await prefixed.SortedSetCombineAsync(SetOperation.Intersect, keys); - await mock.Received().SortedSetCombineAsync(SetOperation.Intersect, keys, null, Aggregate.Sum, CommandFlags.None); + await prefixed.SortedSetCombineAsync(SetOperation.Intersect, ["a", "b"]); + await mock.Received().SortedSetCombineAsync(SetOperation.Intersect, IsKeys("prefix:a", "prefix:b"), null, Aggregate.Sum, CommandFlags.None); } [Fact] public async Task SortedSetCombineWithScoresAsync() { - RedisKey[] keys = ["a", "b"]; - await prefixed.SortedSetCombineWithScoresAsync(SetOperation.Intersect, keys); - await mock.Received().SortedSetCombineWithScoresAsync(SetOperation.Intersect, keys, null, Aggregate.Sum, CommandFlags.None); + await prefixed.SortedSetCombineWithScoresAsync(SetOperation.Intersect, ["a", "b"]); + await mock.Received().SortedSetCombineWithScoresAsync(SetOperation.Intersect, IsKeys("prefix:a", "prefix:b"), null, Aggregate.Sum, CommandFlags.None); } [Fact] @@ -766,9 +757,8 @@ public async Task SortedSetCombineAndStoreAsync_1() public async Task SortedSetCombineAndStoreAsync_2() { RedisKey[] keys = ["a", "b"]; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; await prefixed.SetCombineAndStoreAsync(SetOperation.Intersect, "destination", keys, CommandFlags.None); - await mock.Received().SetCombineAndStoreAsync(SetOperation.Intersect, "prefix:destination", Arg.Is(valid), CommandFlags.None); + await mock.Received().SetCombineAndStoreAsync(SetOperation.Intersect, "prefix:destination", IsKeys("prefix:a", "prefix:b"), CommandFlags.None); } [Fact] @@ -788,9 +778,8 @@ public async Task SortedSetIncrementAsync() [Fact] public async Task SortedSetIntersectionLengthAsync() { - RedisKey[] keys = ["a", "b"]; - await prefixed.SortedSetIntersectionLengthAsync(keys, 1, CommandFlags.None); - await mock.Received().SortedSetIntersectionLengthAsync(keys, 1, CommandFlags.None); + await prefixed.SortedSetIntersectionLengthAsync(["a", "b"], 1, CommandFlags.None); + await mock.Received().SortedSetIntersectionLengthAsync(IsKeys("prefix:a", "prefix:b"), 1, CommandFlags.None); } [Fact] @@ -1171,45 +1160,40 @@ public async Task StringBitOperationAsync_1() public async Task StringBitOperationAsync_2() { RedisKey[] keys = ["a", "b"]; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; await prefixed.StringBitOperationAsync(Bitwise.Xor, "destination", keys, CommandFlags.None); - await mock.Received().StringBitOperationAsync(Bitwise.Xor, "prefix:destination", Arg.Is(valid), CommandFlags.None); + await mock.Received().StringBitOperationAsync(Bitwise.Xor, "prefix:destination", IsKeys("prefix:a", "prefix:b"), CommandFlags.None); } [Fact] public async Task StringBitOperationAsync_Diff() { RedisKey[] keys = ["x", "y1", "y2"]; - Expression> valid = _ => _.Length == 3 && _[0] == "prefix:x" && _[1] == "prefix:y1" && _[2] == "prefix:y2"; await prefixed.StringBitOperationAsync(Bitwise.Diff, "destination", keys, CommandFlags.None); - await mock.Received().StringBitOperationAsync(Bitwise.Diff, "prefix:destination", Arg.Is(valid), CommandFlags.None); + await mock.Received().StringBitOperationAsync(Bitwise.Diff, "prefix:destination", IsKeys("prefix:x", "prefix:y1", "prefix:y2"), CommandFlags.None); } [Fact] public async Task StringBitOperationAsync_Diff1() { RedisKey[] keys = ["x", "y1", "y2"]; - Expression> valid = _ => _.Length == 3 && _[0] == "prefix:x" && _[1] == "prefix:y1" && _[2] == "prefix:y2"; await prefixed.StringBitOperationAsync(Bitwise.Diff1, "destination", keys, CommandFlags.None); - await mock.Received().StringBitOperationAsync(Bitwise.Diff1, "prefix:destination", Arg.Is(valid), CommandFlags.None); + await mock.Received().StringBitOperationAsync(Bitwise.Diff1, "prefix:destination", IsKeys("prefix:x", "prefix:y1", "prefix:y2"), CommandFlags.None); } [Fact] public async Task StringBitOperationAsync_AndOr() { RedisKey[] keys = ["x", "y1", "y2"]; - Expression> valid = _ => _.Length == 3 && _[0] == "prefix:x" && _[1] == "prefix:y1" && _[2] == "prefix:y2"; await prefixed.StringBitOperationAsync(Bitwise.AndOr, "destination", keys, CommandFlags.None); - await mock.Received().StringBitOperationAsync(Bitwise.AndOr, "prefix:destination", Arg.Is(valid), CommandFlags.None); + await mock.Received().StringBitOperationAsync(Bitwise.AndOr, "prefix:destination", IsKeys("prefix:x", "prefix:y1", "prefix:y2"), CommandFlags.None); } [Fact] public async Task StringBitOperationAsync_One() { RedisKey[] keys = ["a", "b", "c"]; - Expression> valid = _ => _.Length == 3 && _[0] == "prefix:a" && _[1] == "prefix:b" && _[2] == "prefix:c"; await prefixed.StringBitOperationAsync(Bitwise.One, "destination", keys, CommandFlags.None); - await mock.Received().StringBitOperationAsync(Bitwise.One, "prefix:destination", Arg.Is(valid), CommandFlags.None); + await mock.Received().StringBitOperationAsync(Bitwise.One, "prefix:destination", IsKeys("prefix:a", "prefix:b", "prefix:c"), CommandFlags.None); } [Fact] @@ -1251,9 +1235,8 @@ public async Task StringGetAsync_1() public async Task StringGetAsync_2() { RedisKey[] keys = ["a", "b"]; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; await prefixed.StringGetAsync(keys, CommandFlags.None); - await mock.Received().StringGetAsync(Arg.Is(valid), CommandFlags.None); + await mock.Received().StringGetAsync(IsKeys("prefix:a", "prefix:b"), CommandFlags.None); } [Fact] @@ -1370,9 +1353,392 @@ public async Task KeyTouchAsync_1() public async Task KeyTouchAsync_2() { RedisKey[] keys = ["a", "b"]; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; await prefixed.KeyTouchAsync(keys, CommandFlags.None); - await mock.Received().KeyTouchAsync(Arg.Is(valid), CommandFlags.None); + await mock.Received().KeyTouchAsync(IsKeys("prefix:a", "prefix:b"), CommandFlags.None); + } + [Fact] + public async Task ExecuteAsync_1() + { + await prefixed.ExecuteAsync("CUSTOM", "arg1", (RedisKey)"arg2"); + await mock.Received().ExecuteAsync("CUSTOM", Arg.Is(args => args.Length == 2 && args[0].Equals("arg1") && args[1].Equals((RedisKey)"prefix:arg2")), CommandFlags.None); + } + + [Fact] + public async Task ExecuteAsync_2() + { + var args = new List { "arg1", (RedisKey)"arg2" }; + await prefixed.ExecuteAsync("CUSTOM", args, CommandFlags.None); + await mock.Received().ExecuteAsync("CUSTOM", Arg.Is?>(a => a != null && a.Count == 2 && a.ElementAt(0).Equals("arg1") && a.ElementAt(1).Equals((RedisKey)"prefix:arg2")), CommandFlags.None); + } + [Fact] + public async Task GeoAddAsync_1() + { + await prefixed.GeoAddAsync("key", 1.23, 4.56, "member", CommandFlags.None); + await mock.Received().GeoAddAsync("prefix:key", 1.23, 4.56, "member", CommandFlags.None); + } + + [Fact] + public async Task GeoAddAsync_2() + { + var geoEntry = new GeoEntry(1.23, 4.56, "member"); + await prefixed.GeoAddAsync("key", geoEntry, CommandFlags.None); + await mock.Received().GeoAddAsync("prefix:key", geoEntry, CommandFlags.None); + } + + [Fact] + public async Task GeoAddAsync_3() + { + var geoEntries = new GeoEntry[] { new GeoEntry(1.23, 4.56, "member1") }; + await prefixed.GeoAddAsync("key", geoEntries, CommandFlags.None); + await mock.Received().GeoAddAsync("prefix:key", geoEntries, CommandFlags.None); + } + + [Fact] + public async Task GeoRemoveAsync() + { + await prefixed.GeoRemoveAsync("key", "member", CommandFlags.None); + await mock.Received().GeoRemoveAsync("prefix:key", "member", CommandFlags.None); + } + + [Fact] + public async Task GeoDistanceAsync() + { + await prefixed.GeoDistanceAsync("key", "member1", "member2", GeoUnit.Meters, CommandFlags.None); + await mock.Received().GeoDistanceAsync("prefix:key", "member1", "member2", GeoUnit.Meters, CommandFlags.None); + } + + [Fact] + public async Task GeoHashAsync_1() + { + await prefixed.GeoHashAsync("key", "member", CommandFlags.None); + await mock.Received().GeoHashAsync("prefix:key", "member", CommandFlags.None); + } + + [Fact] + public async Task GeoHashAsync_2() + { + var members = new RedisValue[] { "member1", "member2" }; + await prefixed.GeoHashAsync("key", members, CommandFlags.None); + await mock.Received().GeoHashAsync("prefix:key", members, CommandFlags.None); + } + + [Fact] + public async Task GeoPositionAsync_1() + { + await prefixed.GeoPositionAsync("key", "member", CommandFlags.None); + await mock.Received().GeoPositionAsync("prefix:key", "member", CommandFlags.None); + } + + [Fact] + public async Task GeoPositionAsync_2() + { + var members = new RedisValue[] { "member1", "member2" }; + await prefixed.GeoPositionAsync("key", members, CommandFlags.None); + await mock.Received().GeoPositionAsync("prefix:key", members, CommandFlags.None); + } + + [Fact] + public async Task GeoRadiusAsync_1() + { + await prefixed.GeoRadiusAsync("key", "member", 100, GeoUnit.Meters, 10, Order.Ascending, GeoRadiusOptions.Default, CommandFlags.None); + await mock.Received().GeoRadiusAsync("prefix:key", "member", 100, GeoUnit.Meters, 10, Order.Ascending, GeoRadiusOptions.Default, CommandFlags.None); + } + + [Fact] + public async Task GeoRadiusAsync_2() + { + await prefixed.GeoRadiusAsync("key", 1.23, 4.56, 100, GeoUnit.Meters, 10, Order.Ascending, GeoRadiusOptions.Default, CommandFlags.None); + await mock.Received().GeoRadiusAsync("prefix:key", 1.23, 4.56, 100, GeoUnit.Meters, 10, Order.Ascending, GeoRadiusOptions.Default, CommandFlags.None); + } + + [Fact] + public async Task GeoSearchAsync_1() + { + var shape = new GeoSearchCircle(100, GeoUnit.Meters); + await prefixed.GeoSearchAsync("key", "member", shape, 10, true, Order.Ascending, GeoRadiusOptions.Default, CommandFlags.None); + await mock.Received().GeoSearchAsync("prefix:key", "member", shape, 10, true, Order.Ascending, GeoRadiusOptions.Default, CommandFlags.None); + } + + [Fact] + public async Task GeoSearchAsync_2() + { + var shape = new GeoSearchCircle(100, GeoUnit.Meters); + await prefixed.GeoSearchAsync("key", 1.23, 4.56, shape, 10, true, Order.Ascending, GeoRadiusOptions.Default, CommandFlags.None); + await mock.Received().GeoSearchAsync("prefix:key", 1.23, 4.56, shape, 10, true, Order.Ascending, GeoRadiusOptions.Default, CommandFlags.None); + } + + [Fact] + public async Task GeoSearchAndStoreAsync_1() + { + var shape = new GeoSearchCircle(100, GeoUnit.Meters); + await prefixed.GeoSearchAndStoreAsync("source", "destination", "member", shape, 10, true, Order.Ascending, false, CommandFlags.None); + await mock.Received().GeoSearchAndStoreAsync("prefix:source", "prefix:destination", "member", shape, 10, true, Order.Ascending, false, CommandFlags.None); + } + + [Fact] + public async Task GeoSearchAndStoreAsync_2() + { + var shape = new GeoSearchCircle(100, GeoUnit.Meters); + await prefixed.GeoSearchAndStoreAsync("source", "destination", 1.23, 4.56, shape, 10, true, Order.Ascending, false, CommandFlags.None); + await mock.Received().GeoSearchAndStoreAsync("prefix:source", "prefix:destination", 1.23, 4.56, shape, 10, true, Order.Ascending, false, CommandFlags.None); + } + [Fact] + public async Task HashFieldExpireAsync_1() + { + var hashFields = new RedisValue[] { "field1", "field2" }; + var expiry = TimeSpan.FromSeconds(60); + await prefixed.HashFieldExpireAsync("key", hashFields, expiry, ExpireWhen.Always, CommandFlags.None); + await mock.Received().HashFieldExpireAsync("prefix:key", hashFields, expiry, ExpireWhen.Always, CommandFlags.None); + } + + [Fact] + public async Task HashFieldExpireAsync_2() + { + var hashFields = new RedisValue[] { "field1", "field2" }; + var expiry = DateTime.Now.AddMinutes(1); + await prefixed.HashFieldExpireAsync("key", hashFields, expiry, ExpireWhen.Always, CommandFlags.None); + await mock.Received().HashFieldExpireAsync("prefix:key", hashFields, expiry, ExpireWhen.Always, CommandFlags.None); + } + + [Fact] + public async Task HashFieldGetExpireDateTimeAsync() + { + var hashFields = new RedisValue[] { "field1", "field2" }; + await prefixed.HashFieldGetExpireDateTimeAsync("key", hashFields, CommandFlags.None); + await mock.Received().HashFieldGetExpireDateTimeAsync("prefix:key", hashFields, CommandFlags.None); + } + + [Fact] + public async Task HashFieldPersistAsync() + { + var hashFields = new RedisValue[] { "field1", "field2" }; + await prefixed.HashFieldPersistAsync("key", hashFields, CommandFlags.None); + await mock.Received().HashFieldPersistAsync("prefix:key", hashFields, CommandFlags.None); + } + + [Fact] + public async Task HashFieldGetTimeToLiveAsync() + { + var hashFields = new RedisValue[] { "field1", "field2" }; + await prefixed.HashFieldGetTimeToLiveAsync("key", hashFields, CommandFlags.None); + await mock.Received().HashFieldGetTimeToLiveAsync("prefix:key", hashFields, CommandFlags.None); + } + [Fact] + public async Task HashGetLeaseAsync() + { + await prefixed.HashGetLeaseAsync("key", "field", CommandFlags.None); + await mock.Received().HashGetLeaseAsync("prefix:key", "field", CommandFlags.None); + } + + [Fact] + public async Task HashFieldGetAndDeleteAsync_1() + { + await prefixed.HashFieldGetAndDeleteAsync("key", "field", CommandFlags.None); + await mock.Received().HashFieldGetAndDeleteAsync("prefix:key", "field", CommandFlags.None); + } + + [Fact] + public async Task HashFieldGetAndDeleteAsync_2() + { + var hashFields = new RedisValue[] { "field1", "field2" }; + await prefixed.HashFieldGetAndDeleteAsync("key", hashFields, CommandFlags.None); + await mock.Received().HashFieldGetAndDeleteAsync("prefix:key", hashFields, CommandFlags.None); + } + + [Fact] + public async Task HashFieldGetLeaseAndDeleteAsync() + { + await prefixed.HashFieldGetLeaseAndDeleteAsync("key", "field", CommandFlags.None); + await mock.Received().HashFieldGetLeaseAndDeleteAsync("prefix:key", "field", CommandFlags.None); + } + + [Fact] + public async Task HashFieldGetAndSetExpiryAsync_1() + { + var expiry = TimeSpan.FromMinutes(5); + await prefixed.HashFieldGetAndSetExpiryAsync("key", "field", expiry, false, CommandFlags.None); + await mock.Received().HashFieldGetAndSetExpiryAsync("prefix:key", "field", expiry, false, CommandFlags.None); + } + + [Fact] + public async Task HashFieldGetAndSetExpiryAsync_2() + { + var expiry = DateTime.Now.AddMinutes(5); + await prefixed.HashFieldGetAndSetExpiryAsync("key", "field", expiry, CommandFlags.None); + await mock.Received().HashFieldGetAndSetExpiryAsync("prefix:key", "field", expiry, CommandFlags.None); + } + + [Fact] + public async Task HashFieldGetLeaseAndSetExpiryAsync_1() + { + var expiry = TimeSpan.FromMinutes(5); + await prefixed.HashFieldGetLeaseAndSetExpiryAsync("key", "field", expiry, false, CommandFlags.None); + await mock.Received().HashFieldGetLeaseAndSetExpiryAsync("prefix:key", "field", expiry, false, CommandFlags.None); + } + + [Fact] + public async Task HashFieldGetLeaseAndSetExpiryAsync_2() + { + var expiry = DateTime.Now.AddMinutes(5); + await prefixed.HashFieldGetLeaseAndSetExpiryAsync("key", "field", expiry, CommandFlags.None); + await mock.Received().HashFieldGetLeaseAndSetExpiryAsync("prefix:key", "field", expiry, CommandFlags.None); + } + [Fact] + public async Task StringGetLeaseAsync() + { + await prefixed.StringGetLeaseAsync("key", CommandFlags.None); + await mock.Received().StringGetLeaseAsync("prefix:key", CommandFlags.None); + } + + [Fact] + public async Task StringGetSetExpiryAsync_1() + { + var expiry = TimeSpan.FromMinutes(5); + await prefixed.StringGetSetExpiryAsync("key", expiry, CommandFlags.None); + await mock.Received().StringGetSetExpiryAsync("prefix:key", expiry, CommandFlags.None); + } + + [Fact] + public async Task StringGetSetExpiryAsync_2() + { + var expiry = DateTime.Now.AddMinutes(5); + await prefixed.StringGetSetExpiryAsync("key", expiry, CommandFlags.None); + await mock.Received().StringGetSetExpiryAsync("prefix:key", expiry, CommandFlags.None); + } + + [Fact] + public async Task StringSetAndGetAsync_1() + { + var expiry = TimeSpan.FromMinutes(5); + await prefixed.StringSetAndGetAsync("key", "value", expiry, When.Always, CommandFlags.None); + await mock.Received().StringSetAndGetAsync("prefix:key", "value", expiry, When.Always, CommandFlags.None); + } + + [Fact] + public async Task StringSetAndGetAsync_2() + { + var expiry = TimeSpan.FromMinutes(5); + await prefixed.StringSetAndGetAsync("key", "value", expiry, false, When.Always, CommandFlags.None); + await mock.Received().StringSetAndGetAsync("prefix:key", "value", expiry, false, When.Always, CommandFlags.None); + } + [Fact] + public async Task StringLongestCommonSubsequenceAsync() + { + await prefixed.StringLongestCommonSubsequenceAsync("key1", "key2", CommandFlags.None); + await mock.Received().StringLongestCommonSubsequenceAsync("prefix:key1", "prefix:key2", CommandFlags.None); + } + + [Fact] + public async Task StringLongestCommonSubsequenceLengthAsync() + { + await prefixed.StringLongestCommonSubsequenceLengthAsync("key1", "key2", CommandFlags.None); + await mock.Received().StringLongestCommonSubsequenceLengthAsync("prefix:key1", "prefix:key2", CommandFlags.None); + } + + [Fact] + public async Task StringLongestCommonSubsequenceWithMatchesAsync() + { + await prefixed.StringLongestCommonSubsequenceWithMatchesAsync("key1", "key2", 5, CommandFlags.None); + await mock.Received().StringLongestCommonSubsequenceWithMatchesAsync("prefix:key1", "prefix:key2", 5, CommandFlags.None); + } + [Fact] + public async Task KeyIdleTimeAsync() + { + await prefixed.KeyIdleTimeAsync("key", CommandFlags.None); + await mock.Received().KeyIdleTimeAsync("prefix:key", CommandFlags.None); + } + [Fact] + public async Task StreamAddAsync_WithTrimMode_1() + { + await prefixed.StreamAddAsync("key", "field", "value", "*", 1000, false, 100, StreamTrimMode.KeepReferences, CommandFlags.None); + await mock.Received().StreamAddAsync("prefix:key", "field", "value", "*", 1000, false, 100, StreamTrimMode.KeepReferences, CommandFlags.None); + } + + [Fact] + public async Task StreamAddAsync_WithTrimMode_2() + { + var fields = new NameValueEntry[] { new NameValueEntry("field", "value") }; + await prefixed.StreamAddAsync("key", fields, "*", 1000, false, 100, StreamTrimMode.KeepReferences, CommandFlags.None); + await mock.Received().StreamAddAsync("prefix:key", fields, "*", 1000, false, 100, StreamTrimMode.KeepReferences, CommandFlags.None); + } + + [Fact] + public async Task StreamTrimAsync_WithMode() + { + await prefixed.StreamTrimAsync("key", 1000, false, 100, StreamTrimMode.KeepReferences, CommandFlags.None); + await mock.Received().StreamTrimAsync("prefix:key", 1000, false, 100, StreamTrimMode.KeepReferences, CommandFlags.None); + } + + [Fact] + public async Task StreamTrimByMinIdAsync_WithMode() + { + await prefixed.StreamTrimByMinIdAsync("key", "1111111111", false, 100, StreamTrimMode.KeepReferences, CommandFlags.None); + await mock.Received().StreamTrimByMinIdAsync("prefix:key", "1111111111", false, 100, StreamTrimMode.KeepReferences, CommandFlags.None); + } + + [Fact] + public async Task StreamReadGroupAsync_WithNoAck_1() + { + await prefixed.StreamReadGroupAsync("key", "group", "consumer", "0-0", 10, true, CommandFlags.None); + await mock.Received().StreamReadGroupAsync("prefix:key", "group", "consumer", "0-0", 10, true, CommandFlags.None); + } + + [Fact] + public async Task StreamReadGroupAsync_WithNoAck_2() + { + var streamPositions = new StreamPosition[] { new StreamPosition("key", "0-0") }; + await prefixed.StreamReadGroupAsync(streamPositions, "group", "consumer", 10, true, CommandFlags.None); + await mock.Received().StreamReadGroupAsync(streamPositions, "group", "consumer", 10, true, CommandFlags.None); + } + + [Fact] + public async Task StreamTrimAsync_Simple() + { + await prefixed.StreamTrimAsync("key", 1000, true, CommandFlags.None); + await mock.Received().StreamTrimAsync("prefix:key", 1000, true, CommandFlags.None); + } + + [Fact] + public async Task StreamReadGroupAsync_Simple_1() + { + await prefixed.StreamReadGroupAsync("key", "group", "consumer", "0-0", 10, CommandFlags.None); + await mock.Received().StreamReadGroupAsync("prefix:key", "group", "consumer", "0-0", 10, CommandFlags.None); + } + + [Fact] + public async Task StreamReadGroupAsync_Simple_2() + { + var streamPositions = new StreamPosition[] { new StreamPosition("key", "0-0") }; + await prefixed.StreamReadGroupAsync(streamPositions, "group", "consumer", 10, CommandFlags.None); + await mock.Received().StreamReadGroupAsync(streamPositions, "group", "consumer", 10, CommandFlags.None); + } + + [Fact] + public void HashScanAsync() + { + var result = prefixed.HashScanAsync("key", "pattern*", 10, 1, 2, CommandFlags.None); + _ = mock.Received().HashScanAsync("prefix:key", "pattern*", 10, 1, 2, CommandFlags.None); + } + + [Fact] + public void HashScanNoValuesAsync() + { + var result = prefixed.HashScanNoValuesAsync("key", "pattern*", 10, 1, 2, CommandFlags.None); + _ = mock.Received().HashScanNoValuesAsync("prefix:key", "pattern*", 10, 1, 2, CommandFlags.None); + } + + [Fact] + public void SetScanAsync() + { + var result = prefixed.SetScanAsync("key", "pattern*", 10, 1, 2, CommandFlags.None); + _ = mock.Received().SetScanAsync("prefix:key", "pattern*", 10, 1, 2, CommandFlags.None); + } + + [Fact] + public void SortedSetScanAsync() + { + var result = prefixed.SortedSetScanAsync("key", "pattern*", 10, 1, 2, CommandFlags.None); + _ = mock.Received().SortedSetScanAsync("prefix:key", "pattern*", 10, 1, 2, CommandFlags.None); } } } diff --git a/tests/StackExchange.Redis.Tests/RespProtocolTests.cs b/tests/StackExchange.Redis.Tests/RespProtocolTests.cs index 08e31b699..855ec96d1 100644 --- a/tests/StackExchange.Redis.Tests/RespProtocolTests.cs +++ b/tests/StackExchange.Redis.Tests/RespProtocolTests.cs @@ -324,17 +324,28 @@ public async Task CheckCommandResult(string command, RedisProtocol protocol, Res var db = muxer.GetDatabase(); if (args.Length > 0) { - await db.KeyDeleteAsync((string)args[0]); - switch (args[0]) + var origKey = (string)args[0]; + switch (origKey) { case "ikey": - await db.StringSetAsync("ikey", "40"); - break; case "skey": - await db.SetAddAsync("skey", ["a", "b", "c"]); - break; case "hkey": - await db.HashSetAsync("hkey", [new("a", 1), new("b", 2), new("c", 3)]); + case "nkey": + var newKey = Me() + "_" + origKey; // disambiguate + args[0] = newKey; + await db.KeyDeleteAsync(newKey); // remove + switch (origKey) // initialize + { + case "ikey": + await db.StringSetAsync(newKey, "40"); + break; + case "skey": + await db.SetAddAsync(newKey, ["a", "b", "c"]); + break; + case "hkey": + await db.HashSetAsync(newKey, [new("a", 1), new("b", 2), new("c", 3)]); + break; + } break; } } diff --git a/version.json b/version.json index be7077002..63f4a5346 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { - "version": "2.8", + "version": "2.9", "versionHeightOffset": -1, "assemblyVersion": "2.0", "publicReleaseRefSpec": [