From f6ce938a1bc9cf12c7e7f7348dcef8716a5f9c6d Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 9 Jul 2026 19:45:55 +0100 Subject: [PATCH 1/9] investigate and fix #3123 --- src/StackExchange.Redis/ResultProcessor.cs | 6 +-- .../Issues/Issue3123.cs | 41 +++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 tests/StackExchange.Redis.Tests/Issues/Issue3123.cs diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 55b502c40..14acb54c0 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -1901,7 +1901,7 @@ private static GeoRadiusResult Parse(ref RespReader reader, GeoRadiusOptions opt if (options == GeoRadiusOptions.None) { // Without any WITH option specified, the command just returns a linear array like ["New York","Milan","Paris"]. - return new GeoRadiusResult(reader.ReadString(), null, null, null); + return new GeoRadiusResult(reader.ReadRedisValue(), null, null, null); } // If WITHCOORD, WITHDIST or WITHHASH options are specified, the command returns an array of arrays, where each sub-array represents a single item. @@ -1913,12 +1913,12 @@ private static GeoRadiusResult Parse(ref RespReader reader, GeoRadiusOptions opt reader.MoveNext(); // Move to first element in the sub-array // the first item in the sub-array is always the name of the returned item. - var member = reader.ReadString(); + var member = reader.ReadRedisValue(); /* The other information is returned in the following order as successive elements of the sub-array. The distance from the center as a floating point number, in the same unit specified in the radius. The geohash integer. -The coordinates as a two items x,y array (longitude,latitude). +The coordinates as an array of two items x,y (longitude,latitude). */ double? distance = null; GeoPosition? position = null; diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue3123.cs b/tests/StackExchange.Redis.Tests/Issues/Issue3123.cs new file mode 100644 index 000000000..6f5153f9f --- /dev/null +++ b/tests/StackExchange.Redis.Tests/Issues/Issue3123.cs @@ -0,0 +1,41 @@ +using System; +using System.Buffers.Binary; +using System.Threading.Tasks; +using Xunit; + +namespace StackExchange.Redis.Tests.Issues; + +public class Issue3123(ITestOutputHelper output, SharedConnectionFixture? fixture) : TestBase(output, fixture) +{ + [Fact] + public async Task Run() + { + await using var conn = Create(); + var db = conn.GetDatabase(); + var key = Me(); + await db.KeyDeleteAsync(key); + + Guid guid = Guid.NewGuid(); + byte[] payload = guid.ToByteArray(); + + await db.GeoAddAsync( + key, + longitude: -77.0365, + latitude: 38.8977, + member: payload); + + GeoSearchCircle commonSearchCircle = new(1, GeoUnit.Kilometers); + + GeoRadiusResult[] results = + await db.GeoSearchAsync( + key, + longitude: -77.0365, + latitude: 38.8977, + shape: commonSearchCircle); + var result = Assert.Single(results); + Log(result.Member); + byte[] final = (byte[])result.Member!; + Assert.True(final.SequenceEqual(payload)); + Assert.Equal(guid, new Guid(final)); + } +} From 8bdeb33ca0ae238bf651647eebbb82b6fe349b3c Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 9 Jul 2026 19:51:16 +0100 Subject: [PATCH 2/9] F+F the test setup --- tests/StackExchange.Redis.Tests/Issues/Issue3123.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue3123.cs b/tests/StackExchange.Redis.Tests/Issues/Issue3123.cs index 6f5153f9f..185ccf842 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue3123.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue3123.cs @@ -13,7 +13,7 @@ public async Task Run() await using var conn = Create(); var db = conn.GetDatabase(); var key = Me(); - await db.KeyDeleteAsync(key); + await db.KeyDeleteAsync(key, flags: CommandFlags.FireAndForget); Guid guid = Guid.NewGuid(); byte[] payload = guid.ToByteArray(); @@ -22,7 +22,8 @@ await db.GeoAddAsync( key, longitude: -77.0365, latitude: 38.8977, - member: payload); + member: payload, + flags: CommandFlags.FireAndForget); GeoSearchCircle commonSearchCircle = new(1, GeoUnit.Kilometers); @@ -33,7 +34,7 @@ await db.GeoSearchAsync( latitude: 38.8977, shape: commonSearchCircle); var result = Assert.Single(results); - Log(result.Member); + byte[] final = (byte[])result.Member!; Assert.True(final.SequenceEqual(payload)); Assert.Equal(guid, new Guid(final)); From 0a3e507654f12a4d1d70130dd8c0de903386230f Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 9 Jul 2026 19:51:48 +0100 Subject: [PATCH 3/9] nit, nuke a using direction --- tests/StackExchange.Redis.Tests/Issues/Issue3123.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue3123.cs b/tests/StackExchange.Redis.Tests/Issues/Issue3123.cs index 185ccf842..5afa10d77 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue3123.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue3123.cs @@ -1,5 +1,4 @@ using System; -using System.Buffers.Binary; using System.Threading.Tasks; using Xunit; From df8ed01711a529f47e2d0c7087e070f4a47b24ed Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 9 Jul 2026 21:50:33 +0100 Subject: [PATCH 4/9] in debug: make all string -> RedisValue *explicit* (incomplete, needs a fix to the ship-file) --- src/RESPite/Shared/Experiments.cs | 5 ++ .../APITypes/ClientKillFilter.cs | 6 +-- .../APITypes/GeoPosition.cs | 10 ++-- .../Configuration/LoggingTunnel.cs | 2 +- .../ConnectionMultiplexer.Sentinel.cs | 2 +- .../ConnectionMultiplexer.cs | 8 +-- src/StackExchange.Redis/ExtensionMethods.cs | 8 ++- .../KeyspaceIsolation/KeyPrefixed.cs | 4 +- src/StackExchange.Redis/MessageWriter.cs | 52 ++++++++++++++++++- .../RedisDatabase.VectorSets.cs | 6 +-- src/StackExchange.Redis/RedisDatabase.cs | 23 ++++---- src/StackExchange.Redis/RedisKey.cs | 2 +- src/StackExchange.Redis/RedisLiterals.cs | 4 +- src/StackExchange.Redis/RedisServer.cs | 47 +++++++++-------- src/StackExchange.Redis/RedisValue.cs | 18 ++++--- src/StackExchange.Redis/Role.cs | 4 +- src/StackExchange.Redis/ServerEndPoint.cs | 10 ++-- src/StackExchange.Redis/StreamConstants.cs | 42 +++++++-------- tests/Directory.Build.targets | 1 + 19 files changed, 164 insertions(+), 90 deletions(-) diff --git a/src/RESPite/Shared/Experiments.cs b/src/RESPite/Shared/Experiments.cs index 1d9a091fa..959965c26 100644 --- a/src/RESPite/Shared/Experiments.cs +++ b/src/RESPite/Shared/Experiments.cs @@ -13,7 +13,12 @@ internal static class Experiments public const string Respite = "SER004"; public const string UnitTesting = "SER005"; public const string Server_8_8 = "SER006"; + // ReSharper restore InconsistentNaming + + // this one is not a real experiment; it exists to help me + // spot bad API uses, via a DEBUG symbol + public const string StringToRedisValue = "StringToRedisValue"; } } diff --git a/src/StackExchange.Redis/APITypes/ClientKillFilter.cs b/src/StackExchange.Redis/APITypes/ClientKillFilter.cs index 3d1883549..ac6ab4e8e 100644 --- a/src/StackExchange.Redis/APITypes/ClientKillFilter.cs +++ b/src/StackExchange.Redis/APITypes/ClientKillFilter.cs @@ -152,17 +152,17 @@ internal List ToList(bool withReplicaCommands) if (Username != null) { parts.Add(RedisLiterals.USERNAME); - parts.Add(Username); + parts.Add(Username.AsRedisValue()); } if (Endpoint != null) { parts.Add(RedisLiterals.ADDR); - parts.Add((RedisValue)Format.ToString(Endpoint)); + parts.Add(Format.ToString(Endpoint).AsRedisValue()); } if (ServerEndpoint != null) { parts.Add(RedisLiterals.LADDR); - parts.Add((RedisValue)Format.ToString(ServerEndpoint)); + parts.Add(Format.ToString(ServerEndpoint).AsRedisValue()); } if (SkipMe != null) { diff --git a/src/StackExchange.Redis/APITypes/GeoPosition.cs b/src/StackExchange.Redis/APITypes/GeoPosition.cs index 6e53b3e32..b3b3cdad7 100644 --- a/src/StackExchange.Redis/APITypes/GeoPosition.cs +++ b/src/StackExchange.Redis/APITypes/GeoPosition.cs @@ -7,12 +7,12 @@ namespace StackExchange.Redis; /// public readonly struct GeoPosition : IEquatable { - internal static string GetRedisUnit(GeoUnit unit) => unit switch + internal static RedisValue GetRedisUnit(GeoUnit unit) => unit switch { - GeoUnit.Meters => "m", - GeoUnit.Kilometers => "km", - GeoUnit.Miles => "mi", - GeoUnit.Feet => "ft", + GeoUnit.Meters => RedisLiterals.m, + GeoUnit.Kilometers => RedisLiterals.km, + GeoUnit.Miles => RedisLiterals.mi, + GeoUnit.Feet => RedisLiterals.ft, _ => throw new ArgumentOutOfRangeException(nameof(unit)), }; diff --git a/src/StackExchange.Redis/Configuration/LoggingTunnel.cs b/src/StackExchange.Redis/Configuration/LoggingTunnel.cs index a89911dc4..42a85b710 100644 --- a/src/StackExchange.Redis/Configuration/LoggingTunnel.cs +++ b/src/StackExchange.Redis/Configuration/LoggingTunnel.cs @@ -193,7 +193,7 @@ public static async Task ReplayAsync(Stream @out, Stream @in, Action(); - return Array.ConvertAll(values, x => (RedisValue)x); + return Array.ConvertAll(values, x => x.AsRedisValue()); } + // just like the implicit conversion operator, but by making it + // explicit, we're making it very clear that this is intentional, + // and avoiding the DEBUG check + internal static RedisValue AsRedisValue(this string? value) + => value is null ? RedisValue.Null : new(value); + /// /// Create an array of strings from an array of values. /// diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs index 6efd8d68e..952fe0ed3 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs @@ -938,10 +938,10 @@ protected RedisValue ToInner(RedisValue outer) => RedisKey.ConcatenateBytes(Prefix, null, (byte[]?)outer); protected RedisValue SortByToInner(RedisValue outer) => - (outer == "nosort") ? outer : ToInner(outer); + (outer == RedisLiterals.nosort) ? outer : ToInner(outer); protected RedisValue SortGetToInner(RedisValue outer) => - (outer == "#") ? outer : ToInner(outer); + (outer == RedisLiterals.HashSymbol) ? outer : ToInner(outer); [return: NotNullIfNotNull("outer")] protected RedisValue[]? SortGetToInner(RedisValue[]? outer) diff --git a/src/StackExchange.Redis/MessageWriter.cs b/src/StackExchange.Redis/MessageWriter.cs index a95edd776..61753eece 100644 --- a/src/StackExchange.Redis/MessageWriter.cs +++ b/src/StackExchange.Redis/MessageWriter.cs @@ -80,12 +80,62 @@ internal void Write(in RedisChannel channel) internal void WriteBulkString(in RedisValue value) => WriteBulkString(value, _writer); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void WriteBulkString(in RedisKey key) + { + if (key.IsNull) + { + WriteRaw(NullBulkString); + } + else if (key.IsEmpty) + { + WriteRaw(EmptyBulkString); + } + else if (key.KeyPrefix is { Length: > 1 }) + { + // this has been optimized in the experimental 3.2+ branch + byte[]? lease = null; + var maxLen = key.MaxByteCount(); + Span span = maxLen <= 128 ? stackalloc byte[128] : (lease = ArrayPool.Shared.Rent(maxLen)); + var len = key.CopyTo(span); + WriteBulkString(span.Slice(0, len)); + if (lease is not null) ArrayPool.Shared.Return(lease); + } + else if (key.KeyValue is string s) + { + // no prefix and not null/empty; value can be string... + WriteBulkString(s); + } + else + { + // ...or a blob + WriteBulkString((byte[])key.KeyValue!); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void WriteBulkString(string? value) + { + if (value is null) + { + WriteRaw(NullBulkString); + } + else if (value.Length is 0) + { + WriteRaw(EmptyBulkString); + } + else + { + WriteUnifiedPrefixedString(_writer, null, value); + } + } + internal static void WriteBulkString(in RedisValue value, IBufferWriter writer) { switch (value.Type) { case RedisValue.StorageType.Null: - WriteUnifiedBlob(writer, (byte[]?)null); + writer.Write(NullBulkString); break; case RedisValue.StorageType.Int64: WriteUnifiedInt64(writer, value.OverlappedValueInt64); diff --git a/src/StackExchange.Redis/RedisDatabase.VectorSets.cs b/src/StackExchange.Redis/RedisDatabase.VectorSets.cs index bcaa0e6c0..7b29b0da9 100644 --- a/src/StackExchange.Redis/RedisDatabase.VectorSets.cs +++ b/src/StackExchange.Redis/RedisDatabase.VectorSets.cs @@ -86,7 +86,7 @@ public bool VectorSetRemove(RedisKey key, RedisValue member, CommandFlags flags public bool VectorSetSetAttributesJson(RedisKey key, RedisValue member, string attributesJson, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create(Database, flags, RedisCommand.VSETATTR, key, member, attributesJson); + var msg = Message.Create(Database, flags, RedisCommand.VSETATTR, key, member, attributesJson.AsRedisValue()); return ExecuteSync(msg, ResultProcessor.Boolean); } @@ -178,7 +178,7 @@ public Task VectorSetRemoveAsync(RedisKey key, RedisValue member, CommandF public Task VectorSetSetAttributesJsonAsync(RedisKey key, RedisValue member, string attributesJson, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create(Database, flags, RedisCommand.VSETATTR, key, member, attributesJson); + var msg = Message.Create(Database, flags, RedisCommand.VSETATTR, key, member, attributesJson.AsRedisValue()); return ExecuteAsync(msg, ResultProcessor.Boolean); } @@ -205,7 +205,7 @@ static RedisValue GetTerminator(RedisValue value, Exclude exclude, bool isStart) if (value.IsNull) return isStart ? RedisLiterals.MinusSymbol : RedisLiterals.PlusSymbol; var mask = isStart ? Exclude.Start : Exclude.Stop; var isExclusive = (exclude & mask) != 0; - return (isExclusive ? "(" : "[") + value; + return ((isExclusive ? "(" : "[") + value).AsRedisValue(); } var from = GetTerminator(start, exclude, true); diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index f06bcc699..b217712bd 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -1311,7 +1311,8 @@ private sealed class KeyMigrateCommandMessage : Message.CommandKeyBase // MIGRAT private readonly MigrateOptions migrateOptions; private readonly int timeoutMilliseconds; private readonly int toDatabase; - private readonly RedisValue toHost, toPort; + private readonly string toHost; + private readonly int toPort; public KeyMigrateCommandMessage(int db, RedisKey key, EndPoint toServer, int toDatabase, int timeoutMilliseconds, MigrateOptions migrateOptions, CommandFlags flags) : base(db, flags, RedisCommand.MIGRATE, key) @@ -1319,7 +1320,7 @@ public KeyMigrateCommandMessage(int db, RedisKey key, EndPoint toServer, int toD if (toServer == null) throw new ArgumentNullException(nameof(toServer)); if (!Format.TryGetHostPort(toServer, out string? toHost, out int? toPort)) throw new ArgumentException($"Couldn't get host and port from {toServer}", nameof(toServer)); this.toHost = toHost; - this.toPort = toPort; + this.toPort = toPort.Value; if (toDatabase < 0) throw new ArgumentOutOfRangeException(nameof(toDatabase)); this.toDatabase = toDatabase; this.timeoutMilliseconds = timeoutMilliseconds; @@ -4392,7 +4393,7 @@ private static RedisValue GetRange(double value, Exclude exclude, bool isStart) { if ((exclude & Exclude.Stop) == 0) return value; // inclusive is default } - return "(" + Format.ToString(value); // '(' prefix means exclusive + return ("(" + Format.ToString(value)).AsRedisValue(); // '(' prefix means exclusive } private Message GetRestoreMessage(RedisKey key, byte[] value, TimeSpan? expiry, CommandFlags flags) @@ -4898,7 +4899,7 @@ private Message GetStreamPendingMessagesMessage(RedisKey key, RedisValue groupNa values[offset++] = groupName; if (minIdleTimeInMs is not null) { - values[offset++] = "IDLE"; + values[offset++] = RedisLiterals.IDLE; values[offset++] = minIdleTimeInMs; } values[offset++] = minId ?? StreamConstants.ReadMinValue; @@ -5480,7 +5481,7 @@ protected override void WriteImpl(in MessageWriter writer) { writer.WriteHeader(Command, 2); writer.WriteRaw("$4\r\nLOAD\r\n"u8); - writer.WriteBulkString((RedisValue)Script); + writer.WriteBulkString(Script); } public override int ArgCount => 2; } @@ -5675,12 +5676,12 @@ protected override void WriteImpl(in MessageWriter writer) else if (asciiHash != null) { writer.WriteHeader(RedisCommand.EVALSHA, 2 + keys.Length + values.Length); - writer.WriteBulkString((RedisValue)asciiHash); + writer.WriteBulkString(asciiHash); } else { writer.WriteHeader(RedisCommand.EVAL, 2 + keys.Length + values.Length); - writer.WriteBulkString((RedisValue)script); + writer.WriteBulkString(script); } writer.WriteBulkString(keys.Length); for (int i = 0; i < keys.Length; i++) @@ -5757,15 +5758,15 @@ private static Message CreateSortedSetRangeStoreMessage( RedisValue formattedStart = exclude switch { - Exclude.Both or Exclude.Start => $"({start}", - _ when sortedSetOrder == SortedSetOrder.ByLex => $"[{start}", + Exclude.Both or Exclude.Start => $"({start}".AsRedisValue(), + _ when sortedSetOrder == SortedSetOrder.ByLex => $"[{start}".AsRedisValue(), _ => start, }; RedisValue formattedStop = exclude switch { - Exclude.Both or Exclude.Stop => $"({stop}", - _ when sortedSetOrder == SortedSetOrder.ByLex => $"[{stop}", + Exclude.Both or Exclude.Stop => $"({stop}".AsRedisValue(), + _ when sortedSetOrder == SortedSetOrder.ByLex => $"[{stop}".AsRedisValue(), _ => stop, }; diff --git a/src/StackExchange.Redis/RedisKey.cs b/src/StackExchange.Redis/RedisKey.cs index 7550e7087..85f9ced59 100644 --- a/src/StackExchange.Redis/RedisKey.cs +++ b/src/StackExchange.Redis/RedisKey.cs @@ -222,7 +222,7 @@ public override int GetHashCode() internal RedisValue AsRedisValue() { - if (KeyPrefix == null && KeyValue is string keyString) return keyString; + if (KeyPrefix == null && KeyValue is string keyString) return keyString.AsRedisValue(); return (byte[]?)this; } diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index 52790f187..714902f24 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -48,9 +48,11 @@ public static readonly RedisValue GETKEYS = RedisValue.FromRaw("GETKEYS"u8), GETNAME = RedisValue.FromRaw("GETNAME"u8), GT = RedisValue.FromRaw("GT"u8), + HashSymbol = RedisValue.FromRaw("#"u8), HISTORY = RedisValue.FromRaw("HISTORY"u8), ID = RedisValue.FromRaw("ID"u8), IDX = RedisValue.FromRaw("IDX"u8), + IDLE = RedisValue.FromRaw("IDLE"u8), IDLETIME = RedisValue.FromRaw("IDLETIME"u8), IDMP = RedisValue.FromRaw("IDMP"u8), IDMPAUTO = RedisValue.FromRaw("IDMPAUTO"u8), @@ -77,6 +79,7 @@ public static readonly RedisValue MODULE = RedisValue.FromRaw("MODULE"u8), NODES = RedisValue.FromRaw("NODES"u8), NOSAVE = RedisValue.FromRaw("NOSAVE"u8), + nosort = RedisValue.FromRaw("nosort"u8), NOT = RedisValue.FromRaw("NOT"u8), NOVALUES = RedisValue.FromRaw("NOVALUES"u8), NUMPAT = RedisValue.FromRaw("NUMPAT"u8), @@ -165,7 +168,6 @@ public static readonly RedisValue // misc (config, etc) databases = RedisValue.FromRaw("databases"u8), - master = RedisValue.FromRaw("master"u8), no = RedisValue.FromRaw("no"u8), normal = RedisValue.FromRaw("normal"u8), pubsub = RedisValue.FromRaw("pubsub"u8), diff --git a/src/StackExchange.Redis/RedisServer.cs b/src/StackExchange.Redis/RedisServer.cs index ef58d3bbf..c531cd2fc 100644 --- a/src/StackExchange.Redis/RedisServer.cs +++ b/src/StackExchange.Redis/RedisServer.cs @@ -55,13 +55,13 @@ public bool AllowReplicaWrites public void ClientKill(EndPoint endpoint, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create(-1, flags, RedisCommand.CLIENT, RedisLiterals.KILL, (RedisValue)Format.ToString(endpoint)); + var msg = Message.Create(-1, flags, RedisCommand.CLIENT, RedisLiterals.KILL, Format.ToString(endpoint).AsRedisValue()); ExecuteSync(msg, ResultProcessor.DemandOK); } public Task ClientKillAsync(EndPoint endpoint, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create(-1, flags, RedisCommand.CLIENT, RedisLiterals.KILL, (RedisValue)Format.ToString(endpoint)); + var msg = Message.Create(-1, flags, RedisCommand.CLIENT, RedisLiterals.KILL, Format.ToString(endpoint).AsRedisValue()); return ExecuteAsync(msg, ResultProcessor.DemandOK); } @@ -602,19 +602,20 @@ public Task TimeAsync(CommandFlags flags = CommandFlags.None) return ExecuteAsync(msg, ResultProcessor.DateTime); } + private static readonly RedisValue NO = RedisValue.FromRaw("NO"u8), ONE = RedisValue.FromRaw("ONE"u8); internal static Message CreateReplicaOfMessage(ServerEndPoint sendMessageTo, EndPoint? primaryEndpoint, CommandFlags flags = CommandFlags.None) { RedisValue host, port; if (primaryEndpoint == null) { - host = "NO"; - port = "ONE"; + host = NO; + port = ONE; } else { if (Format.TryGetHostPort(primaryEndpoint, out string? hostRaw, out int? portRaw)) { - host = hostRaw; + host = hostRaw.AsRedisValue(); port = portRaw; } else @@ -934,61 +935,61 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes public EndPoint? SentinelGetMasterAddressByName(string serviceName, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.GETMASTERADDRBYNAME, (RedisValue)serviceName); + var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.GETMASTERADDRBYNAME, serviceName.AsRedisValue()); return ExecuteSync(msg, ResultProcessor.SentinelPrimaryEndpoint); } public Task SentinelGetMasterAddressByNameAsync(string serviceName, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.GETMASTERADDRBYNAME, (RedisValue)serviceName); + var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.GETMASTERADDRBYNAME, serviceName.AsRedisValue()); return ExecuteAsync(msg, ResultProcessor.SentinelPrimaryEndpoint); } public EndPoint[] SentinelGetSentinelAddresses(string serviceName, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.SENTINELS, (RedisValue)serviceName); + var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.SENTINELS, serviceName.AsRedisValue()); return ExecuteSync(msg, ResultProcessor.SentinelAddressesEndPoints, defaultValue: Array.Empty()); } public Task SentinelGetSentinelAddressesAsync(string serviceName, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.SENTINELS, (RedisValue)serviceName); + var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.SENTINELS, serviceName.AsRedisValue()); return ExecuteAsync(msg, ResultProcessor.SentinelAddressesEndPoints, defaultValue: Array.Empty()); } public EndPoint[] SentinelGetReplicaAddresses(string serviceName, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, Features.ReplicaCommands ? RedisLiterals.REPLICAS : RedisLiterals.SLAVES, (RedisValue)serviceName); + var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, Features.ReplicaCommands ? RedisLiterals.REPLICAS : RedisLiterals.SLAVES, serviceName.AsRedisValue()); return ExecuteSync(msg, ResultProcessor.SentinelAddressesEndPoints, defaultValue: Array.Empty()); } public Task SentinelGetReplicaAddressesAsync(string serviceName, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, Features.ReplicaCommands ? RedisLiterals.REPLICAS : RedisLiterals.SLAVES, (RedisValue)serviceName); + var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, Features.ReplicaCommands ? RedisLiterals.REPLICAS : RedisLiterals.SLAVES, serviceName.AsRedisValue()); return ExecuteAsync(msg, ResultProcessor.SentinelAddressesEndPoints, defaultValue: Array.Empty()); } public KeyValuePair[] SentinelMaster(string serviceName, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.MASTER, (RedisValue)serviceName); + var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.MASTER, serviceName.AsRedisValue()); return ExecuteSync(msg, ResultProcessor.StringPairInterleaved, defaultValue: Array.Empty>()); } public Task[]> SentinelMasterAsync(string serviceName, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.MASTER, (RedisValue)serviceName); + var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.MASTER, serviceName.AsRedisValue()); return ExecuteAsync(msg, ResultProcessor.StringPairInterleaved, defaultValue: Array.Empty>()); } public void SentinelFailover(string serviceName, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.FAILOVER, (RedisValue)serviceName); + var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.FAILOVER, serviceName.AsRedisValue()); ExecuteSync(msg, ResultProcessor.DemandOK); } public Task SentinelFailoverAsync(string serviceName, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.FAILOVER, (RedisValue)serviceName); + var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.FAILOVER, serviceName.AsRedisValue()); return ExecuteAsync(msg, ResultProcessor.DemandOK); } @@ -1010,7 +1011,7 @@ KeyValuePair[][] IServer.SentinelSlaves(string serviceName, Comm public KeyValuePair[][] SentinelReplicas(string serviceName, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, Features.ReplicaCommands ? RedisLiterals.REPLICAS : RedisLiterals.SLAVES, (RedisValue)serviceName); + var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, Features.ReplicaCommands ? RedisLiterals.REPLICAS : RedisLiterals.SLAVES, serviceName.AsRedisValue()); return ExecuteSync(msg, ResultProcessor.SentinelArrayOfArrays, defaultValue: Array.Empty[]>()); } @@ -1020,19 +1021,19 @@ Task[][]> IServer.SentinelSlavesAsync(string servic public Task[][]> SentinelReplicasAsync(string serviceName, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, Features.ReplicaCommands ? RedisLiterals.REPLICAS : RedisLiterals.SLAVES, (RedisValue)serviceName); + var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, Features.ReplicaCommands ? RedisLiterals.REPLICAS : RedisLiterals.SLAVES, serviceName.AsRedisValue()); return ExecuteAsync(msg, ResultProcessor.SentinelArrayOfArrays, defaultValue: Array.Empty[]>()); } public KeyValuePair[][] SentinelSentinels(string serviceName, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.SENTINELS, (RedisValue)serviceName); + var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.SENTINELS, serviceName.AsRedisValue()); return ExecuteSync(msg, ResultProcessor.SentinelArrayOfArrays, defaultValue: Array.Empty[]>()); } public Task[][]> SentinelSentinelsAsync(string serviceName, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.SENTINELS, (RedisValue)serviceName); + var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.SENTINELS, serviceName.AsRedisValue()); return ExecuteAsync(msg, ResultProcessor.SentinelArrayOfArrays, defaultValue: Array.Empty[]>()); } @@ -1096,12 +1097,12 @@ private static Message LatencyResetCommand(string[]? eventNames, CommandFlags fl case 0: return Message.Create(-1, flags, RedisCommand.LATENCY, RedisLiterals.RESET); case 1: - return Message.Create(-1, flags, RedisCommand.LATENCY, RedisLiterals.RESET, (RedisValue)eventNames[0]); + return Message.Create(-1, flags, RedisCommand.LATENCY, RedisLiterals.RESET, eventNames[0].AsRedisValue()); default: var arr = new RedisValue[eventNames.Length + 1]; arr[0] = RedisLiterals.RESET; for (int i = 0; i < eventNames.Length; i++) - arr[i + 1] = eventNames[i]; + arr[i + 1] = eventNames[i].AsRedisValue(); return Message.Create(-1, flags, RedisCommand.LATENCY, arr); } } @@ -1119,13 +1120,13 @@ public long LatencyReset(string[]? eventNames = null, CommandFlags flags = Comma public Task LatencyHistoryAsync(string eventName, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create(-1, flags, RedisCommand.LATENCY, RedisLiterals.HISTORY, (RedisValue)eventName); + var msg = Message.Create(-1, flags, RedisCommand.LATENCY, RedisLiterals.HISTORY, eventName.AsRedisValue()); return ExecuteAsync(msg, LatencyHistoryEntry.ToArray, defaultValue: Array.Empty()); } public LatencyHistoryEntry[] LatencyHistory(string eventName, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create(-1, flags, RedisCommand.LATENCY, RedisLiterals.HISTORY, (RedisValue)eventName); + var msg = Message.Create(-1, flags, RedisCommand.LATENCY, RedisLiterals.HISTORY, eventName.AsRedisValue()); return ExecuteSync(msg, LatencyHistoryEntry.ToArray, defaultValue: Array.Empty()); } diff --git a/src/StackExchange.Redis/RedisValue.cs b/src/StackExchange.Redis/RedisValue.cs index fb2a6c0b0..af9f78df4 100644 --- a/src/StackExchange.Redis/RedisValue.cs +++ b/src/StackExchange.Redis/RedisValue.cs @@ -11,6 +11,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; +using RESPite; namespace StackExchange.Redis { @@ -780,17 +781,17 @@ internal static RedisValue TryParse(object? obj, out bool valid) switch (obj) { case null: return Null; - case string v: return v; + case string v: return new(v); case int v: return v; case uint v: return v; - case double v: return v; - case byte[] v: return v; + case double v: return new(v); + case byte[] v: return new(v); case bool v: return v; case long v: return v; case ulong v: return v; - case float v: return v; - case ReadOnlyMemory v: return v; - case Memory v: return v; + case float v: return new(v); + case ReadOnlyMemory v: return new(v); + case Memory v: return new(v); case RedisValue v: return v; default: valid = false; @@ -894,7 +895,12 @@ public static implicit operator RedisValue(ReadOnlySequence value) /// Creates a new from an . /// /// The to convert to a . +#if DEBUG + [Experimental(Experiments.StringToRedisValue)] +#endif +#pragma warning disable RS0016 public static implicit operator RedisValue(string? value) => value is null ? Null : new(value); +#pragma warning restore RS0016 /// /// Creates a new from an . diff --git a/src/StackExchange.Redis/Role.cs b/src/StackExchange.Redis/Role.cs index 587194026..d69b8ea54 100644 --- a/src/StackExchange.Redis/Role.cs +++ b/src/StackExchange.Redis/Role.cs @@ -8,6 +8,8 @@ namespace StackExchange.Redis /// public abstract class Role { + internal const string LabelForMaster = "master"; + internal static Unknown Null { get; } = new Unknown(""); /// @@ -67,7 +69,7 @@ internal Replica(string ip, int port, long offset) public override string ToString() => $"{Ip}:{Port} - {ReplicationOffset}"; } - internal Master(long offset, ICollection replicas) : base(RedisLiterals.master!) + internal Master(long offset, ICollection replicas) : base(LabelForMaster) { ReplicationOffset = offset; Replicas = replicas; diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index eca06f30d..f191c1c3b 100644 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -1020,14 +1020,14 @@ private async Task HandshakeAsync(PhysicalConnection connection, ILogger? log) if (!string.IsNullOrWhiteSpace(user) && Multiplexer.CommandMap.IsAvailable(RedisCommand.AUTH)) { log?.LogInformationAuthenticatingUserPassword(new(this)); - msg = Message.Create(-1, CommandFlags.FireAndForget | Message.NoFlushFlag, RedisCommand.AUTH, (RedisValue)user, (RedisValue)password); + msg = Message.Create(-1, CommandFlags.FireAndForget | Message.NoFlushFlag, RedisCommand.AUTH, user.AsRedisValue(), password.AsRedisValue()); msg.SetInternalCall(); await WriteDirectOrQueueFireAndForgetAsync(connection, msg, ResultProcessor.DemandOK).ForAwait(); } else if (!string.IsNullOrWhiteSpace(password) && Multiplexer.CommandMap.IsAvailable(RedisCommand.AUTH)) { log?.LogInformationAuthenticatingPassword(new(this)); - msg = Message.Create(-1, CommandFlags.FireAndForget | Message.NoFlushFlag, RedisCommand.AUTH, (RedisValue)password); + msg = Message.Create(-1, CommandFlags.FireAndForget | Message.NoFlushFlag, RedisCommand.AUTH, password.AsRedisValue()); msg.SetInternalCall(); await WriteDirectOrQueueFireAndForgetAsync(connection, msg, ResultProcessor.DemandOK).ForAwait(); } @@ -1037,7 +1037,7 @@ private async Task HandshakeAsync(PhysicalConnection connection, ILogger? log) if (!string.IsNullOrWhiteSpace(clientName)) { log?.LogInformationSettingClientName(new(this), clientName); - msg = Message.Create(-1, CommandFlags.FireAndForget | Message.NoFlushFlag, RedisCommand.CLIENT, RedisLiterals.SETNAME, (RedisValue)clientName); + msg = Message.Create(-1, CommandFlags.FireAndForget | Message.NoFlushFlag, RedisCommand.CLIENT, RedisLiterals.SETNAME, clientName.AsRedisValue()); msg.SetInternalCall(); await WriteDirectOrQueueFireAndForgetAsync(connection, msg, ResultProcessor.DemandOK).ForAwait(); } @@ -1051,7 +1051,7 @@ private async Task HandshakeAsync(PhysicalConnection connection, ILogger? log) var libName = Multiplexer.GetFullLibraryName(); if (!string.IsNullOrWhiteSpace(libName)) { - msg = Message.Create(-1, CommandFlags.FireAndForget | Message.NoFlushFlag, RedisCommand.CLIENT, RedisLiterals.SETINFO, RedisLiterals.lib_name, libName); + msg = Message.Create(-1, CommandFlags.FireAndForget | Message.NoFlushFlag, RedisCommand.CLIENT, RedisLiterals.SETINFO, RedisLiterals.lib_name, libName.AsRedisValue()); msg.SetInternalCall(); await WriteDirectOrQueueFireAndForgetAsync(connection, msg, ResultProcessor.DemandOK).ForAwait(); } @@ -1059,7 +1059,7 @@ private async Task HandshakeAsync(PhysicalConnection connection, ILogger? log) var version = ClientInfoSanitize(Utils.GetLibVersion()); if (!string.IsNullOrWhiteSpace(version)) { - msg = Message.Create(-1, CommandFlags.FireAndForget | Message.NoFlushFlag, RedisCommand.CLIENT, RedisLiterals.SETINFO, RedisLiterals.lib_ver, version); + msg = Message.Create(-1, CommandFlags.FireAndForget | Message.NoFlushFlag, RedisCommand.CLIENT, RedisLiterals.SETINFO, RedisLiterals.lib_ver, version.AsRedisValue()); msg.SetInternalCall(); await WriteDirectOrQueueFireAndForgetAsync(connection, msg, ResultProcessor.DemandOK).ForAwait(); } diff --git a/src/StackExchange.Redis/StreamConstants.cs b/src/StackExchange.Redis/StreamConstants.cs index 92c37222a..b63489e01 100644 --- a/src/StackExchange.Redis/StreamConstants.cs +++ b/src/StackExchange.Redis/StreamConstants.cs @@ -10,64 +10,64 @@ internal static class StreamConstants /// /// The "~" value used with the MAXLEN option. /// - internal static readonly RedisValue ApproximateMaxLen = "~"; + internal static readonly RedisValue ApproximateMaxLen = RedisValue.FromRaw("~"u8); /// /// The "*" value used with the XADD command. /// - internal static readonly RedisValue AutoGeneratedId = "*"; + internal static readonly RedisValue AutoGeneratedId = RedisValue.FromRaw("*"u8); /// /// The "$" value used in the XGROUP command. Indicates reading only new messages from the stream. /// - internal static readonly RedisValue NewMessages = "$"; + internal static readonly RedisValue NewMessages = RedisValue.FromRaw("$"u8); /// /// The "0" value used in the XGROUP command. Indicates reading all messages from the stream. /// - internal static readonly RedisValue AllMessages = "0"; + internal static readonly RedisValue AllMessages = RedisValue.FromRaw("0"u8); /// /// The "-" value used in the XRANGE, XREAD, and XREADGROUP commands. Indicates the minimum message ID from the stream. /// - internal static readonly RedisValue ReadMinValue = "-"; + internal static readonly RedisValue ReadMinValue = RedisValue.FromRaw("-"u8); /// /// The "+" value used in the XRANGE, XREAD, and XREADGROUP commands. Indicates the maximum message ID from the stream. /// - internal static readonly RedisValue ReadMaxValue = "+"; + internal static readonly RedisValue ReadMaxValue = RedisValue.FromRaw("+"u8); /// /// The ">" value used in the XREADGROUP command. Use this to read messages that have not been delivered to a consumer group. /// - internal static readonly RedisValue UndeliveredMessages = ">"; + internal static readonly RedisValue UndeliveredMessages = RedisValue.FromRaw(">"u8); - internal static readonly RedisValue Consumers = "CONSUMERS"; + internal static readonly RedisValue Consumers = RedisValue.FromRaw("CONSUMERS"u8); - internal static readonly RedisValue Count = "COUNT"; + internal static readonly RedisValue Count = RedisValue.FromRaw("COUNT"u8); - internal static readonly RedisValue Create = "CREATE"; + internal static readonly RedisValue Create = RedisValue.FromRaw("CREATE"u8); - internal static readonly RedisValue DeleteConsumer = "DELCONSUMER"; + internal static readonly RedisValue DeleteConsumer = RedisValue.FromRaw("DELCONSUMER"u8); - internal static readonly RedisValue Destroy = "DESTROY"; + internal static readonly RedisValue Destroy = RedisValue.FromRaw("DESTROY"u8); - internal static readonly RedisValue Groups = "GROUPS"; + internal static readonly RedisValue Groups = RedisValue.FromRaw("GROUPS"u8); - internal static readonly RedisValue JustId = "JUSTID"; + internal static readonly RedisValue JustId = RedisValue.FromRaw("JUSTID"u8); - internal static readonly RedisValue SetId = "SETID"; + internal static readonly RedisValue SetId = RedisValue.FromRaw("SETID"u8); - internal static readonly RedisValue MaxLen = "MAXLEN"; - internal static readonly RedisValue MinId = "MINID"; + internal static readonly RedisValue MaxLen = RedisValue.FromRaw("MAXLEN"u8); + internal static readonly RedisValue MinId = RedisValue.FromRaw("MINID"u8); - internal static readonly RedisValue MkStream = "MKSTREAM"; + internal static readonly RedisValue MkStream = RedisValue.FromRaw("MKSTREAM"u8); - internal static readonly RedisValue Stream = "STREAM"; + internal static readonly RedisValue Stream = RedisValue.FromRaw("STREAM"u8); - private static readonly RedisValue KeepRef = "KEEPREF", DelRef = "DELREF", Acked = "ACKED"; + private static readonly RedisValue KeepRef = RedisValue.FromRaw("KEEPREF"u8), DelRef = RedisValue.FromRaw("DELREF"u8), Acked = RedisValue.FromRaw("ACKED"u8); - internal static readonly RedisValue Ids = "IDS"; + internal static readonly RedisValue Ids = RedisValue.FromRaw("IDS"u8); internal static RedisValue GetMode(StreamTrimMode mode) => mode switch { diff --git a/tests/Directory.Build.targets b/tests/Directory.Build.targets index e1d7c8887..bed51fd15 100644 --- a/tests/Directory.Build.targets +++ b/tests/Directory.Build.targets @@ -1,6 +1,7 @@ false + $(NoWarn);StringToRedisValue From c4ac5f006731ac11080f40698872867ca5aa76ba Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 10 Jul 2026 06:41:40 +0100 Subject: [PATCH 5/9] fix shipfiles for DEBUG hack --- src/RESPite.Benchmark/RESPite.Benchmark.csproj | 1 + src/StackExchange.Redis/PublicAPI/Debug/PublicAPI.txt | 2 ++ src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt | 1 - src/StackExchange.Redis/PublicAPI/Release/PublicAPI.txt | 2 ++ src/StackExchange.Redis/StackExchange.Redis.csproj | 2 ++ tests/StackExchange.Redis.Tests/RoleTests.cs | 2 +- .../StackExchange.Redis.Server.csproj | 2 +- 7 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 src/StackExchange.Redis/PublicAPI/Debug/PublicAPI.txt create mode 100644 src/StackExchange.Redis/PublicAPI/Release/PublicAPI.txt diff --git a/src/RESPite.Benchmark/RESPite.Benchmark.csproj b/src/RESPite.Benchmark/RESPite.Benchmark.csproj index d2e755738..8397870af 100644 --- a/src/RESPite.Benchmark/RESPite.Benchmark.csproj +++ b/src/RESPite.Benchmark/RESPite.Benchmark.csproj @@ -16,6 +16,7 @@ 2025 - $([System.DateTime]::Now.Year) Marc Gravell 3 + $(NoWarn);StringToRedisValue diff --git a/src/StackExchange.Redis/PublicAPI/Debug/PublicAPI.txt b/src/StackExchange.Redis/PublicAPI/Debug/PublicAPI.txt new file mode 100644 index 000000000..690e2d9c9 --- /dev/null +++ b/src/StackExchange.Redis/PublicAPI/Debug/PublicAPI.txt @@ -0,0 +1,2 @@ +#nullable enable +[StringToRedisValue]static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(string? value) -> StackExchange.Redis.RedisValue diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index cc104c577..7e8381c6e 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -1837,7 +1837,6 @@ static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.Redi static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(int? value) -> StackExchange.Redis.RedisValue static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(long value) -> StackExchange.Redis.RedisValue static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(long? value) -> StackExchange.Redis.RedisValue -static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(string? value) -> StackExchange.Redis.RedisValue static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(System.Memory value) -> StackExchange.Redis.RedisValue static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(System.ReadOnlyMemory value) -> StackExchange.Redis.RedisValue static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(uint value) -> StackExchange.Redis.RedisValue diff --git a/src/StackExchange.Redis/PublicAPI/Release/PublicAPI.txt b/src/StackExchange.Redis/PublicAPI/Release/PublicAPI.txt new file mode 100644 index 000000000..bb19df634 --- /dev/null +++ b/src/StackExchange.Redis/PublicAPI/Release/PublicAPI.txt @@ -0,0 +1,2 @@ +#nullable enable +static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(string? value) -> StackExchange.Redis.RedisValue diff --git a/src/StackExchange.Redis/StackExchange.Redis.csproj b/src/StackExchange.Redis/StackExchange.Redis.csproj index d3dd60aae..705b07eec 100644 --- a/src/StackExchange.Redis/StackExchange.Redis.csproj +++ b/src/StackExchange.Redis/StackExchange.Redis.csproj @@ -43,6 +43,8 @@ + + diff --git a/tests/StackExchange.Redis.Tests/RoleTests.cs b/tests/StackExchange.Redis.Tests/RoleTests.cs index 92f66e0e4..054e39f48 100644 --- a/tests/StackExchange.Redis.Tests/RoleTests.cs +++ b/tests/StackExchange.Redis.Tests/RoleTests.cs @@ -41,7 +41,7 @@ public async Task PrimaryRole(bool allowAdmin) // should work with or without ad } } Assert.NotNull(role); - Assert.Equal(role.Value, RedisLiterals.master); + Assert.Equal(Role.LabelForMaster, role.Value); var primary = role as Role.Master; Assert.NotNull(primary); Assert.NotNull(primary.Replicas); diff --git a/toys/StackExchange.Redis.Server/StackExchange.Redis.Server.csproj b/toys/StackExchange.Redis.Server/StackExchange.Redis.Server.csproj index 3e800c817..4151146bf 100644 --- a/toys/StackExchange.Redis.Server/StackExchange.Redis.Server.csproj +++ b/toys/StackExchange.Redis.Server/StackExchange.Redis.Server.csproj @@ -9,7 +9,7 @@ Server;Async;Redis;Cache;PubSub;Messaging Library true - $(NoWarn);CS1591 + $(NoWarn);CS1591;StringToRedisValue From 386c8339a5f035aee0d725b1b5081a0f1f23fff5 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 10 Jul 2026 09:54:34 +0100 Subject: [PATCH 6/9] docs --- docs/exp/StringToRedisValue.md | 18 ++++++++++++++++++ src/StackExchange.Redis/RedisValue.cs | 3 ++- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 docs/exp/StringToRedisValue.md diff --git a/docs/exp/StringToRedisValue.md b/docs/exp/StringToRedisValue.md new file mode 100644 index 000000000..961e966f2 --- /dev/null +++ b/docs/exp/StringToRedisValue.md @@ -0,0 +1,18 @@ +StringToRedisValue +- + +Oh dear, it looks like you fell into a `DEBUG` warning. **Don't panic!** You're probably fine. + +Short version, *if you're in the core library when seeing this*: + +1. If you're *writing* data (to the server), you're probably fine - just add `.AsRedisValue()` to say "trust me bro"; all strings are valid as `RedisValue` content - you're fine. If the value + is a literal (i.e. `"some const string"`) or a `const`, you might want to look at `RedisLiterals` and the `.FromRaw(...)` API, which can be more efficient, but otherwise: you do you! +2. However, if you're *reading* data (from the server), then you need to be careful - there's a good chance you've used `reader.ReadString()` when you meant to use `reader.ReadRedisValue()`. + +The impact of *2* would be that *string* payloads work fine, but you silently corrupt *binary* payloads. Additionally, if the value is *small* or an obvious integer, `RedisValue` may +be more efficient (zero-allocation), and even for non-trivial text-like payloads: using `RedisValue` defers the UTF8 decode cost to the caller. So: ask yourself - is this actually +a `string` in all cases? + +This warning only exists in local `DEBUG` builds - it doesn't exist in the public package. + +If you're actually in an auxiliary package (tests, benchmarks, etc): we care less. Feel free to add `$(NoWarn);StringToRedisValue` to the csproj. \ No newline at end of file diff --git a/src/StackExchange.Redis/RedisValue.cs b/src/StackExchange.Redis/RedisValue.cs index af9f78df4..5792abd60 100644 --- a/src/StackExchange.Redis/RedisValue.cs +++ b/src/StackExchange.Redis/RedisValue.cs @@ -896,7 +896,8 @@ public static implicit operator RedisValue(ReadOnlySequence value) /// /// The to convert to a . #if DEBUG - [Experimental(Experiments.StringToRedisValue)] + // if you're here to ask "why is my compiler warning me": go read StringToRedisValue.md + [Experimental(Experiments.StringToRedisValue, UrlFormat = Experiments.UrlFormat)] #endif #pragma warning disable RS0016 public static implicit operator RedisValue(string? value) => value is null ? Null : new(value); From fbbede7afe3afa54222d93beb0ead9232c219d6b Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 10 Jul 2026 10:03:20 +0100 Subject: [PATCH 7/9] fix exp link --- src/RESPite/Shared/Experiments.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/RESPite/Shared/Experiments.cs b/src/RESPite/Shared/Experiments.cs index 959965c26..304b1f624 100644 --- a/src/RESPite/Shared/Experiments.cs +++ b/src/RESPite/Shared/Experiments.cs @@ -5,7 +5,8 @@ // where SomeFeature has the next label, for example "SER042", and /docs/exp/SER042.md exists internal static class Experiments { - public const string UrlFormat = "https://stackexchange.github.io/StackExchange.Redis/exp/"; + // note: {0} is substituted with the DiagnosticId by the analyzer, e.g. .../exp/SER002 + public const string UrlFormat = "https://stackexchange.github.io/StackExchange.Redis/exp/{0}"; // ReSharper disable InconsistentNaming public const string Server_8_4 = "SER002"; From eaddcd25c5ea1961da3341af10d33f2627327161 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 10 Jul 2026 10:07:37 +0100 Subject: [PATCH 8/9] I knew we had a Write...(RedisKey) method somewhere! --- src/StackExchange.Redis/ArrayGrepRequest.cs | 2 +- .../Increx.IncrexMessage.cs | 2 +- src/StackExchange.Redis/MessageWriter.cs | 33 ------------------- 3 files changed, 2 insertions(+), 35 deletions(-) diff --git a/src/StackExchange.Redis/ArrayGrepRequest.cs b/src/StackExchange.Redis/ArrayGrepRequest.cs index 5fd7b0dd2..bc691053f 100644 --- a/src/StackExchange.Redis/ArrayGrepRequest.cs +++ b/src/StackExchange.Redis/ArrayGrepRequest.cs @@ -367,7 +367,7 @@ private static void AddIndex(in MessageWriter writer, RedisArrayIndex? index, Re protected override void WriteImpl(in MessageWriter writer) { writer.WriteHeader(Command, ArgCount); - writer.WriteBulkString(key); + writer.Write(key); if (request.IsReversed) { AddIndex(writer, request.End, "$1\r\n+\r\n"u8); diff --git a/src/StackExchange.Redis/Increx.IncrexMessage.cs b/src/StackExchange.Redis/Increx.IncrexMessage.cs index dc8421c24..6941e60e6 100644 --- a/src/StackExchange.Redis/Increx.IncrexMessage.cs +++ b/src/StackExchange.Redis/Increx.IncrexMessage.cs @@ -32,7 +32,7 @@ public override int ArgCount protected override void WriteImpl(in MessageWriter writer) { writer.WriteHeader(Command, ArgCount); - writer.WriteBulkString(Key); + writer.Write(Key); WriteIncrementKindAndValue(writer); WriteBounds(writer); WriteOptions(writer); diff --git a/src/StackExchange.Redis/MessageWriter.cs b/src/StackExchange.Redis/MessageWriter.cs index 61753eece..93e63b58d 100644 --- a/src/StackExchange.Redis/MessageWriter.cs +++ b/src/StackExchange.Redis/MessageWriter.cs @@ -80,39 +80,6 @@ internal void Write(in RedisChannel channel) internal void WriteBulkString(in RedisValue value) => WriteBulkString(value, _writer); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void WriteBulkString(in RedisKey key) - { - if (key.IsNull) - { - WriteRaw(NullBulkString); - } - else if (key.IsEmpty) - { - WriteRaw(EmptyBulkString); - } - else if (key.KeyPrefix is { Length: > 1 }) - { - // this has been optimized in the experimental 3.2+ branch - byte[]? lease = null; - var maxLen = key.MaxByteCount(); - Span span = maxLen <= 128 ? stackalloc byte[128] : (lease = ArrayPool.Shared.Rent(maxLen)); - var len = key.CopyTo(span); - WriteBulkString(span.Slice(0, len)); - if (lease is not null) ArrayPool.Shared.Return(lease); - } - else if (key.KeyValue is string s) - { - // no prefix and not null/empty; value can be string... - WriteBulkString(s); - } - else - { - // ...or a blob - WriteBulkString((byte[])key.KeyValue!); - } - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void WriteBulkString(string? value) { From a2870a7b7eaf9234164e8280cd5421f306e96b74 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 10 Jul 2026 10:14:08 +0100 Subject: [PATCH 9/9] move NO ONE to RedisLiterals --- src/StackExchange.Redis/RedisLiterals.cs | 1 + src/StackExchange.Redis/RedisServer.cs | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index 714902f24..9709eb02e 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -77,6 +77,7 @@ public static readonly RedisValue MIN = RedisValue.FromRaw("MIN"u8), MINMATCHLEN = RedisValue.FromRaw("MINMATCHLEN"u8), MODULE = RedisValue.FromRaw("MODULE"u8), + NO = RedisValue.FromRaw("NO"u8), NODES = RedisValue.FromRaw("NODES"u8), NOSAVE = RedisValue.FromRaw("NOSAVE"u8), nosort = RedisValue.FromRaw("nosort"u8), diff --git a/src/StackExchange.Redis/RedisServer.cs b/src/StackExchange.Redis/RedisServer.cs index c531cd2fc..61918347f 100644 --- a/src/StackExchange.Redis/RedisServer.cs +++ b/src/StackExchange.Redis/RedisServer.cs @@ -602,14 +602,13 @@ public Task TimeAsync(CommandFlags flags = CommandFlags.None) return ExecuteAsync(msg, ResultProcessor.DateTime); } - private static readonly RedisValue NO = RedisValue.FromRaw("NO"u8), ONE = RedisValue.FromRaw("ONE"u8); internal static Message CreateReplicaOfMessage(ServerEndPoint sendMessageTo, EndPoint? primaryEndpoint, CommandFlags flags = CommandFlags.None) { RedisValue host, port; if (primaryEndpoint == null) { - host = NO; - port = ONE; + host = RedisLiterals.NO; + port = RedisLiterals.ONE; } else {