From 230afe22591cb86cacc62974851b99760ff056fc Mon Sep 17 00:00:00 2001
From: Gabriel Erzse <gabriel.erzse@redis.com>
Date: Thu, 25 Jan 2024 11:41:45 +0200
Subject: [PATCH 1/3] Formatting and typos

Fix some typos in the markdown files and run `dotnet format` on existing
code.
---
 CONTRIBUTING.md | 5 +++--
 README.md       | 4 +++-
 2 files changed, 6 insertions(+), 3 deletions(-)

diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 7c5e007b..ca1c09eb 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -35,7 +35,7 @@ Here's how to get started with your code contribution:
 3.  Write your tests
 
 4.  Use the `docker run -p 6379:6379 -it redis/redis-stack-server:edge` as your local environment for running the functional tests. You can also use Development Container as described below.
-5.  Run dotnet format to make sure your code is formatted
+5.  Run `dotnet format` to make sure your code is formatted
 6.  Make sure your tests pass using `dotnet test`
 7.  Open a pull request
 
@@ -121,6 +121,7 @@ e.g. :
 ```bash
 dotnet test --environment "REDIS_CLUSTER=127.0.0.1:16379" --environment "NUM_REDIS_CLUSTER_NODES=6"
 ```
+
 ## How to Report a Bug
 
 ### Security Vulnerabilities
@@ -145,7 +146,7 @@ issue, so if you're unsure, just email [us](mailto:oss@redis.com).
 When filing an issue, make sure to answer these five questions:
 
 1.  What version of NRedisStack are you using?
-2.  What version of redis are you using?
+2.  What version of Redis are you using?
 3.  What did you do?
 4.  What did you expect to see?
 5.  What did you see instead?
