<NME> RedisDatabase.cs
<BEF> // Copyright (c) Ugo Lattanzi.  All Rights Reserved.  Licensed under the MIT license.  See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

using StackExchange.Redis.Extensions.Core.Abstractions;
using StackExchange.Redis.Extensions.Core.Configuration;
using StackExchange.Redis.Extensions.Core.Extensions;

namespace StackExchange.Redis.Extensions.Core.Implementations
{
    internal class RedisDatabase : IRedisDatabase
    {
        private readonly IConnectionMultiplexer connectionMultiplexer;
        private readonly ServerEnumerationStrategy serverEnumerationStrategy = new ServerEnumerationStrategy();
public partial class RedisDatabase : IRedisDatabase
{
    private readonly IRedisConnectionPoolManager connectionPoolManager;
    private readonly ServerEnumerationStrategy serverEnumerationStrategy;
    private readonly string keyPrefix;
    private readonly uint maxValueLength;
    private readonly int dbNumber;

    /// <summary>
    /// Initializes a new instance of the <see cref="RedisDatabase"/> class.
    /// </summary>
    /// <param name="connectionPoolManager">The connection pool manager.</param>
    /// <param name="serializer">The serializer.</param>
    /// <param name="serverEnumerationStrategy">The server enumeration strategy.</param>
    /// <param name="dbNumber">The database to use.</param>
    /// <param name="maxvalueLength">The max lenght of the cache object.</param>
    /// <param name="keyPrefix">The key prefix.</param>
    public RedisDatabase(
        IRedisConnectionPoolManager connectionPoolManager,
        ISerializer serializer,
        ServerEnumerationStrategy serverEnumerationStrategy,
        int dbNumber,
        uint maxvalueLength,
        string? keyPrefix = null)
    {
        Serializer = serializer ?? throw new ArgumentNullException(nameof(serializer));
        this.connectionPoolManager = connectionPoolManager ?? throw new ArgumentNullException(nameof(connectionPoolManager));
        this.serverEnumerationStrategy = serverEnumerationStrategy;
        this.dbNumber = dbNumber;
        this.keyPrefix = keyPrefix ?? string.Empty;
        maxValueLength = maxvalueLength;
    }

    /// <inheritdoc/>
    public IDatabase Database
    {
        get
        {
            var db = connectionPoolManager.GetConnection().GetDatabase(dbNumber);

            return keyPrefix.Length > 0
                ? db.WithKeyPrefix(keyPrefix)
                : db;
        }
    }

    /// <inheritdoc/>
    public ISerializer Serializer { get; }

    /// <inheritdoc/>
    public Task<bool> ExistsAsync(string key, CommandFlags flags = CommandFlags.None)
    {
        return Database.KeyExistsAsync(key, flags);
    }

    /// <inheritdoc/>
    public Task<bool> RemoveAsync(string key, CommandFlags flags = CommandFlags.None)
    {
        return Database.KeyDeleteAsync(key, flags);
    }

    /// <inheritdoc/>
    public Task<long> RemoveAllAsync(string[] keys, CommandFlags flags = CommandFlags.None)
    {
        var redisKeys = new RedisKey[keys.Length];

        for (var i = 0; i < keys.Length; i++)
            redisKeys[i] = (RedisKey)keys[i];

        return Database.KeyDeleteAsync(redisKeys, flags);
    }

    /// <inheritdoc/>
            return result;
        }

        public async Task<bool> AddAsync<T>(string key, T value, When when = When.Always, CommandFlags flag = CommandFlags.None)
        {
            var entryBytes = Serializer.Serialize(value);
            if (maxValueLength > 0 && entryBytes.Length > maxValueLength)
                throw new ArgumentException("value cannot be longer than the MaxValueLength", nameof(value));

            return await Database.StringSetAsync(key, entryBytes, null, when, flag);
        }

        public Task<bool> ReplaceAsync<T>(string key, T value, When when = When.Always, CommandFlags flag = CommandFlags.None)
        var result = await GetAsync<T>(key, flag).ConfigureAwait(false);
            return AddAsync(key, value, when, flag);
        }

        public async Task<bool> AddAsync<T>(string key, T value, DateTimeOffset expiresAt, When when = When.Always, CommandFlags flag = CommandFlags.None)
        {
            var entryBytes = Serializer.Serialize(value);
            if (maxValueLength > 0 && entryBytes.Length > maxValueLength)
                throw new ArgumentException("value cannot be longer than the MaxValueLength", nameof(value));

            var expiration = expiresAt.UtcDateTime.Subtract(DateTime.UtcNow);

            return await Database.StringSetAsync(key, entryBytes, expiration, when, flag);
        }

