using K4os.Compression.LZ4; using MessagePack; using MessagePack.Resolvers; using System; using System.Linq; using System.Text; using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PublishedCache.NuCache.DataSource { /// /// Serializes/Deserializes document to the SQL Database as bytes using MessagePack /// public class MsgPackContentNestedDataSerializer : IContentCacheDataSerializer { private readonly MessagePackSerializerOptions _options; private readonly IPropertyCompressionOptions _propertyOptions; public MsgPackContentNestedDataSerializer(IPropertyCompressionOptions propertyOptions) { _propertyOptions = propertyOptions ?? throw new ArgumentNullException(nameof(propertyOptions)); var defaultOptions = ContractlessStandardResolver.Options; var resolver = CompositeResolver.Create( // TODO: We want to be able to intern the strings for aliases when deserializing like we do for Newtonsoft but I'm unsure exactly how // to do that but it would seem to be with a custom message pack resolver but I haven't quite figured out based on the docs how // to do that since that is part of the int key -> string mapping operation, might have to see the source code to figure that one out. // resolver custom types first // new ContentNestedDataResolver(), // finally use standard resolver defaultOptions.Resolver ); _options = defaultOptions .WithResolver(resolver) .WithCompression(MessagePackCompression.Lz4BlockArray); } public string ToJson(byte[] bin) { var json = MessagePackSerializer.ConvertToJson(bin, _options); return json; } public ContentCacheDataModel Deserialize(int contentTypeId, string stringData, byte[] byteData) { if (stringData != null) { // NOTE: We don't really support strings but it's possible if manually used (i.e. tests) var bin = Convert.FromBase64String(stringData); var content = MessagePackSerializer.Deserialize(bin, _options); Expand(contentTypeId, content); return content; } else if (byteData != null) { var content = MessagePackSerializer.Deserialize(byteData, _options); Expand(contentTypeId, content); return content; } else { return null; } } public ContentCacheDataSerializationResult Serialize(int contentTypeId, ContentCacheDataModel model) { Compress(contentTypeId, model); var bytes = MessagePackSerializer.Serialize(model, _options); return new ContentCacheDataSerializationResult(null, bytes); } /// /// Used during serialization to compress properties /// /// private void Compress(int contentTypeId, ContentCacheDataModel model) { foreach(var propertyAliasToData in model.PropertyData) { if (_propertyOptions.IsCompressed(contentTypeId, propertyAliasToData.Key)) { foreach(var property in propertyAliasToData.Value.Where(x => x.Value != null && x.Value is string)) { property.Value = LZ4Pickler.Pickle(Encoding.UTF8.GetBytes((string)property.Value), LZ4Level.L00_FAST); } } } } /// /// Used during deserialization to map the property data as lazy or expand the value /// /// private void Expand(int contentTypeId, ContentCacheDataModel nestedData) { foreach (var propertyAliasToData in nestedData.PropertyData) { if (_propertyOptions.IsCompressed(contentTypeId, propertyAliasToData.Key)) { foreach (var property in propertyAliasToData.Value.Where(x => x.Value != null)) { if (property.Value is byte[] byteArrayValue) { property.Value = new LazyCompressedString(byteArrayValue); } } } } } //private class ContentNestedDataResolver : IFormatterResolver //{ // // GetFormatter's get cost should be minimized so use type cache. // public IMessagePackFormatter GetFormatter() => FormatterCache.Formatter; // private static class FormatterCache // { // public static readonly IMessagePackFormatter Formatter; // // generic's static constructor should be minimized for reduce type generation size! // // use outer helper method. // static FormatterCache() // { // Formatter = (IMessagePackFormatter)SampleCustomResolverGetFormatterHelper.GetFormatter(typeof(T)); // } // } //} //internal static class SampleCustomResolverGetFormatterHelper //{ // // If type is concrete type, use type-formatter map // static readonly Dictionary _formatterMap = new Dictionary() // { // {typeof(ContentNestedData), new ContentNestedDataFormatter()} // // add more your own custom serializers. // }; // internal static object GetFormatter(Type t) // { // object formatter; // if (_formatterMap.TryGetValue(t, out formatter)) // { // return formatter; // } // // If target type is generics, use MakeGenericType. // if (t.IsGenericParameter && t.GetGenericTypeDefinition() == typeof(ValueTuple<,>)) // { // return Activator.CreateInstance(typeof(ValueTupleFormatter<,>).MakeGenericType(t.GenericTypeArguments)); // } // // If type can not get, must return null for fallback mechanism. // return null; // } //} //public class ContentNestedDataFormatter : IMessagePackFormatter //{ // public void Serialize(ref MessagePackWriter writer, ContentNestedData value, MessagePackSerializerOptions options) // { // if (value == null) // { // writer.WriteNil(); // return; // } // writer.WriteArrayHeader(3); // writer.WriteString(value.UrlSegment); // writer.WriteString(value.FullName); // writer.WriteString(value.Age); // writer.WriteString(value.FullName); // } // public ContentNestedData Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) // { // if (reader.TryReadNil()) // { // return null; // } // options.Security.DepthStep(ref reader); // var path = reader.ReadString(); // reader.Depth--; // return new FileInfo(path); // } //} } }