152 lines
6.5 KiB
C#
152 lines
6.5 KiB
C#
using System.Text;
|
|
using K4os.Compression.LZ4;
|
|
using MessagePack;
|
|
using MessagePack.Resolvers;
|
|
using Umbraco.Cms.Core.Models;
|
|
using Umbraco.Cms.Core.PropertyEditors;
|
|
|
|
namespace Umbraco.Cms.Infrastructure.HybridCache.Serialization;
|
|
|
|
/// <summary>
|
|
/// Serializes/deserializes <see cref="ContentCacheDataModel" /> documents to the SQL Database as bytes using
|
|
/// MessagePack.
|
|
/// </summary>
|
|
internal sealed class MsgPackContentNestedDataSerializer : IContentCacheDataSerializer
|
|
{
|
|
private readonly MessagePackSerializerOptions _options;
|
|
private readonly IPropertyCacheCompression _propertyOptions;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="MsgPackContentNestedDataSerializer"/> class.
|
|
/// </summary>
|
|
public MsgPackContentNestedDataSerializer(IPropertyCacheCompression propertyOptions)
|
|
{
|
|
_propertyOptions = propertyOptions ?? throw new ArgumentNullException(nameof(propertyOptions));
|
|
|
|
MessagePackSerializerOptions? defaultOptions = ContractlessStandardResolver.Options;
|
|
IFormatterResolver? 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.
|
|
// There are docs here on how to build one of these: https://github.com/neuecc/MessagePack-CSharp/blob/master/README.md#low-level-api-imessagepackformattert
|
|
// and there are a couple examples if you search on google for them but this will need to be a separate project.
|
|
// NOTE: resolver custom types first
|
|
// new ContentNestedDataResolver(),
|
|
|
|
// finally use standard resolver
|
|
defaultOptions.Resolver);
|
|
|
|
_options = defaultOptions
|
|
.WithResolver(resolver)
|
|
.WithCompression(MessagePackCompression.Lz4BlockArray)
|
|
.WithSecurity(MessagePackSecurity.UntrustedData);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ContentCacheDataModel? Deserialize(IReadOnlyContentBase content, string? stringData, byte[]? byteData, bool published)
|
|
{
|
|
if (byteData != null)
|
|
{
|
|
ContentCacheDataModel? cacheModel =
|
|
MessagePackSerializer.Deserialize<ContentCacheDataModel>(byteData, _options);
|
|
Expand(content, cacheModel, published);
|
|
return cacheModel;
|
|
}
|
|
|
|
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);
|
|
ContentCacheDataModel? cacheModel = MessagePackSerializer.Deserialize<ContentCacheDataModel>(bin, _options);
|
|
Expand(content, cacheModel, published);
|
|
return cacheModel;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model, bool published)
|
|
{
|
|
Compress(content, model, published);
|
|
var bytes = MessagePackSerializer.Serialize(model, _options);
|
|
return new ContentCacheDataSerializationResult(null, bytes);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Converts the binary MessagePack data to a JSON string representation.
|
|
/// </summary>
|
|
public string ToJson(byte[] bin) => MessagePackSerializer.ConvertToJson(bin, _options);
|
|
|
|
/// <summary>
|
|
/// Used during serialization to compress properties
|
|
/// </summary>
|
|
/// <param name="content"></param>
|
|
/// <param name="model"></param>
|
|
/// <param name="published"></param>
|
|
/// <remarks>
|
|
/// This will essentially 'double compress' property data. The MsgPack data as a whole will already be compressed
|
|
/// but this will go a step further and double compress property data so that it is stored in the nucache file
|
|
/// as compressed bytes and therefore will exist in memory as compressed bytes. That is, until the bytes are
|
|
/// read/decompressed as a string to be displayed on the front-end. This allows for potentially a significant
|
|
/// memory savings but could also affect performance of first rendering pages while decompression occurs.
|
|
/// </remarks>
|
|
private void Compress(IReadOnlyContentBase content, ContentCacheDataModel model, bool published)
|
|
{
|
|
if (model.PropertyData is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (KeyValuePair<string, PropertyData[]> propertyAliasToData in model.PropertyData)
|
|
{
|
|
if (_propertyOptions.IsCompressed(content, propertyAliasToData.Key, published))
|
|
{
|
|
foreach (PropertyData property in propertyAliasToData.Value.Where(x =>
|
|
x.Value != null && x.Value is string))
|
|
{
|
|
if (property.Value is string propertyValue)
|
|
{
|
|
property.Value = LZ4Pickler.Pickle(Encoding.UTF8.GetBytes(propertyValue));
|
|
}
|
|
}
|
|
|
|
foreach (PropertyData property in propertyAliasToData.Value.Where(x =>
|
|
x.Value != null && x.Value is int intVal))
|
|
{
|
|
property.Value = Convert.ToBoolean((int?)property.Value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Used during deserialization to map the property data as lazy or expand the value
|
|
/// </summary>
|
|
/// <param name="content"></param>
|
|
/// <param name="nestedData"></param>
|
|
/// <param name="published"></param>
|
|
private void Expand(IReadOnlyContentBase content, ContentCacheDataModel nestedData, bool published)
|
|
{
|
|
if (nestedData.PropertyData is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (KeyValuePair<string, PropertyData[]> propertyAliasToData in nestedData.PropertyData)
|
|
{
|
|
if (_propertyOptions.IsCompressed(content, propertyAliasToData.Key, published))
|
|
{
|
|
foreach (PropertyData property in propertyAliasToData.Value.Where(x => x.Value != null))
|
|
{
|
|
if (property.Value is byte[] byteArrayValue)
|
|
{
|
|
property.Value = new LazyCompressedString(byteArrayValue);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|