using System.Text.Json;
using Microsoft.Extensions.Logging;
using StellaOps.Messaging.Abstractions;
using StackExchange.Redis;
namespace StellaOps.Messaging.Transport.Valkey;
///
/// Valkey/Redis implementation of .
/// Uses set commands (SADD, SMEMBERS, SISMEMBER, SREM, etc.).
///
public sealed class ValkeySetStore : ISetStore
where TKey : notnull
{
private readonly ValkeyConnectionFactory _connectionFactory;
private readonly string _name;
private readonly ILogger>? _logger;
private readonly JsonSerializerOptions _jsonOptions;
private readonly Func _keySerializer;
public ValkeySetStore(
ValkeyConnectionFactory connectionFactory,
string name,
ILogger>? logger = null,
JsonSerializerOptions? jsonOptions = null,
Func? keySerializer = null)
{
_connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory));
_name = name ?? throw new ArgumentNullException(nameof(name));
_logger = logger;
_jsonOptions = jsonOptions ?? new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false
};
_keySerializer = keySerializer ?? (key => key?.ToString() ?? throw new ArgumentNullException(nameof(key)));
}
///
public string ProviderName => "valkey";
///
public async ValueTask AddAsync(
TKey setKey,
TElement element,
CancellationToken cancellationToken = default)
{
var redisKey = BuildKey(setKey);
var db = await _connectionFactory.GetDatabaseAsync(cancellationToken).ConfigureAwait(false);
var serialized = Serialize(element);
return await db.SetAddAsync(redisKey, serialized).ConfigureAwait(false);
}
///
public async ValueTask AddRangeAsync(
TKey setKey,
IEnumerable elements,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(elements);
var redisKey = BuildKey(setKey);
var db = await _connectionFactory.GetDatabaseAsync(cancellationToken).ConfigureAwait(false);
var values = elements.Select(e => (RedisValue)Serialize(e)).ToArray();
if (values.Length == 0)
{
return 0;
}
return await db.SetAddAsync(redisKey, values).ConfigureAwait(false);
}
///
public async ValueTask> GetMembersAsync(
TKey setKey,
CancellationToken cancellationToken = default)
{
var redisKey = BuildKey(setKey);
var db = await _connectionFactory.GetDatabaseAsync(cancellationToken).ConfigureAwait(false);
var members = await db.SetMembersAsync(redisKey).ConfigureAwait(false);
var result = new HashSet();
foreach (var member in members)
{
if (!member.IsNullOrEmpty)
{
var element = Deserialize((string)member!);
if (element is not null)
{
result.Add(element);
}
}
}
return result;
}
///
public async ValueTask ContainsAsync(
TKey setKey,
TElement element,
CancellationToken cancellationToken = default)
{
var redisKey = BuildKey(setKey);
var db = await _connectionFactory.GetDatabaseAsync(cancellationToken).ConfigureAwait(false);
var serialized = Serialize(element);
return await db.SetContainsAsync(redisKey, serialized).ConfigureAwait(false);
}
///
public async ValueTask RemoveAsync(
TKey setKey,
TElement element,
CancellationToken cancellationToken = default)
{
var redisKey = BuildKey(setKey);
var db = await _connectionFactory.GetDatabaseAsync(cancellationToken).ConfigureAwait(false);
var serialized = Serialize(element);
return await db.SetRemoveAsync(redisKey, serialized).ConfigureAwait(false);
}
///
public async ValueTask RemoveRangeAsync(
TKey setKey,
IEnumerable elements,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(elements);
var redisKey = BuildKey(setKey);
var db = await _connectionFactory.GetDatabaseAsync(cancellationToken).ConfigureAwait(false);
var values = elements.Select(e => (RedisValue)Serialize(e)).ToArray();
if (values.Length == 0)
{
return 0;
}
return await db.SetRemoveAsync(redisKey, values).ConfigureAwait(false);
}
///
public async ValueTask DeleteAsync(
TKey setKey,
CancellationToken cancellationToken = default)
{
var redisKey = BuildKey(setKey);
var db = await _connectionFactory.GetDatabaseAsync(cancellationToken).ConfigureAwait(false);
return await db.KeyDeleteAsync(redisKey).ConfigureAwait(false);
}
///
public async ValueTask CountAsync(
TKey setKey,
CancellationToken cancellationToken = default)
{
var redisKey = BuildKey(setKey);
var db = await _connectionFactory.GetDatabaseAsync(cancellationToken).ConfigureAwait(false);
return await db.SetLengthAsync(redisKey).ConfigureAwait(false);
}
///
public async ValueTask SetExpirationAsync(
TKey setKey,
TimeSpan ttl,
CancellationToken cancellationToken = default)
{
var redisKey = BuildKey(setKey);
var db = await _connectionFactory.GetDatabaseAsync(cancellationToken).ConfigureAwait(false);
await db.KeyExpireAsync(redisKey, ttl).ConfigureAwait(false);
}
private string BuildKey(TKey setKey)
{
var keyString = _keySerializer(setKey);
return $"set:{_name}:{keyString}";
}
private string Serialize(TElement element)
{
// For primitive types, use ToString directly
if (element is string s)
{
return s;
}
return JsonSerializer.Serialize(element, _jsonOptions);
}
private TElement? Deserialize(string value)
{
// For string types, return directly
if (typeof(TElement) == typeof(string))
{
return (TElement)(object)value;
}
return JsonSerializer.Deserialize(value, _jsonOptions);
}
}
///
/// Factory for creating Valkey set store instances.
///
public sealed class ValkeySetStoreFactory : ISetStoreFactory
{
private readonly ValkeyConnectionFactory _connectionFactory;
private readonly ILoggerFactory? _loggerFactory;
private readonly JsonSerializerOptions? _jsonOptions;
public ValkeySetStoreFactory(
ValkeyConnectionFactory connectionFactory,
ILoggerFactory? loggerFactory = null,
JsonSerializerOptions? jsonOptions = null)
{
_connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory));
_loggerFactory = loggerFactory;
_jsonOptions = jsonOptions;
}
///
public string ProviderName => "valkey";
///
public ISetStore Create(string name)
where TKey : notnull
{
ArgumentNullException.ThrowIfNull(name);
return new ValkeySetStore(
_connectionFactory,
name,
_loggerFactory?.CreateLogger>(),
_jsonOptions);
}
}