diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index d0d01a201..12a76e74e 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -1264,6 +1264,36 @@ public interface IDatabase : IRedis, IDatabaseAsync /// RedisResult ScriptEvaluate(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None); + /// + /// Execute a Lua script against the server. + /// + /// The script to execute. + /// The key to execute against. + /// The values to execute against. + /// The flags to use for this operation. + /// A dynamic representation of the script's result. + /// + /// , + /// + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Not ambiguous due to type delta")] + RedisResult ScriptEvaluate(string script, RedisKey key, ReadOnlyMemory values = default, CommandFlags flags = CommandFlags.None); + + /// + /// Execute a Lua script against the server. + /// + /// The script to execute. + /// The keys to execute against. + /// The values to execute against. + /// The flags to use for this operation. + /// A dynamic representation of the script's result. + /// + /// , + /// + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Not ambiguous due to type delta")] + RedisResult ScriptEvaluate(string script, ReadOnlyMemory keys, ReadOnlyMemory values = default, CommandFlags flags = CommandFlags.None); + /// /// Execute a Lua script against the server using just the SHA1 hash. /// @@ -1316,6 +1346,36 @@ public interface IDatabase : IRedis, IDatabaseAsync /// RedisResult ScriptEvaluateReadOnly(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None); + /// + /// Read-only variant of the EVAL command that cannot execute commands that modify data, Execute a Lua script against the server. + /// + /// The script to execute. + /// The key to execute against. + /// The values to execute against. + /// The flags to use for this operation. + /// A dynamic representation of the script's result. + /// + /// , + /// + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Not ambiguous due to type delta")] + RedisResult ScriptEvaluateReadOnly(string script, RedisKey key, ReadOnlyMemory values = default, CommandFlags flags = CommandFlags.None); + + /// + /// Read-only variant of the EVAL command that cannot execute commands that modify data, Execute a Lua script against the server. + /// + /// The script to execute. + /// The keys to execute against. + /// The values to execute against. + /// The flags to use for this operation. + /// A dynamic representation of the script's result. + /// + /// , + /// + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Not ambiguous due to type delta")] + RedisResult ScriptEvaluateReadOnly(string script, ReadOnlyMemory keys, ReadOnlyMemory values = default, CommandFlags flags = CommandFlags.None); + /// /// Read-only variant of the EVALSHA command that cannot execute commands that modify data, Execute a Lua script against the server using just the SHA1 hash. /// diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 7f90cf1a5..1e4d4de91 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -1240,6 +1240,36 @@ public interface IDatabaseAsync : IRedisAsync /// Task ScriptEvaluateAsync(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None); + /// + /// Execute a Lua script against the server. + /// + /// The script to execute. + /// The key to execute against. + /// The values to execute against. + /// The flags to use for this operation. + /// A dynamic representation of the script's result. + /// + /// , + /// + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Not ambiguous due to type delta")] + Task ScriptEvaluateAsync(string script, RedisKey key, ReadOnlyMemory values = default, CommandFlags flags = CommandFlags.None); + + /// + /// Execute a Lua script against the server. + /// + /// The script to execute. + /// The keys to execute against. + /// The values to execute against. + /// The flags to use for this operation. + /// A dynamic representation of the script's result. + /// + /// , + /// + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Not ambiguous due to type delta")] + Task ScriptEvaluateAsync(string script, ReadOnlyMemory keys, ReadOnlyMemory values = default, CommandFlags flags = CommandFlags.None); + /// /// Execute a Lua script against the server using just the SHA1 hash. /// @@ -1303,6 +1333,36 @@ public interface IDatabaseAsync : IRedisAsync /// Task ScriptEvaluateReadOnlyAsync(byte[] hash, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None); + /// + /// Read-only variant of the EVAL command that cannot execute commands that modify data, Execute a Lua script against the server. + /// + /// The script to execute. + /// The key to execute against. + /// The values to execute against. + /// The flags to use for this operation. + /// A dynamic representation of the script's result. + /// + /// , + /// + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Not ambiguous due to type delta")] + Task ScriptEvaluateReadOnlyAsync(string script, RedisKey key, ReadOnlyMemory values = default, CommandFlags flags = CommandFlags.None); + + /// + /// Read-only variant of the EVAL command that cannot execute commands that modify data, Execute a Lua script against the server. + /// + /// The script to execute. + /// The keys to execute against. + /// The values to execute against. + /// The flags to use for this operation. + /// A dynamic representation of the script's result. + /// + /// , + /// + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Not ambiguous due to type delta")] + Task ScriptEvaluateReadOnlyAsync(string script, ReadOnlyMemory keys, ReadOnlyMemory values = default, CommandFlags flags = CommandFlags.None); + /// /// Add the specified member to the set stored at key. /// Specified members that are already a member of this set are ignored. diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs index 290fbed59..79f336e92 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -332,9 +333,31 @@ public Task ScriptEvaluateAsync(byte[] hash, RedisKey[]? keys = nul // TODO: The return value could contain prefixed keys. It might make sense to 'unprefix' those? Inner.ScriptEvaluateAsync(hash, ToInner(keys), values, flags); - public Task ScriptEvaluateAsync(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) => - // TODO: The return value could contain prefixed keys. It might make sense to 'unprefix' those? - Inner.ScriptEvaluateAsync(script, ToInner(keys), values, flags); + public Task ScriptEvaluateAsync(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) + { // note we may end up using the memory overloads to enable better pooling etc usage + if (keys is null || keys.Length == 0) return Inner.ScriptEvaluateAsync(script, keys, values, flags); + if (keys.Length == 1) return Inner.ScriptEvaluateAsync(script, ToInner(keys[0]), values, flags); + if ((flags & CommandFlags.FireAndForget) != 0) return Inner.ScriptEvaluateAsync(script, ToInnerCopy(keys), values, flags); + return Lease_ScriptEvaluateAsync(script, keys, values, flags); + } + + public Task ScriptEvaluateAsync(string script, RedisKey key, ReadOnlyMemory values = default, CommandFlags flags = CommandFlags.None) => + Inner.ScriptEvaluateAsync(script, ToInner(key), values, flags); + + public Task ScriptEvaluateAsync(string script, ReadOnlyMemory keys, ReadOnlyMemory values = default, CommandFlags flags = CommandFlags.None) + { + if (keys.Length == 0) return Inner.ScriptEvaluateAsync(script, keys, values, flags); + if (keys.Length == 1) return Inner.ScriptEvaluateAsync(script, ToInner(keys.Span[0]), values, flags); + if ((flags & CommandFlags.FireAndForget) != 0) return Inner.ScriptEvaluateAsync(script, ToInnerCopy(keys), values, flags); + return Lease_ScriptEvaluateAsync(script, keys, values, flags); + } + + private async Task Lease_ScriptEvaluateAsync(string script, ReadOnlyMemory keys, ReadOnlyMemory values, CommandFlags flags = CommandFlags.None) + { + var result = await Inner.ScriptEvaluateAsync(script, ToInnerLease(keys, out var lease), values, flags); + ArrayPool.Shared.Return(lease); // happy to only recycle on success + return result; + } public Task ScriptEvaluateAsync(LuaScript script, object? parameters = null, CommandFlags flags = CommandFlags.None) => // TODO: The return value could contain prefixed keys. It might make sense to 'unprefix' those? @@ -346,11 +369,61 @@ public Task ScriptEvaluateAsync(LoadedLuaScript script, object? par public Task ScriptEvaluateReadOnlyAsync(byte[] hash, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) => // TODO: The return value could contain prefixed keys. It might make sense to 'unprefix' those? - Inner.ScriptEvaluateAsync(hash, ToInner(keys), values, flags); + Inner.ScriptEvaluateReadOnlyAsync(hash, ToInner(keys), values, flags); + + public Task ScriptEvaluateReadOnlyAsync(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) + { // note we may end up using the memory overloads to enable better pooling etc usage + if (keys is null || keys.Length == 0) return Inner.ScriptEvaluateReadOnlyAsync(script, keys, values, flags); + if (keys.Length == 1) return Inner.ScriptEvaluateReadOnlyAsync(script, ToInner(keys[0]), values, flags); + if ((flags & CommandFlags.FireAndForget) != 0) return Inner.ScriptEvaluateReadOnlyAsync(script, ToInnerCopy(keys), values, flags); + return Lease_ScriptEvaluateReadOnlyAsync(script, keys, values, flags); + } - public Task ScriptEvaluateReadOnlyAsync(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) => - // TODO: The return value could contain prefixed keys. It might make sense to 'unprefix' those? - Inner.ScriptEvaluateAsync(script, ToInner(keys), values, flags); + public Task ScriptEvaluateReadOnlyAsync(string script, RedisKey key, ReadOnlyMemory values = default, CommandFlags flags = CommandFlags.None) => + Inner.ScriptEvaluateReadOnlyAsync(script, ToInner(key), values, flags); + + public Task ScriptEvaluateReadOnlyAsync(string script, ReadOnlyMemory keys, ReadOnlyMemory values = default, CommandFlags flags = CommandFlags.None) + { + if (keys.Length == 0) return Inner.ScriptEvaluateReadOnlyAsync(script, keys, values, flags); + if (keys.Length == 1) return Inner.ScriptEvaluateReadOnlyAsync(script, ToInner(keys.Span[0]), values, flags); + if ((flags & CommandFlags.FireAndForget) != 0) return Inner.ScriptEvaluateReadOnlyAsync(script, ToInnerCopy(keys), values, flags); + return Lease_ScriptEvaluateReadOnlyAsync(script, keys, values, flags); + } + + private async Task Lease_ScriptEvaluateReadOnlyAsync(string script, ReadOnlyMemory keys, ReadOnlyMemory values, CommandFlags flags) + { + var result = await Inner.ScriptEvaluateReadOnlyAsync(script, ToInnerLease(keys, out var lease), values, flags); + ArrayPool.Shared.Return(lease); // happy to only recycle on success + return result; + } + + protected ReadOnlyMemory ToInnerCopy(ReadOnlyMemory keys) + { + if (keys.Length == 0) return default; + var arr = new RedisKey[keys.Length]; + ToInner(keys, arr); + return arr; + } + + protected ReadOnlyMemory ToInnerLease(ReadOnlyMemory keys, out RedisKey[] lease) + { + if (keys.Length == 0) + { + lease = Array.Empty(); + return default; + } + lease = ArrayPool.Shared.Rent(keys.Length); + return new ReadOnlyMemory(lease, 0, ToInner(keys, lease)); + } + private int ToInner(ReadOnlyMemory from, RedisKey[] arr) + { + int i = 0; + foreach (ref readonly var key in from.Span) + { + arr[i++] = ToInner(key); + } + return i; + } public Task SetAddAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => Inner.SetAddAsync(ToInner(key), values, flags); @@ -734,10 +807,10 @@ public void Wait(Task task) => public void WaitAll(params Task[] tasks) => Inner.WaitAll(tasks); - protected internal RedisKey ToInner(RedisKey outer) => + protected internal RedisKey ToInner(in RedisKey outer) => RedisKey.WithPrefix(Prefix, outer); - protected RedisKey ToInnerOrDefault(RedisKey outer) => + protected RedisKey ToInnerOrDefault(in RedisKey outer) => (outer == default(RedisKey)) ? outer : ToInner(outer); [return: NotNullIfNotNull("args")] @@ -847,6 +920,9 @@ protected RedisChannel ToInner(RedisChannel outer) => private Func? mapFunction; protected Func GetMapFunction() => // create as a delegate when first required, then re-use - mapFunction ??= new Func(ToInner); + mapFunction ??= CreateMapFunction(); // avoid inlining this because of capture scopes etc + + private Func CreateMapFunction() => ToInnerNoRef; + private RedisKey ToInnerNoRef(RedisKey value) => ToInner(value); } } diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs index d1c47aeab..3ecfbaa2c 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs @@ -1,6 +1,8 @@ using System; +using System.Buffers; using System.Collections.Generic; using System.Net; +using System.Threading.Tasks; namespace StackExchange.Redis.KeyspaceIsolation { @@ -321,9 +323,30 @@ public RedisResult ScriptEvaluate(byte[] hash, RedisKey[]? keys = null, RedisVal // TODO: The return value could contain prefixed keys. It might make sense to 'unprefix' those? Inner.ScriptEvaluate(hash, ToInner(keys), values, flags); - public RedisResult ScriptEvaluate(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) => - // TODO: The return value could contain prefixed keys. It might make sense to 'unprefix' those? - Inner.ScriptEvaluate(script, ToInner(keys), values, flags); + public RedisResult ScriptEvaluate(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) + { // note we may end up using the memory overloads to enable better pooling etc usage + if (keys is null || keys.Length == 0) return Inner.ScriptEvaluate(script, keys, values, flags); + if (keys.Length == 1) return Inner.ScriptEvaluate(script, ToInner(keys[0]), values, flags); + if ((flags & CommandFlags.FireAndForget) != 0) return Inner.ScriptEvaluate(script, ToInnerCopy(keys), values, flags); + + var result = Inner.ScriptEvaluate(script, ToInnerLease(keys, out var lease), values, flags); + ArrayPool.Shared.Return(lease); // happy to only recycle on success + return result; + } + + public RedisResult ScriptEvaluate(string script, RedisKey key, ReadOnlyMemory values = default, CommandFlags flags = CommandFlags.None) => + Inner.ScriptEvaluate(script, ToInner(key), values, flags); + + public RedisResult ScriptEvaluate(string script, ReadOnlyMemory keys, ReadOnlyMemory values = default, CommandFlags flags = CommandFlags.None) + { + if (keys.Length == 0) return Inner.ScriptEvaluate(script, keys, values, flags); + if (keys.Length == 1) return Inner.ScriptEvaluate(script, ToInner(keys.Span[0]), values, flags); + if ((flags & CommandFlags.FireAndForget) != 0) return Inner.ScriptEvaluate(script, ToInnerCopy(keys), values, flags); + + var result = Inner.ScriptEvaluate(script, ToInnerLease(keys, out var lease), values, flags); + ArrayPool.Shared.Return(lease); // happy to only recycle on success + return result; + } public RedisResult ScriptEvaluate(LuaScript script, object? parameters = null, CommandFlags flags = CommandFlags.None) => // TODO: The return value could contain prefixed keys. It might make sense to 'unprefix' those? @@ -337,9 +360,30 @@ public RedisResult ScriptEvaluateReadOnly(byte[] hash, RedisKey[]? keys = null, // TODO: The return value could contain prefixed keys. It might make sense to 'unprefix' those? Inner.ScriptEvaluateReadOnly(hash, ToInner(keys), values, flags); - public RedisResult ScriptEvaluateReadOnly(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) => - // TODO: The return value could contain prefixed keys. It might make sense to 'unprefix' those? - Inner.ScriptEvaluateReadOnly(script, ToInner(keys), values, flags); + public RedisResult ScriptEvaluateReadOnly(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) + { // note we may end up using the memory overloads to enable better pooling etc usage + if (keys is null || keys.Length == 0) return Inner.ScriptEvaluateReadOnly(script, keys, values, flags); + if (keys.Length == 1) return Inner.ScriptEvaluateReadOnly(script, ToInner(keys[0]), values, flags); + if ((flags & CommandFlags.FireAndForget) != 0) return Inner.ScriptEvaluateReadOnly(script, ToInnerCopy(keys), values, flags); + + var result = Inner.ScriptEvaluateReadOnly(script, ToInnerLease(keys, out var lease), values, flags); + ArrayPool.Shared.Return(lease); // happy to only recycle on success + return result; + } + + public RedisResult ScriptEvaluateReadOnly(string script, RedisKey key, ReadOnlyMemory values = default, CommandFlags flags = CommandFlags.None) => + Inner.ScriptEvaluateReadOnly(script, ToInner(key), values, flags); + + public RedisResult ScriptEvaluateReadOnly(string script, ReadOnlyMemory keys, ReadOnlyMemory values = default, CommandFlags flags = CommandFlags.None) + { + if (keys.Length == 0) return Inner.ScriptEvaluateReadOnly(script, keys, values, flags); + if (keys.Length == 1) return Inner.ScriptEvaluateReadOnly(script, ToInner(keys.Span[0]), values, flags); + if ((flags & CommandFlags.FireAndForget) != 0) return Inner.ScriptEvaluateReadOnly(script, ToInnerCopy(keys), values, flags); + + var result = Inner.ScriptEvaluateReadOnly(script, ToInnerLease(keys, out var lease), values, flags); + ArrayPool.Shared.Return(lease); // happy to only recycle on success + return result; + } public long SetAdd(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => Inner.SetAdd(ToInner(key), values, flags); diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index 5f282702b..db3ce5e7d 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -1 +1,27 @@ - \ No newline at end of file +override StackExchange.Redis.RedisKeyOrValue.Equals(object? obj) -> bool +override StackExchange.Redis.RedisKeyOrValue.GetHashCode() -> int +override StackExchange.Redis.RedisKeyOrValue.ToString() -> string! +StackExchange.Redis.IDatabase.ScriptEvaluate(string! script, StackExchange.Redis.RedisKey key, System.ReadOnlyMemory values = default(System.ReadOnlyMemory), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult! +StackExchange.Redis.IDatabase.ScriptEvaluate(string! script, System.ReadOnlyMemory keys, System.ReadOnlyMemory values = default(System.ReadOnlyMemory), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult! +StackExchange.Redis.IDatabase.ScriptEvaluateReadOnly(string! script, StackExchange.Redis.RedisKey key, System.ReadOnlyMemory values = default(System.ReadOnlyMemory), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult! +StackExchange.Redis.IDatabase.ScriptEvaluateReadOnly(string! script, System.ReadOnlyMemory keys, System.ReadOnlyMemory values = default(System.ReadOnlyMemory), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult! +StackExchange.Redis.IDatabaseAsync.ScriptEvaluateAsync(string! script, StackExchange.Redis.RedisKey key, System.ReadOnlyMemory values = default(System.ReadOnlyMemory), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.ScriptEvaluateAsync(string! script, System.ReadOnlyMemory keys, System.ReadOnlyMemory values = default(System.ReadOnlyMemory), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.ScriptEvaluateReadOnlyAsync(string! script, StackExchange.Redis.RedisKey key, System.ReadOnlyMemory values = default(System.ReadOnlyMemory), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.ScriptEvaluateReadOnlyAsync(string! script, System.ReadOnlyMemory keys, System.ReadOnlyMemory values = default(System.ReadOnlyMemory), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.RedisKeyOrValue +StackExchange.Redis.RedisKeyOrValue.AsKey() -> StackExchange.Redis.RedisKey +StackExchange.Redis.RedisKeyOrValue.AsValue() -> StackExchange.Redis.RedisValue +StackExchange.Redis.RedisKeyOrValue.Equals(StackExchange.Redis.RedisKey other) -> bool +StackExchange.Redis.RedisKeyOrValue.Equals(StackExchange.Redis.RedisKeyOrValue other) -> bool +StackExchange.Redis.RedisKeyOrValue.Equals(StackExchange.Redis.RedisValue other) -> bool +StackExchange.Redis.RedisKeyOrValue.IsKey.get -> bool +StackExchange.Redis.RedisKeyOrValue.IsValue.get -> bool +StackExchange.Redis.RedisKeyOrValue.RedisKeyOrValue() -> void +static StackExchange.Redis.RedisKey.EmptyKeys.get -> System.ReadOnlyMemory +static StackExchange.Redis.RedisKeyOrValue.explicit operator StackExchange.Redis.RedisKey(StackExchange.Redis.RedisKeyOrValue value) -> StackExchange.Redis.RedisKey +static StackExchange.Redis.RedisKeyOrValue.explicit operator StackExchange.Redis.RedisValue(StackExchange.Redis.RedisKeyOrValue value) -> StackExchange.Redis.RedisValue +static StackExchange.Redis.RedisKeyOrValue.implicit operator StackExchange.Redis.RedisKeyOrValue(StackExchange.Redis.RedisKey key) -> StackExchange.Redis.RedisKeyOrValue +static StackExchange.Redis.RedisKeyOrValue.implicit operator StackExchange.Redis.RedisKeyOrValue(StackExchange.Redis.RedisValue value) -> StackExchange.Redis.RedisKeyOrValue +static StackExchange.Redis.RedisKeyOrValue.Key(StackExchange.Redis.RedisKey key) -> StackExchange.Redis.RedisKeyOrValue +static StackExchange.Redis.RedisKeyOrValue.Value(StackExchange.Redis.RedisValue value) -> StackExchange.Redis.RedisKeyOrValue \ No newline at end of file diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 9df7ac742..53509121b 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -1514,7 +1514,7 @@ public Task ExecuteAsync(string command, ICollection? args, public RedisResult ScriptEvaluate(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) { var command = ResultProcessor.ScriptLoadProcessor.IsSHA1(script) ? RedisCommand.EVALSHA : RedisCommand.EVAL; - var msg = new ScriptEvalMessage(Database, flags, command, script, keys, values); + var msg = new ScriptEvalMultiKeyMessage(Database, flags, command, script, keys, values); try { return ExecuteSync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); @@ -1526,9 +1526,126 @@ public RedisResult ScriptEvaluate(string script, RedisKey[]? keys = null, RedisV } } + public RedisResult ScriptEvaluate(string script, RedisKey key, ReadOnlyMemory values = default, CommandFlags flags = CommandFlags.None) + { + var command = ResultProcessor.ScriptLoadProcessor.IsSHA1(script) ? RedisCommand.EVALSHA : RedisCommand.EVAL; + var msg = new ScriptEvalSingleKeyMessage(Database, flags, command, script, key, values); + try + { + return ExecuteSync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); + } + catch (RedisServerException) when (msg.IsScriptUnavailable) + { + // could be a NOSCRIPT; for a sync call, we can re-issue that without problem + return ExecuteSync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); + } + } + + public RedisResult ScriptEvaluate(string script, ReadOnlyMemory keys, ReadOnlyMemory values = default, CommandFlags flags = CommandFlags.None) + { + var command = ResultProcessor.ScriptLoadProcessor.IsSHA1(script) ? RedisCommand.EVALSHA : RedisCommand.EVAL; + var msg = new ScriptEvalMultiKeyMessage(Database, flags, command, script, keys, values); + try + { + return ExecuteSync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); + } + catch (RedisServerException) when (msg.IsScriptUnavailable) + { + // could be a NOSCRIPT; for a sync call, we can re-issue that without problem + return ExecuteSync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); + } + } + + public RedisResult ScriptEvaluateReadOnly(string script, RedisKey key, ReadOnlyMemory values = default, CommandFlags flags = CommandFlags.None) + { + var command = ResultProcessor.ScriptLoadProcessor.IsSHA1(script) ? RedisCommand.EVALSHA_RO : RedisCommand.EVAL_RO; + var msg = new ScriptEvalSingleKeyMessage(Database, flags, command, script, key, values); + try + { + return ExecuteSync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); + } + catch (RedisServerException) when (msg.IsScriptUnavailable) + { + // could be a NOSCRIPT; for a sync call, we can re-issue that without problem + return ExecuteSync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); + } + } + public RedisResult ScriptEvaluateReadOnly(string script, ReadOnlyMemory keys, ReadOnlyMemory values = default, CommandFlags flags = CommandFlags.None) + { + var command = ResultProcessor.ScriptLoadProcessor.IsSHA1(script) ? RedisCommand.EVALSHA_RO : RedisCommand.EVAL_RO; + var msg = new ScriptEvalMultiKeyMessage(Database, flags, command, script, keys, values); + try + { + return ExecuteSync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); + } + catch (RedisServerException) when (msg.IsScriptUnavailable) + { + // could be a NOSCRIPT; for a sync call, we can re-issue that without problem + return ExecuteSync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); + } + } + public async Task ScriptEvaluateAsync(string script, RedisKey key, ReadOnlyMemory values = default, CommandFlags flags = CommandFlags.None) + { + var command = ResultProcessor.ScriptLoadProcessor.IsSHA1(script) ? RedisCommand.EVALSHA : RedisCommand.EVAL; + var msg = new ScriptEvalSingleKeyMessage(Database, flags, command, script, key, values); + + try + { + return await ExecuteAsync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle).ConfigureAwait(false); + } + catch (RedisServerException) when (msg.IsScriptUnavailable) + { + // could be a NOSCRIPT; for a sync call, we can re-issue that without problem + return await ExecuteAsync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle).ConfigureAwait(false); + } + } + public async Task ScriptEvaluateAsync(string script, ReadOnlyMemory keys, ReadOnlyMemory values = default, CommandFlags flags = CommandFlags.None) + { + var command = ResultProcessor.ScriptLoadProcessor.IsSHA1(script) ? RedisCommand.EVALSHA : RedisCommand.EVAL; + var msg = new ScriptEvalMultiKeyMessage(Database, flags, command, script, keys, values); + + try + { + return await ExecuteAsync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle).ConfigureAwait(false); + } + catch (RedisServerException) when (msg.IsScriptUnavailable) + { + // could be a NOSCRIPT; for a sync call, we can re-issue that without problem + return await ExecuteAsync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle).ConfigureAwait(false); + } + } + public async Task ScriptEvaluateReadOnlyAsync(string script, RedisKey key, ReadOnlyMemory values = default, CommandFlags flags = CommandFlags.None) + { + var command = ResultProcessor.ScriptLoadProcessor.IsSHA1(script) ? RedisCommand.EVALSHA_RO : RedisCommand.EVAL_RO; + var msg = new ScriptEvalSingleKeyMessage(Database, flags, command, script, key, values); + try + { + return await ExecuteAsync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle).ConfigureAwait(false); + } + catch (RedisServerException) when (msg.IsScriptUnavailable) + { + // could be a NOSCRIPT; for a sync call, we can re-issue that without problem + return await ExecuteAsync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle).ConfigureAwait(false); + } + } + public async Task ScriptEvaluateReadOnlyAsync(string script, ReadOnlyMemory keys, ReadOnlyMemory values = default, CommandFlags flags = CommandFlags.None) + { + var command = ResultProcessor.ScriptLoadProcessor.IsSHA1(script) ? RedisCommand.EVALSHA_RO : RedisCommand.EVAL_RO; + var msg = new ScriptEvalMultiKeyMessage(Database, flags, command, script, keys, values); + try + { + return await ExecuteAsync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle).ConfigureAwait(false); + } + catch (RedisServerException) when (msg.IsScriptUnavailable) + { + // could be a NOSCRIPT; for a sync call, we can re-issue that without problem + return await ExecuteAsync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle).ConfigureAwait(false); + } + } + public RedisResult ScriptEvaluate(byte[] hash, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) { - var msg = new ScriptEvalMessage(Database, flags, RedisCommand.EVALSHA, hash, keys, values); + var msg = new ScriptEvalMultiKeyMessage(Database, flags, RedisCommand.EVALSHA, hash, keys, values); return ExecuteSync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); } @@ -1545,7 +1662,7 @@ public RedisResult ScriptEvaluate(LoadedLuaScript script, object? parameters = n public async Task ScriptEvaluateAsync(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) { var command = ResultProcessor.ScriptLoadProcessor.IsSHA1(script) ? RedisCommand.EVALSHA : RedisCommand.EVAL; - var msg = new ScriptEvalMessage(Database, flags, command, script, keys, values); + var msg = new ScriptEvalMultiKeyMessage(Database, flags, command, script, keys, values); try { @@ -1560,7 +1677,7 @@ public async Task ScriptEvaluateAsync(string script, RedisKey[]? ke public Task ScriptEvaluateAsync(byte[] hash, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) { - var msg = new ScriptEvalMessage(Database, flags, RedisCommand.EVALSHA, hash, keys, values); + var msg = new ScriptEvalMultiKeyMessage(Database, flags, RedisCommand.EVALSHA, hash, keys, values); return ExecuteAsync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); } @@ -1577,7 +1694,7 @@ public Task ScriptEvaluateAsync(LoadedLuaScript script, object? par public RedisResult ScriptEvaluateReadOnly(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) { var command = ResultProcessor.ScriptLoadProcessor.IsSHA1(script) ? RedisCommand.EVALSHA_RO : RedisCommand.EVAL_RO; - var msg = new ScriptEvalMessage(Database, flags, command, script, keys, values); + var msg = new ScriptEvalMultiKeyMessage(Database, flags, command, script, keys, values); try { return ExecuteSync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); @@ -1591,20 +1708,28 @@ public RedisResult ScriptEvaluateReadOnly(string script, RedisKey[]? keys = null public RedisResult ScriptEvaluateReadOnly(byte[] hash, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) { - var msg = new ScriptEvalMessage(Database, flags, RedisCommand.EVALSHA_RO, hash, keys, values); + var msg = new ScriptEvalMultiKeyMessage(Database, flags, RedisCommand.EVALSHA_RO, hash, keys, values); return ExecuteSync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); } - public Task ScriptEvaluateReadOnlyAsync(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) + public async Task ScriptEvaluateReadOnlyAsync(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) { var command = ResultProcessor.ScriptLoadProcessor.IsSHA1(script) ? RedisCommand.EVALSHA_RO : RedisCommand.EVAL_RO; - var msg = new ScriptEvalMessage(Database, flags, command, script, keys, values); - return ExecuteAsync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); + var msg = new ScriptEvalMultiKeyMessage(Database, flags, command, script, keys, values); + try + { + return await ExecuteAsync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle).ConfigureAwait(false); + } + catch (RedisServerException) when (msg.IsScriptUnavailable) + { + // could be a NOSCRIPT; for a sync call, we can re-issue that without problem + return await ExecuteAsync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle).ConfigureAwait(false); + } } public Task ScriptEvaluateReadOnlyAsync(byte[] hash, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) { - var msg = new ScriptEvalMessage(Database, flags, RedisCommand.EVALSHA_RO, hash, keys, values); + var msg = new ScriptEvalMultiKeyMessage(Database, flags, RedisCommand.EVALSHA_RO, hash, keys, values); return ExecuteAsync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); } @@ -4778,51 +4903,107 @@ public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) public override int ArgCount => _args.Count; } - private sealed class ScriptEvalMessage : Message, IMultiMessage + private sealed class ScriptEvalSingleKeyMessage : ScriptEvalMessage { - private readonly RedisKey[] keys; + private readonly RedisKey key; + + protected override int KeyCount => 1; + + public ScriptEvalSingleKeyMessage(int db, CommandFlags flags, RedisCommand command, string script, in RedisKey key, ReadOnlyMemory values) + : base(db, flags, command, script, values) + { + key.AssertNotNull(); + this.key = key; + } + + public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) + => serverSelectionStrategy.HashSlot(key); + + protected override void WriteKeys(PhysicalConnection physical) => physical.Write(key); + } + + private sealed class ScriptEvalMultiKeyMessage : ScriptEvalMessage + { + private readonly ReadOnlyMemory keys; + + protected override int KeyCount => keys.Length; + + + public ScriptEvalMultiKeyMessage(int db, CommandFlags flags, RedisCommand command, string script, ReadOnlyMemory keys, ReadOnlyMemory values) + : base(db, flags, command, script, values) + { + AssertNotNull(keys); + this.keys = keys; + + } + + public ScriptEvalMultiKeyMessage(int db, CommandFlags flags, RedisCommand command, byte[] hash, ReadOnlyMemory keys, ReadOnlyMemory values) + : base(db, flags, command, hash, values) + { + AssertNotNull(keys); + this.keys = keys; + } + private static void AssertNotNull(ReadOnlyMemory keys) + { + foreach (ref readonly var key in keys.Span) + { + key.AssertNotNull(); + } + } + + public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) + { + int slot = ServerSelectionStrategy.NoSlot; + foreach (ref readonly var key in keys.Span) + slot = serverSelectionStrategy.CombineSlot(slot, key); + + return slot; + } + protected override void WriteKeys(PhysicalConnection physical) + { + foreach (ref readonly var key in keys.Span) + physical.Write(key); + } + } + + private abstract class ScriptEvalMessage : Message, IMultiMessage + { + protected abstract int KeyCount { get; } + private readonly string? script; - private readonly RedisValue[] values; + + + private readonly ReadOnlyMemory values; private byte[]? asciiHash; private readonly byte[]? hexHash; - public ScriptEvalMessage(int db, CommandFlags flags, RedisCommand command, string script, RedisKey[]? keys, RedisValue[]? values) - : this(db, flags, command, script, null, keys, values) + public ScriptEvalMessage(int db, CommandFlags flags, RedisCommand command, string script, ReadOnlyMemory values) + : this(db, flags, command, script, null, values) { - if (script == null) throw new ArgumentNullException(nameof(script)); + if (script is null) throw new ArgumentNullException(nameof(script)); } - public ScriptEvalMessage(int db, CommandFlags flags, RedisCommand command, byte[] hash, RedisKey[]? keys, RedisValue[]? values) - : this(db, flags, command, null, hash, keys, values) + public ScriptEvalMessage(int db, CommandFlags flags, RedisCommand command, byte[] hash, ReadOnlyMemory values) + : this(db, flags, command, null, hash, values) { - if (hash == null) throw new ArgumentNullException(nameof(hash)); + if (hash is null) throw new ArgumentNullException(nameof(hash)); if (hash.Length != ResultProcessor.ScriptLoadProcessor.Sha1HashLength) throw new ArgumentOutOfRangeException(nameof(hash), "Invalid hash length"); } - private ScriptEvalMessage(int db, CommandFlags flags, RedisCommand command, string? script, byte[]? hexHash, RedisKey[]? keys, RedisValue[]? values) + private ScriptEvalMessage(int db, CommandFlags flags, RedisCommand command, string? script, byte[]? hexHash, ReadOnlyMemory values) : base(db, flags, command) { this.script = script; this.hexHash = hexHash; - if (keys == null) keys = Array.Empty(); - if (values == null) values = Array.Empty(); - for (int i = 0; i < keys.Length; i++) - keys[i].AssertNotNull(); - this.keys = keys; - for (int i = 0; i < values.Length; i++) - values[i].AssertNotNull(); + //foreach (ref readonly var key in keys.Span) + // key.AssertNotNull(); + //multiKeys = keys; + foreach (ref readonly var value in values.Span) + value.AssertNotNull(); this.values = values; } - public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) - { - int slot = ServerSelectionStrategy.NoSlot; - for (int i = 0; i < keys.Length; i++) - slot = serverSelectionStrategy.CombineSlot(slot, keys[i]); - return slot; - } - public IEnumerable GetMessages(PhysicalConnection connection) { PhysicalBridge? bridge; @@ -4844,30 +5025,31 @@ public IEnumerable GetMessages(PhysicalConnection connection) yield return this; } + protected abstract void WriteKeys(PhysicalConnection physical); protected override void WriteImpl(PhysicalConnection physical) { + int keyCount = KeyCount; if (hexHash != null) { - physical.WriteHeader(RedisCommand.EVALSHA, 2 + keys.Length + values.Length); + physical.WriteHeader(RedisCommand.EVALSHA, 2 + keyCount + values.Length); physical.WriteSha1AsHex(hexHash); } else if (asciiHash != null) { - physical.WriteHeader(RedisCommand.EVALSHA, 2 + keys.Length + values.Length); + physical.WriteHeader(RedisCommand.EVALSHA, 2 + keyCount + values.Length); physical.WriteBulkString((RedisValue)asciiHash); } else { - physical.WriteHeader(RedisCommand.EVAL, 2 + keys.Length + values.Length); + physical.WriteHeader(RedisCommand.EVAL, 2 + keyCount + values.Length); physical.WriteBulkString((RedisValue)script); } - physical.WriteBulkString(keys.Length); - for (int i = 0; i < keys.Length; i++) - physical.Write(keys[i]); - for (int i = 0; i < values.Length; i++) - physical.WriteBulkString(values[i]); + physical.WriteBulkString(keyCount); + WriteKeys(physical); + foreach (ref readonly var value in values.Span) + physical.WriteBulkString(value); } - public override int ArgCount => 2 + keys.Length + values.Length; + public override int ArgCount => 2 + KeyCount + values.Length; } private sealed class SetScanResultProcessor : ScanResultProcessor diff --git a/src/StackExchange.Redis/RedisKey.cs b/src/StackExchange.Redis/RedisKey.cs index 28378d116..82521ef78 100644 --- a/src/StackExchange.Redis/RedisKey.cs +++ b/src/StackExchange.Redis/RedisKey.cs @@ -42,6 +42,9 @@ internal bool IsEmpty internal byte[]? KeyPrefix { get; } internal object? KeyValue { get; } + /// Gets an empty chunk of keys as a "memory" + public static ReadOnlyMemory EmptyKeys => default; + /// /// Indicate whether two keys are not equal. /// @@ -314,7 +317,7 @@ public static implicit operator RedisKey(byte[]? key) public static RedisKey operator +(RedisKey x, RedisKey y) => new RedisKey(ConcatenateBytes(x.KeyPrefix, x.KeyValue, y.KeyPrefix), y.KeyValue); - internal static RedisKey WithPrefix(byte[]? prefix, RedisKey value) + internal static RedisKey WithPrefix(byte[]? prefix, in RedisKey value) { if (prefix == null || prefix.Length == 0) return value; if (value.KeyPrefix == null) return new RedisKey(prefix, value.KeyValue); diff --git a/src/StackExchange.Redis/RedisKeyOrValue.cs b/src/StackExchange.Redis/RedisKeyOrValue.cs new file mode 100644 index 000000000..3c1ce098f --- /dev/null +++ b/src/StackExchange.Redis/RedisKeyOrValue.cs @@ -0,0 +1,127 @@ +using System; +using System.Runtime.InteropServices; + +namespace StackExchange.Redis; + +/// +/// Represents a placeholder that could represent a redis key or a redis value +/// +public readonly struct RedisKeyOrValue : IEquatable, IEquatable, IEquatable +{ + private enum Mode + { + Empty, + Key, + Value, + } + + private readonly Mode _mode; + private readonly object? _objectOrSentinel; + private readonly ReadOnlyMemory _memory; + private readonly long _overlappedBits64; + + private RedisKeyOrValue(in RedisKey key) + { + _mode = Mode.Key; + _overlappedBits64 = 0; + _memory = key.KeyPrefix; + _objectOrSentinel = key.KeyValue; + } + + private RedisKeyOrValue(in RedisValue value) + { + _mode = Mode.Value; + _overlappedBits64 = value.DirectOverlappedBits64; + _memory = value.DirectMemory; + _objectOrSentinel = value.DirectObject; + } + + /// + public override int GetHashCode() + => _mode switch + { + Mode.Key => AsKey().GetHashCode(), + Mode.Value => AsValue().GetHashCode(), + _ => 0, + }; + + /// + public override bool Equals(object? obj) => obj switch + { + RedisKeyOrValue other => Equals(other), + RedisKey key => _mode == Mode.Key && AsKey().Equals(key), + RedisValue value => _mode == Mode.Value && AsValue().Equals(value), + _ => false, + }; + + /// + public override string ToString() => _mode switch + { + Mode.Key => AsKey().ToString(), + Mode.Value => AsValue().ToString(), + _ => _mode.ToString(), + }; + + /// Create a new instance representing a key + public static RedisKeyOrValue Key(RedisKey key) => new RedisKeyOrValue(in key); + + /// Create a new instance representing a value + public static RedisKeyOrValue Value(RedisValue value) => new RedisKeyOrValue(in value); + + /// Create a new instance representing a key + public static implicit operator RedisKeyOrValue(RedisKey key) => new RedisKeyOrValue(in key); + + /// Create a new instance representing a value + public static implicit operator RedisKeyOrValue(RedisValue value) => new RedisKeyOrValue(in value); + + /// Obtains the underlying payload as a key + public static explicit operator RedisKey(RedisKeyOrValue value) => value.AsKey(); + + /// Obtains the underlying payload as a value + public static explicit operator RedisValue(RedisKeyOrValue value) => value.AsValue(); + + /// Indicates whether this instance represents a key + public bool IsKey => _mode == Mode.Key; + + /// Indicates whether this instance represents a value + public bool IsValue => _mode == Mode.Value; + + /// Obtains the underlying payload as a key + public RedisKey AsKey() + { + AssertMode(Mode.Key); + byte[]? keyPrefix = null; + if (MemoryMarshal.TryGetArray(_memory, out var segment) && segment.Array is not null && segment.Offset == 0 && segment.Count == segment.Array.Length) + { + keyPrefix = segment.Array; + } + return new RedisKey(keyPrefix, _objectOrSentinel); + } + + /// Obtains the underlying payload as a value + public RedisValue AsValue() + { + AssertMode(Mode.Value); + return new RedisValue(_overlappedBits64, _memory, _objectOrSentinel); + } + + private void AssertMode(Mode mode) + { + if (mode != _mode) Throw(_mode); + static void Throw(Mode mode) => throw new InvalidOperationException($"Operation not valid on {mode} value"); + } + + /// + public bool Equals(RedisKeyOrValue other) => _mode switch + { + Mode.Key => other._mode == Mode.Key && AsKey().Equals(other.AsKey()), + Mode.Value => other._mode == Mode.Value && AsValue().Equals(other.AsValue()), + _ => other._mode == _mode, + }; + + /// + public bool Equals(RedisKey other) => _mode == Mode.Key && AsKey().Equals(other); + + /// + public bool Equals(RedisValue other) => _mode == Mode.Value && AsValue().Equals(other); +} diff --git a/src/StackExchange.Redis/RedisValue.cs b/src/StackExchange.Redis/RedisValue.cs index 45f23474b..261870f95 100644 --- a/src/StackExchange.Redis/RedisValue.cs +++ b/src/StackExchange.Redis/RedisValue.cs @@ -21,7 +21,7 @@ namespace StackExchange.Redis private readonly ReadOnlyMemory _memory; private readonly long _overlappedBits64; - private RedisValue(long overlappedValue64, ReadOnlyMemory memory, object? objectOrSentinel) + internal RedisValue(long overlappedValue64, ReadOnlyMemory memory, object? objectOrSentinel) { _overlappedBits64 = overlappedValue64; _memory = memory; @@ -45,6 +45,8 @@ public RedisValue(string value) : this(0, default, value) { } internal object? DirectObject => _objectOrSentinel; [System.Diagnostics.CodeAnalysis.SuppressMessage("Roslynator", "RCS1085:Use auto-implemented property.", Justification = "Intentional field ref")] internal long DirectOverlappedBits64 => _overlappedBits64; + [System.Diagnostics.CodeAnalysis.SuppressMessage("Roslynator", "RCS1085:Use auto-implemented property.", Justification = "Intentional field ref")] + internal ReadOnlyMemory DirectMemory => _memory; private static readonly object Sentinel_SignedInteger = new(); private static readonly object Sentinel_UnsignedInteger = new(); diff --git a/src/StackExchange.Redis/ScriptParameterMapper.cs b/src/StackExchange.Redis/ScriptParameterMapper.cs index 2c0e76314..4bb2e2c6d 100644 --- a/src/StackExchange.Redis/ScriptParameterMapper.cs +++ b/src/StackExchange.Redis/ScriptParameterMapper.cs @@ -224,12 +224,7 @@ public static bool IsValidParameterHash(Type t, LuaScript script, out string? mi for (var i = 0; i < script.Arguments.Length; i++) { var argName = script.Arguments[i]; - var member = t.GetMember(argName).SingleOrDefault(m => m is PropertyInfo || m is FieldInfo); - if (member is null) - { - throw new ArgumentException($"There was no member found for {argName}"); - } - + var member = t.GetMember(argName).SingleOrDefault(m => m is PropertyInfo || m is FieldInfo) ?? throw new ArgumentException($"There was no member found for {argName}"); var memberType = member is FieldInfo memberFieldInfo ? memberFieldInfo.FieldType : ((PropertyInfo)member).PropertyType; if (memberType == typeof(RedisKey)) diff --git a/tests/StackExchange.Redis.Tests/OverloadCompatTests.cs b/tests/StackExchange.Redis.Tests/OverloadCompatTests.cs index 91abbf5e9..d3f3c6e7d 100644 --- a/tests/StackExchange.Redis.Tests/OverloadCompatTests.cs +++ b/tests/StackExchange.Redis.Tests/OverloadCompatTests.cs @@ -253,4 +253,118 @@ public async Task StringSet() await db.StringSetAsync(key, val, null, When.NotExists); await db.StringSetAsync(key, val, null, When.NotExists, flags); } + + [Fact] + public async Task ScriptEvaluate() + { + using var conn = Create(); + var db = conn.GetDatabase(); + var key = Me(); + + RedisKey[] keyArr = Array.Empty(); + ReadOnlyMemory keyRom = keyArr; + + RedisValue[] valueArr= Array.Empty(); + ReadOnlyMemory valueRom = valueArr; + + const string script = "return 0"; + + // sync + + db.ScriptEvaluate(script); + db.ScriptEvaluate(script, keyArr); + db.ScriptEvaluate(script, keyRom); + // db.ScriptEvaluate(script, default); // BOOM + // db.ScriptEvaluate(script, null); // BOOM + db.ScriptEvaluate(script, (RedisKey)default); + db.ScriptEvaluate(script, (RedisKey[]?)null); + db.ScriptEvaluate(script, (ReadOnlyMemory)null); + db.ScriptEvaluate(script, (RedisKey[]?)default); + db.ScriptEvaluate(script, (ReadOnlyMemory)default); + db.ScriptEvaluate(script, (RedisKey)default); + db.ScriptEvaluate(script, default(RedisKey[]?)); + db.ScriptEvaluate(script, default(RedisKey)); + db.ScriptEvaluate(script, default(ReadOnlyMemory)); + + db.ScriptEvaluate(script, values: valueArr); + db.ScriptEvaluate(script, keyArr, values: valueArr); + db.ScriptEvaluate(script, keyRom, values: valueArr); + db.ScriptEvaluate(script, null, values: valueArr); + db.ScriptEvaluate(script, default, values: valueArr); + + db.ScriptEvaluate(script, keyArr, values: valueRom); + db.ScriptEvaluate(script, keyRom, values: valueRom); + // db.ScriptEvaluate(script, default, values: valueRom); // BOOM + // db.ScriptEvaluate(script, null, values: valueRom); // BOOM + db.ScriptEvaluate(script, (RedisKey)default, values: valueRom); + db.ScriptEvaluate(script, (RedisKey[]?)null, values: valueRom); + db.ScriptEvaluate(script, (ReadOnlyMemory)null, values: valueRom); + db.ScriptEvaluate(script, (RedisKey[]?)default, values: valueRom); + db.ScriptEvaluate(script, (ReadOnlyMemory)default, values: valueRom); + db.ScriptEvaluate(script, (RedisKey)default, values: valueRom); + db.ScriptEvaluate(script, default(RedisKey[]?), values: valueRom); + db.ScriptEvaluate(script, default(RedisKey), values: valueRom); + db.ScriptEvaluate(script, default(ReadOnlyMemory), values: valueRom); + + db.ScriptEvaluate(script, values: null); + db.ScriptEvaluate(script, keyArr, values: null); + db.ScriptEvaluate(script, keyRom, values: null); + db.ScriptEvaluate(script, null, values: null); + db.ScriptEvaluate(script, default, values: null); + + db.ScriptEvaluate(script, values: default); + db.ScriptEvaluate(script, keyArr, values: default); + db.ScriptEvaluate(script, keyRom, values: default); + db.ScriptEvaluate(script, null, values: default); + db.ScriptEvaluate(script, default, values: default); + + // async + + await db.ScriptEvaluateAsync(script); + await db.ScriptEvaluateAsync(script, keyArr); + await db.ScriptEvaluateAsync(script, keyRom); + // await db.ScriptEvaluateAsync(script, default); // BOOM + // await db.ScriptEvaluateAsync(script, null); // BOOM + await db.ScriptEvaluateAsync(script, (RedisKey)default); + await db.ScriptEvaluateAsync(script, (RedisKey[]?)null); + await db.ScriptEvaluateAsync(script, (ReadOnlyMemory)null); + await db.ScriptEvaluateAsync(script, (RedisKey[]?)default); + await db.ScriptEvaluateAsync(script, (ReadOnlyMemory)default); + await db.ScriptEvaluateAsync(script, (RedisKey)default); + await db.ScriptEvaluateAsync(script, default(RedisKey[]?)); + await db.ScriptEvaluateAsync(script, default(RedisKey)); + await db.ScriptEvaluateAsync(script, default(ReadOnlyMemory)); + + await db.ScriptEvaluateAsync(script, values: valueArr); + await db.ScriptEvaluateAsync(script, keyArr, values: valueArr); + await db.ScriptEvaluateAsync(script, keyRom, values: valueArr); + await db.ScriptEvaluateAsync(script, null, values: valueArr); + await db.ScriptEvaluateAsync(script, default, values: valueArr); + + await db.ScriptEvaluateAsync(script, keyArr, values: valueRom); + await db.ScriptEvaluateAsync(script, keyRom, values: valueRom); + // await db.ScriptEvaluateAsync(script, default, values: valueRom); // BOOM + // await db.ScriptEvaluateAsync(script, null, values: valueRom); // BOOM + await db.ScriptEvaluateAsync(script, (RedisKey)default, values: valueRom); + await db.ScriptEvaluateAsync(script, (RedisKey[]?)null, values: valueRom); + await db.ScriptEvaluateAsync(script, (ReadOnlyMemory)null, values: valueRom); + await db.ScriptEvaluateAsync(script, (RedisKey[]?)default, values: valueRom); + await db.ScriptEvaluateAsync(script, (ReadOnlyMemory)default, values: valueRom); + await db.ScriptEvaluateAsync(script, (RedisKey)default, values: valueRom); + await db.ScriptEvaluateAsync(script, default(RedisKey[]?), values: valueRom); + await db.ScriptEvaluateAsync(script, default(RedisKey), values: valueRom); + await db.ScriptEvaluateAsync(script, default(ReadOnlyMemory), values: valueRom); + + await db.ScriptEvaluateAsync(script, values: null); + await db.ScriptEvaluateAsync(script, keyArr, values: null); + await db.ScriptEvaluateAsync(script, keyRom, values: null); + await db.ScriptEvaluateAsync(script, null, values: null); + await db.ScriptEvaluateAsync(script, default, values: null); + + await db.ScriptEvaluateAsync(script, values: default); + await db.ScriptEvaluateAsync(script, keyArr, values: default); + await db.ScriptEvaluateAsync(script, keyRom, values: default); + await db.ScriptEvaluateAsync(script, null, values: default); + await db.ScriptEvaluateAsync(script, default, values: default); + } }