diff --git a/README.md b/README.md
index bc1f65c4..0c96178e 100644
--- a/README.md
+++ b/README.md
@@ -31,15 +31,17 @@ This project builds on [StackExchange.Redis](https://github.com/StackExchange/St
 The complete documentation for Redis  module commands can be found at the [Redis commands website](https://redis.io/commands/).
 
 ### Redis OSS commands
+
 You can use Redis OSS commands in the same way as you use them in [StackExchange.Redis](https://github.com/StackExchange/StackExchange.Redis).
 
 ### Stack commands
+
 Each module has a command class with its own commands.
+
 The supported modules are [Search](https://redis.io/commands/?group=search), [JSON](https://redis.io/commands/?group=json), [TimeSeries](https://redis.io/commands/?group=timeseries), [Bloom Filter](https://redis.io/commands/?group=bf), [Cuckoo Filter](https://redis.io/commands/?group=cf), [T-Digest](https://redis.io/commands/?group=tdigest), [Count-min Sketch](https://redis.io/commands/?group=cms), and [Top-K](https://redis.io/commands/?group=topk).
 
 **Note:** RedisGraph support has been deprecated starting from Redis Stack version 7.2. For more information, please refer to [this blog post](https://redis.com/blog/redisgraph-eol/).
 
-
 # Usage
 
 ## 💻 Installation

From e6da72fcdd67e13c889b931d3b543ddd9abd2ba1 Mon Sep 17 00:00:00 2001
From: Gabriel Erzse <gabriel.erzse@redis.com>
Date: Thu, 25 Jan 2024 11:43:57 +0200
Subject: [PATCH 2/3] Include documentation in the build

Change the settings so that documentation is included in the build.
---
 src/NRedisStack/NRedisStack.csproj | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/NRedisStack/NRedisStack.csproj b/src/NRedisStack/NRedisStack.csproj
index df5fbeb8..7195dde9 100644
--- a/src/NRedisStack/NRedisStack.csproj
+++ b/src/NRedisStack/NRedisStack.csproj
@@ -3,6 +3,7 @@
   <PropertyGroup>
     <Nullable>enable</Nullable>
 	<TargetFrameworks>netstandard2.0;net6.0;net7.0;net8.0</TargetFrameworks>
+	<GenerateDocumentationFile>true</GenerateDocumentationFile>
 	<LangVersion>latest</LangVersion>
 	<ImplicitUsings>enable</ImplicitUsings>
 	<Authors>Redis Open Source</Authors>

From 8948d82062a62a9f32d3ec3ef665ed0bdff77dbb Mon Sep 17 00:00:00 2001
From: Gabriel Erzse <gabriel.erzse@redis.com>
Date: Thu, 25 Jan 2024 11:46:41 +0200
Subject: [PATCH 3/3] Add support for BZMPOP, BZPOPMIN and BZPOPMAX

Add support for the BZMPOP, BZPOPMIN, BZPOPMAX commands.

Issues #232, #233 and #234.

These commands are blocking on the server, so they go against the current
policy of the StackExchange.Redis library. Therefore make it obvious in
the code documentation that attention must be given to the timeout in
the connection multiplexer, client-side.

The StackExchange.Redis library already defines a type for the payload
returned by BZMPOP (which is the same as for ZMPOP), namely the
SortedSetPopResult class. However, the constructor of that class is
internal in the library, so we can't create instances of it. Therefore
roll our out type for a <value, score> pair, and use Tuple to pair a key
with a list of such <value, score> pairs.

Instead of using Order to signal from which end of the sorted set to
pop, define a MinMaxModifier enum, which more clearly expresses the
intention and maps directly to the Redis command being executed.
---
 .../CoreCommands/CoreCommandBuilder.cs        |  53 ++++
 src/NRedisStack/CoreCommands/CoreCommands.cs  | 156 ++++++++++
 .../CoreCommands/DataTypes/MinMaxModifier.cs  |  35 +++
 .../DataTypes/RedisValueWithScore.cs          |  31 ++
 .../CoreCommands/Literals/CommandArgs.cs      |   5 +-
 .../CoreCommands/Literals/Commands.cs         |   5 +-
 src/NRedisStack/NRedisStack.csproj            |   6 +-
 src/NRedisStack/ResponseParser.cs             |  51 ++++
 .../Core Commands/CoreTests.cs                | 274 +++++++++++++++++-
 9 files changed, 608 insertions(+), 8 deletions(-)
 create mode 100644 src/NRedisStack/CoreCommands/DataTypes/MinMaxModifier.cs
 create mode 100644 src/NRedisStack/CoreCommands/DataTypes/RedisValueWithScore.cs

diff --git a/src/NRedisStack/CoreCommands/CoreCommandBuilder.cs b/src/NRedisStack/CoreCommands/CoreCommandBuilder.cs
index 9677824d..6c71a8a2 100644
--- a/src/NRedisStack/CoreCommands/CoreCommandBuilder.cs
+++ b/src/NRedisStack/CoreCommands/CoreCommandBuilder.cs
@@ -1,6 +1,8 @@
 using NRedisStack.RedisStackCommands;
 using NRedisStack.Core.Literals;
 using NRedisStack.Core;
+using NRedisStack.Core.DataTypes;
+using StackExchange.Redis;
 
 namespace NRedisStack
 {
@@ -18,5 +20,56 @@ public static SerializedCommand ClientSetInfo(SetInfoAttr attr, string value)
 
             return new SerializedCommand(RedisCoreCommands.CLIENT, RedisCoreCommands.SETINFO, attrValue, value);
         }
+
+        public static SerializedCommand BzmPop(double timeout, RedisKey[] keys, MinMaxModifier minMaxModifier, long? count)
+        {
+            if (keys.Length == 0)
+            {
+                throw new ArgumentException("At least one key must be provided.");
+            }
+
+            List<object> args = new List<object>();
+
+            args.Add(timeout);
+            args.Add(keys.Length);
+            args.AddRange(keys.Cast<object>());
+            args.Add(minMaxModifier == MinMaxModifier.Min ? CoreArgs.MIN : CoreArgs.MAX);
+
+            if (count != null)
+            {
+                args.Add(CoreArgs.COUNT);
+                args.Add(count);
+            }
+
+            return new SerializedCommand(RedisCoreCommands.BZMPOP, args);
+        }
+
+        public static SerializedCommand BzPopMin(RedisKey[] keys, double timeout)
+        {
+            if (keys.Length == 0)
+            {
+                throw new ArgumentException("At least one key must be provided.");
+            }
+
+            List<object> args = new List<object>();
+            args.AddRange(keys.Cast<object>());
+            args.Add(timeout);
+
+            return new SerializedCommand(RedisCoreCommands.BZPOPMIN, args);
+        }
+
+        public static SerializedCommand BzPopMax(RedisKey[] keys, double timeout)
+        {
+            if (keys.Length == 0)
+            {
+                throw new ArgumentException("At least one key must be provided.");
+            }
+
+            List<object> args = new List<object>();
+            args.AddRange(keys.Cast<object>());
+            args.Add(timeout);
+
+            return new SerializedCommand(RedisCoreCommands.BZPOPMAX, args);
+        }
     }
 }
diff --git a/src/NRedisStack/CoreCommands/CoreCommands.cs b/src/NRedisStack/CoreCommands/CoreCommands.cs
index 4c8917bb..6b69074c 100644
--- a/src/NRedisStack/CoreCommands/CoreCommands.cs
+++ b/src/NRedisStack/CoreCommands/CoreCommands.cs
@@ -1,5 +1,7 @@
 using NRedisStack.Core;
+using NRedisStack.Core.DataTypes;
 using StackExchange.Redis;
+
 namespace NRedisStack
 {
 
@@ -19,5 +21,159 @@ public static bool ClientSetInfo(this IDatabase db, SetInfoAttr attr, string val
                 return false;
             return db.Execute(CoreCommandBuilder.ClientSetInfo(attr, value)).OKtoBoolean();
         }
+
+        /// <summary>
+        /// The BZMPOP command.
+        /// <p/>
+        /// Removes and returns up to <paramref name="count"/> entries from the first non-empty sorted set in
+        /// <paramref name="keys"/>. If none of the sets contain elements, the call blocks on the server until elements
+        /// become available, or the given <paramref name="timeout"/> expires. A <paramref name="timeout"/> of <c>0</c>
+        /// means to wait indefinitely server-side. Returns <c>null</c> if the server timeout expires. 
+        /// <p/>
+        /// When using this, pay attention to the timeout configured in the client, on the
+        /// <see cref="ConnectionMultiplexer"/>, which by default can be too small:
+        /// <code>
+        /// ConfigurationOptions configurationOptions = new ConfigurationOptions();
+        /// configurationOptions.SyncTimeout = 120000; // set a meaningful value here
+        /// configurationOptions.EndPoints.Add("localhost");
+        /// ConnectionMultiplexer redis = ConnectionMultiplexer.Connect(configurationOptions);
+        /// </code>
+        /// If the connection multiplexer timeout expires in the client, a <c>StackExchange.Redis.RedisTimeoutException</c>
+        /// is thrown.
+        /// <p/>
+        /// This is an extension method added to the <see cref="IDatabase"/> class, for convenience.
+        /// </summary>
+        /// <param name="db">The <see cref="IDatabase"/> class where this extension method is applied.</param>
+        /// <param name="timeout">Server-side timeout for the wait. A value of <c>0</c> means to wait indefinitely.</param>
+        /// <param name="keys">The keys to check.</param>
+        /// <param name="minMaxModifier">Specify from which end of the sorted set to pop values. If set to <c>MinMaxModifier.Min</c>
+        /// then the minimum elements will be popped, otherwise the maximum values.</param>
+        /// <param name="count">The maximum number of records to pop out. If set to <c>null</c> then the server default
+        /// will be used.</param>
+        /// <returns>A collection of sorted set entries paired with their scores, together with the key they were popped
+        /// from, or <c>null</c> if the server timeout expires.</returns>
+        /// <remarks><seealso href="https://redis.io/commands/bzmpop"/></remarks>
+        public static Tuple<RedisKey, List<RedisValueWithScore>>? BzmPop(this IDatabase db, double timeout, RedisKey[] keys, MinMaxModifier minMaxModifier, long? count = null)
+        {
+            var command = CoreCommandBuilder.BzmPop(timeout, keys, minMaxModifier, count);
+            return db.Execute(command).ToSortedSetPopResults();
+        }
+
+        /// <summary>
+        /// Syntactic sugar for
+        /// <see cref="BzmPop(StackExchange.Redis.IDatabase,double,StackExchange.Redis.RedisKey[],NRedisStack.Core.DataTypes.MinMaxModifier,System.Nullable{long})"/>,
+        /// where only one key is used.
+        /// </summary>
+        /// <param name="db">The <see cref="IDatabase"/> class where this extension method is applied.</param>
+        /// <param name="timeout">Server-side timeout for the wait. A value of <c>0</c> means to wait indefinitely.</param>
+        /// <param name="key">The key to check.</param>
+        /// <param name="minMaxModifier">Specify from which end of the sorted set to pop values. If set to <c>MinMaxModifier.Min</c>
+        /// then the minimum elements will be popped, otherwise the maximum values.</param>
+        /// <param name="count">The maximum number of records to pop out. If set to <c>null</c> then the server default
+        /// will be used.</param>
+        /// <returns>A collection of sorted set entries paired with their scores, together with the key they were popped
+        /// from, or <c>null</c> if the server timeout expires.</returns>
+        /// <remarks><seealso href="https://redis.io/commands/bzmpop"/></remarks>
+        public static Tuple<RedisKey, List<RedisValueWithScore>>? BzmPop(this IDatabase db, double timeout, RedisKey key, MinMaxModifier minMaxModifier, long? count = null)
+        {
+            return BzmPop(db, timeout, new[] { key }, minMaxModifier, count);
+        }
+
+        /// <summary>
+        /// The BZPOPMIN command.
+        /// <p/>
+        /// Removes and returns the entry with the smallest score from the first non-empty sorted set in
+        /// <paramref name="keys"/>. If none of the sets contain elements, the call blocks on the server until elements
+        /// become available, or the given <paramref name="timeout"/> expires. A <paramref name="timeout"/> of <c>0</c>
+        /// means to wait indefinitely server-side. Returns <c>null</c> if the server timeout expires.
+        /// <p/>
+        /// When using this, pay attention to the timeout configured in the client, on the
+        /// <see cref="ConnectionMultiplexer"/>, which by default can be too small:
+        /// <code>
+        /// ConfigurationOptions configurationOptions = new ConfigurationOptions();
+        /// configurationOptions.SyncTimeout = 120000; // set a meaningful value here
+        /// configurationOptions.EndPoints.Add("localhost");
+        /// ConnectionMultiplexer redis = ConnectionMultiplexer.Connect(configurationOptions);
+        /// </code>
+        /// If the connection multiplexer timeout expires in the client, a <c>StackExchange.Redis.RedisTimeoutException</c>
+        /// is thrown.
+        /// <p/>
+        /// This is an extension method added to the <see cref="IDatabase"/> class, for convenience.
+        /// </summary>
+        /// <param name="db">The <see cref="IDatabase"/> class where this extension method is applied.</param>
+        /// <param name="keys">The keys to check.</param>
+        /// <param name="timeout">Server-side timeout for the wait. A value of <c>0</c> means to wait indefinitely.</param>
+        /// <returns>A sorted set entry paired with its score, together with the key it was popped from, or <c>null</c>
+        /// if the server timeout expires.</returns>
+        /// <remarks><seealso href="https://redis.io/commands/bzpopmin"/></remarks>
+        public static Tuple<RedisKey, RedisValueWithScore>? BzPopMin(this IDatabase db, RedisKey[] keys, double timeout)
+        {
+            var command = CoreCommandBuilder.BzPopMin(keys, timeout);
+            return db.Execute(command).ToSortedSetPopResult();
+        }
+
+        /// <summary>
+        /// Syntactic sugar for <see cref="BzPopMin(StackExchange.Redis.IDatabase,StackExchange.Redis.RedisKey[],double)"/>,
+        /// where only one key is used.
+        /// </summary>
+        /// <param name="db">The <see cref="IDatabase"/> class where this extension method is applied.</param>
+        /// <param name="key">The key to check.</param>
+        /// <param name="timeout">Server-side timeout for the wait. A value of <c>0</c> means to wait indefinitely.</param>
+        /// <returns>A sorted set entry paired with its score, together with the key it was popped from, or <c>null</c>
+        /// if the server timeout expires.</returns>
+        /// <remarks><seealso href="https://redis.io/commands/bzpopmin"/></remarks>
+        public static Tuple<RedisKey, RedisValueWithScore>? BzPopMin(this IDatabase db, RedisKey key, double timeout)
+        {
+            return BzPopMin(db, new[] { key }, timeout);
+        }
+
+
+        /// <summary>
+        /// The BZPOPMAX command.
+        /// <p/>
+        /// Removes and returns the entry with the highest score from the first non-empty sorted set in
+        /// <paramref name="keys"/>. If none of the sets contain elements, the call blocks on the server until elements
+        /// become available, or the given <paramref name="timeout"/> expires. A <paramref name="timeout"/> of <c>0</c>
+        /// means to wait indefinitely server-side. Returns <c>null</c> if the server timeout expires.
+        /// <p/>
+        /// When using this, pay attention to the timeout configured in the client, on the
+        /// <see cref="ConnectionMultiplexer"/>, which by default can be too small:
+        /// <code>
+        /// ConfigurationOptions configurationOptions = new ConfigurationOptions();
+        /// configurationOptions.SyncTimeout = 120000; // set a meaningful value here
+        /// configurationOptions.EndPoints.Add("localhost");
+        /// ConnectionMultiplexer redis = ConnectionMultiplexer.Connect(configurationOptions);
+        /// </code>
+        /// If the connection multiplexer timeout expires in the client, a <c>StackExchange.Redis.RedisTimeoutException</c>
+        /// is thrown.
+        /// <p/>
+        /// This is an extension method added to the <see cref="IDatabase"/> class, for convenience.
+        /// </summary>
+        /// <param name="db">The <see cref="IDatabase"/> class where this extension method is applied.</param>
+        /// <param name="keys">The keys to check.</param>
+        /// <param name="timeout">Server-side timeout for the wait. A value of <c>0</c> means to wait indefinitely.</param>
+        /// <returns>A sorted set entry paired with its score, together with the key it was popped from, or <c>null</c>
+        /// if the server timeout expires.</returns>
+        /// <remarks><seealso href="https://redis.io/commands/bzpopmax"/></remarks>
+        public static Tuple<RedisKey, RedisValueWithScore>? BzPopMax(this IDatabase db, RedisKey[] keys, double timeout)
+        {
+            var command = CoreCommandBuilder.BzPopMax(keys, timeout);
+            return db.Execute(command).ToSortedSetPopResult();
+        }
+
+        /// <summary>
+        /// Syntactic sugar for <see cref="BzPopMax(StackExchange.Redis.IDatabase,StackExchange.Redis.RedisKey[],double)"/>,
+        /// where only one key is used.
+        /// </summary>
+        /// <param name="db">The <see cref="IDatabase"/> class where this extension method is applied.</param>
+        /// <param name="key">The key to check.</param>
+        /// <param name="timeout">Server-side timeout for the wait. A value of <c>0</c> means to wait indefinitely.</param>
+        /// <returns>A sorted set entry paired with its score, together with the key it was popped from, or <c>null</c>
+        /// if the server timeout expires.</returns>
+        /// <remarks><seealso href="https://redis.io/commands/bzpopmax"/></remarks>
+        public static Tuple<RedisKey, RedisValueWithScore>? BzPopMax(this IDatabase db, RedisKey key, double timeout)
+        {
+            return BzPopMax(db, new[] { key }, timeout);
+        }
     }
 }
diff --git a/src/NRedisStack/CoreCommands/DataTypes/MinMaxModifier.cs b/src/NRedisStack/CoreCommands/DataTypes/MinMaxModifier.cs
new file mode 100644
index 00000000..c38e2231
--- /dev/null
+++ b/src/NRedisStack/CoreCommands/DataTypes/MinMaxModifier.cs
@@ -0,0 +1,35 @@
+using StackExchange.Redis;
+
+namespace NRedisStack.Core.DataTypes;
+
+/// <summary>
+/// Modifier that can be used for sorted set commands, where a MIN/MAX argument is expected by the Redis server.
+/// </summary>
+public enum MinMaxModifier
+{
+    /// <summary>
+    /// Maps to the <c>MIN</c> argument on the Redis server.
+    /// </summary>
+    Min,
+
+    /// <summary>
+    /// Maps to the <c>MAX</c> argument on the Redis server.
+    /// </summary>
+    Max
+}
+
+/// <summary>
+/// Conversion methods from/to other common data types.
+/// </summary>
+public static class MinMaxModifierExtensions
+{
+    /// <summary>
+    /// Convert from <see cref="Order"/> to <see cref="MinMaxModifier"/>.
+    /// </summary>
+    public static MinMaxModifier ToMinMax(this Order order) => order switch
+    {
+        Order.Ascending => MinMaxModifier.Min,
+        Order.Descending => MinMaxModifier.Max,
+        _ => throw new ArgumentOutOfRangeException(nameof(order))
+    };
+}
\ No newline at end of file
diff --git a/src/NRedisStack/CoreCommands/DataTypes/RedisValueWithScore.cs b/src/NRedisStack/CoreCommands/DataTypes/RedisValueWithScore.cs
new file mode 100644
index 00000000..6587db45
--- /dev/null
+++ b/src/NRedisStack/CoreCommands/DataTypes/RedisValueWithScore.cs
@@ -0,0 +1,31 @@
+using StackExchange.Redis;
+
+namespace NRedisStack.Core.DataTypes;
+
+/// <summary>
+/// Holds a <see cref="RedisValue"/> with an associated score.
+/// Used when working with sorted sets.
+/// </summary>
+public struct RedisValueWithScore
+{
+    /// <summary>
+    /// Pair a <see cref="RedisValue"/> with a numeric score.
+    /// </summary>
+    public RedisValueWithScore(RedisValue value, double score)
+    {
+        Value = value;
+        Score = score;
+    }
+
+    /// <summary>
+    /// The value of an item stored in a sorted set. For example, in the Redis command
+    /// <c>ZADD my-set 5.1 my-value</c>, the value is <c>my-value</c>.
+    /// </summary>
+    public RedisValue Value { get; }
+
+    /// <summary>
+    /// The score of an item stored in a sorted set. For example, in the Redis command
+    /// <c>ZADD my-set 5.1 my-value</c>, the score is <c>5.1</c>.
+    /// </summary>
+    public double Score { get; }
+}
\ No newline at end of file
diff --git a/src/NRedisStack/CoreCommands/Literals/CommandArgs.cs b/src/NRedisStack/CoreCommands/Literals/CommandArgs.cs
index 8d9aaf4f..4e6e4242 100644
--- a/src/NRedisStack/CoreCommands/Literals/CommandArgs.cs
+++ b/src/NRedisStack/CoreCommands/Literals/CommandArgs.cs
@@ -1,8 +1,11 @@
 namespace NRedisStack.Core.Literals
 {
-    internal class CoreArgs
+    internal static class CoreArgs
     {
+        public const string COUNT = "COUNT";
         public const string lib_name = "LIB-NAME";
         public const string lib_ver = "LIB-VER";
+        public const string MAX = "MAX";
+        public const string MIN = "MIN";
     }
 }
diff --git a/src/NRedisStack/CoreCommands/Literals/Commands.cs b/src/NRedisStack/CoreCommands/Literals/Commands.cs
index aaebaef6..e44c82ed 100644
--- a/src/NRedisStack/CoreCommands/Literals/Commands.cs
+++ b/src/NRedisStack/CoreCommands/Literals/Commands.cs
@@ -3,8 +3,11 @@ namespace NRedisStack.Core.Literals
     /// <summary>
     /// Redis Core command literals
     /// </summary>
-    internal class RedisCoreCommands
+    internal static class RedisCoreCommands
     {
+        public const string BZMPOP = "BZMPOP";
+        public const string BZPOPMAX = "BZPOPMAX";
+        public const string BZPOPMIN = "BZPOPMIN";
         public const string CLIENT = "CLIENT";
         public const string SETINFO = "SETINFO";
     }
diff --git a/src/NRedisStack/NRedisStack.csproj b/src/NRedisStack/NRedisStack.csproj
index 7195dde9..ad0fdb47 100644
--- a/src/NRedisStack/NRedisStack.csproj
+++ b/src/NRedisStack/NRedisStack.csproj
@@ -10,9 +10,9 @@
 	<Owners>Redis OSS</Owners>
 	<Description>.Net Client for Redis Stack</Description>
 	<PackageReadmeFile>README.md</PackageReadmeFile>
-	<Version>0.11.0</Version>
-	<ReleaseVersion>0.11.0</ReleaseVersion>
-	<PackageVersion>0.11.0</PackageVersion>
+	<Version>0.11.1</Version>
+	<ReleaseVersion>0.11.1</ReleaseVersion>
+	<PackageVersion>0.11.1</PackageVersion>
   </PropertyGroup>
 
   <ItemGroup>
diff --git a/src/NRedisStack/ResponseParser.cs b/src/NRedisStack/ResponseParser.cs
index 81f22eb3..b3a7c315 100644
--- a/src/NRedisStack/ResponseParser.cs
+++ b/src/NRedisStack/ResponseParser.cs
@@ -3,6 +3,7 @@
 using NRedisStack.Extensions;
 using StackExchange.Redis;
 using NRedisStack.Bloom.DataTypes;
+using NRedisStack.Core.DataTypes;
 using NRedisStack.CuckooFilter.DataTypes;
 using NRedisStack.CountMinSketch.DataTypes;
 using NRedisStack.TopK.DataTypes;
@@ -84,6 +85,16 @@ public static TimeStamp ToTimeStamp(this RedisResult result)
             return new TimeStamp((long)result);
         }
 
+        public static RedisKey ToRedisKey(this RedisResult result)
+        {
+            return new RedisKey(result.ToString());
+        }
+
+        public static RedisValue ToRedisValue(this RedisResult result)
+        {
+            return new RedisValue(result.ToString());
+        }
+
         public static IReadOnlyList<TimeStamp> ToTimeStampArray(this RedisResult result)
         {
             RedisResult[] redisResults = (RedisResult[])result!;
@@ -715,5 +726,45 @@ public static Dictionary<string, RedisResult>[] ToDictionarys(this RedisResult r
             return dicts;
 
         }
+
+        public static Tuple<RedisKey, RedisValueWithScore>? ToSortedSetPopResult(this RedisResult result)
+        {
+            if (result.IsNull)
+            {
+                return null;
+            }
+
+            var resultArray = (RedisResult[])result!;
+            var resultKey = resultArray[0].ToRedisKey();
+            var value = resultArray[1].ToRedisValue();
+            var score = resultArray[2].ToDouble();
+            var valuesWithScores = new RedisValueWithScore(value, score);
+
+            return new Tuple<RedisKey, RedisValueWithScore>(resultKey, valuesWithScores);
+        }
+
+        public static Tuple<RedisKey, List<RedisValueWithScore>>? ToSortedSetPopResults(this RedisResult result)
+        {
+            if (result.IsNull)
+            {
+                return null;
+            }
+
+            var resultArray = (RedisResult[])result!;
+            var resultKey = resultArray[0].ToRedisKey();
+            var resultSetItems = resultArray[1].ToArray();
+
+            List<RedisValueWithScore> valuesWithScores = new List<RedisValueWithScore>();
+
+            foreach (var resultSetItem in resultSetItems)
+            {
+                var resultSetItemArray = (RedisResult[])resultSetItem!;
+                var value = resultSetItemArray[0].ToRedisValue();
+                var score = resultSetItemArray[1].ToDouble();
+                valuesWithScores.Add(new RedisValueWithScore(value, score));
+            }
+
+            return new Tuple<RedisKey, List<RedisValueWithScore>>(resultKey, valuesWithScores);
+        }
     }
 }
