diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 58de2287b..e5ddbe65d 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -10,6 +10,7 @@ Current package versions: - Support Redis 8.4 CAS/CAD operations (`DIGEST`, and the `IFEQ`, `IFNE`, `IFDEQ`, `IFDNE` modifiers on `SET` / `DEL`) via the new `ValueCondition` abstraction, and use CAS/CAD operations for `Lock*` APIs when possible ([#2978 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2978)) + - **note**: overload resolution for `StringSet[Async]` may be impacted in niche cases, requiring trivial build changes (there are no runtime-breaking changes such as missing methods) - Support `XREADGROUP CLAIM` ([#2972 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2972)) - Support `MSETEX` (Redis 8.4.0) for multi-key operations with expiration ([#2977 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2977)) diff --git a/src/StackExchange.Redis/Expiration.cs b/src/StackExchange.Redis/Expiration.cs index 786cc928c..e04094358 100644 --- a/src/StackExchange.Redis/Expiration.cs +++ b/src/StackExchange.Redis/Expiration.cs @@ -237,7 +237,7 @@ private static void ThrowMode(ExpirationMode mode) => /// public override bool Equals(object? obj) => obj is Expiration other && _valueAndMode == other._valueAndMode; - internal int Tokens => Mode switch + internal int TokenCount => Mode switch { ExpirationMode.Default or ExpirationMode.NotUsed => 0, ExpirationMode.KeepTtl or ExpirationMode.Persist => 1, diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 5556990df..3df162682 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -3182,7 +3182,6 @@ IEnumerable SortedSetScan( /// The condition to enforce. /// The flags to use for this operation. /// See . - [Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)] bool StringDelete(RedisKey key, ValueCondition when, CommandFlags flags = CommandFlags.None); /// @@ -3411,7 +3410,7 @@ IEnumerable SortedSetScan( /// The flags to use for this operation. /// if the string was set, otherwise. /// - bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None); + bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, bool keepTtl, When when = When.Always, CommandFlags flags = CommandFlags.None); /// /// Set to hold the string , if it matches the given condition. @@ -3422,9 +3421,8 @@ IEnumerable SortedSetScan( /// The condition to enforce. /// The flags to use for this operation. /// See . - [Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)] -#pragma warning disable RS0027 - bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, ValueCondition when, CommandFlags flags = CommandFlags.None); +#pragma warning disable RS0027 // Public API with optional parameter(s) should have the most parameters amongst its public overloads + bool StringSet(RedisKey key, RedisValue value, Expiration expiry = default, ValueCondition when = default, CommandFlags flags = CommandFlags.None); #pragma warning restore RS0027 /// diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index b35a685a5..855ea6c8f 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -773,7 +773,6 @@ IAsyncEnumerable SortedSetScanAsync( Task StringDecrementAsync(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None); /// - [Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)] Task StringDeleteAsync(RedisKey key, ValueCondition when, CommandFlags flags = CommandFlags.None); /// @@ -840,12 +839,11 @@ IAsyncEnumerable SortedSetScanAsync( Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags); /// - Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None); + Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, bool keepTtl, When when = When.Always, CommandFlags flags = CommandFlags.None); - /// - [Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)] + /// #pragma warning disable RS0027 // Public API with optional parameter(s) should have the most parameters amongst its public overloads - Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, ValueCondition when, CommandFlags flags = CommandFlags.None); + Task StringSetAsync(RedisKey key, RedisValue value, Expiration expiry = default, ValueCondition when = default, CommandFlags flags = CommandFlags.None); #pragma warning restore RS0027 /// diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs index 9119035a3..fe23b73c1 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs @@ -783,7 +783,7 @@ public Task StringIncrementAsync(RedisKey key, long value = 1, CommandFlag public Task StringLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Inner.StringLengthAsync(ToInner(key), flags); - public Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, ValueCondition when, CommandFlags flags = CommandFlags.None) + public Task StringSetAsync(RedisKey key, RedisValue value, Expiration expiry, ValueCondition when, CommandFlags flags = CommandFlags.None) => Inner.StringSetAsync(ToInner(key), value, expiry, when, flags); public Task StringSetAsync(KeyValuePair[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) => diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs index 3c7e34aa9..69775c15d 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs @@ -765,7 +765,7 @@ public long StringIncrement(RedisKey key, long value = 1, CommandFlags flags = C public long StringLength(RedisKey key, CommandFlags flags = CommandFlags.None) => Inner.StringLength(ToInner(key), flags); - public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, ValueCondition when, CommandFlags flags = CommandFlags.None) + public bool StringSet(RedisKey key, RedisValue value, Expiration expiry, ValueCondition when, CommandFlags flags = CommandFlags.None) => Inner.StringSet(ToInner(key), value, expiry, when, flags); public bool StringSet(KeyValuePair[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) => diff --git a/src/StackExchange.Redis/Message.ValueCondition.cs b/src/StackExchange.Redis/Message.ValueCondition.cs index c8b5febc4..53ddc651b 100644 --- a/src/StackExchange.Redis/Message.ValueCondition.cs +++ b/src/StackExchange.Redis/Message.ValueCondition.cs @@ -7,7 +7,7 @@ internal partial class Message public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in ValueCondition when) => new KeyConditionMessage(db, flags, command, key, when); - public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value, TimeSpan? expiry, in ValueCondition when) + public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value, Expiration expiry, in ValueCondition when) => new KeyValueExpiryConditionMessage(db, flags, command, key, value, expiry, when); private sealed class KeyConditionMessage( @@ -36,35 +36,22 @@ private sealed class KeyValueExpiryConditionMessage( RedisCommand command, in RedisKey key, in RedisValue value, - TimeSpan? expiry, + Expiration expiry, in ValueCondition when) : CommandKeyBase(db, flags, command, key) { private readonly RedisValue _value = value; private readonly ValueCondition _when = when; - private readonly TimeSpan? _expiry = expiry == TimeSpan.MaxValue ? null : expiry; + private readonly Expiration _expiry = expiry; - public override int ArgCount => 2 + _when.TokenCount + (_expiry is null ? 0 : 2); + public override int ArgCount => 2 + _expiry.TokenCount + _when.TokenCount; protected override void WriteImpl(PhysicalConnection physical) { physical.WriteHeader(Command, ArgCount); physical.Write(Key); physical.WriteBulkString(_value); - if (_expiry.HasValue) - { - var ms = (long)_expiry.GetValueOrDefault().TotalMilliseconds; - if ((ms % 1000) == 0) - { - physical.WriteBulkString("EX"u8); - physical.WriteBulkString(ms / 1000); - } - else - { - physical.WriteBulkString("PX"u8); - physical.WriteBulkString(ms); - } - } + _expiry.WriteTo(physical); _when.WriteTo(physical); } } diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index 0eff3ff8d..386d426d8 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -1711,7 +1711,7 @@ public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) // - MSETNX {key1} {value1} [{key2} {value2}...] // - MSETEX {count} {key1} {value1} [{key2} {value2}...] [standard-expiry-tokens] public override int ArgCount => Command == RedisCommand.MSETEX - ? (1 + (2 * values.Length) + expiry.Tokens + (when is When.Exists or When.NotExists ? 1 : 0)) + ? (1 + (2 * values.Length) + expiry.TokenCount + (when is When.Exists or When.NotExists ? 1 : 0)) : (2 * values.Length); // MSET/MSETNX only support simple syntax protected override void WriteImpl(PhysicalConnection physical) diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index dbb710243..5eaa42b3f 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -776,7 +776,6 @@ StackExchange.Redis.IDatabase.StringLength(StackExchange.Redis.RedisKey key, Sta StackExchange.Redis.IDatabase.StringLongestCommonSubsequence(StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> string? StackExchange.Redis.IDatabase.StringLongestCommonSubsequenceLength(StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.StringLongestCommonSubsequenceWithMatches(StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, long minLength = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.LCSMatchResult -StackExchange.Redis.IDatabase.StringSet(StackExchange.Redis.RedisKey key, 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) -> bool StackExchange.Redis.IDatabase.StringSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when) -> bool StackExchange.Redis.IDatabase.StringSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags) -> bool StackExchange.Redis.IDatabase.StringSet(System.Collections.Generic.KeyValuePair[]! values, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags) -> bool @@ -1023,7 +1022,6 @@ StackExchange.Redis.IDatabaseAsync.StringLongestCommonSubsequenceLengthAsync(Sta StackExchange.Redis.IDatabaseAsync.StringLongestCommonSubsequenceWithMatchesAsync(StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, long minLength = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StringSetAndGetAsync(StackExchange.Redis.RedisKey key, 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.StringSetAndGetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! -StackExchange.Redis.IDatabaseAsync.StringSetAsync(StackExchange.Redis.RedisKey key, 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.StringSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StringSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StringSetAsync(System.Collections.Generic.KeyValuePair[]! values, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! @@ -2073,3 +2071,32 @@ static StackExchange.Redis.Expiration.KeepTtl.get -> StackExchange.Redis.Expirat static StackExchange.Redis.Expiration.Persist.get -> StackExchange.Redis.Expiration static StackExchange.Redis.Expiration.implicit operator StackExchange.Redis.Expiration(System.DateTime when) -> StackExchange.Redis.Expiration static StackExchange.Redis.Expiration.implicit operator StackExchange.Redis.Expiration(System.TimeSpan ttl) -> StackExchange.Redis.Expiration +override StackExchange.Redis.ValueCondition.Equals(object? obj) -> bool +override StackExchange.Redis.ValueCondition.GetHashCode() -> int +override StackExchange.Redis.ValueCondition.ToString() -> string! +StackExchange.Redis.RedisFeatures.DeleteWithValueCheck.get -> bool +StackExchange.Redis.RedisFeatures.SetWithValueCheck.get -> bool +StackExchange.Redis.ValueCondition +StackExchange.Redis.ValueCondition.ValueCondition() -> void +static StackExchange.Redis.ValueCondition.Always.get -> StackExchange.Redis.ValueCondition +static StackExchange.Redis.ValueCondition.Exists.get -> StackExchange.Redis.ValueCondition +static StackExchange.Redis.ValueCondition.implicit operator StackExchange.Redis.ValueCondition(StackExchange.Redis.When when) -> StackExchange.Redis.ValueCondition +static StackExchange.Redis.ValueCondition.NotExists.get -> StackExchange.Redis.ValueCondition +static StackExchange.Redis.ValueCondition.operator !(in StackExchange.Redis.ValueCondition value) -> StackExchange.Redis.ValueCondition +StackExchange.Redis.IDatabase.StringDelete(StackExchange.Redis.RedisKey key, StackExchange.Redis.ValueCondition when, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabaseAsync.StringDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.ValueCondition when, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabase.StringSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.Expiration expiry = default(StackExchange.Redis.Expiration), StackExchange.Redis.ValueCondition when = default(StackExchange.Redis.ValueCondition), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.StringSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, bool keepTtl, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabaseAsync.StringSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.Expiration expiry = default(StackExchange.Redis.Expiration), StackExchange.Redis.ValueCondition when = default(StackExchange.Redis.ValueCondition), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StringSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, bool keepTtl, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER002]StackExchange.Redis.IDatabase.StringDigest(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.ValueCondition? +[SER002]StackExchange.Redis.IDatabaseAsync.StringDigestAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER002]StackExchange.Redis.ValueCondition.AsDigest() -> StackExchange.Redis.ValueCondition +[SER002]StackExchange.Redis.ValueCondition.Value.get -> StackExchange.Redis.RedisValue +[SER002]static StackExchange.Redis.ValueCondition.CalculateDigest(System.ReadOnlySpan value) -> StackExchange.Redis.ValueCondition +[SER002]static StackExchange.Redis.ValueCondition.DigestEqual(in StackExchange.Redis.RedisValue value) -> StackExchange.Redis.ValueCondition +[SER002]static StackExchange.Redis.ValueCondition.DigestNotEqual(in StackExchange.Redis.RedisValue value) -> StackExchange.Redis.ValueCondition +[SER002]static StackExchange.Redis.ValueCondition.Equal(in StackExchange.Redis.RedisValue value) -> StackExchange.Redis.ValueCondition +[SER002]static StackExchange.Redis.ValueCondition.NotEqual(in StackExchange.Redis.RedisValue value) -> StackExchange.Redis.ValueCondition +[SER002]static StackExchange.Redis.ValueCondition.ParseDigest(System.ReadOnlySpan digest) -> StackExchange.Redis.ValueCondition +[SER002]static StackExchange.Redis.ValueCondition.ParseDigest(System.ReadOnlySpan digest) -> StackExchange.Redis.ValueCondition diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index 1b8aba3b0..91b0e1a43 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -1,28 +1 @@ -#nullable enable -StackExchange.Redis.RedisFeatures.DeleteWithValueCheck.get -> bool -StackExchange.Redis.RedisFeatures.SetWithValueCheck.get -> bool -[SER002]override StackExchange.Redis.ValueCondition.Equals(object? obj) -> bool -[SER002]override StackExchange.Redis.ValueCondition.GetHashCode() -> int -[SER002]override StackExchange.Redis.ValueCondition.ToString() -> string! -[SER002]StackExchange.Redis.IDatabase.StringDelete(StackExchange.Redis.RedisKey key, StackExchange.Redis.ValueCondition when, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool -[SER002]StackExchange.Redis.IDatabase.StringDigest(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.ValueCondition? -[SER002]StackExchange.Redis.IDatabase.StringSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.ValueCondition when, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool -[SER002]StackExchange.Redis.IDatabaseAsync.StringDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.ValueCondition when, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -[SER002]StackExchange.Redis.IDatabaseAsync.StringDigestAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -[SER002]StackExchange.Redis.IDatabaseAsync.StringSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.ValueCondition when, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -[SER002]StackExchange.Redis.ValueCondition -[SER002]StackExchange.Redis.ValueCondition.AsDigest() -> StackExchange.Redis.ValueCondition -[SER002]StackExchange.Redis.ValueCondition.Value.get -> StackExchange.Redis.RedisValue -[SER002]StackExchange.Redis.ValueCondition.ValueCondition() -> void -[SER002]static StackExchange.Redis.ValueCondition.Always.get -> StackExchange.Redis.ValueCondition -[SER002]static StackExchange.Redis.ValueCondition.CalculateDigest(System.ReadOnlySpan value) -> StackExchange.Redis.ValueCondition -[SER002]static StackExchange.Redis.ValueCondition.DigestEqual(in StackExchange.Redis.RedisValue value) -> StackExchange.Redis.ValueCondition -[SER002]static StackExchange.Redis.ValueCondition.DigestNotEqual(in StackExchange.Redis.RedisValue value) -> StackExchange.Redis.ValueCondition -[SER002]static StackExchange.Redis.ValueCondition.Equal(in StackExchange.Redis.RedisValue value) -> StackExchange.Redis.ValueCondition -[SER002]static StackExchange.Redis.ValueCondition.Exists.get -> StackExchange.Redis.ValueCondition -[SER002]static StackExchange.Redis.ValueCondition.implicit operator StackExchange.Redis.ValueCondition(StackExchange.Redis.When when) -> StackExchange.Redis.ValueCondition -[SER002]static StackExchange.Redis.ValueCondition.NotEqual(in StackExchange.Redis.RedisValue value) -> StackExchange.Redis.ValueCondition -[SER002]static StackExchange.Redis.ValueCondition.NotExists.get -> StackExchange.Redis.ValueCondition -[SER002]static StackExchange.Redis.ValueCondition.operator !(in StackExchange.Redis.ValueCondition value) -> StackExchange.Redis.ValueCondition -[SER002]static StackExchange.Redis.ValueCondition.ParseDigest(System.ReadOnlySpan digest) -> StackExchange.Redis.ValueCondition -[SER002]static StackExchange.Redis.ValueCondition.ParseDigest(System.ReadOnlySpan digest) -> StackExchange.Redis.ValueCondition +#nullable enable \ No newline at end of file diff --git a/src/StackExchange.Redis/RedisDatabase.Strings.cs b/src/StackExchange.Redis/RedisDatabase.Strings.cs index 1323246f9..6fcb7dd3b 100644 --- a/src/StackExchange.Redis/RedisDatabase.Strings.cs +++ b/src/StackExchange.Redis/RedisDatabase.Strings.cs @@ -48,19 +48,19 @@ private Message GetStringDeleteMessage(in RedisKey key, in ValueCondition when, return ExecuteAsync(msg, ResultProcessor.Digest); } - public Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, ValueCondition when, CommandFlags flags = CommandFlags.None) + public Task StringSetAsync(RedisKey key, RedisValue value, Expiration expiry, ValueCondition when, CommandFlags flags = CommandFlags.None) { var msg = GetStringSetMessage(key, value, expiry, when, flags); return ExecuteAsync(msg, ResultProcessor.Boolean); } - public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, ValueCondition when, CommandFlags flags = CommandFlags.None) + public bool StringSet(RedisKey key, RedisValue value, Expiration expiry, ValueCondition when, CommandFlags flags = CommandFlags.None) { var msg = GetStringSetMessage(key, value, expiry, when, flags); return ExecuteSync(msg, ResultProcessor.Boolean); } - private Message GetStringSetMessage(in RedisKey key, in RedisValue value, TimeSpan? expiry, in ValueCondition when, CommandFlags flags, [CallerMemberName] string? operation = null) + private Message GetStringSetMessage(in RedisKey key, in RedisValue value, Expiration expiry, in ValueCondition when, CommandFlags flags, [CallerMemberName] string? operation = null) { switch (when.Kind) { diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 948a9e894..f13571c4e 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -489,7 +489,7 @@ public Task HashFieldGetAndDeleteAsync(RedisKey key, RedisValue[] } private Message HashFieldGetAndSetExpiryMessage(in RedisKey key, in RedisValue hashField, Expiration expiry, CommandFlags flags) => - expiry.Tokens switch + expiry.TokenCount switch { // expiry, for example EX 10 2 => Message.Create(Database, flags, RedisCommand.HGETEX, key, expiry.Operand, expiry.Value, RedisLiterals.FIELDS, 1, hashField), @@ -508,13 +508,13 @@ private Message HashFieldGetAndSetExpiryMessage(in RedisKey key, RedisValue[] ha } // precision, time, FIELDS, hashFields.Length - int extraTokens = expiry.Tokens + 2; + int extraTokens = expiry.TokenCount + 2; - RedisValue[] values = new RedisValue[expiry.Tokens + 2 + hashFields.Length]; + RedisValue[] values = new RedisValue[expiry.TokenCount + 2 + hashFields.Length]; int index = 0; // add PERSIST or expiry values - switch (expiry.Tokens) + switch (expiry.TokenCount) { case 2: values[index++] = expiry.Operand; @@ -620,7 +620,7 @@ private Message HashFieldSetAndSetExpiryMessage(in RedisKey key, in RedisValue f { if (when == When.Always) { - return expiry.Tokens switch + return expiry.TokenCount 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), @@ -637,7 +637,7 @@ private Message HashFieldSetAndSetExpiryMessage(in RedisKey key, in RedisValue f _ => throw new ArgumentOutOfRangeException(nameof(when)), }; - return expiry.Tokens switch + return expiry.TokenCount 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), @@ -654,7 +654,7 @@ private Message HashFieldSetAndSetExpiryMessage(in RedisKey key, HashEntry[] has 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} + var extraTokens = expiry.TokenCount + (when == When.Always ? 2 : 3); // [FXX|FNX] {expiry} FIELDS {length} RedisValue[] values = new RedisValue[(hashFields.Length * 2) + extraTokens]; int index = 0; @@ -671,7 +671,7 @@ private Message HashFieldSetAndSetExpiryMessage(in RedisKey key, HashEntry[] has default: throw new ArgumentOutOfRangeException(nameof(when)); } - switch (expiry.Tokens) + switch (expiry.TokenCount) { case 2: values[index++] = expiry.Operand; @@ -5088,7 +5088,7 @@ private Message GetStringBitOperationMessage(Bitwise operation, RedisKey destina private Message GetStringGetExMessage(in RedisKey key, Expiration expiry, CommandFlags flags = CommandFlags.None) { - return expiry.Tokens switch + return expiry.TokenCount switch { 0 => Message.Create(Database, flags, RedisCommand.GETEX, key), 1 => Message.Create(Database, flags, RedisCommand.GETEX, key, expiry.Operand), diff --git a/src/StackExchange.Redis/StackExchange.Redis.csproj b/src/StackExchange.Redis/StackExchange.Redis.csproj index e66b3874c..983624bc0 100644 --- a/src/StackExchange.Redis/StackExchange.Redis.csproj +++ b/src/StackExchange.Redis/StackExchange.Redis.csproj @@ -12,7 +12,6 @@ $(DefineConstants);VECTOR_SAFE $(DefineConstants);UNIX_SOCKET README.md - $(NoWarn);SER002 diff --git a/src/StackExchange.Redis/ValueCondition.cs b/src/StackExchange.Redis/ValueCondition.cs index 94e9850c4..d61a2f00e 100644 --- a/src/StackExchange.Redis/ValueCondition.cs +++ b/src/StackExchange.Redis/ValueCondition.cs @@ -8,9 +8,8 @@ namespace StackExchange.Redis; /// -/// Represents a check for an existing value, for use in conditional operations such as DELEX or SET ... IFEQ. +/// Represents a check for an existing value - this could be existence (NX/XX), equality (IFEQ/IFNE), or digest equality (IFDEQ/IFDNE). /// -[Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)] public readonly struct ValueCondition { internal enum ConditionKind : byte @@ -108,7 +107,11 @@ public override string ToString() /// /// Gets the underlying value for this condition. /// - public RedisValue Value => _value; + public RedisValue Value + { + [Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)] + get => _value; + } private ValueCondition(ConditionKind kind, in RedisValue value) { @@ -136,26 +139,32 @@ private ValueCondition(ConditionKind kind, in RedisValue value) /// /// Create a value equality condition with the supplied value. /// + [Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)] public static ValueCondition Equal(in RedisValue value) => new(ConditionKind.ValueEquals, value); /// /// Create a value non-equality condition with the supplied value. /// + [Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)] + public static ValueCondition NotEqual(in RedisValue value) => new(ConditionKind.ValueNotEquals, value); /// /// Create a digest equality condition, computing the digest of the supplied value. /// + [Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)] public static ValueCondition DigestEqual(in RedisValue value) => value.Digest(); /// /// Create a digest non-equality condition, computing the digest of the supplied value. /// + [Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)] public static ValueCondition DigestNotEqual(in RedisValue value) => !value.Digest(); /// /// Calculate the digest of a payload, as an equality test. For a non-equality test, use on the result. /// + [Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)] public static ValueCondition CalculateDigest(ReadOnlySpan value) { // the internal impl of XxHash3 uses ulong (not Span), so: use @@ -167,6 +176,7 @@ public static ValueCondition CalculateDigest(ReadOnlySpan value) /// /// Creates an equality match based on the specified digest bytes. /// + [Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)] public static ValueCondition ParseDigest(ReadOnlySpan digest) { if (digest.Length != 2 * DigestBytes) ThrowDigestLength(); @@ -201,6 +211,7 @@ private static byte ParseNibble(int b) /// /// Creates an equality match based on the specified digest bytes. /// + [Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)] public static ValueCondition ParseDigest(ReadOnlySpan digest) { if (digest.Length != 2 * DigestBytes) ThrowDigestLength(); @@ -341,6 +352,7 @@ internal static Span WriteHex(long value, Span target) /// /// Convert a value condition to a digest condition. /// + [Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)] public ValueCondition AsDigest() => _kind switch { ConditionKind.ValueEquals => _value.Digest(), diff --git a/tests/RedisConfigs/start-all.sh b/tests/RedisConfigs/start-all.sh old mode 100644 new mode 100755 diff --git a/tests/RedisConfigs/start-basic.sh b/tests/RedisConfigs/start-basic.sh old mode 100644 new mode 100755 diff --git a/tests/StackExchange.Redis.Tests/DigestIntegrationTests.cs b/tests/StackExchange.Redis.Tests/DigestIntegrationTests.cs index a71e2f910..ec5171075 100644 --- a/tests/StackExchange.Redis.Tests/DigestIntegrationTests.cs +++ b/tests/StackExchange.Redis.Tests/DigestIntegrationTests.cs @@ -4,8 +4,6 @@ namespace StackExchange.Redis.Tests; -#pragma warning disable SER002 // 8.4 - public class DigestIntegrationTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { diff --git a/tests/StackExchange.Redis.Tests/DigestUnitTests.cs b/tests/StackExchange.Redis.Tests/DigestUnitTests.cs index 9f04342d1..e1883c13b 100644 --- a/tests/StackExchange.Redis.Tests/DigestUnitTests.cs +++ b/tests/StackExchange.Redis.Tests/DigestUnitTests.cs @@ -7,8 +7,6 @@ namespace StackExchange.Redis.Tests; -#pragma warning disable SER002 // 8.4 - public class DigestUnitTests(ITestOutputHelper output) : TestBase(output) { [Theory] diff --git a/tests/StackExchange.Redis.Tests/ExpiryTokenTests.cs b/tests/StackExchange.Redis.Tests/ExpiryTokenTests.cs index fea4d4885..6012422ed 100644 --- a/tests/StackExchange.Redis.Tests/ExpiryTokenTests.cs +++ b/tests/StackExchange.Redis.Tests/ExpiryTokenTests.cs @@ -10,7 +10,7 @@ public void Persist_Seconds() { TimeSpan? time = TimeSpan.FromMilliseconds(5000); var ex = CreateOrPersist(time, false); - Assert.Equal(2, ex.Tokens); + Assert.Equal(2, ex.TokenCount); Assert.Equal("EX 5", ex.ToString()); } @@ -19,7 +19,7 @@ public void Persist_Milliseconds() { TimeSpan? time = TimeSpan.FromMilliseconds(5001); var ex = CreateOrPersist(time, false); - Assert.Equal(2, ex.Tokens); + Assert.Equal(2, ex.TokenCount); Assert.Equal("PX 5001", ex.ToString()); } @@ -28,7 +28,7 @@ public void Persist_None_False() { TimeSpan? time = null; var ex = CreateOrPersist(time, false); - Assert.Equal(0, ex.Tokens); + Assert.Equal(0, ex.TokenCount); Assert.Equal("", ex.ToString()); } @@ -37,7 +37,7 @@ public void Persist_None_True() { TimeSpan? time = null; var ex = CreateOrPersist(time, true); - Assert.Equal(1, ex.Tokens); + Assert.Equal(1, ex.TokenCount); Assert.Equal("PERSIST", ex.ToString()); } @@ -55,7 +55,7 @@ public void KeepTtl_Seconds() { TimeSpan? time = TimeSpan.FromMilliseconds(5000); var ex = CreateOrKeepTtl(time, false); - Assert.Equal(2, ex.Tokens); + Assert.Equal(2, ex.TokenCount); Assert.Equal("EX 5", ex.ToString()); } @@ -64,7 +64,7 @@ public void KeepTtl_Milliseconds() { TimeSpan? time = TimeSpan.FromMilliseconds(5001); var ex = CreateOrKeepTtl(time, false); - Assert.Equal(2, ex.Tokens); + Assert.Equal(2, ex.TokenCount); Assert.Equal("PX 5001", ex.ToString()); } @@ -73,7 +73,7 @@ public void KeepTtl_None_False() { TimeSpan? time = null; var ex = CreateOrKeepTtl(time, false); - Assert.Equal(0, ex.Tokens); + Assert.Equal(0, ex.TokenCount); Assert.Equal("", ex.ToString()); } @@ -82,7 +82,7 @@ public void KeepTtl_None_True() { TimeSpan? time = null; var ex = CreateOrKeepTtl(time, true); - Assert.Equal(1, ex.Tokens); + Assert.Equal(1, ex.TokenCount); Assert.Equal("KEEPTTL", ex.ToString()); } @@ -100,7 +100,7 @@ public void DateTime_Seconds() { var when = new DateTime(2025, 7, 23, 10, 4, 14, DateTimeKind.Utc); var ex = new Expiration(when); - Assert.Equal(2, ex.Tokens); + Assert.Equal(2, ex.TokenCount); Assert.Equal("EXAT 1753265054", ex.ToString()); } @@ -110,7 +110,7 @@ public void DateTime_Milliseconds() var when = new DateTime(2025, 7, 23, 10, 4, 14, DateTimeKind.Utc); when = when.AddMilliseconds(14); var ex = new Expiration(when); - Assert.Equal(2, ex.Tokens); + Assert.Equal(2, ex.TokenCount); Assert.Equal("PXAT 1753265054014", ex.ToString()); } } diff --git a/tests/StackExchange.Redis.Tests/OverloadCompatTests.cs b/tests/StackExchange.Redis.Tests/OverloadCompatTests.cs index c695488f2..0acadc74b 100644 --- a/tests/StackExchange.Redis.Tests/OverloadCompatTests.cs +++ b/tests/StackExchange.Redis.Tests/OverloadCompatTests.cs @@ -226,7 +226,7 @@ public async Task StringSet() db.StringSet(key, val, expiresIn, When.NotExists); db.StringSet(key, val, expiresIn, When.NotExists, flags); - db.StringSet(key, val, null); + db.StringSet(key, val, expiry: default); db.StringSet(key, val, null, When.NotExists); db.StringSet(key, val, null, When.NotExists, flags); @@ -241,7 +241,7 @@ public async Task StringSet() await db.StringSetAsync(key, val, expiresIn, When.NotExists); await db.StringSetAsync(key, val, expiresIn, When.NotExists, flags); - await db.StringSetAsync(key, val, null); + await db.StringSetAsync(key, val, expiry: default); await db.StringSetAsync(key, val, null, When.NotExists); await db.StringSetAsync(key, val, null, When.NotExists, flags); } diff --git a/tests/StackExchange.Redis.Tests/StringTests.cs b/tests/StackExchange.Redis.Tests/StringTests.cs index 85bcc7dd5..1ade532d7 100644 --- a/tests/StackExchange.Redis.Tests/StringTests.cs +++ b/tests/StackExchange.Redis.Tests/StringTests.cs @@ -301,9 +301,9 @@ public async Task SetKeepTtl() Assert.True(await x2 > TimeSpan.FromMinutes(9), "Over 9"); Assert.True(await x2 <= TimeSpan.FromMinutes(10), "Under 10"); - db.StringSet(prefix + "1", "def", keepTtl: true, flags: CommandFlags.FireAndForget); + db.StringSet(prefix + "1", "def", Expiration.KeepTtl, flags: CommandFlags.FireAndForget); db.StringSet(prefix + "2", "def", flags: CommandFlags.FireAndForget); - db.StringSet(prefix + "3", "def", keepTtl: true, flags: CommandFlags.FireAndForget); + db.StringSet(prefix + "3", "def", Expiration.KeepTtl, flags: CommandFlags.FireAndForget); var y0 = db.KeyTimeToLiveAsync(prefix + "1"); var y1 = db.KeyTimeToLiveAsync(prefix + "2");