        public Task<bool> ReplaceAsync<T>(string key, T value, DateTimeOffset expiresAt, When when = When.Always, CommandFlags flag = CommandFlags.None)
    }
            return AddAsync(key, value, expiresAt, when, flag);
        }

        public async Task<bool> AddAsync<T>(string key, T value, TimeSpan expiresIn, When when = When.Always, CommandFlags flag = CommandFlags.None)
        {
            var entryBytes = Serializer.Serialize(value);
            if (maxValueLength > 0 && entryBytes.Length > maxValueLength)
                throw new ArgumentException("value cannot be longer than the MaxValueLength", nameof(value));

            return await Database.StringSetAsync(key, entryBytes, expiresIn, when, flag);
        }

        public Task<bool> ReplaceAsync<T>(string key, T value, TimeSpan expiresIn, When when = When.Always, CommandFlags flag = CommandFlags.None)
        return AddAsync(key, value, when, flag);
    }

    /// <inheritdoc/>
    public Task<bool> AddAsync<T>(string key, T value, DateTimeOffset expiresAt, When when = When.Always, CommandFlags flag = CommandFlags.None, HashSet<string>? tags = null)
        {
            var redisKeys = keys.Select(x => (RedisKey)x).ToArray();
            var result = await Database.StringGetAsync(redisKeys);
            var dict = new Dictionary<string, T>(StringComparer.Ordinal);
            for (var index = 0; index < redisKeys.Length; index++)
            {
                var value = result[index];
    }

    /// <inheritdoc/>
    public Task<bool> ReplaceAsync<T>(string key, T value, DateTimeOffset expiresAt, When when = When.Always, CommandFlags flag = CommandFlags.None)
    {

        public async Task<IDictionary<string, T>> GetAllAsync<T>(IEnumerable<string> keys, DateTimeOffset expiresAt)
        {
            var result = await GetAllAsync<T>(keys);
            await UpdateExpiryAllAsync(keys.ToArray(), expiresAt);
            return result;
        }

        public async Task<IDictionary<string, T>> GetAllAsync<T>(IEnumerable<string> keys, TimeSpan expiresIn)
        {
            var result = await GetAllAsync<T>(keys);
            await UpdateExpiryAllAsync(keys.ToArray(), expiresIn);
            return result;
        }

        public async Task<bool> AddAllAsync<T>(IList<Tuple<string, T>> items, When when = When.Always, CommandFlags flag = CommandFlags.None)
        {
            var values = GetItemsInMaxLengthLimit(items);

            return await Database.StringSetAsync(values, when, flag);
        }

        public async Task<bool> AddAllAsync<T>(IList<Tuple<string, T>> items, DateTimeOffset expiresAt, When when = When.Always, CommandFlags flag = CommandFlags.None)
        var dict = new Dictionary<string, T?>(redisKeys.Length, StringComparer.Ordinal);

        for (var index = 0; index < redisKeys.Length; index++)
        {
            var value = result[index];
            dict.Add(redisKeys[index], value == RedisValue.Null
                ? default
                : Serializer.Deserialize<T>(value));
        }

        return dict;
    }

    /// <inheritdoc/>
    public async Task<IDictionary<string, T?>> GetAllAsync<T>(string[] keys, DateTimeOffset expiresAt)
    {
        var tsk1 = GetAllAsync<T>(keys);
        var tsk2 = UpdateExpiryAllAsync(keys, expiresAt);
            return result;
        }

        public async Task<bool> SetAddAsync<T>(string key, T item, CommandFlags flag = CommandFlags.None) where T : class
        {
            if (string.IsNullOrEmpty(key))
                throw new ArgumentException("key cannot be empty.", nameof(key));
    public async Task<IDictionary<string, T?>> GetAllAsync<T>(string[] keys, TimeSpan expiresIn)
    {

            var serializedObject = Serializer.Serialize(item);

            return await Database.SetAddAsync(key, serializedObject, flag);
        }

        public async Task<long> SetAddAllAsync<T>(string key, CommandFlags flag = CommandFlags.None, params T[] items) where T : class
        {
            if (string.IsNullOrEmpty(key)) throw new ArgumentException("key cannot be empty.", nameof(key));

            if (items == null) throw new ArgumentNullException(nameof(items), "items cannot be null.");

            if (items.Any(item => item == null)) throw new ArgumentException("items cannot contains any null item.", nameof(items));

            return await Database.SetAddAsync(key, items.Select(item => Serializer.Serialize(item)).Select(x => (RedisValue)x).ToArray(), flag);
        }

        public async Task<bool> SetRemoveAsync<T>(string key, T item, CommandFlags flag = CommandFlags.None) where T : class
        {
            if (string.IsNullOrEmpty(key)) throw new ArgumentException("key cannot be empty.", nameof(key));

            .Select(x => new KeyValuePair<RedisKey, RedisValue>(x.Key, x.Value))

            var serializedObject = Serializer.Serialize(item);

            return await Database.SetRemoveAsync(key, serializedObject, flag);
        }

        public async Task<long> SetRemoveAllAsync<T>(string key, CommandFlags flag = CommandFlags.None, params T[] items) where T : class
        {
            if (string.IsNullOrEmpty(key)) throw new ArgumentException("key cannot be empty.", nameof(key));

            if (items == null) throw new ArgumentNullException(nameof(items), "items cannot be null.");

            if (items.Any(item => item == null)) throw new ArgumentException("items cannot contains any null item.", nameof(items));

            return await Database.SetRemoveAsync(key, items.Select(item => Serializer.Serialize(item)).Select(x => (RedisValue)x).ToArray(), flag);
        }

        public async Task<string[]> SetMemberAsync(string memberName, CommandFlags flag = CommandFlags.None)
        var tasks = new Task[values.Length];
        await Database.StringSetAsync(values, when, flag).ConfigureAwait(false);

        for (var i = 0; i < values.Length; i++)
            tasks[i] = Database.KeyExpireAsync(values[i].Key, expiresOn, flag);

        await Task.WhenAll(tasks).ConfigureAwait(false);

        return ((Task<bool>)tasks[0]).Result;
    }

    /// <inheritdoc/>
    public Task<bool> SetAddAsync<T>(string key, T item, CommandFlags flag = CommandFlags.None)
    {
        if (string.IsNullOrEmpty(key))
            throw new ArgumentException("key cannot be empty.", nameof(key));

        if (item == null)
            throw new ArgumentNullException(nameof(item), "item cannot be null.");

        var serializedObject = Serializer.Serialize(item);

        return Database.SetAddAsync(key, serializedObject, flag);
    }

    /// <inheritdoc/>
    public async Task<T?> SetPopAsync<T>(string key, CommandFlags flag = CommandFlags.None)
    {
        if (string.IsNullOrEmpty(key))
            throw new ArgumentException("key cannot be empty.", nameof(key));

        var item = await Database.SetPopAsync(key, flag).ConfigureAwait(false);

        return item == RedisValue.Null
            ? default
            : Serializer.Deserialize<T>(item);
    }

    /// <inheritdoc/>
    public async Task<IEnumerable<T?>> SetPopAsync<T>(string key, long count, CommandFlags flag = CommandFlags.None)
    {
        if (string.IsNullOrEmpty(key))
            throw new ArgumentException("key cannot be empty.", nameof(key));

        var items = await Database.SetPopAsync(key, count, flag).ConfigureAwait(false);

        return items.Select(item => item == RedisValue.Null ? default : Serializer.Deserialize<T>(item));
    }

    /// <inheritdoc/>
    public Task<bool> SetContainsAsync<T>(string key, T item, CommandFlags flag = CommandFlags.None)
    {
        if (string.IsNullOrEmpty(key))
            throw new ArgumentException("key cannot be empty.", nameof(key));

        if (item == null)
            throw new ArgumentNullException(nameof(item), "item cannot be null.");

        var serializedObject = Serializer.Serialize(item);

        return Database.SetContainsAsync(key, serializedObject, flag);
    }

    /// <inheritdoc/>
    public Task<long> SetAddAllAsync<T>(string key, CommandFlags flag = CommandFlags.None, params T[] items)
    {
        if (string.IsNullOrEmpty(key))
            throw new ArgumentException("key cannot be empty.", nameof(key));

        if (items == null)
            throw new ArgumentNullException(nameof(items), "items cannot be null.");

        if (items.Any(item => item == null))
            throw new ArgumentException("items cannot contains any null item.", nameof(items));

        return Database
            .SetAddAsync(
                key,
            return ParseCategorizedInfo(info);
        }

        public async Task<long> PublishAsync<T>(RedisChannel channel, T message, CommandFlags flags = CommandFlags.None)
        {
            var sub = connectionMultiplexer.GetSubscriber();
            return await sub.PublishAsync(channel, Serializer.Serialize(message), flags);
        }

        public async Task SubscribeAsync<T>(RedisChannel channel, Func<T, Task> handler, CommandFlags flags = CommandFlags.None)
        {
            if (handler == null)
                throw new ArgumentNullException(nameof(handler));

            var sub = connectionMultiplexer.GetSubscriber();
            await
                sub.SubscribeAsync(channel, async (redisChannel, value) => await handler(Serializer.Deserialize<T>(value)), flags);
        }

        public async Task UnsubscribeAsync<T>(RedisChannel channel, Func<T, Task> handler, CommandFlags flags = CommandFlags.None)
        {
            if (handler == null)
                throw new ArgumentNullException(nameof(handler));

            var sub = connectionMultiplexer.GetSubscriber();
            await sub.UnsubscribeAsync(channel, (redisChannel, value) => handler(Serializer.Deserialize<T>(value)), flags);
        }

        public async Task UnsubscribeAllAsync(CommandFlags flags = CommandFlags.None)
        {
            var sub = connectionMultiplexer.GetSubscriber();
            await sub.UnsubscribeAllAsync(flags);
        }

        public async Task<long> ListAddToLeftAsync<T>(string key, T item, When when = When.Always, CommandFlags flags = CommandFlags.None) where T : class
        {
            if (string.IsNullOrEmpty(key))
                throw new ArgumentException("key cannot be empty.", nameof(key));

            if (item == null)
                throw new ArgumentNullException(nameof(item), "item cannot be null.");

            var serializedItem = Serializer.Serialize(item);

            return await Database.ListLeftPushAsync(key, serializedItem, when, flags);
        }

        public async Task<T> ListGetFromRightAsync<T>(string key, CommandFlags flags = CommandFlags.None) where T : class
        {
            if (string.IsNullOrEmpty(key)) throw new ArgumentException("key cannot be empty.", nameof(key));

            var item = await Database.ListRightPopAsync(key, flags);

            if (item == RedisValue.Null) return null;

            return item == RedisValue.Null ? null : Serializer.Deserialize<T>(item);
        }

        public async Task<bool> HashDeleteAsync(string hashKey, string key, CommandFlags commandFlags = CommandFlags.None)
        {
            return await Database.HashDeleteAsync(hashKey, key, commandFlags);
        }

        public async Task<long> HashDeleteAsync(string hashKey, IEnumerable<string> keys, CommandFlags commandFlags = CommandFlags.None)
        {
            return await Database.HashDeleteAsync(hashKey, keys.Select(x => (RedisValue)x).ToArray(), commandFlags);
        }

        public async Task<bool> HashExistsAsync(string hashKey, string key, CommandFlags commandFlags = CommandFlags.None)
        {
            return await Database.HashExistsAsync(hashKey, key, commandFlags);
        }

        public async Task<T> HashGetAsync<T>(string hashKey, string key, CommandFlags commandFlags = CommandFlags.None)
        {
            var redisValue = await Database.HashGetAsync(hashKey, key, commandFlags);
            return redisValue.HasValue ? Serializer.Deserialize<T>(redisValue) : default;
        }

        public async Task<Dictionary<string, T>> HashGetAsync<T>(string hashKey, IEnumerable<string> keys, CommandFlags commandFlags = CommandFlags.None)
        {
            var result = new Dictionary<string, T>();
            foreach (var key in keys)
            {
                var value = await HashGetAsync<T>(hashKey, key, commandFlags);

                result.Add(key, value);
            }

            return result;
        }

        public async Task<Dictionary<string, T>> HashGetAllAsync<T>(string hashKey, CommandFlags commandFlags = CommandFlags.None)
        {
            return (await Database
                    .HashGetAllAsync(hashKey, commandFlags))
                .ToDictionary(
                    x => x.Name.ToString(),
                    x => Serializer.Deserialize<T>(x.Value),
                    StringComparer.Ordinal);
        }

        public async Task<long> HashIncerementByAsync(string hashKey, string key, long value, CommandFlags commandFlags = CommandFlags.None)
        {
            return await Database.HashIncrementAsync(hashKey, key, value, commandFlags);
        }

        public async Task<double> HashIncerementByAsync(string hashKey, string key, double value, CommandFlags commandFlags = CommandFlags.None)
        {
            return await Database.HashIncrementAsync(hashKey, key, value, commandFlags);
        }

        public async Task<IEnumerable<string>> HashKeysAsync(string hashKey, CommandFlags commandFlags = CommandFlags.None)
        {
            return (await Database.HashKeysAsync(hashKey, commandFlags)).Select(x => x.ToString());
        }

        public async Task<long> HashLengthAsync(string hashKey, CommandFlags commandFlags = CommandFlags.None)
        {
            return await Database.HashLengthAsync(hashKey, commandFlags);
        }

        public async Task<bool> HashSetAsync<T>(string hashKey, string key, T value, bool nx = false, CommandFlags commandFlags = CommandFlags.None)
        {
            return await Database.HashSetAsync(hashKey, key, Serializer.Serialize(value), nx ? When.NotExists : When.Always, commandFlags);
        }

        public async Task HashSetAsync<T>(string hashKey, IDictionary<string, T> values, CommandFlags commandFlags = CommandFlags.None)
        {
            var entries = values.Select(kv => new HashEntry(kv.Key, Serializer.Serialize(kv.Value)));
            await Database.HashSetAsync(hashKey, entries.ToArray(), commandFlags);
        }

        public async Task<IEnumerable<T>> HashValuesAsync<T>(string hashKey, CommandFlags commandFlags = CommandFlags.None)
        {
            return (await Database.HashValuesAsync(hashKey, commandFlags)).Select(x => Serializer.Deserialize<T>(x));
        }

        public async Task<Dictionary<string, T>> HashScanAsync<T>(string hashKey, string pattern, int pageSize = 10, CommandFlags commandFlags = CommandFlags.None)
        {
            return (await Task.Run(() => Database.HashScan(hashKey, pattern, pageSize, commandFlags)))
                .ToDictionary(x => x.Name.ToString(), x => Serializer.Deserialize<T>(x.Value), StringComparer.Ordinal);
        }

        public async Task<bool> UpdateExpiryAsync(string key, DateTimeOffset expiresAt, CommandFlags flags = CommandFlags.None)
        {
            if (await Database.KeyExistsAsync(key))
                return await Database.KeyExpireAsync(key, expiresAt.UtcDateTime.Subtract(DateTime.UtcNow), flags);

            return false;
        }

        public async Task<bool> UpdateExpiryAsync(string key, TimeSpan expiresIn, CommandFlags flags = CommandFlags.None)
        {
            if (await Database.KeyExistsAsync(key))
                return await Database.KeyExpireAsync(key, expiresIn, flags);

            return false;
        }

        public async Task<IDictionary<string, bool>> UpdateExpiryAllAsync(string[] keys, DateTimeOffset expiresAt, CommandFlags flags = CommandFlags.None)
        {
            var results = new Dictionary<string, bool>(StringComparer.Ordinal);

            for (var i = 0; i < keys.Length; i++)
                results.Add(keys[i], await UpdateExpiryAsync(keys[i], expiresAt.UtcDateTime, flags));

            return results;
        }

        public async Task<IDictionary<string, bool>> UpdateExpiryAllAsync(string[] keys, TimeSpan expiresIn, CommandFlags flags = CommandFlags.None)
        {
            var results = new Dictionary<string, bool>(StringComparer.Ordinal);

            for (var i = 0; i < keys.Length; i++)
                results.Add(keys[i], await UpdateExpiryAsync(keys[i], expiresIn, flags));

            return results;
        }

        public async Task<bool> SortedSetAddAsync<T>(string key, T value, double score, CommandFlags commandFlags = CommandFlags.None)
        {
            var entryBytes = Serializer.Serialize(value);

            return await Database.SortedSetAddAsync(key, entryBytes, score, commandFlags);
        }

        public async Task<bool> SortedSetRemoveAsync<T>(string key, T value, CommandFlags commandFlags = CommandFlags.None)
        {
            var entryBytes = Serializer.Serialize(value);

            return await Database.SortedSetRemoveAsync(key, entryBytes, commandFlags);
        }

        public IEnumerable<T> SortedSetRangeByScore<T>(string key, double start = double.NegativeInfinity, double stop = double.PositiveInfinity, Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0L,
            long take = -1L, CommandFlags commandFlags = CommandFlags.None)
        {
            var result = Database.SortedSetRangeByScore(key, start, stop, exclude, order, skip, take, commandFlags);

            return result.Select(m => m == RedisValue.Null ? default : Serializer.Deserialize<T>(m));
        }

        public async Task<IEnumerable<T>> SortedSetRangeByScoreAsync<T>(string key, double start = double.NegativeInfinity, double stop = double.PositiveInfinity, Exclude exclude = Exclude.None, Order order = Order.Ascending,
            long skip = 0L,
            long take = -1L, CommandFlags commandFlags = CommandFlags.None)
        {
            var result = await Database.SortedSetRangeByScoreAsync(key, start, stop, exclude, order, skip, take, commandFlags);

            return result.Select(m => m == RedisValue.Null ? default : Serializer.Deserialize<T>(m));
        }

        private Dictionary<string, string> ParseInfo(string info)
        {
            // Call Parse Categorized Info to cut back on duplicated code.

    /// <inheritdoc/>
    public Task<bool> SetRemoveAsync<T>(string key, T item, CommandFlags flag = CommandFlags.None)
    {
        if (string.IsNullOrEmpty(key))
            throw new ArgumentException("key cannot be empty.", nameof(key));

        if (item == null)
            throw new ArgumentNullException(nameof(item), "item cannot be null.");

        var serializedObject = Serializer.Serialize(item);

        return Database.SetRemoveAsync(key, serializedObject, flag);
    }

    /// <inheritdoc/>
    public Task<long> SetRemoveAllAsync<T>(string key, CommandFlags flag = CommandFlags.None, params T[] items)
    {
        if (string.IsNullOrEmpty(key))
            throw new ArgumentException("key cannot be empty.", nameof(key));

        if (items == null)
            throw new ArgumentNullException(nameof(items), "items cannot be null.");

        if (items.Any(item => item == null))
            throw new ArgumentException("items cannot contains any null item.", nameof(items));

        return Database.SetRemoveAsync(key, items
            .Select(item => Serializer.Serialize(item))
            .Select(x => (RedisValue)x)
            .ToArray(), flag);
    }

    /// <inheritdoc/>
    public async Task<string[]> SetMemberAsync(string memberName, CommandFlags flag = CommandFlags.None)
    {
        var members = await Database.SetMembersAsync(memberName, flag).ConfigureAwait(false);
        return members.Select(x => x.ToString()).ToArray();
    }

    /// <inheritdoc/>
    public async Task<T[]> SetMembersAsync<T>(string key, CommandFlags flag = CommandFlags.None)
    {
        var members = await Database.SetMembersAsync(key, flag).ConfigureAwait(false);

        if (members.Length == 0)
            return Array.Empty<T>();

        var result = new T[members.Length];

        for (var i = 0; i < members.Length; i++)
        {
            var m = members[i];

            result[i] = Serializer.Deserialize<T>(m);
        }

        return result;
    }

    /// <inheritdoc/>
    public async Task<IEnumerable<string>> SearchKeysAsync(string pattern)
    {
        pattern = $"{keyPrefix}{pattern}";
        var keys = new HashSet<string>();

        var servers = ServerIteratorFactory
            .GetServers(connectionPoolManager.GetConnection(), serverEnumerationStrategy)
            .ToArray();

        if (servers.Length == 0)
            throw new("No server found to serve the KEYS command.");

        foreach (var unused in servers)
        {
            long nextCursor = 0;
            do
            {
                var redisResult = await Database.ExecuteAsync("SCAN", nextCursor.ToString(), "MATCH", pattern, "COUNT", "1000").ConfigureAwait(false);
                var innerResult = (RedisResult[])redisResult;

                nextCursor = long.Parse((string)innerResult[0]);

                var resultLines = ((string[])innerResult[1]).ToArray();
                keys.UnionWith(resultLines);
            }
            while (nextCursor != 0);
        }

        return !string.IsNullOrEmpty(keyPrefix)
            ? keys.Select(k => k.Substring(keyPrefix.Length))
            : keys;
    }

    /// <inheritdoc/>
    public Task FlushDbAsync()
    {
        var endPoints = Database.Multiplexer.GetEndPoints();

        var tasks = new List<Task>(endPoints.Length);

        for (var i = 0; i < endPoints.Length; i++)
        {
            var server = Database.Multiplexer.GetServer(endPoints[i]);

            if (!server.IsReplica)
                tasks.Add(server.FlushDatabaseAsync(Database.Database));
        }

        return Task.WhenAll(tasks);
    }

    /// <inheritdoc/>
    public Task SaveAsync(SaveType saveType, CommandFlags flags = CommandFlags.None)
    {
        var endPoints = Database.Multiplexer.GetEndPoints();

        var tasks = new Task[endPoints.Length];

        for (var i = 0; i < endPoints.Length; i++)
            tasks[i] = Database.Multiplexer.GetServer(endPoints[i]).SaveAsync(saveType, flags);

        return Task.WhenAll(tasks);
    }

    /// <inheritdoc/>
    public async Task<Dictionary<string, string>> GetInfoAsync()
    {
        var info = (await Database.ScriptEvaluateAsync("return redis.call('INFO')").ConfigureAwait(false)).ToString();

        return string.IsNullOrEmpty(info)
            ? new Dictionary<string, string>()
            : ParseInfo(info);
    }

    /// <inheritdoc/>
    public async Task<InfoDetail[]> GetInfoCategorizedAsync()
    {
        var info = (await Database.ScriptEvaluateAsync("return redis.call('INFO')").ConfigureAwait(false)).ToString();

        return string.IsNullOrEmpty(info)
            ? Array.Empty<InfoDetail>()
            : ParseCategorizedInfo(info);
    }

    /// <inheritdoc/>
    public Task<double> SortedSetAddIncrementAsync<T>(string key, T? value, double score, CommandFlags commandFlags = CommandFlags.None)
    {
        var entryBytes = Serializer.Serialize(value);
        return Database.SortedSetIncrementAsync(key, entryBytes, score, commandFlags);
    }

    private Dictionary<string, string> ParseInfo(string info)
    {
        // Call Parse Categorized Info to cut back on duplicated code.
        var data = ParseCategorizedInfo(info);

        // Return a dictionary of the Info Key and Info value
        return data.ToDictionary(x => x.Key, x => x.InfoValue);
    }

    private InfoDetail[] ParseCategorizedInfo(string info)
    {
        var data = new List<InfoDetail>();
        var category = string.Empty;

        var lines = info.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);

        foreach (var line in lines.Where(x => !string.IsNullOrWhiteSpace(x)))
        {
            if (line[0] == '#')
            {
                category = line.Replace("#", string.Empty).Trim();
                continue;
            }

            var idx = line.IndexOf(':');

            if (idx > 0)
            {
                var key = line.Substring(0, idx);
                var infoValue = line.Substring(idx + 1).Trim();

                data.Add(new(category, key, infoValue));
            }
        }

        return data.ToArray();
    }
}

<MSG> Small threads optimizations

<DFF> @@ -11,7 +11,7 @@ using StackExchange.Redis.KeyspaceIsolation;
 
 namespace StackExchange.Redis.Extensions.Core.Implementations
 {
-    internal class RedisDatabase : IRedisDatabase
+    internal partial class RedisDatabase : IRedisDatabase
     {
         private readonly IConnectionMultiplexer connectionMultiplexer;
         private readonly ServerEnumerationStrategy serverEnumerationStrategy = new ServerEnumerationStrategy();
@@ -91,13 +91,14 @@ namespace StackExchange.Redis.Extensions.Core.Implementations
             return result;
         }
 
-        public async Task<bool> AddAsync<T>(string key, T value, When when = When.Always, CommandFlags flag = CommandFlags.None)
+        public Task<bool> AddAsync<T>(string key, T value, When when = When.Always, CommandFlags flag = CommandFlags.None)
         {
             var entryBytes = Serializer.Serialize(value);
+
             if (maxValueLength > 0 && entryBytes.Length > maxValueLength)
                 throw new ArgumentException("value cannot be longer than the MaxValueLength", nameof(value));
 
-            return await Database.StringSetAsync(key, entryBytes, null, when, flag);
+            return Database.StringSetAsync(key, entryBytes, null, when, flag);
         }
 
         public Task<bool> ReplaceAsync<T>(string key, T value, When when = When.Always, CommandFlags flag = CommandFlags.None)
@@ -105,15 +106,16 @@ namespace StackExchange.Redis.Extensions.Core.Implementations
             return AddAsync(key, value, when, flag);
         }
 
-        public async Task<bool> AddAsync<T>(string key, T value, DateTimeOffset expiresAt, When when = When.Always, CommandFlags flag = CommandFlags.None)
+        public Task<bool> AddAsync<T>(string key, T value, DateTimeOffset expiresAt, When when = When.Always, CommandFlags flag = CommandFlags.None)
         {
             var entryBytes = Serializer.Serialize(value);
+
             if (maxValueLength > 0 && entryBytes.Length > maxValueLength)
                 throw new ArgumentException("value cannot be longer than the MaxValueLength", nameof(value));
 
             var expiration = expiresAt.UtcDateTime.Subtract(DateTime.UtcNow);
 
-            return await Database.StringSetAsync(key, entryBytes, expiration, when, flag);
+            return Database.StringSetAsync(key, entryBytes, expiration, when, flag);
         }
 
         public Task<bool> ReplaceAsync<T>(string key, T value, DateTimeOffset expiresAt, When when = When.Always, CommandFlags flag = CommandFlags.None)
@@ -121,13 +123,14 @@ namespace StackExchange.Redis.Extensions.Core.Implementations
             return AddAsync(key, value, expiresAt, when, flag);
         }
 
