2020-08-13 23:09:40 +10:00
|
|
|
|
using K4os.Compression.LZ4;
|
|
|
|
|
|
using MessagePack;
|
2020-07-03 13:30:40 +10:00
|
|
|
|
using MessagePack.Resolvers;
|
2020-07-03 12:11:05 +10:00
|
|
|
|
using System;
|
2020-08-13 23:09:40 +10:00
|
|
|
|
using System.Linq;
|
|
|
|
|
|
using System.Text;
|
2020-09-24 18:37:24 +10:00
|
|
|
|
using Umbraco.Core.PropertyEditors;
|
2020-07-03 12:11:05 +10:00
|
|
|
|
|
|
|
|
|
|
namespace Umbraco.Web.PublishedCache.NuCache.DataSource
|
|
|
|
|
|
{
|
2020-09-25 00:32:11 +10:00
|
|
|
|
|
2020-08-13 22:15:09 +10:00
|
|
|
|
/// <summary>
|
2020-09-25 00:32:11 +10:00
|
|
|
|
/// Serializes/Deserializes <see cref="ContentCacheDataModel"/> document to the SQL Database as bytes using MessagePack
|
2020-08-13 22:15:09 +10:00
|
|
|
|
/// </summary>
|
2020-09-25 00:32:11 +10:00
|
|
|
|
public class MsgPackContentNestedDataSerializer : IContentCacheDataSerializer
|
2020-07-03 12:11:05 +10:00
|
|
|
|
{
|
2020-09-25 00:32:11 +10:00
|
|
|
|
private readonly MessagePackSerializerOptions _options;
|
2020-08-26 15:57:13 +10:00
|
|
|
|
private readonly IPropertyCompressionOptions _propertyOptions;
|
2020-07-03 12:11:05 +10:00
|
|
|
|
|
2020-09-25 00:32:11 +10:00
|
|
|
|
public MsgPackContentNestedDataSerializer(IPropertyCompressionOptions propertyOptions)
|
2020-07-03 12:11:05 +10:00
|
|
|
|
{
|
2020-09-25 00:32:11 +10:00
|
|
|
|
_propertyOptions = propertyOptions ?? throw new ArgumentNullException(nameof(propertyOptions));
|
2020-07-03 13:30:40 +10:00
|
|
|
|
|
2020-09-25 00:32:11 +10:00
|
|
|
|
var defaultOptions = ContractlessStandardResolver.Options;
|
2020-07-03 13:30:40 +10:00
|
|
|
|
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)
|
2020-09-25 00:32:11 +10:00
|
|
|
|
.WithCompression(MessagePackCompression.Lz4BlockArray);
|
2020-07-03 12:11:05 +10:00
|
|
|
|
}
|
|
|
|
|
|
|
2020-09-25 00:32:11 +10:00
|
|
|
|
public string ToJson(byte[] bin)
|
2020-07-03 12:11:05 +10:00
|
|
|
|
{
|
|
|
|
|
|
var json = MessagePackSerializer.ConvertToJson(bin, _options);
|
|
|
|
|
|
return json;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-09-25 00:32:11 +10:00
|
|
|
|
public ContentCacheDataModel Deserialize(int contentTypeId, string stringData, byte[] byteData)
|
2020-08-26 11:43:43 +10:00
|
|
|
|
{
|
2020-11-24 10:10:38 +13:00
|
|
|
|
if (byteData != null)
|
2020-09-25 00:32:11 +10:00
|
|
|
|
{
|
2020-11-24 10:10:38 +13:00
|
|
|
|
var content = MessagePackSerializer.Deserialize<ContentCacheDataModel>(byteData, _options);
|
2020-09-25 00:32:11 +10:00
|
|
|
|
Expand(contentTypeId, content);
|
|
|
|
|
|
return content;
|
|
|
|
|
|
}
|
2020-11-24 10:10:38 +13:00
|
|
|
|
else if (stringData != null)
|
2020-09-25 00:32:11 +10:00
|
|
|
|
{
|
2020-11-24 10:10:38 +13:00
|
|
|
|
// 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<ContentCacheDataModel>(bin, _options);
|
2020-09-25 00:32:11 +10:00
|
|
|
|
Expand(contentTypeId, content);
|
|
|
|
|
|
return content;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
2020-08-26 11:43:43 +10:00
|
|
|
|
}
|
|
|
|
|
|
|
2020-09-25 00:32:11 +10:00
|
|
|
|
public ContentCacheDataSerializationResult Serialize(int contentTypeId, ContentCacheDataModel model)
|
2020-08-26 11:43:43 +10:00
|
|
|
|
{
|
2020-09-25 00:32:11 +10:00
|
|
|
|
Compress(contentTypeId, model);
|
|
|
|
|
|
var bytes = MessagePackSerializer.Serialize(model, _options);
|
|
|
|
|
|
return new ContentCacheDataSerializationResult(null, bytes);
|
2020-08-26 11:43:43 +10:00
|
|
|
|
}
|
|
|
|
|
|
|
2020-08-13 23:09:40 +10:00
|
|
|
|
/// <summary>
|
2020-08-26 15:57:13 +10:00
|
|
|
|
/// Used during serialization to compress properties
|
2020-08-13 23:09:40 +10:00
|
|
|
|
/// </summary>
|
2020-09-25 00:32:11 +10:00
|
|
|
|
/// <param name="model"></param>
|
|
|
|
|
|
private void Compress(int contentTypeId, ContentCacheDataModel model)
|
2020-08-13 23:09:40 +10:00
|
|
|
|
{
|
2020-09-25 00:32:11 +10:00
|
|
|
|
foreach(var propertyAliasToData in model.PropertyData)
|
2020-08-13 23:09:40 +10:00
|
|
|
|
{
|
2020-08-26 15:57:13 +10:00
|
|
|
|
if (_propertyOptions.IsCompressed(contentTypeId, propertyAliasToData.Key))
|
2020-08-13 23:09:40 +10:00
|
|
|
|
{
|
2020-08-26 15:57:13 +10:00
|
|
|
|
foreach(var property in propertyAliasToData.Value.Where(x => x.Value != null && x.Value is string))
|
2020-08-13 23:09:40 +10:00
|
|
|
|
{
|
2020-08-26 15:57:13 +10:00
|
|
|
|
property.Value = LZ4Pickler.Pickle(Encoding.UTF8.GetBytes((string)property.Value), LZ4Level.L00_FAST);
|
2020-08-13 23:09:40 +10:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-08-26 11:43:43 +10:00
|
|
|
|
/// <summary>
|
2020-08-26 15:57:13 +10:00
|
|
|
|
/// Used during deserialization to map the property data as lazy or expand the value
|
2020-08-26 11:43:43 +10:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="nestedData"></param>
|
2020-09-25 00:32:11 +10:00
|
|
|
|
private void Expand(int contentTypeId, ContentCacheDataModel nestedData)
|
2020-08-13 23:32:05 +10:00
|
|
|
|
{
|
2020-08-26 15:57:13 +10:00
|
|
|
|
foreach (var propertyAliasToData in nestedData.PropertyData)
|
2020-08-26 11:43:43 +10:00
|
|
|
|
{
|
2020-08-26 15:57:13 +10:00
|
|
|
|
if (_propertyOptions.IsCompressed(contentTypeId, propertyAliasToData.Key))
|
2020-08-26 11:43:43 +10:00
|
|
|
|
{
|
2020-08-26 15:57:13 +10:00
|
|
|
|
foreach (var property in propertyAliasToData.Value.Where(x => x.Value != null))
|
2020-08-26 11:43:43 +10:00
|
|
|
|
{
|
2020-08-26 15:57:13 +10:00
|
|
|
|
if (property.Value is byte[] byteArrayValue)
|
2020-08-26 11:43:43 +10:00
|
|
|
|
{
|
2020-08-26 15:57:13 +10:00
|
|
|
|
property.Value = new LazyCompressedString(byteArrayValue);
|
2020-08-26 11:43:43 +10:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2020-08-13 23:32:05 +10:00
|
|
|
|
}
|
2020-07-03 15:41:25 +10:00
|
|
|
|
|
2020-09-25 00:32:11 +10:00
|
|
|
|
|
|
|
|
|
|
|
2020-07-03 13:30:40 +10:00
|
|
|
|
//private class ContentNestedDataResolver : IFormatterResolver
|
|
|
|
|
|
//{
|
|
|
|
|
|
// // GetFormatter<T>'s get cost should be minimized so use type cache.
|
|
|
|
|
|
// public IMessagePackFormatter<T> GetFormatter<T>() => FormatterCache<T>.Formatter;
|
|
|
|
|
|
|
|
|
|
|
|
// private static class FormatterCache<T>
|
|
|
|
|
|
// {
|
|
|
|
|
|
// public static readonly IMessagePackFormatter<T> Formatter;
|
|
|
|
|
|
|
|
|
|
|
|
// // generic's static constructor should be minimized for reduce type generation size!
|
|
|
|
|
|
// // use outer helper method.
|
|
|
|
|
|
// static FormatterCache()
|
|
|
|
|
|
// {
|
|
|
|
|
|
// Formatter = (IMessagePackFormatter<T>)SampleCustomResolverGetFormatterHelper.GetFormatter(typeof(T));
|
|
|
|
|
|
// }
|
|
|
|
|
|
// }
|
|
|
|
|
|
//}
|
|
|
|
|
|
|
|
|
|
|
|
//internal static class SampleCustomResolverGetFormatterHelper
|
|
|
|
|
|
//{
|
|
|
|
|
|
// // If type is concrete type, use type-formatter map
|
|
|
|
|
|
// static readonly Dictionary<Type, object> _formatterMap = new Dictionary<Type, object>()
|
|
|
|
|
|
// {
|
|
|
|
|
|
// {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<ContentNestedData>
|
|
|
|
|
|
//{
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
// }
|
|
|
|
|
|
//}
|
2020-07-03 12:11:05 +10:00
|
|
|
|
}
|
|
|
|
|
|
}
|