// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.Reflection; using System.Text.Json; using System.Threading.Tasks; using EpicGames.Core; using EpicGames.Redis.Converters; using EpicGames.Serialization; using Google.Protobuf; using ProtoBuf; using StackExchange.Redis; namespace EpicGames.Redis { /// /// Attribute specifying the converter type to use for a class /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] public sealed class RedisConverterAttribute : Attribute { /// /// Type of the converter to use /// public Type ConverterType { get; } /// /// Constructor /// /// The converter type public RedisConverterAttribute(Type converterType) { ConverterType = converterType; } } /// /// Converter to and from RedisValue types /// public interface IRedisConverter { /// /// Serailize an object to a RedisValue /// /// /// RedisValue ToRedisValue(T value); /// /// Deserialize an object from a RedisValue /// /// /// T FromRedisValue(RedisValue value); } /// /// Redis serializer that uses compact binary to serialize objects /// /// public sealed class RedisCbConverter : IRedisConverter { /// public RedisValue ToRedisValue(T value) { return CbSerializer.Serialize(value).GetView(); } /// public T FromRedisValue(RedisValue value) { return CbSerializer.Deserialize(new CbField((byte[])value!)); } } /// /// Redis serializer that uses JSON to serialize objects /// public sealed class RedisJsonConverter : IRedisConverter { static readonly JsonSerializerOptions s_options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; /// public RedisValue ToRedisValue(T value) { return JsonSerializer.Serialize(value, s_options); } /// public T FromRedisValue(RedisValue value) { return JsonSerializer.Deserialize((string?)value ?? String.Empty, s_options) ?? throw new FormatException("Expected non-null value"); } } /// /// Handles serialization of types to RedisValue instances /// public static class RedisSerializer { class RedisStringConverter : IRedisConverter { readonly TypeConverter _typeConverter; public RedisStringConverter(TypeConverter typeConverter) { _typeConverter = typeConverter; } public RedisValue ToRedisValue(T value) => (string?)_typeConverter.ConvertTo(value, typeof(string)); public T FromRedisValue(RedisValue value) => (T)_typeConverter.ConvertFrom((string)value!)!; } class RedisUtf8StringConverter : IRedisConverter { readonly TypeConverter _typeConverter; public RedisUtf8StringConverter(TypeConverter typeConverter) { _typeConverter = typeConverter; } public RedisValue ToRedisValue(T value) => ((Utf8String)_typeConverter.ConvertTo(value, typeof(Utf8String))!).Memory; public T FromRedisValue(RedisValue value) => (T)_typeConverter.ConvertFrom(new Utf8String((ReadOnlyMemory)value!))!; } class RedisNativeConverter : IRedisConverter { readonly Func _fromRedisValueFunc; readonly Func _toRedisValueFunc; public RedisNativeConverter(Func fromRedisValueFunc, Func toRedisValueFunc) { _fromRedisValueFunc = fromRedisValueFunc; _toRedisValueFunc = toRedisValueFunc; } public T FromRedisValue(RedisValue value) => _fromRedisValueFunc(value); public RedisValue ToRedisValue(T value) => _toRedisValueFunc(value); } static readonly Dictionary s_nativeConverters = CreateNativeConverterLookup(); static Dictionary CreateNativeConverterLookup() { KeyValuePair[] converters = { CreateNativeConverter(x => x, x => x), CreateNativeConverter(x => (bool)x, x => x), CreateNativeConverter(x => (int)x, x => x), CreateNativeConverter(x => (int?)x, x => x), CreateNativeConverter(x => (uint)x, x => x), CreateNativeConverter(x => (uint?)x, x => x), CreateNativeConverter(x => (long)x, x => x), CreateNativeConverter(x => (long?)x, x => x), CreateNativeConverter(x => (ulong)x, x => x), CreateNativeConverter(x => (ulong?)x, x => x), CreateNativeConverter(x => (double)x, x => x), CreateNativeConverter(x => (double?)x, x => x), CreateNativeConverter(x => (ReadOnlyMemory)x, x => x), CreateNativeConverter(x => (byte[])x!, x => x), CreateNativeConverter(x => (string)x!, x => x), CreateNativeConverter(x => new DateTime((long)x, DateTimeKind.Utc), x => x.ToUniversalTime().Ticks) }; return new Dictionary(converters); } static KeyValuePair CreateNativeConverter(Func fromRedisValueFunc, Func toRedisValueFunc) { return new KeyValuePair(typeof(T), new RedisNativeConverter(fromRedisValueFunc, toRedisValueFunc)); } static readonly Dictionary s_typeToConverterType = new Dictionary(); class RedisObjectConverter : IRedisConverter { public object FromRedisValue(RedisValue value) => GetConverter().FromRedisValue(value)!; public RedisValue ToRedisValue(object value) => GetConverter().ToRedisValue((T)value); } static readonly ConcurrentDictionary> s_typeToObjectConverter = new ConcurrentDictionary>(); /// /// Register a custom converter for a particular type /// public static void RegisterConverter() where TConverter : IRedisConverter { lock (s_typeToConverterType) { if (!s_typeToConverterType.TryGetValue(typeof(T), out Type? converterType) || converterType != typeof(TConverter)) { s_typeToConverterType.Add(typeof(T), typeof(TConverter)); } } } /// /// Creates a converter for a given type /// static IRedisConverter CreateConverter() { Type type = typeof(T); // Check for a registered converter type lock (s_typeToConverterType) { if (s_typeToConverterType.TryGetValue(type, out Type? converterType)) { return (IRedisConverter)Activator.CreateInstance(converterType)!; } } // Check for a custom converter RedisConverterAttribute? attribute = type.GetCustomAttribute(); if (attribute != null) { Type converterType = attribute.ConverterType; if (converterType.IsGenericTypeDefinition) { converterType = converterType.MakeGenericType(type); } return (IRedisConverter)Activator.CreateInstance(converterType)!; } // Check for known basic types object? nativeConverter; if (s_nativeConverters.TryGetValue(typeof(T), out nativeConverter)) { return (IRedisConverter)nativeConverter; } // Check if the type is a protobuf message if (type.IsAssignableTo(typeof(IMessage))) { Type converterType = typeof(RedisProtobufConverter<>).MakeGenericType(type); return (IRedisConverter)Activator.CreateInstance(converterType)!; } // Check if the type supports protobuf serialization ProtoContractAttribute? protoAttribute = type.GetCustomAttribute(); if (protoAttribute != null) { return new RedisProtobufNetConverter(); } // Check if there's a regular converter we can use to convert to/from a string TypeConverter? converter = TypeDescriptor.GetConverter(type); if (converter != null) { if (converter.CanConvertFrom(typeof(Utf8String)) && converter.CanConvertTo(typeof(Utf8String))) { return new RedisUtf8StringConverter(converter); } if (converter.CanConvertFrom(typeof(string)) && converter.CanConvertTo(typeof(string))) { return new RedisStringConverter(converter); } } // If it's a compound type, try to create a class converter if (type.IsClass) { return new RedisClassConverter(); } // Otherwise fail throw new Exception($"Unable to find Redis converter for {type.Name}"); } /// /// Static class for caching converter lookups /// /// class CachedConverter { public static IRedisConverter Converter = CreateConverter(); } /// /// Gets the converter for a particular type /// /// /// public static IRedisConverter GetConverter() { return CachedConverter.Converter; } /// /// Gets a type converter which casts to/from an object value /// /// The concrete type for the converter /// public static IRedisConverter GetObjectConverter(Type type) { IRedisConverter? converter; if (!s_typeToObjectConverter.TryGetValue(type, out converter)) { converter = s_typeToObjectConverter.GetOrAdd(type, (IRedisConverter)Activator.CreateInstance(typeof(RedisObjectConverter<>).MakeGenericType(type))!); } return converter; } /// /// Serialize an object to a /// /// /// Type of the object /// public static RedisValue Serialize(object? value, Type type) { return GetObjectConverter(type).ToRedisValue(value!); } /// /// Serialize an object to a /// /// /// /// public static RedisValue Serialize(T value) { return CachedConverter.Converter.ToRedisValue(value); } /// /// Serialize an object to a /// /// /// /// public static RedisValue[] Serialize(T[] inputs) { RedisValue[] outputs = new RedisValue[inputs.Length]; for (int idx = 0; idx < inputs.Length; idx++) { outputs[idx] = Serialize(inputs[idx]); } return outputs; } /// /// Deserialize a /// /// /// Type of the value to return /// public static object? Deserialize(RedisValue value, Type type) { return GetObjectConverter(type).FromRedisValue(value); } /// /// Deserialize a /// /// /// /// public static T Deserialize(RedisValue value) { return CachedConverter.Converter.FromRedisValue(value); } /// /// Deserialize an array of objects /// /// /// /// public static T[] Deserialize(RedisValue[] inputs) { T[] outputs = new T[inputs.Length]; for (int idx = 0; idx < inputs.Length; idx++) { outputs[idx] = Deserialize(inputs[idx]); } return outputs; } } /// /// Extension methods for serialization /// public static class RedisSerializerExtensions { /// /// Deserialize a /// /// /// /// public static async Task DeserializeAsync(this Task value) { RedisValue result = await value; if (result.IsNull) { return default!; } else { return RedisSerializer.Deserialize(await value); } } /// /// Deserialize a /// /// /// /// public static async Task DeserializeAsync(this Task values) { return RedisSerializer.Deserialize(await values); } } }