-        public async Task<bool> AddAsync<T>(string key, T value, TimeSpan expiresIn, When when = When.Always, CommandFlags flag = CommandFlags.None)
+        public Task<bool> AddAsync<T>(string key, T value, TimeSpan expiresIn, When when = When.Always, CommandFlags flag = CommandFlags.None)
         {
             var entryBytes = Serializer.Serialize(value);
+
             if (maxValueLength > 0 && entryBytes.Length > maxValueLength)
                 throw new ArgumentException("value cannot be longer than the MaxValueLength", nameof(value));
 
-            return await Database.StringSetAsync(key, entryBytes, expiresIn, when, flag);
+            return Database.StringSetAsync(key, entryBytes, expiresIn, when, flag);
         }
 
         public Task<bool> ReplaceAsync<T>(string key, T value, TimeSpan expiresIn, When when = When.Always, CommandFlags flag = CommandFlags.None)
@@ -139,7 +142,8 @@ namespace StackExchange.Redis.Extensions.Core.Implementations
         {
             var redisKeys = keys.Select(x => (RedisKey)x).ToArray();
             var result = await Database.StringGetAsync(redisKeys);
-            var dict = new Dictionary<string, T>(StringComparer.Ordinal);
+            var dict = new Dictionary<string, T>(redisKeys.Length, StringComparer.Ordinal);
+
             for (var index = 0; index < redisKeys.Length; index++)
             {
                 var value = result[index];
@@ -151,23 +155,29 @@ namespace StackExchange.Redis.Extensions.Core.Implementations
 
         public async Task<IDictionary<string, T>> GetAllAsync<T>(IEnumerable<string> keys, DateTimeOffset expiresAt)
         {
-            var result = await GetAllAsync<T>(keys);
-            await UpdateExpiryAllAsync(keys.ToArray(), expiresAt);
-            return result;
+            var tsk1 = GetAllAsync<T>(keys);
+            var tsk2 = UpdateExpiryAllAsync(keys.ToArray(), expiresAt);
+
+            await Task.WhenAll(tsk1, tsk2);
+
+            return tsk1.Result;
         }
 
         public async Task<IDictionary<string, T>> GetAllAsync<T>(IEnumerable<string> keys, TimeSpan expiresIn)
         {
-            var result = await GetAllAsync<T>(keys);
-            await UpdateExpiryAllAsync(keys.ToArray(), expiresIn);
-            return result;
+            var tsk1 = GetAllAsync<T>(keys);
+            var tsk2 = UpdateExpiryAllAsync(keys.ToArray(), expiresIn);
+
+            await Task.WhenAll(tsk1, tsk2);
+
+            return tsk1.Result;
         }
 
-        public async Task<bool> AddAllAsync<T>(IList<Tuple<string, T>> items, When when = When.Always, CommandFlags flag = CommandFlags.None)
+        public Task<bool> AddAllAsync<T>(IList<Tuple<string, T>> items, When when = When.Always, CommandFlags flag = CommandFlags.None)
         {
             var values = GetItemsInMaxLengthLimit(items);
 
-            return await Database.StringSetAsync(values, when, flag);
+            return Database.StringSetAsync(values, when, flag);
         }
 
         public async Task<bool> AddAllAsync<T>(IList<Tuple<string, T>> items, DateTimeOffset expiresAt, When when = When.Always, CommandFlags flag = CommandFlags.None)
@@ -192,7 +202,7 @@ namespace StackExchange.Redis.Extensions.Core.Implementations
             return result;
         }
 
-        public async Task<bool> SetAddAsync<T>(string key, T item, CommandFlags flag = CommandFlags.None) where T : class
+        public Task<bool> SetAddAsync<T>(string key, T item, CommandFlags flag = CommandFlags.None) where T : class
         {
             if (string.IsNullOrEmpty(key))
                 throw new ArgumentException("key cannot be empty.", nameof(key));
@@ -201,21 +211,24 @@ namespace StackExchange.Redis.Extensions.Core.Implementations
 
             var serializedObject = Serializer.Serialize(item);
 
-            return await Database.SetAddAsync(key, serializedObject, flag);
+            return Database.SetAddAsync(key, serializedObject, flag);
         }
 
-        public async Task<long> SetAddAllAsync<T>(string key, CommandFlags flag = CommandFlags.None, params T[] items) where T : class
+        public Task<long> SetAddAllAsync<T>(string key, CommandFlags flag = CommandFlags.None, params T[] items) where T : class
         {
-            if (string.IsNullOrEmpty(key)) throw new ArgumentException("key cannot be empty.", nameof(key));
+            if (string.IsNullOrEmpty(key))
+                throw new ArgumentException("key cannot be empty.", nameof(key));
 
-            if (items == null) throw new ArgumentNullException(nameof(items), "items cannot be null.");
+            if (items == null)
+                throw new ArgumentNullException(nameof(items), "items cannot be null.");
 
-            if (items.Any(item => item == null)) throw new ArgumentException("items cannot contains any null item.", nameof(items));
+            if (items.Any(item => item == null))
+                throw new ArgumentException("items cannot contains any null item.", nameof(items));
 
-            return await Database.SetAddAsync(key, items.Select(item => Serializer.Serialize(item)).Select(x => (RedisValue)x).ToArray(), flag);
+            return Database.SetAddAsync(key, items.Select(item => Serializer.Serialize(item)).Select(x => (RedisValue)x).ToArray(), flag);
         }
 
-        public async Task<bool> SetRemoveAsync<T>(string key, T item, CommandFlags flag = CommandFlags.None) where T : class
+        public Task<bool> SetRemoveAsync<T>(string key, T item, CommandFlags flag = CommandFlags.None) where T : class
         {
             if (string.IsNullOrEmpty(key)) throw new ArgumentException("key cannot be empty.", nameof(key));
 
@@ -223,18 +236,21 @@ namespace StackExchange.Redis.Extensions.Core.Implementations
 
             var serializedObject = Serializer.Serialize(item);
 
-            return await Database.SetRemoveAsync(key, serializedObject, flag);
+            return Database.SetRemoveAsync(key, serializedObject, flag);
         }
 
-        public async Task<long> SetRemoveAllAsync<T>(string key, CommandFlags flag = CommandFlags.None, params T[] items) where T : class
+        public Task<long> SetRemoveAllAsync<T>(string key, CommandFlags flag = CommandFlags.None, params T[] items) where T : class
         {
-            if (string.IsNullOrEmpty(key)) throw new ArgumentException("key cannot be empty.", nameof(key));
+            if (string.IsNullOrEmpty(key))
+                throw new ArgumentException("key cannot be empty.", nameof(key));
 
-            if (items == null) throw new ArgumentNullException(nameof(items), "items cannot be null.");
+            if (items == null)
+                throw new ArgumentNullException(nameof(items), "items cannot be null.");
 
-            if (items.Any(item => item == null)) throw new ArgumentException("items cannot contains any null item.", nameof(items));
+            if (items.Any(item => item == null))
+                throw new ArgumentException("items cannot contains any null item.", nameof(items));
 
-            return await Database.SetRemoveAsync(key, items.Select(item => Serializer.Serialize(item)).Select(x => (RedisValue)x).ToArray(), flag);
+            return Database.SetRemoveAsync(key, items.Select(item => Serializer.Serialize(item)).Select(x => (RedisValue)x).ToArray(), flag);
         }
 
         public async Task<string[]> SetMemberAsync(string memberName, CommandFlags flag = CommandFlags.None)
@@ -319,214 +335,6 @@ namespace StackExchange.Redis.Extensions.Core.Implementations
             return ParseCategorizedInfo(info);
         }
 
