diff --git a/Directory.Build.props b/Directory.Build.props
index 9f512d5e9..42de5875c 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -10,7 +10,7 @@
true
$(MSBuildThisFileDirectory)Shared.ruleset
NETSDK1069
- NU5105;NU1507
+ $(NoWarn);NU5105;NU1507;SER001
https://stackexchange.github.io/StackExchange.Redis/ReleaseNotes
https://stackexchange.github.io/StackExchange.Redis/
MIT
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 79c404dc2..df8c078a3 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -8,6 +8,10 @@
+
+
+
+
@@ -23,6 +27,7 @@
+
diff --git a/StackExchange.Redis.sln b/StackExchange.Redis.sln
index 8f772ae42..2ed4ebfb3 100644
--- a/StackExchange.Redis.sln
+++ b/StackExchange.Redis.sln
@@ -122,6 +122,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "docs", "docs\docs.csproj",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StackExchange.Redis.Benchmarks", "tests\StackExchange.Redis.Benchmarks\StackExchange.Redis.Benchmarks.csproj", "{59889284-FFEE-82E7-94CB-3B43E87DA6CF}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "eng", "eng", "{5FA0958E-6EBD-45F4-808E-3447A293F96F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StackExchange.Redis.Build", "eng\StackExchange.Redis.Build\StackExchange.Redis.Build.csproj", "{190742E1-FA50-4E36-A8C4-88AE87654340}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -180,6 +184,10 @@ Global
{59889284-FFEE-82E7-94CB-3B43E87DA6CF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{59889284-FFEE-82E7-94CB-3B43E87DA6CF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{59889284-FFEE-82E7-94CB-3B43E87DA6CF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {190742E1-FA50-4E36-A8C4-88AE87654340}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {190742E1-FA50-4E36-A8C4-88AE87654340}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {190742E1-FA50-4E36-A8C4-88AE87654340}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {190742E1-FA50-4E36-A8C4-88AE87654340}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -202,6 +210,7 @@ Global
{A0F89B8B-32A3-4C28-8F1B-ADE343F16137} = {73A5C363-CA1F-44C4-9A9B-EF791A76BA6A}
{69A0ACF2-DF1F-4F49-B554-F732DCA938A3} = {73A5C363-CA1F-44C4-9A9B-EF791A76BA6A}
{59889284-FFEE-82E7-94CB-3B43E87DA6CF} = {73A5C363-CA1F-44C4-9A9B-EF791A76BA6A}
+ {190742E1-FA50-4E36-A8C4-88AE87654340} = {5FA0958E-6EBD-45F4-808E-3447A293F96F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {193AA352-6748-47C1-A5FC-C9AA6B5F000B}
diff --git a/StackExchange.Redis.sln.DotSettings b/StackExchange.Redis.sln.DotSettings
index de893e54d..b72a49d2c 100644
--- a/StackExchange.Redis.sln.DotSettings
+++ b/StackExchange.Redis.sln.DotSettings
@@ -1,4 +1,5 @@
OK
PONG
- True
\ No newline at end of file
+ True
+ True
\ No newline at end of file
diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md
index c782d3f5a..a3e85a875 100644
--- a/docs/ReleaseNotes.md
+++ b/docs/ReleaseNotes.md
@@ -13,6 +13,7 @@ Current package versions:
- Add `Condition.SortedSet[Not]ContainsStarting` condition for transactions ([#2638 by ArnoKoll](https://github.com/StackExchange/StackExchange.Redis/pull/2638))
- Add support for XPENDING Idle time filter ([#2822 by david-brink-talogy](https://github.com/StackExchange/StackExchange.Redis/pull/2822))
- Improve `double` formatting performance on net8+ ([#2928 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2928))
+- Add vector-set support ([#2939 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2939))
- Add `GetServer(RedisKey, ...)` API ([#2936 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2936))
- Fix error constructing `StreamAdd` message ([#2941 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2941))
diff --git a/docs/exp/SER001.md b/docs/exp/SER001.md
new file mode 100644
index 000000000..2def8be6e
--- /dev/null
+++ b/docs/exp/SER001.md
@@ -0,0 +1,22 @@
+At the current time, [Redis documents that](https://redis.io/docs/latest/commands/vadd/):
+
+> Vector set is a new data type that is currently in preview and may be subject to change.
+
+As such, the corresponding library feature must also be considered subject to change:
+
+1. Existing bindings may cease working correctly if the underlying server API changes.
+2. Changes to the server API may require changes to the library API, manifesting in either/both of build-time
+ or run-time breaks.
+
+While this seems *unlikely*, it must be considered a possibility. If you acknowledge this, you can suppress
+this warning by adding the following to your `csproj` file:
+
+```xml
+$(NoWarn);SER001
+```
+
+or more granularly / locally in C#:
+
+``` c#
+#pragma warning disable SER001
+```
\ No newline at end of file
diff --git a/eng/StackExchange.Redis.Build/FastHashGenerator.cs b/eng/StackExchange.Redis.Build/FastHashGenerator.cs
new file mode 100644
index 000000000..cdbc94ebe
--- /dev/null
+++ b/eng/StackExchange.Redis.Build/FastHashGenerator.cs
@@ -0,0 +1,215 @@
+using System.Buffers;
+using System.Collections.Immutable;
+using System.Reflection;
+using System.Text;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace StackExchange.Redis.Build;
+
+[Generator(LanguageNames.CSharp)]
+public class FastHashGenerator : IIncrementalGenerator
+{
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ var literals = context.SyntaxProvider
+ .CreateSyntaxProvider(Predicate, Transform)
+ .Where(pair => pair.Name is { Length: > 0 })
+ .Collect();
+
+ context.RegisterSourceOutput(literals, Generate);
+ }
+
+ private bool Predicate(SyntaxNode node, CancellationToken cancellationToken)
+ {
+ // looking for [FastHash] partial static class Foo { }
+ if (node is ClassDeclarationSyntax decl
+ && decl.Modifiers.Any(SyntaxKind.StaticKeyword)
+ && decl.Modifiers.Any(SyntaxKind.PartialKeyword))
+ {
+ foreach (var attribList in decl.AttributeLists)
+ {
+ foreach (var attrib in attribList.Attributes)
+ {
+ if (attrib.Name.ToString() is "FastHashAttribute" or "FastHash") return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private static string GetName(INamedTypeSymbol type)
+ {
+ if (type.ContainingType is null) return type.Name;
+ var stack = new Stack();
+ while (true)
+ {
+ stack.Push(type.Name);
+ if (type.ContainingType is null) break;
+ type = type.ContainingType;
+ }
+ var sb = new StringBuilder(stack.Pop());
+ while (stack.Count != 0)
+ {
+ sb.Append('.').Append(stack.Pop());
+ }
+ return sb.ToString();
+ }
+
+ private (string Namespace, string ParentType, string Name, string Value) Transform(
+ GeneratorSyntaxContext ctx,
+ CancellationToken cancellationToken)
+ {
+ // extract the name and value (defaults to name, but can be overridden via attribute) and the location
+ if (ctx.SemanticModel.GetDeclaredSymbol(ctx.Node) is not INamedTypeSymbol named) return default;
+ string ns = "", parentType = "";
+ if (named.ContainingType is { } containingType)
+ {
+ parentType = GetName(containingType);
+ ns = containingType.ContainingNamespace.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat);
+ }
+ else if (named.ContainingNamespace is { } containingNamespace)
+ {
+ ns = containingNamespace.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat);
+ }
+
+ string name = named.Name, value = "";
+ foreach (var attrib in named.GetAttributes())
+ {
+ if (attrib.AttributeClass?.Name == "FastHashAttribute")
+ {
+ if (attrib.ConstructorArguments.Length == 1)
+ {
+ if (attrib.ConstructorArguments[0].Value?.ToString() is { Length: > 0 } val)
+ {
+ value = val;
+ break;
+ }
+ }
+ }
+ }
+
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ value = name.Replace("_", "-"); // if nothing explicit: infer from name
+ }
+
+ return (ns, parentType, name, value);
+ }
+
+ private string GetVersion()
+ {
+ var asm = GetType().Assembly;
+ if (asm.GetCustomAttributes(typeof(AssemblyFileVersionAttribute), false).FirstOrDefault() is
+ AssemblyFileVersionAttribute { Version: { Length: > 0 } } version)
+ {
+ return version.Version;
+ }
+
+ return asm.GetName().Version?.ToString() ?? "??";
+ }
+
+ private void Generate(
+ SourceProductionContext ctx,
+ ImmutableArray<(string Namespace, string ParentType, string Name, string Value)> literals)
+ {
+ if (literals.IsDefaultOrEmpty) return;
+
+ var sb = new StringBuilder("// ")
+ .AppendLine().Append("// ").Append(GetType().Name).Append(" v").Append(GetVersion()).AppendLine();
+
+ // lease a buffer that is big enough for the longest string
+ var buffer = ArrayPool.Shared.Rent(
+ Encoding.UTF8.GetMaxByteCount(literals.Max(l => l.Value.Length)));
+ int indent = 0;
+
+ StringBuilder NewLine() => sb.AppendLine().Append(' ', indent * 4);
+ NewLine().Append("using System;");
+ NewLine().Append("using StackExchange.Redis;");
+ NewLine().Append("#pragma warning disable CS8981");
+ foreach (var grp in literals.GroupBy(l => (l.Namespace, l.ParentType)))
+ {
+ NewLine();
+ int braces = 0;
+ if (!string.IsNullOrWhiteSpace(grp.Key.Namespace))
+ {
+ NewLine().Append("namespace ").Append(grp.Key.Namespace);
+ NewLine().Append("{");
+ indent++;
+ braces++;
+ }
+ if (!string.IsNullOrWhiteSpace(grp.Key.ParentType))
+ {
+ if (grp.Key.ParentType.Contains('.')) // nested types
+ {
+ foreach (var part in grp.Key.ParentType.Split('.'))
+ {
+ NewLine().Append("partial class ").Append(part);
+ NewLine().Append("{");
+ indent++;
+ braces++;
+ }
+ }
+ else
+ {
+ NewLine().Append("partial class ").Append(grp.Key.ParentType);
+ NewLine().Append("{");
+ indent++;
+ braces++;
+ }
+ }
+
+ foreach (var literal in grp)
+ {
+ int len;
+ unsafe
+ {
+ fixed (byte* bPtr = buffer) // netstandard2.0 forces fallback API
+ {
+ fixed (char* cPtr = literal.Value)
+ {
+ len = Encoding.UTF8.GetBytes(cPtr, literal.Value.Length, bPtr, buffer.Length);
+ }
+ }
+ }
+
+ // perform string escaping on the generated value (this includes the quotes, note)
+ var csValue = SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(literal.Value)).ToFullString();
+
+ var hash = FastHash.Hash64(buffer.AsSpan(0, len));
+ NewLine().Append("static partial class ").Append(literal.Name);
+ NewLine().Append("{");
+ indent++;
+ NewLine().Append("public const int Length = ").Append(len).Append(';');
+ NewLine().Append("public const long Hash = ").Append(hash).Append(';');
+ NewLine().Append("public static ReadOnlySpan U8 => ").Append(csValue).Append("u8;");
+ NewLine().Append("public const string Text = ").Append(csValue).Append(';');
+ if (len <= 8)
+ {
+ // the hash enforces all the values
+ NewLine().Append("public static bool Is(long hash, in RawResult value) => hash == Hash && value.Payload.Length == Length;");
+ NewLine().Append("public static bool Is(long hash, ReadOnlySpan value) => hash == Hash & value.Length == Length;");
+ }
+ else
+ {
+ NewLine().Append("public static bool Is(long hash, in RawResult value) => hash == Hash && value.IsEqual(U8);");
+ NewLine().Append("public static bool Is(long hash, ReadOnlySpan value) => hash == Hash && value.SequenceEqual(U8);");
+ }
+ indent--;
+ NewLine().Append("}");
+ }
+
+ // handle any closing braces
+ while (braces-- > 0)
+ {
+ indent--;
+ NewLine().Append("}");
+ }
+ }
+
+ ArrayPool.Shared.Return(buffer);
+ ctx.AddSource("FastHash.generated.cs", sb.ToString());
+ }
+}
diff --git a/eng/StackExchange.Redis.Build/FastHashGenerator.md b/eng/StackExchange.Redis.Build/FastHashGenerator.md
new file mode 100644
index 000000000..7fc5103ae
--- /dev/null
+++ b/eng/StackExchange.Redis.Build/FastHashGenerator.md
@@ -0,0 +1,64 @@
+# FastHashGenerator
+
+Efficient matching of well-known short string tokens is a high-volume scenario, for example when matching RESP literals.
+
+The purpose of this generator is to interpret inputs like:
+
+``` c#
+[FastHash] public static partial class bin { }
+[FastHash] public static partial class f32 { }
+```
+
+Usually the token is inferred from the name; `[FastHash("real value")]` can be used if the token is not a valid identifier.
+Underscore is replaced with hyphen, so a field called `my_token` has the default value `"my-token"`.
+The generator demands *all* of `[FastHash] public static partial class`, and note that any *containing* types must
+*also* be declared `partial`.
+
+The output is of the form:
+
+``` c#
+static partial class bin
+{
+ public const int Length = 3;
+ public const long Hash = 7235938;
+ public static ReadOnlySpan U8 => @"bin"u8;
+ public static string Text => @"bin";
+ public static bool Is(long hash, in RawResult value) => ...
+ public static bool Is(long hash, in ReadOnlySpan value) => ...
+}
+static partial class f32
+{
+ public const int Length = 3;
+ public const long Hash = 3289958;
+ public static ReadOnlySpan U8 => @"f32"u8;
+ public const string Text = @"f32";
+ public static bool Is(long hash, in RawResult value) => ...
+ public static bool Is(long hash, in ReadOnlySpan value) => ...
+}
+```
+
+(this API is strictly an internal implementation detail, and can change at any time)
+
+This generated code allows for fast, efficient, and safe matching of well-known tokens, for example:
+
+``` c#
+var key = ...
+var hash = key.Hash64();
+switch (key.Length)
+{
+ case bin.Length when bin.Is(hash, key):
+ // handle bin
+ break;
+ case f32.Length when f32.Is(hash, key):
+ // handle f32
+ break;
+}
+```
+
+The switch on the `Length` is optional, but recommended - these low values can often be implemented (by the compiler)
+as a simple jump-table, which is very fast. However, switching on the hash itself is also valid. All hash matches
+must also perform a sequence equality check - the `Is(hash, value)` convenience method validates both hash and equality.
+
+Note that `switch` requires `const` values, hence why we use generated *types* rather than partial-properties
+that emit an instance with the known values. Also, the `"..."u8` syntax emits a span which is awkward to store, but
+easy to return via a property.
diff --git a/eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj b/eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj
new file mode 100644
index 000000000..f875133ba
--- /dev/null
+++ b/eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj
@@ -0,0 +1,20 @@
+
+
+
+ netstandard2.0
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+ FastHash.cs
+
+
+
+
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index 29eadff61..06e403ebb 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -10,4 +10,7 @@
+
+
+
diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs
index 52f0b134d..7a0c2f08d 100644
--- a/src/StackExchange.Redis/Enums/RedisCommand.cs
+++ b/src/StackExchange.Redis/Enums/RedisCommand.cs
@@ -206,6 +206,19 @@ internal enum RedisCommand
UNSUBSCRIBE,
UNWATCH,
+ VADD,
+ VCARD,
+ VDIM,
+ VEMB,
+ VGETATTR,
+ VINFO,
+ VISMEMBER,
+ VLINKS,
+ VRANDMEMBER,
+ VREM,
+ VSETATTR,
+ VSIM,
+
WATCH,
XACK,
@@ -352,6 +365,9 @@ internal static bool IsPrimaryOnly(this RedisCommand command)
case RedisCommand.SWAPDB:
case RedisCommand.TOUCH:
case RedisCommand.UNLINK:
+ case RedisCommand.VADD:
+ case RedisCommand.VREM:
+ case RedisCommand.VSETATTR:
case RedisCommand.XAUTOCLAIM:
case RedisCommand.ZADD:
case RedisCommand.ZDIFFSTORE:
@@ -499,6 +515,15 @@ internal static bool IsPrimaryOnly(this RedisCommand command)
case RedisCommand.ZSCORE:
case RedisCommand.ZUNION:
case RedisCommand.UNKNOWN:
+ case RedisCommand.VCARD:
+ case RedisCommand.VDIM:
+ case RedisCommand.VEMB:
+ case RedisCommand.VGETATTR:
+ case RedisCommand.VINFO:
+ case RedisCommand.VISMEMBER:
+ case RedisCommand.VLINKS:
+ case RedisCommand.VRANDMEMBER:
+ case RedisCommand.VSIM:
// Writable commands, but allowed for the writable-replicas scenario
case RedisCommand.COPY:
case RedisCommand.GEOADD:
diff --git a/src/StackExchange.Redis/Experiments.cs b/src/StackExchange.Redis/Experiments.cs
new file mode 100644
index 000000000..577c9f8c9
--- /dev/null
+++ b/src/StackExchange.Redis/Experiments.cs
@@ -0,0 +1,41 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace StackExchange.Redis
+{
+ // example usage:
+ // [Experimental(Experiments.SomeFeature, UrlFormat = Experiments.UrlFormat)]
+ // 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/";
+ public const string VectorSets = "SER001";
+ }
+}
+
+#if !NET8_0_OR_GREATER
+#pragma warning disable SA1403
+namespace System.Diagnostics.CodeAnalysis
+#pragma warning restore SA1403
+{
+ [AttributeUsage(
+ AttributeTargets.Assembly |
+ AttributeTargets.Module |
+ AttributeTargets.Class |
+ AttributeTargets.Struct |
+ AttributeTargets.Enum |
+ AttributeTargets.Constructor |
+ AttributeTargets.Method |
+ AttributeTargets.Property |
+ AttributeTargets.Field |
+ AttributeTargets.Event |
+ AttributeTargets.Interface |
+ AttributeTargets.Delegate,
+ Inherited = false)]
+ internal sealed class ExperimentalAttribute(string diagnosticId) : Attribute
+ {
+ public string DiagnosticId { get; } = diagnosticId;
+ public string? UrlFormat { get; set; }
+ public string? Message { get; set; }
+ }
+}
+#endif
diff --git a/src/StackExchange.Redis/FastHash.cs b/src/StackExchange.Redis/FastHash.cs
new file mode 100644
index 000000000..49eb01b31
--- /dev/null
+++ b/src/StackExchange.Redis/FastHash.cs
@@ -0,0 +1,137 @@
+using System;
+using System.Buffers;
+using System.Buffers.Binary;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace StackExchange.Redis;
+
+///
+/// This type is intended to provide fast hashing functions for small strings, for example well-known
+/// RESP literals that are usually identifiable by their length and initial bytes; it is not intended
+/// for general purpose hashing. All matches must also perform a sequence equality check.
+///
+/// See HastHashGenerator.md for more information and intended usage.
+[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
+[Conditional("DEBUG")] // evaporate in release
+internal sealed class FastHashAttribute(string token = "") : Attribute
+{
+ public string Token => token;
+}
+
+internal static class FastHash
+{
+ /* not sure we need this, but: retain for reference
+
+ // Perform case-insensitive hash by masking (X and x differ by only 1 bit); this halves
+ // our entropy, but is still useful when case doesn't matter.
+ private const long CaseMask = ~0x2020202020202020;
+
+ public static long Hash64CI(this ReadOnlySequence value)
+ => value.Hash64() & CaseMask;
+ public static long Hash64CI(this scoped ReadOnlySpan value)
+ => value.Hash64() & CaseMask;
+*/
+
+ public static long Hash64(this ReadOnlySequence value)
+ {
+#if NETCOREAPP3_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
+ var first = value.FirstSpan;
+#else
+ var first = value.First.Span;
+#endif
+ return first.Length >= sizeof(long) || value.IsSingleSegment
+ ? first.Hash64() : SlowHash64(value);
+
+ static long SlowHash64(ReadOnlySequence value)
+ {
+ Span buffer = stackalloc byte[sizeof(long)];
+ if (value.Length < sizeof(long))
+ {
+ value.CopyTo(buffer);
+ buffer.Slice((int)value.Length).Clear();
+ }
+ else
+ {
+ value.Slice(0, sizeof(long)).CopyTo(buffer);
+ }
+ return BitConverter.IsLittleEndian
+ ? Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference(buffer))
+ : BinaryPrimitives.ReadInt64LittleEndian(buffer);
+ }
+ }
+
+ public static long Hash64(this scoped ReadOnlySpan value)
+ {
+ if (BitConverter.IsLittleEndian)
+ {
+ ref byte data = ref MemoryMarshal.GetReference(value);
+ return value.Length switch
+ {
+ 0 => 0,
+ 1 => data, // 0000000A
+ 2 => Unsafe.ReadUnaligned(ref data), // 000000BA
+ 3 => Unsafe.ReadUnaligned(ref data) | // 000000BA
+ (Unsafe.Add(ref data, 2) << 16), // 00000C00
+ 4 => Unsafe.ReadUnaligned(ref data), // 0000DCBA
+ 5 => Unsafe.ReadUnaligned(ref data) | // 0000DCBA
+ ((long)Unsafe.Add(ref data, 4) << 32), // 000E0000
+ 6 => Unsafe.ReadUnaligned(ref data) | // 0000DCBA
+ ((long)Unsafe.ReadUnaligned(ref Unsafe.Add(ref data, 4)) << 32), // 00FE0000
+ 7 => Unsafe.ReadUnaligned(ref data) | // 0000DCBA
+ ((long)Unsafe.ReadUnaligned(ref Unsafe.Add(ref data, 4)) << 32) | // 00FE0000
+ ((long)Unsafe.Add(ref data, 6) << 48), // 0G000000
+ _ => Unsafe.ReadUnaligned(ref data), // HGFEDCBA
+ };
+ }
+
+#pragma warning disable CS0618 // Type or member is obsolete
+ return Hash64Fallback(value);
+#pragma warning restore CS0618 // Type or member is obsolete
+ }
+
+ [Obsolete("Only exists for benchmarks (to show that we don't need to use it) and unit tests (for correctness)")]
+ internal static unsafe long Hash64Unsafe(scoped ReadOnlySpan value)
+ {
+ if (BitConverter.IsLittleEndian)
+ {
+ fixed (byte* ptr = &MemoryMarshal.GetReference(value))
+ {
+ return value.Length switch
+ {
+ 0 => 0,
+ 1 => *ptr, // 0000000A
+ 2 => *(ushort*)ptr, // 000000BA
+ 3 => *(ushort*)ptr | // 000000BA
+ (ptr[2] << 16), // 00000C00
+ 4 => *(int*)ptr, // 0000DCBA
+ 5 => (long)*(int*)ptr | // 0000DCBA
+ ((long)ptr[4] << 32), // 000E0000
+ 6 => (long)*(int*)ptr | // 0000DCBA
+ ((long)*(ushort*)(ptr + 4) << 32), // 00FE0000
+ 7 => (long)*(int*)ptr | // 0000DCBA
+ ((long)*(ushort*)(ptr + 4) << 32) | // 00FE0000
+ ((long)ptr[6] << 48), // 0G000000
+ _ => *(long*)ptr, // HGFEDCBA
+ };
+ }
+ }
+
+ return Hash64Fallback(value);
+ }
+
+ [Obsolete("Only exists for unit tests and fallback")]
+ internal static long Hash64Fallback(scoped ReadOnlySpan value)
+ {
+ if (value.Length < sizeof(long))
+ {
+ Span tmp = stackalloc byte[sizeof(long)];
+ value.CopyTo(tmp); // ABC*****
+ tmp.Slice(value.Length).Clear(); // ABC00000
+ return BinaryPrimitives.ReadInt64LittleEndian(tmp); // 00000CBA
+ }
+
+ return BinaryPrimitives.ReadInt64LittleEndian(value); // HGFEDCBA
+ }
+}
diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.VectorSets.cs b/src/StackExchange.Redis/Interfaces/IDatabase.VectorSets.cs
new file mode 100644
index 000000000..039075ec8
--- /dev/null
+++ b/src/StackExchange.Redis/Interfaces/IDatabase.VectorSets.cs
@@ -0,0 +1,183 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+
+// ReSharper disable once CheckNamespace
+namespace StackExchange.Redis;
+
+///
+/// Describes functionality that is common to both standalone redis servers and redis clusters.
+///
+public partial interface IDatabase
+{
+ // Vector Set operations
+
+ ///
+ /// Add a vector to a vectorset.
+ ///
+ /// The key of the vectorset.
+ /// The data to add.
+ /// The flags to use for this operation.
+ /// if the element was added; if it already existed.
+ ///
+ [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)]
+ bool VectorSetAdd(
+ RedisKey key,
+ VectorSetAddRequest request,
+ CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Get the cardinality (number of elements) of a vectorset.
+ ///
+ /// The key of the vectorset.
+ /// The flags to use for this operation.
+ /// The cardinality of the vectorset.
+ ///
+ [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)]
+ long VectorSetLength(RedisKey key, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Get the dimension of vectors in a vectorset.
+ ///
+ /// The key of the vectorset.
+ /// The flags to use for this operation.
+ /// The dimension of vectors in the vectorset.
+ ///
+ [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)]
+ int VectorSetDimension(RedisKey key, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Get the vector for a member.
+ ///
+ /// The key of the vectorset.
+ /// The member name.
+ /// The flags to use for this operation.
+ /// The vector as a pooled memory lease.
+ ///
+ [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)]
+ Lease? VectorSetGetApproximateVector(
+ RedisKey key,
+ RedisValue member,
+ CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Get JSON attributes for a member in a vectorset.
+ ///
+ /// The key of the vectorset.
+ /// The member name.
+ /// The flags to use for this operation.
+ /// The attributes as a JSON string.
+ ///
+ [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)]
+ string? VectorSetGetAttributesJson(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Get information about a vectorset.
+ ///
+ /// The key of the vectorset.
+ /// The flags to use for this operation.
+ /// Information about the vectorset.
+ ///
+ [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)]
+ VectorSetInfo? VectorSetInfo(RedisKey key, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Check if a member exists in a vectorset.
+ ///
+ /// The key of the vectorset.
+ /// The member name.
+ /// The flags to use for this operation.
+ /// True if the member exists, false otherwise.
+ ///
+ [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)]
+ bool VectorSetContains(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Get links/connections for a member in a vectorset.
+ ///
+ /// The key of the vectorset.
+ /// The member name.
+ /// The flags to use for this operation.
+ /// The linked members.
+ ///
+ [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)]
+ Lease? VectorSetGetLinks(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Get links/connections with scores for a member in a vectorset.
+ ///
+ /// The key of the vectorset.
+ /// The member name.
+ /// The flags to use for this operation.
+ /// The linked members with their similarity scores.
+ ///
+ [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)]
+ Lease? VectorSetGetLinksWithScores(
+ RedisKey key,
+ RedisValue member,
+ CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Get a random member from a vectorset.
+ ///
+ /// The key of the vectorset.
+ /// The flags to use for this operation.
+ /// A random member from the vectorset, or null if the vectorset is empty.
+ ///
+ [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)]
+ RedisValue VectorSetRandomMember(RedisKey key, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Get random members from a vectorset.
+ ///
+ /// The key of the vectorset.
+ /// The number of random members to return.
+ /// The flags to use for this operation.
+ /// Random members from the vectorset.
+ ///
+ [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)]
+ RedisValue[] VectorSetRandomMembers(RedisKey key, long count, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Remove a member from a vectorset.
+ ///
+ /// The key of the vectorset.
+ /// The member to remove.
+ /// The flags to use for this operation.
+ /// if the member was removed; if it was not found.
+ ///
+ [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)]
+ bool VectorSetRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Set JSON attributes for a member in a vectorset.
+ ///
+ /// The key of the vectorset.
+ /// The member name.
+ /// The attributes to set as a JSON string.
+ /// The flags to use for this operation.
+ /// True if successful.
+ ///
+ [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)]
+ bool VectorSetSetAttributesJson(
+ RedisKey key,
+ RedisValue member,
+#if NET7_0_OR_GREATER
+ [StringSyntax(StringSyntaxAttribute.Json)]
+#endif
+ string attributesJson,
+ CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Find similar vectors using vector similarity search.
+ ///
+ /// The key of the vectorset.
+ /// The query to execute.
+ /// The flags to use for this operation.
+ /// Similar vectors with their similarity scores.
+ ///
+ [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)]
+ Lease? VectorSetSimilaritySearch(
+ RedisKey key,
+ VectorSetSimilaritySearchRequest query,
+ CommandFlags flags = CommandFlags.None);
+}
diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs
index b6caafabe..6c52e89bd 100644
--- a/src/StackExchange.Redis/Interfaces/IDatabase.cs
+++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs
@@ -3,12 +3,13 @@
using System.ComponentModel;
using System.Net;
+// ReSharper disable once CheckNamespace
namespace StackExchange.Redis
{
///
/// Describes functionality that is common to both standalone redis servers and redis clusters.
///
- public interface IDatabase : IRedis, IDatabaseAsync
+ public partial interface IDatabase : IRedis, IDatabaseAsync
{
///
/// The numeric identifier of this database.
diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.VectorSets.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.VectorSets.cs
new file mode 100644
index 000000000..863095140
--- /dev/null
+++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.VectorSets.cs
@@ -0,0 +1,95 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading.Tasks;
+
+// ReSharper disable once CheckNamespace
+namespace StackExchange.Redis;
+
+///
+/// Describes functionality that is common to both standalone redis servers and redis clusters.
+///
+public partial interface IDatabaseAsync
+{
+ // Vector Set operations
+
+ ///
+ [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)]
+ Task VectorSetAddAsync(
+ RedisKey key,
+ VectorSetAddRequest request,
+ CommandFlags flags = CommandFlags.None);
+
+ ///
+ [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)]
+ Task VectorSetLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None);
+
+ ///
+ [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)]
+ Task VectorSetDimensionAsync(RedisKey key, CommandFlags flags = CommandFlags.None);
+
+ ///
+ [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)]
+ Task?> VectorSetGetApproximateVectorAsync(
+ RedisKey key,
+ RedisValue member,
+ CommandFlags flags = CommandFlags.None);
+
+ ///
+ [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)]
+ Task VectorSetGetAttributesJsonAsync(
+ RedisKey key,
+ RedisValue member,
+ CommandFlags flags = CommandFlags.None);
+
+ ///
+ [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)]
+ Task VectorSetInfoAsync(RedisKey key, CommandFlags flags = CommandFlags.None);
+
+ ///
+ [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)]
+ Task VectorSetContainsAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None);
+
+ ///
+ [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)]
+ Task?> VectorSetGetLinksAsync(
+ RedisKey key,
+ RedisValue member,
+ CommandFlags flags = CommandFlags.None);
+
+ ///
+ [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)]
+ Task?> VectorSetGetLinksWithScoresAsync(
+ RedisKey key,
+ RedisValue member,
+ CommandFlags flags = CommandFlags.None);
+
+ ///
+ [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)]
+ Task VectorSetRandomMemberAsync(RedisKey key, CommandFlags flags = CommandFlags.None);
+
+ ///
+ [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)]
+ Task VectorSetRandomMembersAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None);
+
+ ///
+ [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)]
+ Task VectorSetRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None);
+
+ ///
+ [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)]
+ Task VectorSetSetAttributesJsonAsync(
+ RedisKey key,
+ RedisValue member,
+#if NET7_0_OR_GREATER
+ [StringSyntax(StringSyntaxAttribute.Json)]
+#endif
+ string attributesJson,
+ CommandFlags flags = CommandFlags.None);
+
+ ///
+ [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)]
+ Task?> VectorSetSimilaritySearchAsync(
+ RedisKey key,
+ VectorSetSimilaritySearchRequest query,
+ CommandFlags flags = CommandFlags.None);
+}
diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs
index 51a15d7d5..0bc7b4867 100644
--- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs
+++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs
@@ -4,12 +4,13 @@
using System.Net;
using System.Threading.Tasks;
+// ReSharper disable once CheckNamespace
namespace StackExchange.Redis
{
///
/// Describes functionality that is common to both standalone redis servers and redis clusters.
///
- public interface IDatabaseAsync : IRedisAsync
+ public partial interface IDatabaseAsync : IRedisAsync
{
///
/// Indicates whether the instance can communicate with the server (resolved using the supplied key and optional flags).
diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.VectorSets.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.VectorSets.cs
new file mode 100644
index 000000000..809adad97
--- /dev/null
+++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.VectorSets.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading.Tasks;
+
+// ReSharper disable once CheckNamespace
+namespace StackExchange.Redis.KeyspaceIsolation;
+
+internal partial class KeyPrefixed
+{
+ // Vector Set operations - async methods
+ [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)]
+ public Task VectorSetAddAsync(
+ RedisKey key,
+ VectorSetAddRequest request,
+ CommandFlags flags = CommandFlags.None) =>
+ Inner.VectorSetAddAsync(ToInner(key), request, flags);
+
+ public Task VectorSetLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) =>
+ Inner.VectorSetLengthAsync(ToInner(key), flags);
+
+ public Task VectorSetDimensionAsync(RedisKey key, CommandFlags flags = CommandFlags.None) =>
+ Inner.VectorSetDimensionAsync(ToInner(key), flags);
+
+ public Task?> VectorSetGetApproximateVectorAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) =>
+ Inner.VectorSetGetApproximateVectorAsync(ToInner(key), member, flags);
+
+ public Task VectorSetGetAttributesJsonAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) =>
+ Inner.VectorSetGetAttributesJsonAsync(ToInner(key), member, flags);
+
+ public Task VectorSetInfoAsync(RedisKey key, CommandFlags flags = CommandFlags.None) =>
+ Inner.VectorSetInfoAsync(ToInner(key), flags);
+
+ public Task VectorSetContainsAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) =>
+ Inner.VectorSetContainsAsync(ToInner(key), member, flags);
+
+ public Task?> VectorSetGetLinksAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) =>
+ Inner.VectorSetGetLinksAsync(ToInner(key), member, flags);
+
+ public Task?> VectorSetGetLinksWithScoresAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) =>
+ Inner.VectorSetGetLinksWithScoresAsync(ToInner(key), member, flags);
+
+ public Task VectorSetRandomMemberAsync(RedisKey key, CommandFlags flags = CommandFlags.None) =>
+ Inner.VectorSetRandomMemberAsync(ToInner(key), flags);
+
+ public Task VectorSetRandomMembersAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) =>
+ Inner.VectorSetRandomMembersAsync(ToInner(key), count, flags);
+
+ public Task VectorSetRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) =>
+ Inner.VectorSetRemoveAsync(ToInner(key), member, flags);
+
+ public Task VectorSetSetAttributesJsonAsync(RedisKey key, RedisValue member, string attributesJson, CommandFlags flags = CommandFlags.None) =>
+ Inner.VectorSetSetAttributesJsonAsync(ToInner(key), member, attributesJson, flags);
+
+ public Task?> VectorSetSimilaritySearchAsync(
+ RedisKey key,
+ VectorSetSimilaritySearchRequest query,
+ CommandFlags flags = CommandFlags.None) =>
+ Inner.VectorSetSimilaritySearchAsync(ToInner(key), query, flags);
+}
diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs
index 32c76f4d2..61a6f44c4 100644
--- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs
+++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs
@@ -7,7 +7,7 @@
namespace StackExchange.Redis.KeyspaceIsolation
{
- internal class KeyPrefixed : IDatabaseAsync where TInner : IDatabaseAsync
+ internal partial class KeyPrefixed : IDatabaseAsync where TInner : IDatabaseAsync
{
internal KeyPrefixed(TInner inner, byte[] keyPrefix)
{
diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.VectorSets.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.VectorSets.cs
new file mode 100644
index 000000000..62f4e9202
--- /dev/null
+++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.VectorSets.cs
@@ -0,0 +1,56 @@
+using System;
+
+// ReSharper disable once CheckNamespace
+namespace StackExchange.Redis.KeyspaceIsolation;
+
+internal sealed partial class KeyPrefixedDatabase
+{
+ // Vector Set operations
+ public bool VectorSetAdd(
+ RedisKey key,
+ VectorSetAddRequest request,
+ CommandFlags flags = CommandFlags.None) =>
+ Inner.VectorSetAdd(ToInner(key), request, flags);
+
+ public long VectorSetLength(RedisKey key, CommandFlags flags = CommandFlags.None) =>
+ Inner.VectorSetLength(ToInner(key), flags);
+
+ public int VectorSetDimension(RedisKey key, CommandFlags flags = CommandFlags.None) =>
+ Inner.VectorSetDimension(ToInner(key), flags);
+
+ public Lease? VectorSetGetApproximateVector(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) =>
+ Inner.VectorSetGetApproximateVector(ToInner(key), member, flags);
+
+ public string? VectorSetGetAttributesJson(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) =>
+ Inner.VectorSetGetAttributesJson(ToInner(key), member, flags);
+
+ public VectorSetInfo? VectorSetInfo(RedisKey key, CommandFlags flags = CommandFlags.None) =>
+ Inner.VectorSetInfo(ToInner(key), flags);
+
+ public bool VectorSetContains(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) =>
+ Inner.VectorSetContains(ToInner(key), member, flags);
+
+ public Lease? VectorSetGetLinks(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) =>
+ Inner.VectorSetGetLinks(ToInner(key), member, flags);
+
+ public Lease? VectorSetGetLinksWithScores(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) =>
+ Inner.VectorSetGetLinksWithScores(ToInner(key), member, flags);
+
+ public RedisValue VectorSetRandomMember(RedisKey key, CommandFlags flags = CommandFlags.None) =>
+ Inner.VectorSetRandomMember(ToInner(key), flags);
+
+ public RedisValue[] VectorSetRandomMembers(RedisKey key, long count, CommandFlags flags = CommandFlags.None) =>
+ Inner.VectorSetRandomMembers(ToInner(key), count, flags);
+
+ public bool VectorSetRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) =>
+ Inner.VectorSetRemove(ToInner(key), member, flags);
+
+ public bool VectorSetSetAttributesJson(RedisKey key, RedisValue member, string attributesJson, CommandFlags flags = CommandFlags.None) =>
+ Inner.VectorSetSetAttributesJson(ToInner(key), member, attributesJson, flags);
+
+ public Lease? VectorSetSimilaritySearch(
+ RedisKey key,
+ VectorSetSimilaritySearchRequest query,
+ CommandFlags flags = CommandFlags.None) =>
+ Inner.VectorSetSimilaritySearch(ToInner(key), query, flags);
+}
diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs
index 755bec64e..2a139694e 100644
--- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs
+++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs
@@ -4,7 +4,7 @@
namespace StackExchange.Redis.KeyspaceIsolation
{
- internal sealed class KeyPrefixedDatabase : KeyPrefixed, IDatabase
+ internal sealed partial class KeyPrefixedDatabase : KeyPrefixed, IDatabase
{
public KeyPrefixedDatabase(IDatabase inner, byte[] prefix) : base(inner, prefix)
{
diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs
index cd3d29947..5973bd55b 100644
--- a/src/StackExchange.Redis/Message.cs
+++ b/src/StackExchange.Redis/Message.cs
@@ -705,6 +705,7 @@ internal void SetWriteTime()
_writeTickCount = Environment.TickCount; // note this might be reset if we resend a message, cluster-moved etc; I'm OK with that
}
private int _writeTickCount;
+
public int GetWriteTime() => Volatile.Read(ref _writeTickCount);
///
diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs
index c587241a0..129fd9e07 100644
--- a/src/StackExchange.Redis/PhysicalConnection.cs
+++ b/src/StackExchange.Redis/PhysicalConnection.cs
@@ -859,6 +859,14 @@ internal static void WriteBulkString(in RedisValue value, PipeWriter? maybeNullW
}
}
+ internal void WriteBulkString(ReadOnlySpan value)
+ {
+ if (_ioPipe?.Output is { } writer)
+ {
+ WriteUnifiedSpan(writer, value);
+ }
+ }
+
internal const int REDIS_MAX_ARGS = 1024 * 1024; // there is a <= 1024*1024 max constraint inside redis itself: https://github.com/antirez/redis/blob/6c60526db91e23fb2d666fc52facc9a11780a2a3/src/networking.c#L1024
internal void WriteHeader(RedisCommand command, int arguments, CommandBytes commandBytes = default)
diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt
index e82af2bee..10044dc9b 100644
--- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt
+++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt
@@ -1962,3 +1962,92 @@ StackExchange.Redis.ConnectionMultiplexer.GetServer(StackExchange.Redis.RedisKey
StackExchange.Redis.IConnectionMultiplexer.GetServer(StackExchange.Redis.RedisKey key, object? asyncState = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.IServer!
StackExchange.Redis.IServer.Execute(int? database, string! command, System.Collections.Generic.ICollection