\ No newline at end of file
diff --git a/tests/NRedisStack.Tests/Core Commands/CoreTests.cs b/tests/NRedisStack.Tests/Core Commands/CoreTests.cs
index 9a3d763a..7c6e82fa 100644
--- a/tests/NRedisStack.Tests/Core Commands/CoreTests.cs	
+++ b/tests/NRedisStack.Tests/Core Commands/CoreTests.cs	
@@ -1,10 +1,8 @@
 using Xunit;
 using NRedisStack.Core;
-using NRedisStack;
 using static NRedisStack.Auxiliary;
 using StackExchange.Redis;
-using System.Xml.Linq;
-using System.Reflection;
+using NRedisStack.Core.DataTypes;
 using NRedisStack.RedisStackCommands;
 
 
@@ -146,4 +144,274 @@ public async Task TestSetInfoNullAsync()
         // Assert that the extracted sub-strings are equal
         Assert.Equal(infoAfterLibNameToEnd, infoBeforeLibNameToEnd);
     }
+
+    [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.0.0")]
+    public void TestBzmPop()
+    {
+        var redis = ConnectionMultiplexer.Connect("localhost");
+
+        var db = redis.GetDatabase(null);
+        db.Execute("FLUSHALL");
+
+        var sortedSetKey = "my-set";
+
+        db.SortedSetAdd(sortedSetKey, "a", 1.5);
+        db.SortedSetAdd(sortedSetKey, "b", 5.1);
+        db.SortedSetAdd(sortedSetKey, "c", 3.7);
+        db.SortedSetAdd(sortedSetKey, "d", 9.4);
+        db.SortedSetAdd(sortedSetKey, "e", 7.76);
+
+        // Pop two items with default order, which means it will pop the minimum values.
+        var resultWithDefaultOrder = db.BzmPop(0, sortedSetKey, MinMaxModifier.Min, 2);
+
+        Assert.NotNull(resultWithDefaultOrder);
+        Assert.Equal(sortedSetKey, resultWithDefaultOrder!.Item1);
+        Assert.Equal(2, resultWithDefaultOrder.Item2.Count);
+        Assert.Equal("a", resultWithDefaultOrder.Item2[0].Value.ToString());
+        Assert.Equal("c", resultWithDefaultOrder.Item2[1].Value.ToString());
+
+        // Pop one more item, with descending order, which means it will pop the maximum value.
+        var resultWithDescendingOrder = db.BzmPop(0, sortedSetKey, MinMaxModifier.Max, 1);
+
+        Assert.NotNull(resultWithDescendingOrder);
+        Assert.Equal(sortedSetKey, resultWithDescendingOrder!.Item1);
+        Assert.Single(resultWithDescendingOrder.Item2);
+        Assert.Equal("d", resultWithDescendingOrder.Item2[0].Value.ToString());
+    }
+
+    [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.0.0")]
+    public void TestBzmPopNull()
+    {
+        var redis = ConnectionMultiplexer.Connect("localhost");
+
+        var db = redis.GetDatabase(null);
+        db.Execute("FLUSHALL");
+
+        // Nothing in the set, and a short server timeout, which yields null.
+        var result = db.BzmPop(0.5, "my-set", MinMaxModifier.Min, null);
+
+        Assert.Null(result);
+    }
+
+    [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.0.0")]
+    public void TestBzmPopMultiplexerTimeout()
+    {
+        var configurationOptions = new ConfigurationOptions();
+        configurationOptions.SyncTimeout = 1000;
+        configurationOptions.EndPoints.Add("localhost");
+        var redis = ConnectionMultiplexer.Connect(configurationOptions);
+
+        var db = redis.GetDatabase(null);
+        db.Execute("FLUSHALL");
+
+        // Server would wait forever, but the multiplexer times out in 1 second.
+        Assert.Throws<RedisTimeoutException>(() => db.BzmPop(0, "my-set", MinMaxModifier.Min));
+    }
+
+    [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.0.0")]
+    public void TestBzmPopMultipleSets()
+    {
+        var redis = ConnectionMultiplexer.Connect("localhost");
+
+        var db = redis.GetDatabase(null);
+        db.Execute("FLUSHALL");
+
+        db.SortedSetAdd("set-one", "a", 1.5);
+        db.SortedSetAdd("set-one", "b", 5.1);
+        db.SortedSetAdd("set-one", "c", 3.7);
+        db.SortedSetAdd("set-two", "d", 9.4);
+        db.SortedSetAdd("set-two", "e", 7.76);
+
+        var result = db.BzmPop(0, "set-two", MinMaxModifier.Max);
+
+        Assert.NotNull(result);
+        Assert.Equal("set-two", result!.Item1);
+        Assert.Single(result.Item2);
+        Assert.Equal("d", result.Item2[0].Value.ToString());
+
+        result = db.BzmPop(0, new[] { new RedisKey("set-two"), new RedisKey("set-one") }, MinMaxModifier.Min);
+
+        Assert.NotNull(result);
+        Assert.Equal("set-two", result!.Item1);
+        Assert.Single(result.Item2);
+        Assert.Equal("e", result.Item2[0].Value.ToString());
+
+        result = db.BzmPop(0, new[] { new RedisKey("set-two"), new RedisKey("set-one") }, MinMaxModifier.Max);
+
+        Assert.NotNull(result);
+        Assert.Equal("set-one", result!.Item1);
+        Assert.Single(result.Item2);
+        Assert.Equal("b", result.Item2[0].Value.ToString());
+
+        result = db.BzmPop(0, "set-one", MinMaxModifier.Min, count: 2);
+
+        Assert.NotNull(result);
+        Assert.Equal("set-one", result!.Item1);
+        Assert.Equal(2, result.Item2.Count);
+        Assert.Equal("a", result.Item2[0].Value.ToString());
+        Assert.Equal("c", result.Item2[1].Value.ToString());
+    }
+
+    [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.0.0")]
+    public void TestBzmPopNoKeysProvided()
+    {
+        var redis = ConnectionMultiplexer.Connect("localhost");
+
+        var db = redis.GetDatabase(null);
+        db.Execute("FLUSHALL");
+
+        // Server would wait forever, but the multiplexer times out in 1 second.
+        Assert.Throws<ArgumentException>(() => db.BzmPop(0, Array.Empty<RedisKey>(), MinMaxModifier.Min));
+    }
+
+    [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "7.0.0")]
+    public void TestBzmPopWithOrderEnum()
+    {
+        var redis = ConnectionMultiplexer.Connect("localhost");
+
+        var db = redis.GetDatabase(null);
+        db.Execute("FLUSHALL");
+
+        var sortedSetKey = "my-set";
+
+        db.SortedSetAdd(sortedSetKey, "a", 1.5);
+        db.SortedSetAdd(sortedSetKey, "b", 5.1);
+        db.SortedSetAdd(sortedSetKey, "c", 3.7);
+
+        // Pop two items with default order, which means it will pop the minimum values.
+        var resultWithDefaultOrder = db.BzmPop(0, sortedSetKey, Order.Ascending.ToMinMax());
+
+        Assert.NotNull(resultWithDefaultOrder);
+        Assert.Equal(sortedSetKey, resultWithDefaultOrder!.Item1);
+        Assert.Single(resultWithDefaultOrder.Item2);
+        Assert.Equal("a", resultWithDefaultOrder.Item2[0].Value.ToString());
+
+        // Pop one more item, with descending order, which means it will pop the maximum value.
+        var resultWithDescendingOrder = db.BzmPop(0, sortedSetKey, Order.Descending.ToMinMax());
+
+        Assert.NotNull(resultWithDescendingOrder);
+        Assert.Equal(sortedSetKey, resultWithDescendingOrder!.Item1);
+        Assert.Single(resultWithDescendingOrder.Item2);
+        Assert.Equal("b", resultWithDescendingOrder.Item2[0].Value.ToString());
+    }
+
+    [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "5.0.0")]
+    public void TestBzPopMin()
+    {
+        var redis = ConnectionMultiplexer.Connect("localhost");
+
+        var db = redis.GetDatabase(null);
+        db.Execute("FLUSHALL");
+
+        var sortedSetKey = "my-set";
+
+        db.SortedSetAdd(sortedSetKey, "a", 1.5);
+        db.SortedSetAdd(sortedSetKey, "b", 5.1);
+
+        var result = db.BzPopMin(sortedSetKey, 0);
+
+        Assert.NotNull(result);
+        Assert.Equal(sortedSetKey, result!.Item1);
+        Assert.Equal("a", result.Item2.Value.ToString());
+        Assert.Equal(1.5, result.Item2.Score);
+    }
+
+    [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "5.0.0")]
+    public void TestBzPopMinNull()
+    {
+        var redis = ConnectionMultiplexer.Connect("localhost");
+
+        var db = redis.GetDatabase(null);
+        db.Execute("FLUSHALL");
+
+        // Nothing in the set, and a short server timeout, which yields null.
+        var result = db.BzPopMin("my-set", 0.5);
+
+        Assert.Null(result);
+    }
+
+    [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "5.0.0")]
+    public void TestBzPopMinMultipleSets()
+    {
+        var redis = ConnectionMultiplexer.Connect("localhost");
+
+        var db = redis.GetDatabase(null);
+        db.Execute("FLUSHALL");
+
+        db.SortedSetAdd("set-one", "a", 1.5);
+        db.SortedSetAdd("set-one", "b", 5.1);
+        db.SortedSetAdd("set-two", "e", 7.76);
+
+        var result = db.BzPopMin(new[] { new RedisKey("set-two"), new RedisKey("set-one") }, 0);
+
+        Assert.NotNull(result);
+        Assert.Equal("set-two", result!.Item1);
+        Assert.Equal("e", result.Item2.Value.ToString());
+
+        result = db.BzPopMin(new[] { new RedisKey("set-two"), new RedisKey("set-one") }, 0);
+
+        Assert.NotNull(result);
+        Assert.Equal("set-one", result!.Item1);
+        Assert.Equal("a", result.Item2.Value.ToString());
+    }
+
+    [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "5.0.0")]
+    public void TestBzPopMax()
+    {
+        var redis = ConnectionMultiplexer.Connect("localhost");
+
+        var db = redis.GetDatabase(null);
+        db.Execute("FLUSHALL");
+
+        var sortedSetKey = "my-set";
+
+        db.SortedSetAdd(sortedSetKey, "a", 1.5);
+        db.SortedSetAdd(sortedSetKey, "b", 5.1);
+
+        var result = db.BzPopMax(sortedSetKey, 0);
+
+        Assert.NotNull(result);
+        Assert.Equal(sortedSetKey, result!.Item1);
+        Assert.Equal("b", result.Item2.Value.ToString());
+        Assert.Equal(5.1, result.Item2.Score);
+    }
+
+    [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "5.0.0")]
+    public void TestBzPopMaxNull()
+    {
+        var redis = ConnectionMultiplexer.Connect("localhost");
+
+        var db = redis.GetDatabase(null);
+        db.Execute("FLUSHALL");
+
+        // Nothing in the set, and a short server timeout, which yields null.
+        var result = db.BzPopMax("my-set", 0.5);
+
+        Assert.Null(result);
+    }
+
+    [SkipIfRedis(Is.OSSCluster, Comparison.LessThan, "5.0.0")]
+    public void TestBzPopMaxMultipleSets()
+    {
+        var redis = ConnectionMultiplexer.Connect("localhost");
+
+        var db = redis.GetDatabase(null);
+        db.Execute("FLUSHALL");
+
+        db.SortedSetAdd("set-one", "a", 1.5);
+        db.SortedSetAdd("set-one", "b", 5.1);
+        db.SortedSetAdd("set-two", "e", 7.76);
+
+        var result = db.BzPopMax(new[] { new RedisKey("set-two"), new RedisKey("set-one") }, 0);
+
+        Assert.NotNull(result);
+        Assert.Equal("set-two", result!.Item1);
+        Assert.Equal("e", result.Item2.Value.ToString());
+
+        result = db.BzPopMax(new[] { new RedisKey("set-two"), new RedisKey("set-one") }, 0);
+
+        Assert.NotNull(result);
+        Assert.Equal("set-one", result!.Item1);
+        Assert.Equal("b", result.Item2.Value.ToString());
+    }
 }
\ No newline at end of file