Files
UnrealEngine/Engine/Source/Programs/Shared/EpicGames.Redis/RedisHashKey.cs
2025-05-18 13:04:45 +08:00

465 lines
21 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading.Tasks;
using StackExchange.Redis;
namespace EpicGames.Redis
{
/// <summary>
/// Represents a redis hash key, with members corresponding to the property names of a type
/// </summary>
/// <typeparam name="T">Type of the hash fields</typeparam>
public record struct RedisHashKey<T>(RedisKey Inner) : IRedisTypedKey
{
/// <summary>
/// Implicit conversion to typed redis key.
/// </summary>
/// <param name="key">Key to convert</param>
public static implicit operator RedisHashKey<T>(string key) => new RedisHashKey<T>(new RedisKey(key));
}
/// <summary>
/// Represents a typed Redis hash with given key/value types
/// </summary>
/// <typeparam name="TName">Type of the hash key</typeparam>
/// <typeparam name="TValue">Type of the hash value</typeparam>
public record struct RedisHashKey<TName, TValue>(RedisKey Inner) : IRedisTypedKey
{
/// <summary>
/// Implicit conversion to typed redis key.
/// </summary>
/// <param name="key">Key to convert</param>
public static implicit operator RedisHashKey<TName, TValue>(string key) => new RedisHashKey<TName, TValue>(new RedisKey(key));
}
/// <inheritdoc cref="HashEntry"/>
public readonly struct HashEntry<TName, TValue>
{
/// <inheritdoc cref="HashEntry.Name"/>
public readonly TName Name { get; }
/// <inheritdoc cref="HashEntry.Value"/>
public readonly TValue Value { get; }
/// <summary>
/// Constructor
/// </summary>
/// <param name="name"></param>
/// <param name="value"></param>
public HashEntry(TName name, TValue value)
{
Name = name;
Value = value;
}
/// <summary>
/// Deconstructor helper method
/// </summary>
public void Deconstruct(out TName name, out TValue value)
{
name = Name;
value = Value;
}
/// <summary>
/// Implicit conversion to a <see cref="HashEntry"/>
/// </summary>
/// <param name="entry"></param>
public static implicit operator HashEntry(HashEntry<TName, TValue> entry)
{
return new HashEntry(RedisSerializer.Serialize(entry.Name), RedisSerializer.Serialize(entry.Value));
}
/// <summary>
/// Implicit conversion to a <see cref="KeyValuePair{TName, TValue}"/>
/// </summary>
/// <param name="entry"></param>
public static implicit operator KeyValuePair<TName, TValue>(HashEntry<TName, TValue> entry)
{
return new KeyValuePair<TName, TValue>(entry.Name, entry.Value);
}
}
/// <summary>
/// Extension methods for hashes
/// </summary>
public static class RedisHashKeyExtensions
{
/// <summary>
/// Helper method to convert an array of hash entries into a dictionary
/// </summary>
public static async Task<Dictionary<TName, TValue>> ToDictionaryAsync<TName, TValue>(this Task<HashEntry<TName, TValue>[]> entries) where TName : notnull
=> (await entries).ToDictionary(x => x.Name, x => x.Value);
/// <summary>
/// Helper method to convert an array of hash entries into a dictionary
/// </summary>
public static async Task<Dictionary<TName, TValue>> ToDictionaryAsync<TName, TValue>(this Task<HashEntry<TName, TValue>[]> entries, IEqualityComparer<TName>? comparer) where TName : notnull
=> (await entries).ToDictionary(x => x.Name, x => x.Value, comparer);
#region Conditions
/// <inheritdoc cref="Condition.HashEqual(RedisKey, RedisValue, RedisValue)"/>
public static Condition HashEqual<TRecord, TValue>(this RedisHashKey<TRecord> key, Expression<Func<TRecord, TValue>> selector, TValue value)
{
MemberExpression memberExpression = (selector.Body as MemberExpression) ?? throw new InvalidOperationException("Expression must be a property accessor");
return Condition.HashEqual(key.Inner, memberExpression.Member.Name, RedisSerializer.Serialize(value));
}
/// <inheritdoc cref="Condition.HashEqual(RedisKey, RedisValue, RedisValue)"/>
public static Condition HashEqual<TName, TValue>(this RedisHashKey<TName, TValue> key, TName name, TValue value)
=> Condition.HashEqual(key.Inner, RedisSerializer.Serialize(name), RedisSerializer.Serialize(value));
/// <inheritdoc cref="Condition.HashExists(RedisKey, RedisValue)"/>
public static Condition HashExists<TRecord, TValue>(this RedisHashKey<TRecord> key, Expression<Func<TRecord, TValue>> selector)
{
MemberExpression memberExpression = (selector.Body as MemberExpression) ?? throw new InvalidOperationException("Expression must be a property accessor");
return Condition.HashExists(key.Inner, memberExpression.Member.Name);
}
/// <inheritdoc cref="Condition.HashExists(RedisKey, RedisValue)"/>
public static Condition HashExists<TName, TValue>(this RedisHashKey<TName, TValue> key, TName name)
=> Condition.HashExists(key.Inner, RedisSerializer.Serialize(name));
/// <inheritdoc cref="Condition.HashLengthEqual(RedisKey, Int64)"/>
public static Condition HashLengthEqual<TName, TValue>(this RedisHashKey<TName, TValue> key, long length)
=> Condition.HashLengthEqual(key.Inner, length);
/// <inheritdoc cref="Condition.HashLengthGreaterThan(RedisKey, Int64)"/>
public static Condition HashLengthGreaterThan<TName, TValue>(this RedisHashKey<TName, TValue> key, long length)
=> Condition.HashLengthGreaterThan(key.Inner, length);
/// <inheritdoc cref="Condition.HashLengthLessThan(RedisKey, Int64)"/>
public static Condition HashLengthLessThan<TName, TValue>(this RedisHashKey<TName, TValue> key, long length)
=> Condition.HashLengthLessThan(key.Inner, length);
/// <inheritdoc cref="Condition.HashNotExists(RedisKey, RedisValue)"/>
public static Condition HashNotExists<TRecord, TValue>(this RedisHashKey<TRecord> key, Expression<Func<TRecord, TValue>> selector)
{
MemberExpression memberExpression = (selector.Body as MemberExpression) ?? throw new InvalidOperationException("Expression must be a property accessor");
return Condition.HashNotExists(key.Inner, memberExpression.Member.Name);
}
/// <inheritdoc cref="Condition.HashNotExists(RedisKey, RedisValue)"/>
public static Condition HashNotExists<TName, TValue>(this RedisHashKey<TName, TValue> key, TName name)
=> Condition.HashNotExists(key.Inner, RedisSerializer.Serialize(name));
/// <inheritdoc cref="Condition.HashEqual(RedisKey, RedisValue, RedisValue)"/>
public static Condition HashNotEqual<TRecord, TValue>(this RedisHashKey<TRecord> key, Expression<Func<TRecord, TValue>> selector, TValue value)
{
MemberExpression memberExpression = (selector.Body as MemberExpression) ?? throw new InvalidOperationException("Expression must be a property accessor");
return Condition.HashNotEqual(key.Inner, memberExpression.Member.Name, RedisSerializer.Serialize(value));
}
/// <inheritdoc cref="Condition.HashNotEqual(RedisKey, RedisValue, RedisValue)"/>
public static Condition HashNotEqual<TName, TValue>(this RedisHashKey<TName, TValue> key, TName name, TValue value)
=> Condition.HashNotEqual(key.Inner, RedisSerializer.Serialize(name), RedisSerializer.Serialize(value));
#endregion
#region HashDecrementAsync
/// <inheritdoc cref="IDatabaseAsync.HashDecrementAsync(RedisKey, RedisValue, Int64, CommandFlags)"/>
public static Task<long> HashDecrementAsync<TRecord>(this IDatabaseAsync target, RedisHashKey<TRecord> key, Expression<Func<TRecord, long>> selector, long value = 1L, CommandFlags flags = CommandFlags.None)
{
MemberExpression memberExpression = (selector.Body as MemberExpression) ?? throw new InvalidOperationException("Expression must be a property accessor");
return target.HashDecrementAsync(key.Inner, memberExpression.Member.Name, value, flags);
}
/// <inheritdoc cref="IDatabaseAsync.HashDecrementAsync(RedisKey, RedisValue, Double, CommandFlags)"/>
public static Task<double> HashDecrementAsync<TRecord>(this IDatabaseAsync target, RedisHashKey<TRecord> key, Expression<Func<TRecord, double>> selector, double value = 1.0, CommandFlags flags = CommandFlags.None)
{
MemberExpression memberExpression = (selector.Body as MemberExpression) ?? throw new InvalidOperationException("Expression must be a property accessor");
return target.HashDecrementAsync(key.Inner, memberExpression.Member.Name, value, flags);
}
/// <inheritdoc cref="IDatabaseAsync.HashDecrementAsync(RedisKey, RedisValue, Int64, CommandFlags)"/>
public static Task<long> HashDecrementAsync<TName>(this IDatabaseAsync target, RedisHashKey<TName, long> key, TName name, long value = 1L, CommandFlags flags = CommandFlags.None)
{
return target.HashDecrementAsync(key.Inner, RedisSerializer.Serialize<TName>(name), value, flags);
}
/// <inheritdoc cref="IDatabaseAsync.HashDecrementAsync(RedisKey, RedisValue, Double, CommandFlags)"/>
public static Task<double> HashDecrementAsync<TName>(this IDatabaseAsync target, RedisHashKey<TName, double> key, TName name, double value = 1.0, CommandFlags flags = CommandFlags.None)
{
return target.HashDecrementAsync(key.Inner, RedisSerializer.Serialize<TName>(name), value, flags);
}
#endregion
#region HashDeleteAsync
/// <inheritdoc cref="IDatabaseAsync.HashDeleteAsync(RedisKey, RedisValue, CommandFlags)"/>
public static Task<bool> HashDeleteAsync<TRecord>(this IDatabaseAsync target, RedisHashKey<TRecord> key, Expression<Func<TRecord, double>> selector, CommandFlags flags = CommandFlags.None)
{
MemberExpression memberExpression = (selector.Body as MemberExpression) ?? throw new InvalidOperationException("Expression must be a property accessor");
return target.HashDeleteAsync(key.Inner, memberExpression.Member.Name, flags);
}
/// <inheritdoc cref="IDatabaseAsync.HashDeleteAsync(RedisKey, RedisValue, CommandFlags)"/>
public static Task<bool> HashDeleteAsync<TName, TValue>(this IDatabaseAsync target, RedisHashKey<TName, TValue> key, TName name, CommandFlags flags = CommandFlags.None)
{
return target.HashDeleteAsync(key.Inner, RedisSerializer.Serialize(name), flags);
}
/// <inheritdoc cref="IDatabaseAsync.HashDeleteAsync(RedisKey, RedisValue[], CommandFlags)"/>
public static Task<long> HashDeleteAsync<TName, TValue>(this IDatabaseAsync target, RedisHashKey<TName, TValue> key, TName[] names, CommandFlags flags = CommandFlags.None)
{
return target.HashDeleteAsync(key.Inner, RedisSerializer.Serialize(names), flags);
}
#endregion
#region HashExistsAsync
/// <inheritdoc cref="IDatabaseAsync.HashExistsAsync(RedisKey, RedisValue, CommandFlags)"/>
public static Task<bool> HashExistsAsync<TName, TValue>(this IDatabaseAsync target, RedisHashKey<TName, TValue> key, TName name, CommandFlags flags = CommandFlags.None)
{
return target.HashExistsAsync(key.Inner, RedisSerializer.Serialize(name), flags);
}
#endregion
#region HashGetAsync
/// <inheritdoc cref="IDatabaseAsync.HashGetAsync(RedisKey, RedisValue[], CommandFlags)"/>
public static async Task<TRecord> HashGetAsync<TRecord>(this IDatabaseAsync target, RedisHashKey<TRecord> key, Expression<Func<TRecord, object>>[] selectors, CommandFlags flags = CommandFlags.None) where TRecord : new()
{
RedisValue[] names = new RedisValue[selectors.Length];
for (int idx = 0; idx < selectors.Length; idx++)
{
Expression expr = selectors[idx].Body;
if (expr is UnaryExpression unaryExpr && unaryExpr.NodeType == ExpressionType.Convert)
{
expr = unaryExpr.Operand;
}
MemberExpression memberExpression = (expr as MemberExpression) ?? throw new InvalidOperationException("Expression must be a property accessor");
names[idx] = memberExpression.Member.Name;
}
RedisValue[] values = await target.HashGetAsync(key.Inner, RedisSerializer.Serialize(names), flags);
HashRecordInfo<TRecord> recordInfo = HashRecordInfo<TRecord>.Instance;
TRecord record = new TRecord();
for (int idx = 0; idx < selectors.Length; idx++)
{
recordInfo.SetProperty(record, names[idx], values[idx]);
}
return record;
}
/// <inheritdoc cref="IDatabaseAsync.HashGetAsync(RedisKey, RedisValue, CommandFlags)"/>
public static Task<TValue> HashGetAsync<TRecord, TValue>(this IDatabaseAsync target, RedisHashKey<TRecord> key, Expression<Func<TRecord, TValue>> selector, CommandFlags flags = CommandFlags.None)
{
MemberExpression memberExpression = (selector.Body as MemberExpression) ?? throw new InvalidOperationException("Expression must be a property accessor");
return target.HashGetAsync(key.Inner, memberExpression.Member.Name, flags).DeserializeAsync<TValue>();
}
/// <inheritdoc cref="IDatabaseAsync.HashGetAsync(RedisKey, RedisValue, CommandFlags)"/>
public static Task<TValue> HashGetAsync<TName, TValue>(this IDatabaseAsync target, RedisHashKey<TName, TValue> key, TName name, CommandFlags flags = CommandFlags.None)
{
return target.HashGetAsync(key.Inner, RedisSerializer.Serialize(name), flags).DeserializeAsync<TValue>();
}
/// <inheritdoc cref="IDatabaseAsync.HashGetAsync(RedisKey, RedisValue[], CommandFlags)"/>
public static Task<TValue[]> HashGetAsync<TName, TValue>(this IDatabaseAsync target, RedisHashKey<TName, TValue> key, TName[] names, CommandFlags flags = CommandFlags.None)
{
return target.HashGetAsync(key.Inner, RedisSerializer.Serialize(names), flags).DeserializeAsync<TValue>();
}
#endregion
#region HashGetAllAsync
/// <inheritdoc cref="IDatabaseAsync.HashGetAllAsync(RedisKey, CommandFlags)"/>
public static async Task<HashEntry<TName, TValue>[]> HashGetAllAsync<TName, TValue>(this IDatabaseAsync target, RedisHashKey<TName, TValue> key, CommandFlags flags = CommandFlags.None)
{
HashEntry[] entries = await target.HashGetAllAsync(key.Inner, flags);
return Array.ConvertAll(entries, x => new HashEntry<TName, TValue>(RedisSerializer.Deserialize<TName>(x.Name)!, RedisSerializer.Deserialize<TValue>(x.Value)!));
}
/// <inheritdoc cref="IDatabaseAsync.HashGetAllAsync(RedisKey, CommandFlags)"/>
public static async Task<TRecord> HashGetAllAsync<TRecord>(this IDatabaseAsync target, RedisHashKey<TRecord> key, CommandFlags flags = CommandFlags.None) where TRecord : new()
{
HashRecordInfo<TRecord> recordInfo = HashRecordInfo<TRecord>.Instance;
TRecord value = new TRecord();
HashEntry[] entries = await target.HashGetAllAsync(key.Inner, flags);
foreach (HashEntry entry in entries)
{
recordInfo.SetProperty(value, entry.Name, entry.Value);
}
return value;
}
#endregion
#region HashIncrementAsync
/// <inheritdoc cref="IDatabaseAsync.HashIncrementAsync(RedisKey, RedisValue, Int64, CommandFlags)"/>
public static Task<long> HashIncrementAsync<TRecord>(this IDatabaseAsync target, RedisHashKey<TRecord> key, Expression<Func<TRecord, long>> selector, long value = 1L, CommandFlags flags = CommandFlags.None)
{
MemberExpression memberExpression = (selector.Body as MemberExpression) ?? throw new InvalidOperationException("Expression must be a property accessor");
return target.HashIncrementAsync(key.Inner, memberExpression.Member.Name, value, flags);
}
/// <inheritdoc cref="IDatabaseAsync.HashIncrementAsync(RedisKey, RedisValue, Double, CommandFlags)"/>
public static Task<double> HashIncrementAsync<TRecord>(this IDatabaseAsync target, RedisHashKey<TRecord> key, Expression<Func<TRecord, double>> selector, double value = 1.0, CommandFlags flags = CommandFlags.None)
{
MemberExpression memberExpression = (selector.Body as MemberExpression) ?? throw new InvalidOperationException("Expression must be a property accessor");
return target.HashIncrementAsync(key.Inner, memberExpression.Member.Name, value, flags);
}
/// <inheritdoc cref="IDatabaseAsync.HashIncrementAsync(RedisKey, RedisValue, Int64, CommandFlags)"/>
public static Task<long> HashIncrementAsync<TName>(this IDatabaseAsync target, RedisHashKey<TName, long> key, TName name, long value = 1L, CommandFlags flags = CommandFlags.None)
{
return target.HashIncrementAsync(key.Inner, RedisSerializer.Serialize<TName>(name), value, flags);
}
/// <inheritdoc cref="IDatabaseAsync.HashIncrementAsync(RedisKey, RedisValue, Double, CommandFlags)"/>
public static Task<double> HashIncrementAsync<TName>(this IDatabaseAsync target, RedisHashKey<TName, double> key, TName name, double value = 1.0, CommandFlags flags = CommandFlags.None)
{
return target.HashIncrementAsync(key.Inner, RedisSerializer.Serialize<TName>(name), value, flags);
}
#endregion
#region HashKeysAsync
/// <inheritdoc cref="IDatabaseAsync.HashKeysAsync(RedisKey, CommandFlags)"/>
public static Task<TName[]> HashKeysAsync<TName, TValue>(this IDatabaseAsync target, RedisHashKey<TName, TValue> key, CommandFlags flags = CommandFlags.None)
{
return target.HashKeysAsync(key.Inner, flags).DeserializeAsync<TName>();
}
#endregion
#region HashLengthAsync
/// <inheritdoc cref="IDatabaseAsync.HashLengthAsync(RedisKey, CommandFlags)"/>
public static Task<long> HashLengthAsync<TName, TValue>(this IDatabaseAsync target, RedisHashKey<TName, TValue> key, CommandFlags flags = CommandFlags.None)
{
return target.HashLengthAsync(key, flags);
}
#endregion
#region HashScanAsync
/// <inheritdoc cref="IDatabaseAsync.HashScanAsync(RedisKey, RedisValue, Int32, Int64, Int32, CommandFlags)"/>
public static async IAsyncEnumerable<HashEntry<TName, TValue>> HashScanAsync<TName, TValue>(this IDatabaseAsync target, RedisHashKey<TName, TValue> key, RedisValue pattern, int pageSize = 250, long cursor = 0, int pageOffset = 0, CommandFlags flags = CommandFlags.None)
{
await foreach (HashEntry entry in target.HashScanAsync(key.Inner, pattern, pageSize, cursor, pageOffset, flags))
{
yield return new HashEntry<TName, TValue>(RedisSerializer.Deserialize<TName>(entry.Name), RedisSerializer.Deserialize<TValue>(entry.Value));
}
}
#endregion
#region HashSetAsync
class HashRecordInfo<T>
{
public static HashRecordInfo<T> Instance { get; } = new HashRecordInfo<T>();
public IReadOnlyList<HashPropertyInfo<T>> Properties { get; }
public IReadOnlyDictionary<string, HashPropertyInfo<T>> PropertiesByName { get; }
public HashRecordInfo()
{
List<HashPropertyInfo<T>> properties = new List<HashPropertyInfo<T>>();
foreach (PropertyInfo propertyInfo in typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
properties.Add(new HashPropertyInfo<T>(propertyInfo));
}
Properties = properties;
PropertiesByName = properties.ToDictionary(x => x.Name, x => x);
}
public bool SetProperty(T record, RedisValue name, RedisValue value)
{
string? nameStr = (string?)name;
if (nameStr != null)
{
HashPropertyInfo<T>? propertyInfo;
if (PropertiesByName.TryGetValue(nameStr, out propertyInfo))
{
propertyInfo.SetValue(record, value);
return true;
}
}
return false;
}
}
record class HashPropertyInfo<T>(string Name, Func<T, RedisValue> GetValue, Action<T, RedisValue> SetValue)
{
public HashPropertyInfo(PropertyInfo propertyInfo)
: this(
propertyInfo.Name,
record => RedisSerializer.Serialize(propertyInfo.GetValue(record), propertyInfo.PropertyType),
(record, value) => propertyInfo.SetValue(record, RedisSerializer.Deserialize(value, propertyInfo.PropertyType))
)
{ }
}
/// <inheritdoc cref="IDatabaseAsync.HashSetAsync(RedisKey, RedisValue, RedisValue, When, CommandFlags)"/>
public static Task<bool> HashSetAsync<TRecord, TValue>(this IDatabaseAsync target, RedisHashKey<TRecord> key, Expression<Func<TRecord, TValue>> selector, TValue value, When when = When.Always, CommandFlags flags = CommandFlags.None)
{
MemberExpression memberExpression = (selector.Body as MemberExpression) ?? throw new InvalidOperationException("Expression must be a property accessor");
return target.HashSetAsync(key.Inner, memberExpression.Member.Name, RedisSerializer.Serialize(value), when, flags);
}
/// <inheritdoc cref="IDatabaseAsync.HashSetAsync(RedisKey, HashEntry[], CommandFlags)"/>
public static Task HashSetAsync<TRecord>(this IDatabaseAsync target, RedisHashKey<TRecord> key, TRecord value, CommandFlags flags = CommandFlags.None)
{
HashRecordInfo<TRecord> recordInfo = HashRecordInfo<TRecord>.Instance;
HashEntry[] entries = new HashEntry[recordInfo.Properties.Count];
for (int idx = 0; idx < recordInfo.Properties.Count; idx++)
{
HashPropertyInfo<TRecord> property = recordInfo.Properties[idx];
entries[idx] = new HashEntry(property.Name, property.GetValue(value));
}
return target.HashSetAsync(key.Inner, entries, flags);
}
/// <inheritdoc cref="IDatabaseAsync.HashSetAsync(RedisKey, RedisValue, RedisValue, When, CommandFlags)"/>
public static Task<bool> HashSetAsync<TName, TValue>(this IDatabaseAsync target, RedisHashKey<TName, TValue> key, TName name, TValue value, When when = When.Always, CommandFlags flags = CommandFlags.None)
{
return target.HashSetAsync(key.Inner, RedisSerializer.Serialize(name), RedisSerializer.Serialize(value), when, flags);
}
/// <inheritdoc cref="IDatabaseAsync.HashSetAsync(RedisKey, HashEntry[], CommandFlags)"/>
public static Task HashSetAsync<TName, TValue>(this IDatabaseAsync target, RedisHashKey<TName, TValue> key, IEnumerable<HashEntry<TName, TValue>> entries, CommandFlags flags = CommandFlags.None)
{
return target.HashSetAsync(key.Inner, entries.Select(x => (HashEntry)x).ToArray(), flags);
}
#endregion
#region HashValuesAsync
/// <inheritdoc cref="IDatabaseAsync.HashValuesAsync(RedisKey, CommandFlags)"/>
public static Task<TValue[]> HashValuesAsync<TName, TValue>(this IDatabaseAsync target, RedisHashKey<TName, TValue> key, CommandFlags flags = CommandFlags.None)
{
return target.HashValuesAsync(key.Inner, flags).DeserializeAsync<TValue>();
}
#endregion
}
}