mirror of https://github.com/abpframework/abp.git
4 changed files with 772 additions and 23 deletions
@ -0,0 +1,496 @@ |
|||||
|
// This software is part of the DOTNET extensions
|
||||
|
// Copyright (c) .NET Foundation and Contributors
|
||||
|
// https://dotnet.microsoft.com/
|
||||
|
//
|
||||
|
// All rights reserved.
|
||||
|
//
|
||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||
|
// in the Software without restriction, including without limitation the rights
|
||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||
|
// furnished to do so, subject to the following conditions:
|
||||
|
//
|
||||
|
// The above copyright notice and this permission notice shall be included in all
|
||||
|
// copies or substantial portions of the Software.
|
||||
|
//
|
||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
|
// SOFTWARE.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.Extensions.Caching.Distributed; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using StackExchange.Redis; |
||||
|
|
||||
|
namespace Microsoft.Extensions.Caching.StackExchangeRedis |
||||
|
{ |
||||
|
public class RedisCache : IDistributedCache, IDisposable |
||||
|
{ |
||||
|
// KEYS[1] = = key
|
||||
|
// ARGV[1] = absolute-expiration - ticks as long (-1 for none)
|
||||
|
// ARGV[2] = sliding-expiration - ticks as long (-1 for none)
|
||||
|
// ARGV[3] = relative-expiration (long, in seconds, -1 for none) - Min(absolute-expiration - Now, sliding-expiration)
|
||||
|
// ARGV[4] = data - byte[]
|
||||
|
// this order should not change LUA script depends on it
|
||||
|
protected const string SetScript = (@"
|
||||
|
redis.call('HMSET', KEYS[1], 'absexp', ARGV[1], 'sldexp', ARGV[2], 'data', ARGV[4]) |
||||
|
if ARGV[3] ~= '-1' then |
||||
|
redis.call('EXPIRE', KEYS[1], ARGV[3]) |
||||
|
end |
||||
|
return 1");
|
||||
|
|
||||
|
protected const string AbsoluteExpirationKey = "absexp"; |
||||
|
protected const string SlidingExpirationKey = "sldexp"; |
||||
|
protected const string DataKey = "data"; |
||||
|
protected const long NotPresent = -1; |
||||
|
|
||||
|
protected volatile ConnectionMultiplexer Connection; |
||||
|
protected IDatabase Cache; |
||||
|
|
||||
|
protected readonly RedisCacheOptions Options; |
||||
|
protected readonly string Instance; |
||||
|
|
||||
|
protected readonly SemaphoreSlim ConnectionLock = new SemaphoreSlim(initialCount: 1, maxCount: 1); |
||||
|
|
||||
|
public RedisCache(IOptions<RedisCacheOptions> optionsAccessor) |
||||
|
{ |
||||
|
if (optionsAccessor == null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(optionsAccessor)); |
||||
|
} |
||||
|
|
||||
|
Options = optionsAccessor.Value; |
||||
|
|
||||
|
// This allows partitioning a single backend cache for use with multiple apps/services.
|
||||
|
Instance = Options.InstanceName ?? string.Empty; |
||||
|
} |
||||
|
|
||||
|
public virtual byte[] Get(string key) |
||||
|
{ |
||||
|
if (key == null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(key)); |
||||
|
} |
||||
|
|
||||
|
return GetAndRefresh(key, getData: true); |
||||
|
} |
||||
|
|
||||
|
public virtual async Task<byte[]> GetAsync( |
||||
|
string key, |
||||
|
CancellationToken token = default) |
||||
|
{ |
||||
|
if (key == null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(key)); |
||||
|
} |
||||
|
|
||||
|
token.ThrowIfCancellationRequested(); |
||||
|
|
||||
|
return await GetAndRefreshAsync(key, getData: true, token: token); |
||||
|
} |
||||
|
|
||||
|
public virtual void Set( |
||||
|
string key, |
||||
|
byte[] value, |
||||
|
DistributedCacheEntryOptions options) |
||||
|
{ |
||||
|
if (key == null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(key)); |
||||
|
} |
||||
|
|
||||
|
if (value == null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(value)); |
||||
|
} |
||||
|
|
||||
|
if (options == null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(options)); |
||||
|
} |
||||
|
|
||||
|
Connect(); |
||||
|
|
||||
|
var creationTime = DateTimeOffset.UtcNow; |
||||
|
|
||||
|
var absoluteExpiration = GetAbsoluteExpiration(creationTime, options); |
||||
|
|
||||
|
Cache.ScriptEvaluate(SetScript, new RedisKey[] {Instance + key}, |
||||
|
new RedisValue[] |
||||
|
{ |
||||
|
absoluteExpiration?.Ticks ?? NotPresent, |
||||
|
options.SlidingExpiration?.Ticks ?? NotPresent, |
||||
|
GetExpirationInSeconds(creationTime, absoluteExpiration, options) ?? NotPresent, |
||||
|
value |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public virtual async Task SetAsync( |
||||
|
string key, |
||||
|
byte[] value, |
||||
|
DistributedCacheEntryOptions options, |
||||
|
CancellationToken token = default) |
||||
|
{ |
||||
|
if (key == null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(key)); |
||||
|
} |
||||
|
|
||||
|
if (value == null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(value)); |
||||
|
} |
||||
|
|
||||
|
if (options == null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(options)); |
||||
|
} |
||||
|
|
||||
|
token.ThrowIfCancellationRequested(); |
||||
|
|
||||
|
await ConnectAsync(token); |
||||
|
|
||||
|
var creationTime = DateTimeOffset.UtcNow; |
||||
|
|
||||
|
var absoluteExpiration = GetAbsoluteExpiration(creationTime, options); |
||||
|
|
||||
|
await Cache.ScriptEvaluateAsync(SetScript, new RedisKey[] {Instance + key}, |
||||
|
new RedisValue[] |
||||
|
{ |
||||
|
absoluteExpiration?.Ticks ?? NotPresent, |
||||
|
options.SlidingExpiration?.Ticks ?? NotPresent, |
||||
|
GetExpirationInSeconds(creationTime, absoluteExpiration, options) ?? NotPresent, |
||||
|
value |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public virtual void Refresh(string key) |
||||
|
{ |
||||
|
if (key == null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(key)); |
||||
|
} |
||||
|
|
||||
|
GetAndRefresh(key, getData: false); |
||||
|
} |
||||
|
|
||||
|
public virtual async Task RefreshAsync( |
||||
|
string key, |
||||
|
CancellationToken token = default) |
||||
|
{ |
||||
|
if (key == null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(key)); |
||||
|
} |
||||
|
|
||||
|
token.ThrowIfCancellationRequested(); |
||||
|
|
||||
|
await GetAndRefreshAsync(key, getData: false, token: token); |
||||
|
} |
||||
|
|
||||
|
protected virtual void Connect() |
||||
|
{ |
||||
|
if (Cache != null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
ConnectionLock.Wait(); |
||||
|
try |
||||
|
{ |
||||
|
if (Cache == null) |
||||
|
{ |
||||
|
if (Options.ConfigurationOptions != null) |
||||
|
{ |
||||
|
Connection = ConnectionMultiplexer.Connect(Options.ConfigurationOptions); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
Connection = ConnectionMultiplexer.Connect(Options.Configuration); |
||||
|
} |
||||
|
|
||||
|
Cache = Connection.GetDatabase(); |
||||
|
} |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
ConnectionLock.Release(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected virtual async Task ConnectAsync(CancellationToken token = default) |
||||
|
{ |
||||
|
token.ThrowIfCancellationRequested(); |
||||
|
|
||||
|
if (Cache != null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
await ConnectionLock.WaitAsync(token); |
||||
|
try |
||||
|
{ |
||||
|
if (Cache == null) |
||||
|
{ |
||||
|
if (Options.ConfigurationOptions != null) |
||||
|
{ |
||||
|
Connection = await ConnectionMultiplexer.ConnectAsync(Options.ConfigurationOptions); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
Connection = await ConnectionMultiplexer.ConnectAsync(Options.Configuration); |
||||
|
} |
||||
|
|
||||
|
Cache = Connection.GetDatabase(); |
||||
|
} |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
ConnectionLock.Release(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected virtual byte[] GetAndRefresh(string key, bool getData) |
||||
|
{ |
||||
|
if (key == null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(key)); |
||||
|
} |
||||
|
|
||||
|
Connect(); |
||||
|
|
||||
|
// This also resets the LRU status as desired.
|
||||
|
// TODO: Can this be done in one operation on the server side? Probably, the trick would just be the DateTimeOffset math.
|
||||
|
RedisValue[] results; |
||||
|
if (getData) |
||||
|
{ |
||||
|
results = Cache.HashMemberGet(Instance + key, AbsoluteExpirationKey, SlidingExpirationKey, DataKey); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
results = Cache.HashMemberGet(Instance + key, AbsoluteExpirationKey, SlidingExpirationKey); |
||||
|
} |
||||
|
|
||||
|
// TODO: Error handling
|
||||
|
if (results.Length >= 2) |
||||
|
{ |
||||
|
MapMetadata(results, out DateTimeOffset? absExpr, out TimeSpan? sldExpr); |
||||
|
Refresh(key, absExpr, sldExpr); |
||||
|
} |
||||
|
|
||||
|
if (results.Length >= 3 && results[2].HasValue) |
||||
|
{ |
||||
|
return results[2]; |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
protected virtual async Task<byte[]> GetAndRefreshAsync( |
||||
|
string key, |
||||
|
bool getData, |
||||
|
CancellationToken token = default) |
||||
|
{ |
||||
|
if (key == null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(key)); |
||||
|
} |
||||
|
|
||||
|
token.ThrowIfCancellationRequested(); |
||||
|
|
||||
|
await ConnectAsync(token); |
||||
|
|
||||
|
// This also resets the LRU status as desired.
|
||||
|
// TODO: Can this be done in one operation on the server side? Probably, the trick would just be the DateTimeOffset math.
|
||||
|
RedisValue[] results; |
||||
|
if (getData) |
||||
|
{ |
||||
|
results = await Cache.HashMemberGetAsync(Instance + key, AbsoluteExpirationKey, SlidingExpirationKey, |
||||
|
DataKey); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
results = await Cache.HashMemberGetAsync(Instance + key, AbsoluteExpirationKey, SlidingExpirationKey); |
||||
|
} |
||||
|
|
||||
|
// TODO: Error handling
|
||||
|
if (results.Length >= 2) |
||||
|
{ |
||||
|
MapMetadata(results, out DateTimeOffset? absExpr, out TimeSpan? sldExpr); |
||||
|
await RefreshAsync(key, absExpr, sldExpr, token); |
||||
|
} |
||||
|
|
||||
|
if (results.Length >= 3 && results[2].HasValue) |
||||
|
{ |
||||
|
return results[2]; |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
public virtual void Remove(string key) |
||||
|
{ |
||||
|
if (key == null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(key)); |
||||
|
} |
||||
|
|
||||
|
Connect(); |
||||
|
|
||||
|
Cache.KeyDelete(Instance + key); |
||||
|
// TODO: Error handling
|
||||
|
} |
||||
|
|
||||
|
public virtual async Task RemoveAsync( |
||||
|
string key, |
||||
|
CancellationToken token = default) |
||||
|
{ |
||||
|
if (key == null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(key)); |
||||
|
} |
||||
|
|
||||
|
await ConnectAsync(token); |
||||
|
|
||||
|
await Cache.KeyDeleteAsync(Instance + key); |
||||
|
// TODO: Error handling
|
||||
|
} |
||||
|
|
||||
|
protected virtual void MapMetadata( |
||||
|
RedisValue[] results, |
||||
|
out DateTimeOffset? absoluteExpiration, |
||||
|
out TimeSpan? slidingExpiration) |
||||
|
{ |
||||
|
absoluteExpiration = null; |
||||
|
slidingExpiration = null; |
||||
|
var absoluteExpirationTicks = (long?) results[0]; |
||||
|
if (absoluteExpirationTicks.HasValue && absoluteExpirationTicks.Value != NotPresent) |
||||
|
{ |
||||
|
absoluteExpiration = new DateTimeOffset(absoluteExpirationTicks.Value, TimeSpan.Zero); |
||||
|
} |
||||
|
|
||||
|
var slidingExpirationTicks = (long?) results[1]; |
||||
|
if (slidingExpirationTicks.HasValue && slidingExpirationTicks.Value != NotPresent) |
||||
|
{ |
||||
|
slidingExpiration = new TimeSpan(slidingExpirationTicks.Value); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected virtual void Refresh( |
||||
|
string key, |
||||
|
DateTimeOffset? absExpr, |
||||
|
TimeSpan? sldExpr) |
||||
|
{ |
||||
|
if (key == null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(key)); |
||||
|
} |
||||
|
|
||||
|
// Note Refresh has no effect if there is just an absolute expiration (or neither).
|
||||
|
TimeSpan? expr = null; |
||||
|
if (sldExpr.HasValue) |
||||
|
{ |
||||
|
if (absExpr.HasValue) |
||||
|
{ |
||||
|
var relExpr = absExpr.Value - DateTimeOffset.Now; |
||||
|
expr = relExpr <= sldExpr.Value ? relExpr : sldExpr; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
expr = sldExpr; |
||||
|
} |
||||
|
|
||||
|
Cache.KeyExpire(Instance + key, expr); |
||||
|
// TODO: Error handling
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected virtual async Task RefreshAsync( |
||||
|
string key, |
||||
|
DateTimeOffset? absExpr, |
||||
|
TimeSpan? sldExpr, |
||||
|
CancellationToken token = default) |
||||
|
{ |
||||
|
if (key == null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(key)); |
||||
|
} |
||||
|
|
||||
|
token.ThrowIfCancellationRequested(); |
||||
|
|
||||
|
// Note Refresh has no effect if there is just an absolute expiration (or neither).
|
||||
|
TimeSpan? expr = null; |
||||
|
if (sldExpr.HasValue) |
||||
|
{ |
||||
|
if (absExpr.HasValue) |
||||
|
{ |
||||
|
var relExpr = absExpr.Value - DateTimeOffset.Now; |
||||
|
expr = relExpr <= sldExpr.Value ? relExpr : sldExpr; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
expr = sldExpr; |
||||
|
} |
||||
|
|
||||
|
await Cache.KeyExpireAsync(Instance + key, expr); |
||||
|
// TODO: Error handling
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected static long? GetExpirationInSeconds( |
||||
|
DateTimeOffset creationTime, |
||||
|
DateTimeOffset? absoluteExpiration, |
||||
|
DistributedCacheEntryOptions options) |
||||
|
{ |
||||
|
if (absoluteExpiration.HasValue && options.SlidingExpiration.HasValue) |
||||
|
{ |
||||
|
return (long) Math.Min( |
||||
|
(absoluteExpiration.Value - creationTime).TotalSeconds, |
||||
|
options.SlidingExpiration.Value.TotalSeconds); |
||||
|
} |
||||
|
else if (absoluteExpiration.HasValue) |
||||
|
{ |
||||
|
return (long) (absoluteExpiration.Value - creationTime).TotalSeconds; |
||||
|
} |
||||
|
else if (options.SlidingExpiration.HasValue) |
||||
|
{ |
||||
|
return (long) options.SlidingExpiration.Value.TotalSeconds; |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
protected static DateTimeOffset? GetAbsoluteExpiration( |
||||
|
DateTimeOffset creationTime, |
||||
|
DistributedCacheEntryOptions options) |
||||
|
{ |
||||
|
if (options.AbsoluteExpiration.HasValue && options.AbsoluteExpiration <= creationTime) |
||||
|
{ |
||||
|
throw new ArgumentOutOfRangeException( |
||||
|
nameof(DistributedCacheEntryOptions.AbsoluteExpiration), |
||||
|
options.AbsoluteExpiration.Value, |
||||
|
"The absolute expiration value must be in the future."); |
||||
|
} |
||||
|
|
||||
|
var absoluteExpiration = options.AbsoluteExpiration; |
||||
|
if (options.AbsoluteExpirationRelativeToNow.HasValue) |
||||
|
{ |
||||
|
absoluteExpiration = creationTime + options.AbsoluteExpirationRelativeToNow; |
||||
|
} |
||||
|
|
||||
|
return absoluteExpiration; |
||||
|
} |
||||
|
|
||||
|
public virtual void Dispose() |
||||
|
{ |
||||
|
Connection?.Close(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,70 @@ |
|||||
|
// This software is part of the DOTNET extensions
|
||||
|
// Copyright (c) .NET Foundation and Contributors
|
||||
|
// https://dotnet.microsoft.com/
|
||||
|
//
|
||||
|
// All rights reserved.
|
||||
|
//
|
||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||
|
// in the Software without restriction, including without limitation the rights
|
||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||
|
// furnished to do so, subject to the following conditions:
|
||||
|
//
|
||||
|
// The above copyright notice and this permission notice shall be included in all
|
||||
|
// copies or substantial portions of the Software.
|
||||
|
//
|
||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
|
// SOFTWARE.
|
||||
|
|
||||
|
using System.Threading.Tasks; |
||||
|
using StackExchange.Redis; |
||||
|
|
||||
|
namespace Microsoft.Extensions.Caching.StackExchangeRedis |
||||
|
{ |
||||
|
internal static class RedisExtensions |
||||
|
{ |
||||
|
private const string HmGetScript = (@"return redis.call('HMGET', KEYS[1], unpack(ARGV))"); |
||||
|
|
||||
|
internal static RedisValue[] HashMemberGet(this IDatabase cache, string key, params string[] members) |
||||
|
{ |
||||
|
var result = cache.ScriptEvaluate( |
||||
|
HmGetScript, |
||||
|
new RedisKey[] { key }, |
||||
|
GetRedisMembers(members)); |
||||
|
|
||||
|
// TODO: Error checking?
|
||||
|
return (RedisValue[])result; |
||||
|
} |
||||
|
|
||||
|
internal static async Task<RedisValue[]> HashMemberGetAsync( |
||||
|
this IDatabase cache, |
||||
|
string key, |
||||
|
params string[] members) |
||||
|
{ |
||||
|
var result = await cache.ScriptEvaluateAsync( |
||||
|
HmGetScript, |
||||
|
new RedisKey[] { key }, |
||||
|
GetRedisMembers(members)).ConfigureAwait(false); |
||||
|
|
||||
|
// TODO: Error checking?
|
||||
|
return (RedisValue[])result; |
||||
|
} |
||||
|
|
||||
|
private static RedisValue[] GetRedisMembers(params string[] members) |
||||
|
{ |
||||
|
var redisMembers = new RedisValue[members.Length]; |
||||
|
for (int i = 0; i < members.Length; i++) |
||||
|
{ |
||||
|
redisMembers[i] = (RedisValue)members[i]; |
||||
|
} |
||||
|
|
||||
|
return redisMembers; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,42 @@ |
|||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using StackExchange.Redis; |
||||
|
|
||||
|
namespace Volo.Abp.Caching.StackExchangeRedis |
||||
|
{ |
||||
|
public static class AbpRedisExtensions |
||||
|
{ |
||||
|
private const string HmGetManyScript = (@"
|
||||
|
local res = {}; |
||||
|
for i , key in ipairs(KEYS) do |
||||
|
res[#res+1] = redis.call('HMGET', key,unpack(ARGV)) |
||||
|
end |
||||
|
return res");
|
||||
|
|
||||
|
public static RedisValue[][] HashMemberGetMany( |
||||
|
this IDatabase cache, |
||||
|
string[] keys, |
||||
|
params string[] members) |
||||
|
{ |
||||
|
var results = cache.ScriptEvaluate( |
||||
|
HmGetManyScript, |
||||
|
keys.Select(key => (RedisKey) key).ToArray(), |
||||
|
members.Select(member => (RedisValue) member).ToArray()); |
||||
|
|
||||
|
return ((RedisResult[]) results).Select(result => (RedisValue[]) result).ToArray(); |
||||
|
} |
||||
|
|
||||
|
public static async Task<RedisValue[][]> HashMemberGetManyAsync( |
||||
|
this IDatabase cache, |
||||
|
string[] keys, |
||||
|
params string[] members) |
||||
|
{ |
||||
|
var results = await cache.ScriptEvaluateAsync( |
||||
|
HmGetManyScript, |
||||
|
keys.Select(key => (RedisKey) key).ToArray(), |
||||
|
members.Select(member => (RedisValue) member).ToArray()); |
||||
|
|
||||
|
return ((RedisResult[]) results).Select(result => (RedisValue[]) result).ToArray(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue