275 lines
8.8 KiB
C#
275 lines
8.8 KiB
C#
using adas_core.Application.Services.Interfaces;
|
|
using adas_core.Domain.Models.AppSettings;
|
|
using adas_core.Domain.Utils;
|
|
using adas_core.Domain.Enums;
|
|
using adas_core.Domain.Models.GroupedObservations;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using Newtonsoft.Json;
|
|
using MongoDB.Bson;
|
|
using Newtonsoft.Json.Converters;
|
|
using Newtonsoft.Json.Serialization;
|
|
using StackExchange.Redis;
|
|
|
|
namespace adas_core.Application.Services.Caching
|
|
{
|
|
public class RedisService : ICacheService
|
|
{
|
|
private readonly ILogger<RedisService> _logger;
|
|
private readonly CacheSettings _cacheSettings;
|
|
private readonly LockManagerService _lockManager;
|
|
private ConnectionMultiplexer? _connection;
|
|
private IDatabase? _database;
|
|
private IServer? _server;
|
|
|
|
public IDatabase? Database => _database;
|
|
private bool _isRedisAvailable;
|
|
|
|
public RedisService(
|
|
IOptions<CacheSettings> options,
|
|
ILogger<RedisService> logger,
|
|
LockManagerService lockManager)
|
|
{
|
|
_cacheSettings = options.Value;
|
|
_logger = logger;
|
|
_lockManager = lockManager;
|
|
|
|
if (!string.IsNullOrEmpty(_cacheSettings.Redis.ConnectionString))
|
|
_ = InitializeRedisConnectionAsync();
|
|
}
|
|
|
|
|
|
// GET OR SET (string key)
|
|
public async Task<T> GetOrSetObjectAsync<T>(
|
|
string key,
|
|
Func<Task<T>> factory,
|
|
TimeSpan? ttl = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (!_isRedisAvailable)
|
|
return await factory();
|
|
|
|
var direct = await GetObjectAsync<T>(key);
|
|
|
|
if (direct is not null)
|
|
return direct;
|
|
|
|
return await _lockManager.WithLockAsync(
|
|
$"getorset:{key}",
|
|
TimeSpan.FromSeconds(5),
|
|
async () =>
|
|
{
|
|
var again = await GetObjectAsync<T>(key);
|
|
if (again is not null)
|
|
return again;
|
|
|
|
var created = await factory();
|
|
|
|
if (created != null)
|
|
await SetObjectAsync(key, created, ttl, true);
|
|
|
|
return created!;
|
|
},
|
|
cancellationToken);
|
|
}
|
|
|
|
public async Task<string?> GetOrSetValueAsync(
|
|
string key,
|
|
Func<Task<string>> loader,
|
|
TimeSpan? ttl = null)
|
|
{
|
|
if (!_isRedisAvailable)
|
|
return await loader();
|
|
|
|
var direct = GetValue(key);
|
|
if (direct is not null)
|
|
return direct;
|
|
|
|
return await _lockManager.WithLockAsync(
|
|
$"getorset:{key}",
|
|
TimeSpan.FromSeconds(5),
|
|
async () =>
|
|
{
|
|
var again = GetValue(key);
|
|
if (again is not null)
|
|
return again;
|
|
|
|
var created = await loader();
|
|
SetValue(key, created);
|
|
|
|
return created;
|
|
});
|
|
}
|
|
|
|
|
|
// GET OR SET (GroupedField + patientId)
|
|
private static string BuildGroupedKey(GroupedField gf, ObjectId patientId)
|
|
=> $"GroupedObs:{patientId}:{gf.Name}";
|
|
|
|
public async Task<T> GetOrSetObjectAsync<T>(
|
|
GroupedField groupedField,
|
|
ObjectId patientId,
|
|
Func<Task<T>> factory,
|
|
TimeSpan? ttl = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var key = BuildGroupedKey(groupedField, patientId);
|
|
|
|
if (!_isRedisAvailable)
|
|
return await factory();
|
|
|
|
var direct = await GetObjectAsync<T>(key);
|
|
if (direct is not null)
|
|
return direct;
|
|
|
|
return await _lockManager.WithLockAsync(
|
|
$"getorset:{key}",
|
|
TimeSpan.FromSeconds(5),
|
|
async () =>
|
|
{
|
|
var again = await GetObjectAsync<T>(key);
|
|
if (again is not null)
|
|
return again;
|
|
|
|
var created = await factory();
|
|
|
|
if (created != null)
|
|
await SetObjectAsync(key, created, ttl, true);
|
|
|
|
return created!;
|
|
},
|
|
cancellationToken);
|
|
}
|
|
|
|
|
|
// BASIC OPERATIONS
|
|
public void SetValue(string key, string value)
|
|
=> _database?.StringSet(key, value, GetEntityTtl(key), true);
|
|
|
|
public string? GetValue(string key)
|
|
{
|
|
var val = _database?.StringGet(key);
|
|
if (val.HasValue && ShouldRenewTtl(key))
|
|
_database?.KeyExpire(key, GetEntityTtl(key));
|
|
return val;
|
|
}
|
|
|
|
public async Task<T?> GetObjectAsync<T>(string key, bool updateExpiration = true)
|
|
{
|
|
if (!_isRedisAvailable)
|
|
return default;
|
|
|
|
var json = await _database!.StringGetAsync(key);
|
|
if (json.IsNullOrEmpty)
|
|
return default;
|
|
|
|
if (updateExpiration)
|
|
_database!.KeyExpire(key, GetEntityTtl(key));
|
|
|
|
var settings = new JsonSerializerSettings
|
|
{
|
|
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
|
Converters = new List<JsonConverter> { new StringEnumConverter(), new ObjectIdConverter() }
|
|
};
|
|
try
|
|
{
|
|
return JsonConvert.DeserializeObject<T>(json!, settings);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
_logger.LogError("Error deserializing object in RedisService {e}",e.ToString());
|
|
throw new Exception($"Error deserializing object in RedisService {e}", e);
|
|
}
|
|
}
|
|
|
|
public async Task SetObjectAsync<T>(
|
|
string key,
|
|
T obj,
|
|
bool updateExpiration = true)
|
|
=> await SetObjectAsync(key, obj, null, updateExpiration);
|
|
|
|
public async Task SetObjectAsync<T>(
|
|
string key,
|
|
T obj,
|
|
TimeSpan? ttlOverride,
|
|
bool updateExpiration)
|
|
{
|
|
if (!_isRedisAvailable)
|
|
return;
|
|
var settings = new JsonSerializerSettings
|
|
{
|
|
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
|
Converters = new List<JsonConverter> { new StringEnumConverter(), new ObjectIdConverter() }
|
|
};
|
|
var json = JsonConvert.SerializeObject(obj, settings);
|
|
await _database!.StringSetAsync(key, json, ttlOverride ?? GetEntityTtl(key), true);
|
|
}
|
|
|
|
public async Task DeleteObjectAsync(string key)
|
|
{
|
|
if (_isRedisAvailable)
|
|
await _database!.KeyDeleteAsync(key);
|
|
}
|
|
|
|
public async Task<long> DeleteByPatternAsync(string pattern)
|
|
{
|
|
if (!_isRedisAvailable || _server == null)
|
|
return 0;
|
|
|
|
var keys = _server.Keys(pattern: pattern).ToArray();
|
|
|
|
foreach (var key in keys)
|
|
await _database!.KeyDeleteAsync(key);
|
|
|
|
return keys.Length;
|
|
}
|
|
|
|
public void CleanCache()
|
|
=> _server?.FlushDatabase();
|
|
|
|
|
|
// TTL
|
|
private TimeSpan? GetEntityTtl(string key)
|
|
{
|
|
var entity = CacheKeyClassifier.Classify(key);
|
|
var ttl = _cacheSettings.Redis.Ttl;
|
|
|
|
int? seconds = entity switch
|
|
{
|
|
CacheEnum.EntityType.Patients => ttl.PatientsSeconds,
|
|
CacheEnum.EntityType.Displays => ttl.DisplaysSeconds,
|
|
_ => ttl.GlobalSeconds
|
|
};
|
|
|
|
return seconds > 0 ? TimeSpan.FromSeconds(seconds.Value) : null;
|
|
}
|
|
|
|
private bool ShouldRenewTtl(string key)
|
|
=> GetEntityTtl(key) != null;
|
|
|
|
|
|
// INITIALIZATION
|
|
private async Task InitializeRedisConnectionAsync()
|
|
{
|
|
_isRedisAvailable = false;
|
|
try
|
|
{
|
|
if (_cacheSettings.Redis.ConnectionString == null)
|
|
return;
|
|
|
|
_connection = await ConnectionMultiplexer.ConnectAsync(_cacheSettings.Redis.ConnectionString);
|
|
_database = _connection.GetDatabase();
|
|
_server = _connection.GetServer(_cacheSettings.Redis.ConnectionString);
|
|
|
|
_isRedisAvailable = true;
|
|
}
|
|
catch(Exception ex)
|
|
{
|
|
_logger.LogError( "Error Initializing Redis Connection. Exception:{ex}", ex.Message);
|
|
}
|
|
}
|
|
|
|
public Task<T?> GetObjectAsync<T>(string key, TimeSpan? ttlOverride, bool updateExpiration)
|
|
=> GetObjectAsync<T>(key, updateExpiration);
|
|
}
|
|
} |