diff --git a/dotnet/src/dotnetcore/GxNetCoreStartup/SessionHelper.cs b/dotnet/src/dotnetcore/GxNetCoreStartup/SessionHelper.cs index 98e8eee23..46201846e 100644 --- a/dotnet/src/dotnetcore/GxNetCoreStartup/SessionHelper.cs +++ b/dotnet/src/dotnetcore/GxNetCoreStartup/SessionHelper.cs @@ -1,25 +1,23 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Caching.StackExchangeRedis; +using System; using System.Collections.Concurrent; -using System.Threading.Tasks; +using System.Linq; using System.Threading; -using System; +using System.Threading.Tasks; using GeneXus.Services; -using System.Linq; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Caching.Distributed; +using StackExchange.Redis; namespace GeneXus.Application { public class TenantRedisCache : IDistributedCache { private readonly IHttpContextAccessor _httpContextAccessor; - private readonly IServiceProvider _serviceProvider; - private readonly ConcurrentDictionary _redisCaches = new(); + private readonly ConcurrentDictionary _redisCaches = new(); - public TenantRedisCache(IHttpContextAccessor httpContextAccessor, IServiceProvider serviceProvider) + public TenantRedisCache(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; - _serviceProvider = serviceProvider; } private IDistributedCache GetTenantCache() @@ -29,12 +27,7 @@ private IDistributedCache GetTenantCache() return _redisCaches.GetOrAdd(tenantId, id => { ISessionService sessionService = GXSessionServiceFactory.GetProvider(); - var options = new RedisCacheOptions - { - Configuration = sessionService.ConnectionString, - InstanceName = $"{id}:" - }; - return new RedisCache(options); + return new CustomRedisSessionStore(sessionService.ConnectionString, TimeSpan.FromMinutes(sessionService.SessionTimeout), id); }); } @@ -48,6 +41,80 @@ private IDistributedCache GetTenantCache() public Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default) => GetTenantCache().SetAsync(key, value, options, token); } + public class CustomRedisSessionStore : IDistributedCache + { + private readonly IDatabase _db; + private readonly TimeSpan _idleTimeout; + private readonly TimeSpan _refreshThreshold; + private readonly string _instanceName; + + public CustomRedisSessionStore(string connectionString, TimeSpan idleTimeout, string instanceName) + { + var mux = ConnectionMultiplexer.Connect(connectionString); + _db = mux.GetDatabase(); + _idleTimeout = idleTimeout; + _refreshThreshold = TimeSpan.FromTicks((long)(idleTimeout.Ticks * 0.2)); + _instanceName = instanceName ?? string.Empty; + } + + private string FormatKey(string key) => string.IsNullOrEmpty(_instanceName) ? key : $"{_instanceName}:{key}"; + + public byte[] Get(string key) => _db.StringGet(FormatKey(key)); + + public async Task GetAsync(string key, CancellationToken token = default) + { + string redisKey = FormatKey(key); + var value = await _db.StringGetAsync(redisKey); + + await RefreshKeyIfNeededAsync(redisKey); + + return value; + } + + public void Refresh(string key) + { + string redisKey = FormatKey(key); + + var ttl = _db.KeyTimeToLive(redisKey); + + if (ShouldRefreshKey(ttl)) + { + _db.KeyExpire(redisKey, _idleTimeout); + } + } + private bool ShouldRefreshKey(TimeSpan? ttl) + { + return ttl.HasValue && ttl.Value < _refreshThreshold; + } + public async Task RefreshAsync(string key, CancellationToken token = default) + { + string redisKey = FormatKey(key); + await RefreshKeyIfNeededAsync(redisKey); + } + private async Task RefreshKeyIfNeededAsync(string redisKey) + { + var ttl = await _db.KeyTimeToLiveAsync(redisKey); + + if (ShouldRefreshKey(ttl)) + { + _ = _db.KeyExpireAsync(redisKey, _idleTimeout); + } + } + public void Remove(string key) => _db.KeyDelete(FormatKey(key)); + + public Task RemoveAsync(string key, CancellationToken token = default) + => _db.KeyDeleteAsync(FormatKey(key)); + + public void Set(string key, byte[] value, DistributedCacheEntryOptions options) + { + _db.StringSet(FormatKey(key), value, _idleTimeout); + } + + public Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default) + { + return _db.StringSetAsync(FormatKey(key), value, _idleTimeout); + } + } public class TenantMiddleware { diff --git a/dotnet/src/dotnetcore/GxNetCoreStartup/Startup.cs b/dotnet/src/dotnetcore/GxNetCoreStartup/Startup.cs index 7e11e9e7b..7c8fc52d2 100644 --- a/dotnet/src/dotnetcore/GxNetCoreStartup/Startup.cs +++ b/dotnet/src/dotnetcore/GxNetCoreStartup/Startup.cs @@ -468,12 +468,11 @@ private void ConfigureSessionService(IServiceCollection services, ISessionServic } else { - services.AddStackExchangeRedisCache(options => - { - GXLogging.Info(log, $"Using Redis for Distributed session, ConnectionString:{sessionService.ConnectionString}, InstanceName: {sessionService.InstanceName}"); - options.Configuration = sessionService.ConnectionString; - options.InstanceName = sessionService.InstanceName; - }); + GXLogging.Info(log, $"Using Redis for Distributed session, ConnectionString:{sessionService.ConnectionString}, InstanceName: {sessionService.InstanceName}"); + + services.AddSingleton(sp => + new CustomRedisSessionStore(sessionService.ConnectionString, TimeSpan.FromMinutes(Preferences.SessionTimeout), sessionService.InstanceName)); + services.AddDataProtection().PersistKeysToStackExchangeRedis(ConnectionMultiplexer.Connect(sessionService.ConnectionString), DATA_PROTECTION_KEYS).SetApplicationName(sessionService.InstanceName); } }