// 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 { /// /// Represents a redis hash key, with members corresponding to the property names of a type /// /// Type of the hash fields public record struct RedisHashKey(RedisKey Inner) : IRedisTypedKey { /// /// Implicit conversion to typed redis key. /// /// Key to convert public static implicit operator RedisHashKey(string key) => new RedisHashKey(new RedisKey(key)); } /// /// Represents a typed Redis hash with given key/value types /// /// Type of the hash key /// Type of the hash value public record struct RedisHashKey(RedisKey Inner) : IRedisTypedKey { /// /// Implicit conversion to typed redis key. /// /// Key to convert public static implicit operator RedisHashKey(string key) => new RedisHashKey(new RedisKey(key)); } /// public readonly struct HashEntry { /// public readonly TName Name { get; } /// public readonly TValue Value { get; } /// /// Constructor /// /// /// public HashEntry(TName name, TValue value) { Name = name; Value = value; } /// /// Deconstructor helper method /// public void Deconstruct(out TName name, out TValue value) { name = Name; value = Value; } /// /// Implicit conversion to a /// /// public static implicit operator HashEntry(HashEntry entry) { return new HashEntry(RedisSerializer.Serialize(entry.Name), RedisSerializer.Serialize(entry.Value)); } /// /// Implicit conversion to a /// /// public static implicit operator KeyValuePair(HashEntry entry) { return new KeyValuePair(entry.Name, entry.Value); } } /// /// Extension methods for hashes /// public static class RedisHashKeyExtensions { /// /// Helper method to convert an array of hash entries into a dictionary /// public static async Task> ToDictionaryAsync(this Task[]> entries) where TName : notnull => (await entries).ToDictionary(x => x.Name, x => x.Value); /// /// Helper method to convert an array of hash entries into a dictionary /// public static async Task> ToDictionaryAsync(this Task[]> entries, IEqualityComparer? comparer) where TName : notnull => (await entries).ToDictionary(x => x.Name, x => x.Value, comparer); #region Conditions /// public static Condition HashEqual(this RedisHashKey key, Expression> 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)); } /// public static Condition HashEqual(this RedisHashKey key, TName name, TValue value) => Condition.HashEqual(key.Inner, RedisSerializer.Serialize(name), RedisSerializer.Serialize(value)); /// public static Condition HashExists(this RedisHashKey key, Expression> selector) { MemberExpression memberExpression = (selector.Body as MemberExpression) ?? throw new InvalidOperationException("Expression must be a property accessor"); return Condition.HashExists(key.Inner, memberExpression.Member.Name); } /// public static Condition HashExists(this RedisHashKey key, TName name) => Condition.HashExists(key.Inner, RedisSerializer.Serialize(name)); /// public static Condition HashLengthEqual(this RedisHashKey key, long length) => Condition.HashLengthEqual(key.Inner, length); /// public static Condition HashLengthGreaterThan(this RedisHashKey key, long length) => Condition.HashLengthGreaterThan(key.Inner, length); /// public static Condition HashLengthLessThan(this RedisHashKey key, long length) => Condition.HashLengthLessThan(key.Inner, length); /// public static Condition HashNotExists(this RedisHashKey key, Expression> selector) { MemberExpression memberExpression = (selector.Body as MemberExpression) ?? throw new InvalidOperationException("Expression must be a property accessor"); return Condition.HashNotExists(key.Inner, memberExpression.Member.Name); } /// public static Condition HashNotExists(this RedisHashKey key, TName name) => Condition.HashNotExists(key.Inner, RedisSerializer.Serialize(name)); /// public static Condition HashNotEqual(this RedisHashKey key, Expression> 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)); } /// public static Condition HashNotEqual(this RedisHashKey key, TName name, TValue value) => Condition.HashNotEqual(key.Inner, RedisSerializer.Serialize(name), RedisSerializer.Serialize(value)); #endregion #region HashDecrementAsync /// public static Task HashDecrementAsync(this IDatabaseAsync target, RedisHashKey key, Expression> 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); } /// public static Task HashDecrementAsync(this IDatabaseAsync target, RedisHashKey key, Expression> 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); } /// public static Task HashDecrementAsync(this IDatabaseAsync target, RedisHashKey key, TName name, long value = 1L, CommandFlags flags = CommandFlags.None) { return target.HashDecrementAsync(key.Inner, RedisSerializer.Serialize(name), value, flags); } /// public static Task HashDecrementAsync(this IDatabaseAsync target, RedisHashKey key, TName name, double value = 1.0, CommandFlags flags = CommandFlags.None) { return target.HashDecrementAsync(key.Inner, RedisSerializer.Serialize(name), value, flags); } #endregion #region HashDeleteAsync /// public static Task HashDeleteAsync(this IDatabaseAsync target, RedisHashKey key, Expression> 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); } /// public static Task HashDeleteAsync(this IDatabaseAsync target, RedisHashKey key, TName name, CommandFlags flags = CommandFlags.None) { return target.HashDeleteAsync(key.Inner, RedisSerializer.Serialize(name), flags); } /// public static Task HashDeleteAsync(this IDatabaseAsync target, RedisHashKey key, TName[] names, CommandFlags flags = CommandFlags.None) { return target.HashDeleteAsync(key.Inner, RedisSerializer.Serialize(names), flags); } #endregion #region HashExistsAsync /// public static Task HashExistsAsync(this IDatabaseAsync target, RedisHashKey key, TName name, CommandFlags flags = CommandFlags.None) { return target.HashExistsAsync(key.Inner, RedisSerializer.Serialize(name), flags); } #endregion #region HashGetAsync /// public static async Task HashGetAsync(this IDatabaseAsync target, RedisHashKey key, Expression>[] 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 recordInfo = HashRecordInfo.Instance; TRecord record = new TRecord(); for (int idx = 0; idx < selectors.Length; idx++) { recordInfo.SetProperty(record, names[idx], values[idx]); } return record; } /// public static Task HashGetAsync(this IDatabaseAsync target, RedisHashKey key, Expression> 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(); } /// public static Task HashGetAsync(this IDatabaseAsync target, RedisHashKey key, TName name, CommandFlags flags = CommandFlags.None) { return target.HashGetAsync(key.Inner, RedisSerializer.Serialize(name), flags).DeserializeAsync(); } /// public static Task HashGetAsync(this IDatabaseAsync target, RedisHashKey key, TName[] names, CommandFlags flags = CommandFlags.None) { return target.HashGetAsync(key.Inner, RedisSerializer.Serialize(names), flags).DeserializeAsync(); } #endregion #region HashGetAllAsync /// public static async Task[]> HashGetAllAsync(this IDatabaseAsync target, RedisHashKey key, CommandFlags flags = CommandFlags.None) { HashEntry[] entries = await target.HashGetAllAsync(key.Inner, flags); return Array.ConvertAll(entries, x => new HashEntry(RedisSerializer.Deserialize(x.Name)!, RedisSerializer.Deserialize(x.Value)!)); } /// public static async Task HashGetAllAsync(this IDatabaseAsync target, RedisHashKey key, CommandFlags flags = CommandFlags.None) where TRecord : new() { HashRecordInfo recordInfo = HashRecordInfo.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 /// public static Task HashIncrementAsync(this IDatabaseAsync target, RedisHashKey key, Expression> 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); } /// public static Task HashIncrementAsync(this IDatabaseAsync target, RedisHashKey key, Expression> 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); } /// public static Task HashIncrementAsync(this IDatabaseAsync target, RedisHashKey key, TName name, long value = 1L, CommandFlags flags = CommandFlags.None) { return target.HashIncrementAsync(key.Inner, RedisSerializer.Serialize(name), value, flags); } /// public static Task HashIncrementAsync(this IDatabaseAsync target, RedisHashKey key, TName name, double value = 1.0, CommandFlags flags = CommandFlags.None) { return target.HashIncrementAsync(key.Inner, RedisSerializer.Serialize(name), value, flags); } #endregion #region HashKeysAsync /// public static Task HashKeysAsync(this IDatabaseAsync target, RedisHashKey key, CommandFlags flags = CommandFlags.None) { return target.HashKeysAsync(key.Inner, flags).DeserializeAsync(); } #endregion #region HashLengthAsync /// public static Task HashLengthAsync(this IDatabaseAsync target, RedisHashKey key, CommandFlags flags = CommandFlags.None) { return target.HashLengthAsync(key, flags); } #endregion #region HashScanAsync /// public static async IAsyncEnumerable> HashScanAsync(this IDatabaseAsync target, RedisHashKey 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(RedisSerializer.Deserialize(entry.Name), RedisSerializer.Deserialize(entry.Value)); } } #endregion #region HashSetAsync class HashRecordInfo { public static HashRecordInfo Instance { get; } = new HashRecordInfo(); public IReadOnlyList> Properties { get; } public IReadOnlyDictionary> PropertiesByName { get; } public HashRecordInfo() { List> properties = new List>(); foreach (PropertyInfo propertyInfo in typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance)) { properties.Add(new HashPropertyInfo(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? propertyInfo; if (PropertiesByName.TryGetValue(nameStr, out propertyInfo)) { propertyInfo.SetValue(record, value); return true; } } return false; } } record class HashPropertyInfo(string Name, Func GetValue, Action 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)) ) { } } /// public static Task HashSetAsync(this IDatabaseAsync target, RedisHashKey key, Expression> 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); } /// public static Task HashSetAsync(this IDatabaseAsync target, RedisHashKey key, TRecord value, CommandFlags flags = CommandFlags.None) { HashRecordInfo recordInfo = HashRecordInfo.Instance; HashEntry[] entries = new HashEntry[recordInfo.Properties.Count]; for (int idx = 0; idx < recordInfo.Properties.Count; idx++) { HashPropertyInfo property = recordInfo.Properties[idx]; entries[idx] = new HashEntry(property.Name, property.GetValue(value)); } return target.HashSetAsync(key.Inner, entries, flags); } /// public static Task HashSetAsync(this IDatabaseAsync target, RedisHashKey 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); } /// public static Task HashSetAsync(this IDatabaseAsync target, RedisHashKey key, IEnumerable> entries, CommandFlags flags = CommandFlags.None) { return target.HashSetAsync(key.Inner, entries.Select(x => (HashEntry)x).ToArray(), flags); } #endregion #region HashValuesAsync /// public static Task HashValuesAsync(this IDatabaseAsync target, RedisHashKey key, CommandFlags flags = CommandFlags.None) { return target.HashValuesAsync(key.Inner, flags).DeserializeAsync(); } #endregion } }