From fc1086b22d33f25de1c34411086b3c70b893021c Mon Sep 17 00:00:00 2001 From: claudiamurialdo Date: Mon, 20 Oct 2025 19:35:17 -0300 Subject: [PATCH 1/6] Add local memory cache to Redis implementation to reduce roundtrips (cherry picked from commit 2b922f6043a895fc68c7b0860400ff8ad53dab60) # Conflicts: # dotnet/src/dotnetcore/Providers/Cache/GxRedis/GxRedis.csproj --- .../Services/Session/GXSessionFactory.cs | 6 +- .../Providers/Cache/GxRedis/GxRedis.cs | 433 ++++++++++++++++++ .../Providers/Cache/GxRedis/GxRedis.csproj | 10 +- .../GxClasses/Services/Storage/GXServices.cs | 3 + 4 files changed, 442 insertions(+), 10 deletions(-) create mode 100644 dotnet/src/dotnetcore/Providers/Cache/GxRedis/GxRedis.cs diff --git a/dotnet/src/dotnetcore/GxClasses/Services/Session/GXSessionFactory.cs b/dotnet/src/dotnetcore/GxClasses/Services/Session/GXSessionFactory.cs index 5a480a1a6..6c91f80a6 100644 --- a/dotnet/src/dotnetcore/GxClasses/Services/Session/GXSessionFactory.cs +++ b/dotnet/src/dotnetcore/GxClasses/Services/Session/GXSessionFactory.cs @@ -13,8 +13,6 @@ public class GXSessionServiceFactory private static readonly IGXLogger log = GXLoggerFactory.GetLogger(); static ISessionService sessionService; - static string REDIS = "REDIS"; - static string DATABASE = "DATABASE"; public static ISessionService GetProvider() { if (sessionService != null) @@ -29,9 +27,9 @@ public static ISessionService GetProvider() //Compatibility if (string.IsNullOrEmpty(className)) { - if (providerService.Name.Equals(REDIS, StringComparison.OrdinalIgnoreCase)) + if (providerService.Name.Equals(GXServices.REDIS_CACHE_SERVICE, StringComparison.OrdinalIgnoreCase)) type = typeof(GxRedisSession); - else if (providerService.Name.Equals(DATABASE, StringComparison.OrdinalIgnoreCase)) + else if (providerService.Name.Equals(GXServices.DATABASE_CACHE_SERVICE, StringComparison.OrdinalIgnoreCase)) type = typeof(GxDatabaseSession); } else diff --git a/dotnet/src/dotnetcore/Providers/Cache/GxRedis/GxRedis.cs b/dotnet/src/dotnetcore/Providers/Cache/GxRedis/GxRedis.cs new file mode 100644 index 000000000..efe1d4004 --- /dev/null +++ b/dotnet/src/dotnetcore/Providers/Cache/GxRedis/GxRedis.cs @@ -0,0 +1,433 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using GeneXus.Application; +using GeneXus.Configuration; +using GeneXus.Encryption; +using GeneXus.Services; +using GeneXus.Utils; +using GxClasses.Helpers; +using Microsoft.Extensions.Caching.Memory; +using StackExchange.Redis; +using StackExchange.Redis.KeyspaceIsolation; + +namespace GeneXus.Cache +{ + public sealed class Redis : ICacheService2 + { + private static readonly IGXLogger log = GXLoggerFactory.GetLogger(); + + ConnectionMultiplexer _redisConnection; + IDatabase _redisDatabase; + bool _multitenant; + MemoryCache _localCache; + private const double DEFAULT_LOCAL_CACHE_FACTOR = 0.8; + private static readonly TimeSpan LOCAL_CACHE_PERSISTENT_KEY_TTL = TimeSpan.FromMinutes(5); + ConfigurationOptions _redisConnectionOptions; + private const int REDIS_DEFAULT_PORT = 6379; + public int redisSessionTimeout; + private string _instanceName; + + + public Redis(string connectionString) + { + _redisConnectionOptions = ConfigurationOptions.Parse(connectionString); + _redisConnectionOptions.AllowAdmin = true; + InitLocalCache(); + } + + public Redis(string connectionString, int sessionTimeout):this(connectionString) + { + redisSessionTimeout = sessionTimeout; + } + public Redis() + { + GXService providerService = ServiceFactory.GetGXServices()?.Get(GXServices.CACHE_SERVICE); + if (providerService != null) + { + string address, password; + address = providerService.Properties.Get("CACHE_PROVIDER_ADDRESS"); + password = providerService.Properties.Get("CACHE_PROVIDER_PASSWORD"); + _instanceName = providerService.Properties.Get("CACHE_PROVIDER_INSTANCE_NAME"); + + if (!string.IsNullOrEmpty(password)) + { + string ret = string.Empty; + if (CryptoImpl.Decrypt(ref ret, password)) + { + password = ret; + } + } + + if (string.IsNullOrEmpty(address)) + address = String.Format("localhost:{0}", REDIS_DEFAULT_PORT); + + if (!string.IsNullOrEmpty(password)) + { + if (!address.Contains(':')) + { + address = $"{address}:{REDIS_DEFAULT_PORT}"; + } + address = string.Format("{0},password={1}", address.Trim(), password.Trim()); + _redisConnectionOptions = ConfigurationOptions.Parse(address); + } + else + { + _redisConnectionOptions = ConfigurationOptions.Parse(address); + } + _redisConnectionOptions.AllowAdmin = true; + + InitLocalCache(); + + } + } + + private void InitLocalCache() + { + if (EnvVarReader.GetEnvironmentValue(GXServices.CACHE_SERVICE, GXServices.REDIS_CACHE_SERVICE, "HYBRID", out string envVarValue) && envVarValue.Equals(true.ToString(), StringComparison.OrdinalIgnoreCase)) + { + GXLogging.Debug(log, "Using Redis Hybrid mode with local memory cache."); + _localCache = new MemoryCache(new MemoryCacheOptions()); + } + else + { + GXLogging.Debug(log, "Using Redis only mode without local memory cache."); + } + } + + IDatabase RedisDatabase + { + get + { + if (_redisDatabase == null) + { + _redisConnection = ConnectionMultiplexer.Connect(_redisConnectionOptions); + IDatabase db = _redisConnection.GetDatabase(); + + if (!string.IsNullOrEmpty(_instanceName)) + { + if (_instanceName == CacheFactory.SUBDOMAIN) + { + _multitenant = true; + GXLogging.Debug(log, "Using Redis multitenant (key prefix):" + CacheFactory.SUBDOMAIN); + _redisDatabase = db; + } + else + { + string prefixKey = _instanceName.EndsWith(":") ? _instanceName : _instanceName + ":"; + _redisDatabase = db.WithKeyPrefix(_instanceName); + GXLogging.Debug(log, "Using Redis instance name (key prefix): " + prefixKey); + } + } + else + { + _redisDatabase = db; + } + } + return _redisDatabase; + } + } + public void Clear(string cacheid, string key) + { + ClearKey(Key(cacheid, key)); + } + + public void ClearKey(string key) + { + RedisDatabase.KeyDelete(key); + ClearKeyLocal(key); + } + public void ClearCache(string cacheid) + { + Nullable prefix = new Nullable(KeyPrefix(cacheid).Value + 1); + RedisDatabase.StringSet(cacheid, prefix); + SetPersistentLocal(cacheid, prefix); + } + + + public void ClearAllCaches() + { + IConnectionMultiplexer multiplexer = RedisDatabase.Multiplexer; + System.Net.EndPoint[] endpoints = multiplexer.GetEndPoints(true); + foreach (var endpoint in endpoints) + { + var server = multiplexer.GetServer(endpoint); + server.FlushAllDatabases(); + } + ClearAllCachesLocal(); + } + + public bool KeyExpire(string cacheid, string key, TimeSpan expiry, CommandFlags flags = CommandFlags.None) + { + string fullKey = Key(cacheid, key); + bool expirationSaved = RedisDatabase.KeyExpire(fullKey, expiry, flags); + if (expirationSaved) + KeyExpireLocal(fullKey); + return expirationSaved; + } + + public bool KeyExists(string cacheid, string key) + { + string fullKey = Key(cacheid, key); + + if (KeyExistsLocal(fullKey)) + { + GXLogging.Debug(log, $"KeyExists hit local cache {fullKey}"); + return true; + } + + return RedisDatabase.KeyExists(fullKey); + } + + private bool Get(string key, out T value) + { + if (GetLocal(key, out value)) + { + GXLogging.Debug(log, $"Get hit local cache {key}"); + return true; + } + + if (default(T) == null) + { + value = Deserialize(RedisDatabase.StringGet(key)); + if (value == null) + GXLogging.Debug(log, "Get, misses key '" + key + "'"); + else + SetLocal(key, value); + return value != null; + } + else + { + if (RedisDatabase.KeyExists(key)) + { + value = Deserialize(RedisDatabase.StringGet(key)); + SetLocal(key, value); + return true; + } + else + { + GXLogging.Debug(log, "Get, misses key '" + key + "'"); + value = default(T); + return false; + } + } + } + + public IDictionary GetAll(string cacheid, IEnumerable keys) + { + if (keys == null) return null; + + var results = new Dictionary(); + var keysToFetch = new List(); + + foreach (string k in keys) + { + string fullKey = Key(cacheid, k); + if (GetLocal(fullKey, out T value)) + { + GXLogging.Debug(log, $"Get hit local cache {fullKey}"); + results[k] = value; + } + else + { + keysToFetch.Add(k); + } + } + + if (keysToFetch.Count > 0) + { + var prefixedKeys = Key(cacheid, keysToFetch); + RedisValue[] values = RedisDatabase.StringGet(prefixedKeys.ToArray()); + + int i = 0; + foreach (string k in keysToFetch) + { + string fullKey = Key(cacheid, k); + T value = Deserialize(values[i]); + results[k] = value; + + SetLocal(fullKey, value); + i++; + } + } + + return results; + } + public void SetAll(string cacheid, IEnumerable keys, IEnumerable values, int duration = 0) + { + if (keys == null || values == null || keys.Count() != values.Count()) + return; + + IEnumerable prefixedKeys = Key(cacheid, keys); + IEnumerator valuesEnumerator = values.GetEnumerator(); + KeyValuePair[] redisBatch = new KeyValuePair[prefixedKeys.Count()]; + + int i = 0; + foreach (RedisKey redisKey in prefixedKeys) + { + if (valuesEnumerator.MoveNext()) + { + T value = valuesEnumerator.Current; + redisBatch[i] = new KeyValuePair(redisKey, Serialize(value)); + SetLocal(redisKey.ToString(), value, duration); + } + i++; + } + if (redisBatch.Length > 0) + { + if (duration > 0) + { + foreach (var pair in redisBatch) + RedisDatabase.StringSet(pair.Key, pair.Value, TimeSpan.FromMinutes(duration)); + } + else + { + RedisDatabase.StringSet(redisBatch); + } + } + } + + private void Set(string key, T value, int duration) + { + GXLogging.Debug(log, "Set key:" + key + " value " + value + " valuetype:" + value.GetType()); + if (duration > 0) + RedisDatabase.StringSet(key, Serialize(value), TimeSpan.FromMinutes(duration)); + else + RedisDatabase.StringSet(key, Serialize(value)); + + SetLocal(key, value, duration); + } + private void Set(string key, T value) + { + Set(key, value, 0); + } + public bool Get(string cacheid, string key, out T value) + { + GXLogging.Debug(log, "Get cacheid:" + cacheid + " key:" + key); + return Get(Key(cacheid, key), out value); + } + + public void Set(string cacheid, string key, T value) + { + Set(Key(cacheid, key), value); + } + public void Set(string cacheid, string key, T value, int durationMinutes) + { + string fullKey = Key(cacheid, key); + Set(fullKey, value, durationMinutes); + } + private string Key(string cacheid, string key) + { + return FormatKey(cacheid, key, KeyPrefix(cacheid)); + } + private IEnumerable Key(string cacheid, IEnumerable key) + { + long? prefix = KeyPrefix(cacheid); + return key.Select(k => new RedisKey().Append(FormatKey(cacheid, k, prefix))); + } + private string FormatKey(string cacheid, string key, Nullable prefix) + { + if (_multitenant) + { + return String.Format("{0}:{1}_{2}_{3}", GxContext.Current.TenantId, cacheid, prefix, GXUtil.GetHash(key)); + } + return String.Format("{0}_{1}_{2}", cacheid, prefix, GXUtil.GetHash(key)); + } + private Nullable KeyPrefix(string cacheid) + { + Nullable prefix; + if (!Get>(cacheid, out prefix)) + { + prefix = DateTime.Now.Ticks; + Set>(cacheid, prefix); + } + return prefix; + } + static string Serialize(object o) + { + if (o == null) + { + return null; + } + JsonSerializerOptions opts = new JsonSerializerOptions(); + opts.Converters.Add(new DBNullConverter()); + return JsonSerializer.Serialize(o, opts); + } + + static T Deserialize(string value) + { + if (value == null) + { + return default(T); + } + JsonSerializerOptions opts = new JsonSerializerOptions(); + opts.Converters.Add(new ObjectToInferredTypesConverter()); + return JsonSerializer.Deserialize(value, opts); + } + private TimeSpan LocalCacheTTL(int durationMinutes) + { + return LocalCacheTTL(durationMinutes > 0 ? TimeSpan.FromMinutes(durationMinutes) : (TimeSpan?)null); + } + private TimeSpan LocalCacheTTL(TimeSpan? ttl) + { + return ttl.HasValue ? TimeSpan.FromTicks((long)(ttl.Value.Ticks * DEFAULT_LOCAL_CACHE_FACTOR)) : LOCAL_CACHE_PERSISTENT_KEY_TTL; + } + + private void ClearKeyLocal(string key) + { + if (_localCache == null) + return; + _localCache.Remove(key); + } + void ClearAllCachesLocal() + { + if (_localCache == null) + return; + _localCache.Compact(1.0); + } + + private void KeyExpireLocal(string fullKey) + { + if (_localCache == null) + return; + _localCache.Remove(fullKey); + } + private bool KeyExistsLocal(string fullKey) + { + if (_localCache == null) + return false; + if (_localCache.TryGetValue(fullKey, out _)) + return true; + return false; + } + + private void SetLocal(string key, T value) + { + if (_localCache != null) + { + TimeSpan? redisTTL = RedisDatabase.KeyTimeToLive(key); + _localCache.Set(key, value, LocalCacheTTL(redisTTL)); + } + } + private void SetPersistentLocal(string cacheid, long? prefix) + { + if (_localCache != null) + _localCache.Set(cacheid, prefix, LocalCacheTTL(LOCAL_CACHE_PERSISTENT_KEY_TTL)); + } + private void SetLocal(string key, T value, int duration) + { + if (_localCache != null) + _localCache.Set(key, value, LocalCacheTTL(duration)); + } + private bool GetLocal(string key, out T value) + { + if (_localCache == null) + { + value = default(T); + return false; + } + return _localCache.TryGetValue(key, out value); + } + } +} \ No newline at end of file diff --git a/dotnet/src/dotnetcore/Providers/Cache/GxRedis/GxRedis.csproj b/dotnet/src/dotnetcore/Providers/Cache/GxRedis/GxRedis.csproj index f616027d3..60b3221eb 100644 --- a/dotnet/src/dotnetcore/Providers/Cache/GxRedis/GxRedis.csproj +++ b/dotnet/src/dotnetcore/Providers/Cache/GxRedis/GxRedis.csproj @@ -1,8 +1,9 @@ - - net6.0;net8.0 + + net8.0 Redis GeneXus.Redis.Core + NETCORE Properties @@ -15,10 +16,7 @@ - - - - + diff --git a/dotnet/src/dotnetframework/GxClasses/Services/Storage/GXServices.cs b/dotnet/src/dotnetframework/GxClasses/Services/Storage/GXServices.cs index fc59e7b5c..390fe5c1e 100644 --- a/dotnet/src/dotnetframework/GxClasses/Services/Storage/GXServices.cs +++ b/dotnet/src/dotnetframework/GxClasses/Services/Storage/GXServices.cs @@ -15,6 +15,8 @@ public class GXServices public static string STORAGE_SERVICE = "Storage"; public static string STORAGE_APISERVICE = "StorageAPI"; public static string CACHE_SERVICE = "Cache"; + public static string REDIS_CACHE_SERVICE = "Redis"; + public static string DATABASE_CACHE_SERVICE = "DATABASE"; public static string DATA_ACCESS_SERVICE = "DataAccess"; public static string SESSION_SERVICE = "Session"; public static string WEBNOTIFICATIONS_SERVICE = "WebNotifications"; @@ -47,6 +49,7 @@ public static GXServices Instance } set { } } + public void AddService(string name, GXService service) { services[name] = service; From 2413d2db27a3a8890316f9721c32f1809dff00a0 Mon Sep 17 00:00:00 2001 From: claudiamurialdo Date: Mon, 20 Oct 2025 19:52:55 -0300 Subject: [PATCH 2/6] It seems better to keep GxRedis.cs unified for both .NET Framework and .NET Core. (cherry picked from commit f97a2539e0aa71ae2050daf698763b679a225bab) # Conflicts: # dotnet/src/dotnetframework/Providers/Cache/GxRedis/GxRedis.cs --- .../Providers/Cache/GxRedis/GxRedis.cs | 433 ------------------ .../Providers/Cache/GxRedis/GxRedis.csproj | 3 + .../Providers/Cache/GxRedis/GxRedis.cs | 283 +++++++++++- 3 files changed, 270 insertions(+), 449 deletions(-) delete mode 100644 dotnet/src/dotnetcore/Providers/Cache/GxRedis/GxRedis.cs diff --git a/dotnet/src/dotnetcore/Providers/Cache/GxRedis/GxRedis.cs b/dotnet/src/dotnetcore/Providers/Cache/GxRedis/GxRedis.cs deleted file mode 100644 index efe1d4004..000000000 --- a/dotnet/src/dotnetcore/Providers/Cache/GxRedis/GxRedis.cs +++ /dev/null @@ -1,433 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; -using GeneXus.Application; -using GeneXus.Configuration; -using GeneXus.Encryption; -using GeneXus.Services; -using GeneXus.Utils; -using GxClasses.Helpers; -using Microsoft.Extensions.Caching.Memory; -using StackExchange.Redis; -using StackExchange.Redis.KeyspaceIsolation; - -namespace GeneXus.Cache -{ - public sealed class Redis : ICacheService2 - { - private static readonly IGXLogger log = GXLoggerFactory.GetLogger(); - - ConnectionMultiplexer _redisConnection; - IDatabase _redisDatabase; - bool _multitenant; - MemoryCache _localCache; - private const double DEFAULT_LOCAL_CACHE_FACTOR = 0.8; - private static readonly TimeSpan LOCAL_CACHE_PERSISTENT_KEY_TTL = TimeSpan.FromMinutes(5); - ConfigurationOptions _redisConnectionOptions; - private const int REDIS_DEFAULT_PORT = 6379; - public int redisSessionTimeout; - private string _instanceName; - - - public Redis(string connectionString) - { - _redisConnectionOptions = ConfigurationOptions.Parse(connectionString); - _redisConnectionOptions.AllowAdmin = true; - InitLocalCache(); - } - - public Redis(string connectionString, int sessionTimeout):this(connectionString) - { - redisSessionTimeout = sessionTimeout; - } - public Redis() - { - GXService providerService = ServiceFactory.GetGXServices()?.Get(GXServices.CACHE_SERVICE); - if (providerService != null) - { - string address, password; - address = providerService.Properties.Get("CACHE_PROVIDER_ADDRESS"); - password = providerService.Properties.Get("CACHE_PROVIDER_PASSWORD"); - _instanceName = providerService.Properties.Get("CACHE_PROVIDER_INSTANCE_NAME"); - - if (!string.IsNullOrEmpty(password)) - { - string ret = string.Empty; - if (CryptoImpl.Decrypt(ref ret, password)) - { - password = ret; - } - } - - if (string.IsNullOrEmpty(address)) - address = String.Format("localhost:{0}", REDIS_DEFAULT_PORT); - - if (!string.IsNullOrEmpty(password)) - { - if (!address.Contains(':')) - { - address = $"{address}:{REDIS_DEFAULT_PORT}"; - } - address = string.Format("{0},password={1}", address.Trim(), password.Trim()); - _redisConnectionOptions = ConfigurationOptions.Parse(address); - } - else - { - _redisConnectionOptions = ConfigurationOptions.Parse(address); - } - _redisConnectionOptions.AllowAdmin = true; - - InitLocalCache(); - - } - } - - private void InitLocalCache() - { - if (EnvVarReader.GetEnvironmentValue(GXServices.CACHE_SERVICE, GXServices.REDIS_CACHE_SERVICE, "HYBRID", out string envVarValue) && envVarValue.Equals(true.ToString(), StringComparison.OrdinalIgnoreCase)) - { - GXLogging.Debug(log, "Using Redis Hybrid mode with local memory cache."); - _localCache = new MemoryCache(new MemoryCacheOptions()); - } - else - { - GXLogging.Debug(log, "Using Redis only mode without local memory cache."); - } - } - - IDatabase RedisDatabase - { - get - { - if (_redisDatabase == null) - { - _redisConnection = ConnectionMultiplexer.Connect(_redisConnectionOptions); - IDatabase db = _redisConnection.GetDatabase(); - - if (!string.IsNullOrEmpty(_instanceName)) - { - if (_instanceName == CacheFactory.SUBDOMAIN) - { - _multitenant = true; - GXLogging.Debug(log, "Using Redis multitenant (key prefix):" + CacheFactory.SUBDOMAIN); - _redisDatabase = db; - } - else - { - string prefixKey = _instanceName.EndsWith(":") ? _instanceName : _instanceName + ":"; - _redisDatabase = db.WithKeyPrefix(_instanceName); - GXLogging.Debug(log, "Using Redis instance name (key prefix): " + prefixKey); - } - } - else - { - _redisDatabase = db; - } - } - return _redisDatabase; - } - } - public void Clear(string cacheid, string key) - { - ClearKey(Key(cacheid, key)); - } - - public void ClearKey(string key) - { - RedisDatabase.KeyDelete(key); - ClearKeyLocal(key); - } - public void ClearCache(string cacheid) - { - Nullable prefix = new Nullable(KeyPrefix(cacheid).Value + 1); - RedisDatabase.StringSet(cacheid, prefix); - SetPersistentLocal(cacheid, prefix); - } - - - public void ClearAllCaches() - { - IConnectionMultiplexer multiplexer = RedisDatabase.Multiplexer; - System.Net.EndPoint[] endpoints = multiplexer.GetEndPoints(true); - foreach (var endpoint in endpoints) - { - var server = multiplexer.GetServer(endpoint); - server.FlushAllDatabases(); - } - ClearAllCachesLocal(); - } - - public bool KeyExpire(string cacheid, string key, TimeSpan expiry, CommandFlags flags = CommandFlags.None) - { - string fullKey = Key(cacheid, key); - bool expirationSaved = RedisDatabase.KeyExpire(fullKey, expiry, flags); - if (expirationSaved) - KeyExpireLocal(fullKey); - return expirationSaved; - } - - public bool KeyExists(string cacheid, string key) - { - string fullKey = Key(cacheid, key); - - if (KeyExistsLocal(fullKey)) - { - GXLogging.Debug(log, $"KeyExists hit local cache {fullKey}"); - return true; - } - - return RedisDatabase.KeyExists(fullKey); - } - - private bool Get(string key, out T value) - { - if (GetLocal(key, out value)) - { - GXLogging.Debug(log, $"Get hit local cache {key}"); - return true; - } - - if (default(T) == null) - { - value = Deserialize(RedisDatabase.StringGet(key)); - if (value == null) - GXLogging.Debug(log, "Get, misses key '" + key + "'"); - else - SetLocal(key, value); - return value != null; - } - else - { - if (RedisDatabase.KeyExists(key)) - { - value = Deserialize(RedisDatabase.StringGet(key)); - SetLocal(key, value); - return true; - } - else - { - GXLogging.Debug(log, "Get, misses key '" + key + "'"); - value = default(T); - return false; - } - } - } - - public IDictionary GetAll(string cacheid, IEnumerable keys) - { - if (keys == null) return null; - - var results = new Dictionary(); - var keysToFetch = new List(); - - foreach (string k in keys) - { - string fullKey = Key(cacheid, k); - if (GetLocal(fullKey, out T value)) - { - GXLogging.Debug(log, $"Get hit local cache {fullKey}"); - results[k] = value; - } - else - { - keysToFetch.Add(k); - } - } - - if (keysToFetch.Count > 0) - { - var prefixedKeys = Key(cacheid, keysToFetch); - RedisValue[] values = RedisDatabase.StringGet(prefixedKeys.ToArray()); - - int i = 0; - foreach (string k in keysToFetch) - { - string fullKey = Key(cacheid, k); - T value = Deserialize(values[i]); - results[k] = value; - - SetLocal(fullKey, value); - i++; - } - } - - return results; - } - public void SetAll(string cacheid, IEnumerable keys, IEnumerable values, int duration = 0) - { - if (keys == null || values == null || keys.Count() != values.Count()) - return; - - IEnumerable prefixedKeys = Key(cacheid, keys); - IEnumerator valuesEnumerator = values.GetEnumerator(); - KeyValuePair[] redisBatch = new KeyValuePair[prefixedKeys.Count()]; - - int i = 0; - foreach (RedisKey redisKey in prefixedKeys) - { - if (valuesEnumerator.MoveNext()) - { - T value = valuesEnumerator.Current; - redisBatch[i] = new KeyValuePair(redisKey, Serialize(value)); - SetLocal(redisKey.ToString(), value, duration); - } - i++; - } - if (redisBatch.Length > 0) - { - if (duration > 0) - { - foreach (var pair in redisBatch) - RedisDatabase.StringSet(pair.Key, pair.Value, TimeSpan.FromMinutes(duration)); - } - else - { - RedisDatabase.StringSet(redisBatch); - } - } - } - - private void Set(string key, T value, int duration) - { - GXLogging.Debug(log, "Set key:" + key + " value " + value + " valuetype:" + value.GetType()); - if (duration > 0) - RedisDatabase.StringSet(key, Serialize(value), TimeSpan.FromMinutes(duration)); - else - RedisDatabase.StringSet(key, Serialize(value)); - - SetLocal(key, value, duration); - } - private void Set(string key, T value) - { - Set(key, value, 0); - } - public bool Get(string cacheid, string key, out T value) - { - GXLogging.Debug(log, "Get cacheid:" + cacheid + " key:" + key); - return Get(Key(cacheid, key), out value); - } - - public void Set(string cacheid, string key, T value) - { - Set(Key(cacheid, key), value); - } - public void Set(string cacheid, string key, T value, int durationMinutes) - { - string fullKey = Key(cacheid, key); - Set(fullKey, value, durationMinutes); - } - private string Key(string cacheid, string key) - { - return FormatKey(cacheid, key, KeyPrefix(cacheid)); - } - private IEnumerable Key(string cacheid, IEnumerable key) - { - long? prefix = KeyPrefix(cacheid); - return key.Select(k => new RedisKey().Append(FormatKey(cacheid, k, prefix))); - } - private string FormatKey(string cacheid, string key, Nullable prefix) - { - if (_multitenant) - { - return String.Format("{0}:{1}_{2}_{3}", GxContext.Current.TenantId, cacheid, prefix, GXUtil.GetHash(key)); - } - return String.Format("{0}_{1}_{2}", cacheid, prefix, GXUtil.GetHash(key)); - } - private Nullable KeyPrefix(string cacheid) - { - Nullable prefix; - if (!Get>(cacheid, out prefix)) - { - prefix = DateTime.Now.Ticks; - Set>(cacheid, prefix); - } - return prefix; - } - static string Serialize(object o) - { - if (o == null) - { - return null; - } - JsonSerializerOptions opts = new JsonSerializerOptions(); - opts.Converters.Add(new DBNullConverter()); - return JsonSerializer.Serialize(o, opts); - } - - static T Deserialize(string value) - { - if (value == null) - { - return default(T); - } - JsonSerializerOptions opts = new JsonSerializerOptions(); - opts.Converters.Add(new ObjectToInferredTypesConverter()); - return JsonSerializer.Deserialize(value, opts); - } - private TimeSpan LocalCacheTTL(int durationMinutes) - { - return LocalCacheTTL(durationMinutes > 0 ? TimeSpan.FromMinutes(durationMinutes) : (TimeSpan?)null); - } - private TimeSpan LocalCacheTTL(TimeSpan? ttl) - { - return ttl.HasValue ? TimeSpan.FromTicks((long)(ttl.Value.Ticks * DEFAULT_LOCAL_CACHE_FACTOR)) : LOCAL_CACHE_PERSISTENT_KEY_TTL; - } - - private void ClearKeyLocal(string key) - { - if (_localCache == null) - return; - _localCache.Remove(key); - } - void ClearAllCachesLocal() - { - if (_localCache == null) - return; - _localCache.Compact(1.0); - } - - private void KeyExpireLocal(string fullKey) - { - if (_localCache == null) - return; - _localCache.Remove(fullKey); - } - private bool KeyExistsLocal(string fullKey) - { - if (_localCache == null) - return false; - if (_localCache.TryGetValue(fullKey, out _)) - return true; - return false; - } - - private void SetLocal(string key, T value) - { - if (_localCache != null) - { - TimeSpan? redisTTL = RedisDatabase.KeyTimeToLive(key); - _localCache.Set(key, value, LocalCacheTTL(redisTTL)); - } - } - private void SetPersistentLocal(string cacheid, long? prefix) - { - if (_localCache != null) - _localCache.Set(cacheid, prefix, LocalCacheTTL(LOCAL_CACHE_PERSISTENT_KEY_TTL)); - } - private void SetLocal(string key, T value, int duration) - { - if (_localCache != null) - _localCache.Set(key, value, LocalCacheTTL(duration)); - } - private bool GetLocal(string key, out T value) - { - if (_localCache == null) - { - value = default(T); - return false; - } - return _localCache.TryGetValue(key, out value); - } - } -} \ No newline at end of file diff --git a/dotnet/src/dotnetcore/Providers/Cache/GxRedis/GxRedis.csproj b/dotnet/src/dotnetcore/Providers/Cache/GxRedis/GxRedis.csproj index 60b3221eb..f312f6e6a 100644 --- a/dotnet/src/dotnetcore/Providers/Cache/GxRedis/GxRedis.csproj +++ b/dotnet/src/dotnetcore/Providers/Cache/GxRedis/GxRedis.csproj @@ -15,6 +15,9 @@ false + + + diff --git a/dotnet/src/dotnetframework/Providers/Cache/GxRedis/GxRedis.cs b/dotnet/src/dotnetframework/Providers/Cache/GxRedis/GxRedis.cs index 7007d736a..dfdba2384 100644 --- a/dotnet/src/dotnetframework/Providers/Cache/GxRedis/GxRedis.cs +++ b/dotnet/src/dotnetframework/Providers/Cache/GxRedis/GxRedis.cs @@ -2,10 +2,16 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json; -using System.Threading.Tasks; +#if NETCORE +using GeneXus.Application; +using GxClasses.Helpers; +using Microsoft.Extensions.Caching.Memory; +#endif +using GeneXus.Encryption; using GeneXus.Services; using GeneXus.Utils; using StackExchange.Redis; +using StackExchange.Redis.KeyspaceIsolation; namespace GeneXus.Cache { @@ -15,19 +21,27 @@ public sealed class Redis : ICacheService2 ConnectionMultiplexer _redisConnection; IDatabase _redisDatabase; +#if NETCORE + bool _multitenant; + MemoryCache _localCache; + private const double DEFAULT_LOCAL_CACHE_FACTOR = 0.8; + private static readonly TimeSpan LOCAL_CACHE_PERSISTENT_KEY_TTL = TimeSpan.FromMinutes(5); + +#endif ConfigurationOptions _redisConnectionOptions; private const int REDIS_DEFAULT_PORT = 6379; public int redisSessionTimeout; + private string _instanceName; + public Redis(string connectionString) { _redisConnectionOptions = ConfigurationOptions.Parse(connectionString); _redisConnectionOptions.AllowAdmin = true; + InitLocalCache(); } - public Redis(string connectionString, int sessionTimeout) + public Redis(string connectionString, int sessionTimeout):this(connectionString) { - _redisConnectionOptions = ConfigurationOptions.Parse(connectionString); - _redisConnectionOptions.AllowAdmin = true; redisSessionTimeout = sessionTimeout; } public Redis() @@ -38,6 +52,16 @@ public Redis() string address, password; address = providerService.Properties.Get("CACHE_PROVIDER_ADDRESS"); password = providerService.Properties.Get("CACHE_PROVIDER_PASSWORD"); + _instanceName = providerService.Properties.Get("CACHE_PROVIDER_INSTANCE_NAME"); + + if (!string.IsNullOrEmpty(password)) + { + string ret = string.Empty; + if (CryptoImpl.Decrypt(ref ret, password)) + { + password = ret; + } + } if (string.IsNullOrEmpty(address)) address = String.Format("localhost:{0}", REDIS_DEFAULT_PORT); @@ -56,8 +80,24 @@ public Redis() _redisConnectionOptions = ConfigurationOptions.Parse(address); } _redisConnectionOptions.AllowAdmin = true; + InitLocalCache(); } } + private void InitLocalCache() + { +#if NETCORE + if (EnvVarReader.GetEnvironmentValue(GXServices.CACHE_SERVICE, GXServices.REDIS_CACHE_SERVICE, "HYBRID", out string envVarValue) && envVarValue.Equals(true.ToString(), StringComparison.OrdinalIgnoreCase)) + { + GXLogging.Debug(log, "Using Redis Hybrid mode with local memory cache."); + _localCache = new MemoryCache(new MemoryCacheOptions()); + } + else + { + GXLogging.Debug(log, "Using Redis only mode without local memory cache."); + } +#endif + } + IDatabase RedisDatabase { get @@ -65,7 +105,29 @@ IDatabase RedisDatabase if (_redisDatabase == null) { _redisConnection = ConnectionMultiplexer.Connect(_redisConnectionOptions); - _redisDatabase = _redisConnection.GetDatabase(); + IDatabase db = _redisConnection.GetDatabase(); + + if (!string.IsNullOrEmpty(_instanceName)) + { +#if NETCORE + if (_instanceName == CacheFactory.SUBDOMAIN) + { + _multitenant = true; + GXLogging.Debug(log, "Using Redis multitenant (key prefix):" + CacheFactory.SUBDOMAIN); + _redisDatabase = db; + } + else +#endif + { + string prefixKey = _instanceName.EndsWith(":") ? _instanceName : _instanceName + ":"; + _redisDatabase = db.WithKeyPrefix(_instanceName); + GXLogging.Debug(log, "Using Redis instance name (key prefix): " + prefixKey); + } + } + else + { + _redisDatabase = db; + } } return _redisDatabase; } @@ -78,43 +140,65 @@ public void Clear(string cacheid, string key) public void ClearKey(string key) { RedisDatabase.KeyDelete(key); + ClearKeyLocal(key); } public void ClearCache(string cacheid) { Nullable prefix = new Nullable(KeyPrefix(cacheid).Value + 1); RedisDatabase.StringSet(cacheid, prefix); + SetPersistentLocal(cacheid, prefix); } public void ClearAllCaches() { - var endpoints = _redisConnection.GetEndPoints(true); + IConnectionMultiplexer multiplexer = RedisDatabase.Multiplexer; + System.Net.EndPoint[] endpoints = multiplexer.GetEndPoints(true); foreach (var endpoint in endpoints) { - var server = _redisConnection.GetServer(endpoint); + var server = multiplexer.GetServer(endpoint); server.FlushAllDatabases(); } + ClearAllCachesLocal(); } public bool KeyExpire(string cacheid, string key, TimeSpan expiry, CommandFlags flags = CommandFlags.None) { - Task t = RedisDatabase.KeyExpireAsync(Key(cacheid, key), expiry, flags); - t.Wait(); - return t.Result; + string fullKey = Key(cacheid, key); + bool expirationSaved = RedisDatabase.KeyExpire(fullKey, expiry, flags); + if (expirationSaved) + KeyExpireLocal(fullKey); + return expirationSaved; } public bool KeyExists(string cacheid, string key) { - Task t = RedisDatabase.KeyExistsAsync(Key(cacheid, key)); - t.Wait(); - return t.Result; + string fullKey = Key(cacheid, key); + + if (KeyExistsLocal(fullKey)) + { + GXLogging.Debug(log, $"KeyExists hit local cache {fullKey}"); + return true; + } + + return RedisDatabase.KeyExists(fullKey); } + private bool Get(string key, out T value) { + if (GetLocal(key, out value)) + { + GXLogging.Debug(log, $"Get hit local cache {key}"); + return true; + } + if (default(T) == null) { value = Deserialize(RedisDatabase.StringGet(key)); - if (value == null) GXLogging.Debug(log, "Get, misses key '" + key + "'"); + if (value == null) + GXLogging.Debug(log, "Get, misses key '" + key + "'"); + else + SetLocal(key, value); return value != null; } else @@ -122,6 +206,7 @@ private bool Get(string key, out T value) if (RedisDatabase.KeyExists(key)) { value = Deserialize(RedisDatabase.StringGet(key)); + SetLocal(key, value); return true; } else @@ -133,6 +218,81 @@ private bool Get(string key, out T value) } } +#if NETCORE + public IDictionary GetAll(string cacheid, IEnumerable keys) + { + if (keys == null) return null; + + var results = new Dictionary(); + var keysToFetch = new List(); + + foreach (string k in keys) + { + string fullKey = Key(cacheid, k); + if (GetLocal(fullKey, out T value)) + { + GXLogging.Debug(log, $"Get hit local cache {fullKey}"); + results[k] = value; + } + else + { + keysToFetch.Add(k); + } + } + + if (keysToFetch.Count > 0) + { + var prefixedKeys = Key(cacheid, keysToFetch); + RedisValue[] values = RedisDatabase.StringGet(prefixedKeys.ToArray()); + + int i = 0; + foreach (string k in keysToFetch) + { + string fullKey = Key(cacheid, k); + T value = Deserialize(values[i]); + results[k] = value; + + SetLocal(fullKey, value); + i++; + } + } + + return results; + } + public void SetAll(string cacheid, IEnumerable keys, IEnumerable values, int duration = 0) + { + if (keys == null || values == null || keys.Count() != values.Count()) + return; + + IEnumerable prefixedKeys = Key(cacheid, keys); + IEnumerator valuesEnumerator = values.GetEnumerator(); + KeyValuePair[] redisBatch = new KeyValuePair[prefixedKeys.Count()]; + + int i = 0; + foreach (RedisKey redisKey in prefixedKeys) + { + if (valuesEnumerator.MoveNext()) + { + T value = valuesEnumerator.Current; + redisBatch[i] = new KeyValuePair(redisKey, Serialize(value)); + SetLocal(redisKey.ToString(), value, duration); + } + i++; + } + if (redisBatch.Length > 0) + { + if (duration > 0) + { + foreach (var pair in redisBatch) + RedisDatabase.StringSet(pair.Key, pair.Value, TimeSpan.FromMinutes(duration)); + } + else + { + RedisDatabase.StringSet(redisBatch); + } + } + } +#else public IDictionary GetAll(string cacheid, IEnumerable keys) { if (keys != null) @@ -170,7 +330,7 @@ public void SetAll(string cacheid, IEnumerable keys, IEnumerable v RedisDatabase.StringSet(dictionary); } } - +#endif private void Set(string key, T value, int duration) { GXLogging.Debug(log, "Set key:" + key + " value " + value + " valuetype:" + value.GetType()); @@ -178,11 +338,12 @@ private void Set(string key, T value, int duration) RedisDatabase.StringSet(key, Serialize(value), TimeSpan.FromMinutes(duration)); else RedisDatabase.StringSet(key, Serialize(value)); + SetLocal(key, value, duration); } private void Set(string key, T value) { - RedisDatabase.StringSet(key, Serialize(value)); + Set(key, value, 0); } public bool Get(string cacheid, string key, out T value) @@ -212,6 +373,12 @@ private IEnumerable Key(string cacheid, IEnumerable key) } private string FormatKey(string cacheid, string key, Nullable prefix) { +#if NETCORE + if (_multitenant) + { + return String.Format("{0}:{1}_{2}_{3}", GxContext.Current.TenantId, cacheid, prefix, GXUtil.GetHash(key)); + } +#endif return String.Format("{0}_{1}_{2}", cacheid, prefix, GXUtil.GetHash(key)); } private Nullable KeyPrefix(string cacheid) @@ -245,5 +412,89 @@ static T Deserialize(string value) opts.Converters.Add(new ObjectToInferredTypesConverter()); return JsonSerializer.Deserialize(value, opts); } +#if NETCORE + private TimeSpan LocalCacheTTL(int durationMinutes) + { + return LocalCacheTTL(durationMinutes > 0 ? TimeSpan.FromMinutes(durationMinutes) : (TimeSpan?)null); + } + private TimeSpan LocalCacheTTL(TimeSpan? ttl) + { + return ttl.HasValue ? TimeSpan.FromTicks((long)(ttl.Value.Ticks * DEFAULT_LOCAL_CACHE_FACTOR)) : LOCAL_CACHE_PERSISTENT_KEY_TTL; + } +#endif + private void ClearKeyLocal(string key) + { +#if NETCORE + if (_localCache == null) + return; + _localCache.Remove(key); +#endif + } + void ClearAllCachesLocal() + { +#if NETCORE + if (_localCache == null) + return; + _localCache.Compact(1.0); +#endif + } + + private void KeyExpireLocal(string fullKey) + { +#if NETCORE + if (_localCache == null) + return; + _localCache.Remove(fullKey); +#endif + } + private bool KeyExistsLocal(string fullKey) + { +#if NETCORE + if (_localCache == null) + return false; + if (_localCache.TryGetValue(fullKey, out _)) + return true; +#endif + return false; + } + + private void SetLocal(string key, T value) + { +#if NETCORE + if (_localCache != null) + { + TimeSpan? redisTTL = RedisDatabase.KeyTimeToLive(key); + _localCache.Set(key, value, LocalCacheTTL(redisTTL)); + } +#endif + } + private void SetPersistentLocal(string cacheid, long? prefix) + { +#if NETCORE + if (_localCache != null) + _localCache.Set(cacheid, prefix, LocalCacheTTL(LOCAL_CACHE_PERSISTENT_KEY_TTL)); +#endif + } + private void SetLocal(string key, T value, int duration) + { +#if NETCORE + if (_localCache != null) + _localCache.Set(key, value, LocalCacheTTL(duration)); +#endif + } + private bool GetLocal(string key, out T value) + { +#if NETCORE + if (_localCache == null) + { + value = default(T); + return false; + } + return _localCache.TryGetValue(key, out value); +#else + value = default(T); + return false; +#endif + } } } \ No newline at end of file From ec16dd91f9fb3ee38859b95fc9dde42227798a30 Mon Sep 17 00:00:00 2001 From: claudiamurialdo Date: Tue, 21 Oct 2025 18:21:21 -0300 Subject: [PATCH 3/6] =?UTF-8?q?Applied=20Fazzato=E2=80=99s=20review=20sugg?= =?UTF-8?q?estions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit e46abf12477bdbbc2d0abd2a57f2598142372138) --- .../Providers/Cache/GxRedis/GxRedis.cs | 24 ++++++------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/dotnet/src/dotnetframework/Providers/Cache/GxRedis/GxRedis.cs b/dotnet/src/dotnetframework/Providers/Cache/GxRedis/GxRedis.cs index dfdba2384..d3cc95496 100644 --- a/dotnet/src/dotnetframework/Providers/Cache/GxRedis/GxRedis.cs +++ b/dotnet/src/dotnetframework/Providers/Cache/GxRedis/GxRedis.cs @@ -86,7 +86,7 @@ public Redis() private void InitLocalCache() { #if NETCORE - if (EnvVarReader.GetEnvironmentValue(GXServices.CACHE_SERVICE, GXServices.REDIS_CACHE_SERVICE, "HYBRID", out string envVarValue) && envVarValue.Equals(true.ToString(), StringComparison.OrdinalIgnoreCase)) + if (EnvVarReader.GetEnvironmentValue(GXServices.CACHE_SERVICE, GXServices.REDIS_CACHE_SERVICE, "HYBRID", out string envVarValue) && envVarValue.Equals(bool.TrueString, StringComparison.OrdinalIgnoreCase)) { GXLogging.Debug(log, "Using Redis Hybrid mode with local memory cache."); _localCache = new MemoryCache(new MemoryCacheOptions()); @@ -425,34 +425,26 @@ private TimeSpan LocalCacheTTL(TimeSpan? ttl) private void ClearKeyLocal(string key) { #if NETCORE - if (_localCache == null) - return; - _localCache.Remove(key); + _localCache?.Remove(key); #endif } void ClearAllCachesLocal() { #if NETCORE - if (_localCache == null) - return; - _localCache.Compact(1.0); + _localCache?.Compact(1.0); #endif } private void KeyExpireLocal(string fullKey) { #if NETCORE - if (_localCache == null) - return; - _localCache.Remove(fullKey); + _localCache?.Remove(fullKey); #endif } private bool KeyExistsLocal(string fullKey) { #if NETCORE - if (_localCache == null) - return false; - if (_localCache.TryGetValue(fullKey, out _)) + if (_localCache?.TryGetValue(fullKey, out _)) return true; #endif return false; @@ -471,15 +463,13 @@ private void SetLocal(string key, T value) private void SetPersistentLocal(string cacheid, long? prefix) { #if NETCORE - if (_localCache != null) - _localCache.Set(cacheid, prefix, LocalCacheTTL(LOCAL_CACHE_PERSISTENT_KEY_TTL)); + _localCache?.Set(cacheid, prefix, LocalCacheTTL(LOCAL_CACHE_PERSISTENT_KEY_TTL)); #endif } private void SetLocal(string key, T value, int duration) { #if NETCORE - if (_localCache != null) - _localCache.Set(key, value, LocalCacheTTL(duration)); + _localCache?.Set(key, value, LocalCacheTTL(duration)); #endif } private bool GetLocal(string key, out T value) From 4781b94a5c6b97dd329f179224a8de287666696a Mon Sep 17 00:00:00 2001 From: claudiamurialdo Date: Tue, 21 Oct 2025 19:44:07 -0300 Subject: [PATCH 4/6] Fix build error --- .../GxClasses/Properties/AssemblyInfo.cs | 1 + .../Providers/Cache/GxRedis/GxRedis.cs | 48 ++----------------- 2 files changed, 5 insertions(+), 44 deletions(-) diff --git a/dotnet/src/dotnetcore/GxClasses/Properties/AssemblyInfo.cs b/dotnet/src/dotnetcore/GxClasses/Properties/AssemblyInfo.cs index 1c8dea312..89d215406 100644 --- a/dotnet/src/dotnetcore/GxClasses/Properties/AssemblyInfo.cs +++ b/dotnet/src/dotnetcore/GxClasses/Properties/AssemblyInfo.cs @@ -18,3 +18,4 @@ [assembly: InternalsVisibleTo("DotNetCoreChunkedTest")] [assembly: InternalsVisibleTo("DotNetCoreChunkedTest")] [assembly: InternalsVisibleTo("GeneXus.OpenTelemetry.Diagnostics")] +[assembly: InternalsVisibleTo("GxRedis")] diff --git a/dotnet/src/dotnetframework/Providers/Cache/GxRedis/GxRedis.cs b/dotnet/src/dotnetframework/Providers/Cache/GxRedis/GxRedis.cs index d3cc95496..e9eb9606c 100644 --- a/dotnet/src/dotnetframework/Providers/Cache/GxRedis/GxRedis.cs +++ b/dotnet/src/dotnetframework/Providers/Cache/GxRedis/GxRedis.cs @@ -22,7 +22,6 @@ public sealed class Redis : ICacheService2 ConnectionMultiplexer _redisConnection; IDatabase _redisDatabase; #if NETCORE - bool _multitenant; MemoryCache _localCache; private const double DEFAULT_LOCAL_CACHE_FACTOR = 0.8; private static readonly TimeSpan LOCAL_CACHE_PERSISTENT_KEY_TTL = TimeSpan.FromMinutes(5); @@ -31,7 +30,6 @@ public sealed class Redis : ICacheService2 ConfigurationOptions _redisConnectionOptions; private const int REDIS_DEFAULT_PORT = 6379; public int redisSessionTimeout; - private string _instanceName; public Redis(string connectionString) { @@ -52,16 +50,6 @@ public Redis() string address, password; address = providerService.Properties.Get("CACHE_PROVIDER_ADDRESS"); password = providerService.Properties.Get("CACHE_PROVIDER_PASSWORD"); - _instanceName = providerService.Properties.Get("CACHE_PROVIDER_INSTANCE_NAME"); - - if (!string.IsNullOrEmpty(password)) - { - string ret = string.Empty; - if (CryptoImpl.Decrypt(ref ret, password)) - { - password = ret; - } - } if (string.IsNullOrEmpty(address)) address = String.Format("localhost:{0}", REDIS_DEFAULT_PORT); @@ -105,29 +93,7 @@ IDatabase RedisDatabase if (_redisDatabase == null) { _redisConnection = ConnectionMultiplexer.Connect(_redisConnectionOptions); - IDatabase db = _redisConnection.GetDatabase(); - - if (!string.IsNullOrEmpty(_instanceName)) - { -#if NETCORE - if (_instanceName == CacheFactory.SUBDOMAIN) - { - _multitenant = true; - GXLogging.Debug(log, "Using Redis multitenant (key prefix):" + CacheFactory.SUBDOMAIN); - _redisDatabase = db; - } - else -#endif - { - string prefixKey = _instanceName.EndsWith(":") ? _instanceName : _instanceName + ":"; - _redisDatabase = db.WithKeyPrefix(_instanceName); - GXLogging.Debug(log, "Using Redis instance name (key prefix): " + prefixKey); - } - } - else - { - _redisDatabase = db; - } + _redisDatabase = _redisConnection.GetDatabase(); } return _redisDatabase; } @@ -373,12 +339,6 @@ private IEnumerable Key(string cacheid, IEnumerable key) } private string FormatKey(string cacheid, string key, Nullable prefix) { -#if NETCORE - if (_multitenant) - { - return String.Format("{0}:{1}_{2}_{3}", GxContext.Current.TenantId, cacheid, prefix, GXUtil.GetHash(key)); - } -#endif return String.Format("{0}_{1}_{2}", cacheid, prefix, GXUtil.GetHash(key)); } private Nullable KeyPrefix(string cacheid) @@ -444,10 +404,10 @@ private void KeyExpireLocal(string fullKey) private bool KeyExistsLocal(string fullKey) { #if NETCORE - if (_localCache?.TryGetValue(fullKey, out _)) - return true; -#endif + return _localCache?.TryGetValue(fullKey, out _) ?? false; +#else return false; +#endif } private void SetLocal(string key, T value) From 057c5b7e011c7d27a765cb609f9db2be6711d96b Mon Sep 17 00:00:00 2001 From: claudiamurialdo Date: Tue, 21 Oct 2025 20:22:54 -0300 Subject: [PATCH 5/6] Do not change version of StackExchange.Redis --- dotnet/src/dotnetcore/Providers/Cache/GxRedis/GxRedis.csproj | 4 ++-- dotnet/src/dotnetframework/Providers/Cache/GxRedis/GxRedis.cs | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/dotnet/src/dotnetcore/Providers/Cache/GxRedis/GxRedis.csproj b/dotnet/src/dotnetcore/Providers/Cache/GxRedis/GxRedis.csproj index f312f6e6a..748fbb640 100644 --- a/dotnet/src/dotnetcore/Providers/Cache/GxRedis/GxRedis.csproj +++ b/dotnet/src/dotnetcore/Providers/Cache/GxRedis/GxRedis.csproj @@ -1,6 +1,6 @@ - net8.0 + net6.0;net8.0 Redis GeneXus.Redis.Core NETCORE @@ -19,7 +19,7 @@ - + diff --git a/dotnet/src/dotnetframework/Providers/Cache/GxRedis/GxRedis.cs b/dotnet/src/dotnetframework/Providers/Cache/GxRedis/GxRedis.cs index e9eb9606c..d87675ec4 100644 --- a/dotnet/src/dotnetframework/Providers/Cache/GxRedis/GxRedis.cs +++ b/dotnet/src/dotnetframework/Providers/Cache/GxRedis/GxRedis.cs @@ -7,11 +7,9 @@ using GxClasses.Helpers; using Microsoft.Extensions.Caching.Memory; #endif -using GeneXus.Encryption; using GeneXus.Services; using GeneXus.Utils; using StackExchange.Redis; -using StackExchange.Redis.KeyspaceIsolation; namespace GeneXus.Cache { From b8ad0b13131b6f3aad0e2b00c507c5466a516258 Mon Sep 17 00:00:00 2001 From: claudiamurialdo Date: Wed, 22 Oct 2025 16:23:10 -0300 Subject: [PATCH 6/6] Read ENABLE_MEMORY_CACHE from provider settings --- .../dotnetframework/Providers/Cache/GxRedis/GxRedis.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dotnet/src/dotnetframework/Providers/Cache/GxRedis/GxRedis.cs b/dotnet/src/dotnetframework/Providers/Cache/GxRedis/GxRedis.cs index d87675ec4..7802fcbb7 100644 --- a/dotnet/src/dotnetframework/Providers/Cache/GxRedis/GxRedis.cs +++ b/dotnet/src/dotnetframework/Providers/Cache/GxRedis/GxRedis.cs @@ -33,7 +33,6 @@ public Redis(string connectionString) { _redisConnectionOptions = ConfigurationOptions.Parse(connectionString); _redisConnectionOptions.AllowAdmin = true; - InitLocalCache(); } public Redis(string connectionString, int sessionTimeout):this(connectionString) @@ -66,13 +65,14 @@ public Redis() _redisConnectionOptions = ConfigurationOptions.Parse(address); } _redisConnectionOptions.AllowAdmin = true; - InitLocalCache(); + InitLocalCache(providerService); } } - private void InitLocalCache() + private void InitLocalCache(GXService providerService) { #if NETCORE - if (EnvVarReader.GetEnvironmentValue(GXServices.CACHE_SERVICE, GXServices.REDIS_CACHE_SERVICE, "HYBRID", out string envVarValue) && envVarValue.Equals(bool.TrueString, StringComparison.OrdinalIgnoreCase)) + string localCache = providerService.Properties.Get("ENABLE_MEMORY_CACHE"); + if (!string.IsNullOrEmpty(localCache) && localCache.Equals(bool.TrueString, StringComparison.OrdinalIgnoreCase)) { GXLogging.Debug(log, "Using Redis Hybrid mode with local memory cache."); _localCache = new MemoryCache(new MemoryCacheOptions());