// 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
}
}