244 lines
7.8 KiB
C#
244 lines
7.8 KiB
C#
using System.Text.Json;
|
|
using Microsoft.Extensions.Logging;
|
|
using StellaOps.Messaging.Abstractions;
|
|
using StackExchange.Redis;
|
|
|
|
namespace StellaOps.Messaging.Transport.Valkey;
|
|
|
|
/// <summary>
|
|
/// Valkey/Redis implementation of <see cref="ISetStore{TKey, TElement}"/>.
|
|
/// Uses set commands (SADD, SMEMBERS, SISMEMBER, SREM, etc.).
|
|
/// </summary>
|
|
public sealed class ValkeySetStore<TKey, TElement> : ISetStore<TKey, TElement>
|
|
where TKey : notnull
|
|
{
|
|
private readonly ValkeyConnectionFactory _connectionFactory;
|
|
private readonly string _name;
|
|
private readonly ILogger<ValkeySetStore<TKey, TElement>>? _logger;
|
|
private readonly JsonSerializerOptions _jsonOptions;
|
|
private readonly Func<TKey, string> _keySerializer;
|
|
|
|
public ValkeySetStore(
|
|
ValkeyConnectionFactory connectionFactory,
|
|
string name,
|
|
ILogger<ValkeySetStore<TKey, TElement>>? logger = null,
|
|
JsonSerializerOptions? jsonOptions = null,
|
|
Func<TKey, string>? 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)));
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public string ProviderName => "valkey";
|
|
|
|
/// <inheritdoc />
|
|
public async ValueTask<bool> 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);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async ValueTask<long> AddRangeAsync(
|
|
TKey setKey,
|
|
IEnumerable<TElement> 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);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async ValueTask<IReadOnlySet<TElement>> 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<TElement>();
|
|
|
|
foreach (var member in members)
|
|
{
|
|
if (!member.IsNullOrEmpty)
|
|
{
|
|
var element = Deserialize((string)member!);
|
|
if (element is not null)
|
|
{
|
|
result.Add(element);
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async ValueTask<bool> 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);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async ValueTask<bool> 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);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async ValueTask<long> RemoveRangeAsync(
|
|
TKey setKey,
|
|
IEnumerable<TElement> 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);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async ValueTask<bool> 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);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async ValueTask<long> 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);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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<TElement>(value, _jsonOptions);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Factory for creating Valkey set store instances.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public string ProviderName => "valkey";
|
|
|
|
/// <inheritdoc />
|
|
public ISetStore<TKey, TElement> Create<TKey, TElement>(string name)
|
|
where TKey : notnull
|
|
{
|
|
ArgumentNullException.ThrowIfNull(name);
|
|
return new ValkeySetStore<TKey, TElement>(
|
|
_connectionFactory,
|
|
name,
|
|
_loggerFactory?.CreateLogger<ValkeySetStore<TKey, TElement>>(),
|
|
_jsonOptions);
|
|
}
|
|
}
|