-        public async Task<long> PublishAsync<T>(RedisChannel channel, T message, CommandFlags flags = CommandFlags.None)
-        {
-            var sub = connectionMultiplexer.GetSubscriber();
-            return await sub.PublishAsync(channel, Serializer.Serialize(message), flags);
-        }
-
-        public async Task SubscribeAsync<T>(RedisChannel channel, Func<T, Task> handler, CommandFlags flags = CommandFlags.None)
-        {
-            if (handler == null)
-                throw new ArgumentNullException(nameof(handler));
-
-            var sub = connectionMultiplexer.GetSubscriber();
-            await
-                sub.SubscribeAsync(channel, async (redisChannel, value) => await handler(Serializer.Deserialize<T>(value)), flags);
-        }
-
-        public async Task UnsubscribeAsync<T>(RedisChannel channel, Func<T, Task> handler, CommandFlags flags = CommandFlags.None)
-        {
-            if (handler == null)
-                throw new ArgumentNullException(nameof(handler));
-
-            var sub = connectionMultiplexer.GetSubscriber();
-            await sub.UnsubscribeAsync(channel, (redisChannel, value) => handler(Serializer.Deserialize<T>(value)), flags);
-        }
-
-        public async Task UnsubscribeAllAsync(CommandFlags flags = CommandFlags.None)
-        {
-            var sub = connectionMultiplexer.GetSubscriber();
-            await sub.UnsubscribeAllAsync(flags);
-        }
-
-        public async Task<long> ListAddToLeftAsync<T>(string key, T item, When when = When.Always, CommandFlags flags = CommandFlags.None) where T : class
-        {
-            if (string.IsNullOrEmpty(key))
-                throw new ArgumentException("key cannot be empty.", nameof(key));
-
-            if (item == null)
-                throw new ArgumentNullException(nameof(item), "item cannot be null.");
-
-            var serializedItem = Serializer.Serialize(item);
-
-            return await Database.ListLeftPushAsync(key, serializedItem, when, flags);
-        }
-
-        public async Task<T> ListGetFromRightAsync<T>(string key, CommandFlags flags = CommandFlags.None) where T : class
-        {
-            if (string.IsNullOrEmpty(key)) throw new ArgumentException("key cannot be empty.", nameof(key));
-
-            var item = await Database.ListRightPopAsync(key, flags);
-
-            if (item == RedisValue.Null) return null;
-
-            return item == RedisValue.Null ? null : Serializer.Deserialize<T>(item);
-        }
-
-        public async Task<bool> HashDeleteAsync(string hashKey, string key, CommandFlags commandFlags = CommandFlags.None)
-        {
-            return await Database.HashDeleteAsync(hashKey, key, commandFlags);
-        }
-
-        public async Task<long> HashDeleteAsync(string hashKey, IEnumerable<string> keys, CommandFlags commandFlags = CommandFlags.None)
-        {
-            return await Database.HashDeleteAsync(hashKey, keys.Select(x => (RedisValue)x).ToArray(), commandFlags);
-        }
-
-        public async Task<bool> HashExistsAsync(string hashKey, string key, CommandFlags commandFlags = CommandFlags.None)
-        {
-            return await Database.HashExistsAsync(hashKey, key, commandFlags);
-        }
-
-        public async Task<T> HashGetAsync<T>(string hashKey, string key, CommandFlags commandFlags = CommandFlags.None)
-        {
-            var redisValue = await Database.HashGetAsync(hashKey, key, commandFlags);
-            return redisValue.HasValue ? Serializer.Deserialize<T>(redisValue) : default;
-        }
-
-        public async Task<Dictionary<string, T>> HashGetAsync<T>(string hashKey, IEnumerable<string> keys, CommandFlags commandFlags = CommandFlags.None)
-        {
-            var result = new Dictionary<string, T>();
-            foreach (var key in keys)
-            {
-                var value = await HashGetAsync<T>(hashKey, key, commandFlags);
-
-                result.Add(key, value);
-            }
-
-            return result;
-        }
-
-        public async Task<Dictionary<string, T>> HashGetAllAsync<T>(string hashKey, CommandFlags commandFlags = CommandFlags.None)
-        {
-            return (await Database
-                    .HashGetAllAsync(hashKey, commandFlags))
-                .ToDictionary(
-                    x => x.Name.ToString(),
-                    x => Serializer.Deserialize<T>(x.Value),
-                    StringComparer.Ordinal);
-        }
-
-        public async Task<long> HashIncerementByAsync(string hashKey, string key, long value, CommandFlags commandFlags = CommandFlags.None)
-        {
-            return await Database.HashIncrementAsync(hashKey, key, value, commandFlags);
-        }
-
-        public async Task<double> HashIncerementByAsync(string hashKey, string key, double value, CommandFlags commandFlags = CommandFlags.None)
-        {
-            return await Database.HashIncrementAsync(hashKey, key, value, commandFlags);
-        }
-
-        public async Task<IEnumerable<string>> HashKeysAsync(string hashKey, CommandFlags commandFlags = CommandFlags.None)
-        {
-            return (await Database.HashKeysAsync(hashKey, commandFlags)).Select(x => x.ToString());
-        }
-
-        public async Task<long> HashLengthAsync(string hashKey, CommandFlags commandFlags = CommandFlags.None)
-        {
-            return await Database.HashLengthAsync(hashKey, commandFlags);
-        }
-
-        public async Task<bool> HashSetAsync<T>(string hashKey, string key, T value, bool nx = false, CommandFlags commandFlags = CommandFlags.None)
-        {
-            return await Database.HashSetAsync(hashKey, key, Serializer.Serialize(value), nx ? When.NotExists : When.Always, commandFlags);
-        }
-
-        public async Task HashSetAsync<T>(string hashKey, IDictionary<string, T> values, CommandFlags commandFlags = CommandFlags.None)
-        {
-            var entries = values.Select(kv => new HashEntry(kv.Key, Serializer.Serialize(kv.Value)));
-            await Database.HashSetAsync(hashKey, entries.ToArray(), commandFlags);
-        }
-
-        public async Task<IEnumerable<T>> HashValuesAsync<T>(string hashKey, CommandFlags commandFlags = CommandFlags.None)
-        {
-            return (await Database.HashValuesAsync(hashKey, commandFlags)).Select(x => Serializer.Deserialize<T>(x));
-        }
-
-        public async Task<Dictionary<string, T>> HashScanAsync<T>(string hashKey, string pattern, int pageSize = 10, CommandFlags commandFlags = CommandFlags.None)
-        {
-            return (await Task.Run(() => Database.HashScan(hashKey, pattern, pageSize, commandFlags)))
-                .ToDictionary(x => x.Name.ToString(), x => Serializer.Deserialize<T>(x.Value), StringComparer.Ordinal);
-        }
-
-        public async Task<bool> UpdateExpiryAsync(string key, DateTimeOffset expiresAt, CommandFlags flags = CommandFlags.None)
-        {
-            if (await Database.KeyExistsAsync(key))
-                return await Database.KeyExpireAsync(key, expiresAt.UtcDateTime.Subtract(DateTime.UtcNow), flags);
-
-            return false;
-        }
-
-        public async Task<bool> UpdateExpiryAsync(string key, TimeSpan expiresIn, CommandFlags flags = CommandFlags.None)
-        {
-            if (await Database.KeyExistsAsync(key))
-                return await Database.KeyExpireAsync(key, expiresIn, flags);
-
-            return false;
-        }
-
-        public async Task<IDictionary<string, bool>> UpdateExpiryAllAsync(string[] keys, DateTimeOffset expiresAt, CommandFlags flags = CommandFlags.None)
-        {
-            var results = new Dictionary<string, bool>(StringComparer.Ordinal);
-
-            for (var i = 0; i < keys.Length; i++)
-                results.Add(keys[i], await UpdateExpiryAsync(keys[i], expiresAt.UtcDateTime, flags));
-
-            return results;
-        }
-
-        public async Task<IDictionary<string, bool>> UpdateExpiryAllAsync(string[] keys, TimeSpan expiresIn, CommandFlags flags = CommandFlags.None)
-        {
-            var results = new Dictionary<string, bool>(StringComparer.Ordinal);
-
-            for (var i = 0; i < keys.Length; i++)
-                results.Add(keys[i], await UpdateExpiryAsync(keys[i], expiresIn, flags));
-
-            return results;
-        }
-
-        public async Task<bool> SortedSetAddAsync<T>(string key, T value, double score, CommandFlags commandFlags = CommandFlags.None)
-        {
-            var entryBytes = Serializer.Serialize(value);
-
-            return await Database.SortedSetAddAsync(key, entryBytes, score, commandFlags);
-        }
-
-        public async Task<bool> SortedSetRemoveAsync<T>(string key, T value, CommandFlags commandFlags = CommandFlags.None)
-        {
-            var entryBytes = Serializer.Serialize(value);
-
-            return await Database.SortedSetRemoveAsync(key, entryBytes, commandFlags);
-        }
-
-        public IEnumerable<T> SortedSetRangeByScore<T>(string key, double start = double.NegativeInfinity, double stop = double.PositiveInfinity, Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0L,
-            long take = -1L, CommandFlags commandFlags = CommandFlags.None)
-        {
-            var result = Database.SortedSetRangeByScore(key, start, stop, exclude, order, skip, take, commandFlags);
-
-            return result.Select(m => m == RedisValue.Null ? default : Serializer.Deserialize<T>(m));
-        }
-
-        public async Task<IEnumerable<T>> SortedSetRangeByScoreAsync<T>(string key, double start = double.NegativeInfinity, double stop = double.PositiveInfinity, Exclude exclude = Exclude.None, Order order = Order.Ascending,
-            long skip = 0L,
-            long take = -1L, CommandFlags commandFlags = CommandFlags.None)
-        {
-            var result = await Database.SortedSetRangeByScoreAsync(key, start, stop, exclude, order, skip, take, commandFlags);
-
-            return result.Select(m => m == RedisValue.Null ? default : Serializer.Deserialize<T>(m));
-        }
-
         private Dictionary<string, string> ParseInfo(string info)
         {
             // Call Parse Categorized Info to cut back on duplicated code.

Small threads optimizations