V14: Migrate nucache to use System.Text.Json (#15685)

* Create system text serializer

* Assign property names with system text

* Use the new serializer

* Impement AutoInterningStringConverter with System.Text.Json

* Implement TextAutoInterningStringKeyCaseInsensitiveDictionaryConverter

* Make CaseInsensitiveDictionaryConverter

* Force datetimes to be read as UTC

* Remove usages of Newtonsoft.Json

* Remove text prefixes

* Remove unused Newtonsoft converter

* Remove more newtonsoft

* Remove duplicate implementation

* Rmove usage of missing class in tests

* Ignore null values

* Fix tests

* Remove Newtonstoft reference from NuCache

---------

Co-authored-by: Elitsa <elm@umbraco.dk>
This commit is contained in:
Mole
2024-02-14 12:10:45 +01:00
committed by GitHub
parent 4f04669dce
commit 2dcdff5392
13 changed files with 167 additions and 248 deletions

View File

@@ -1,6 +1,6 @@
using System.Runtime.Serialization;
using System.Text.Json.Serialization;
using MessagePack;
using Newtonsoft.Json;
using Umbraco.Cms.Infrastructure.Serialization;
namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource;
@@ -14,34 +14,33 @@ public class ContentCacheDataModel
// TODO: We don't want to allocate empty arrays
// dont serialize empty properties
[DataMember(Order = 0)]
[JsonProperty("pd")]
[JsonPropertyName("pd")]
[JsonConverter(typeof(AutoInterningStringKeyCaseInsensitiveDictionaryConverter<PropertyData[]>))]
[MessagePackFormatter(typeof(MessagePackAutoInterningStringKeyCaseInsensitiveDictionaryFormatter<PropertyData[]>))]
public Dictionary<string, PropertyData[]>? PropertyData { get; set; }
[DataMember(Order = 1)]
[JsonProperty("cd")]
[JsonPropertyName("cd")]
[JsonConverter(typeof(AutoInterningStringKeyCaseInsensitiveDictionaryConverter<CultureVariation>))]
[MessagePackFormatter(
typeof(MessagePackAutoInterningStringKeyCaseInsensitiveDictionaryFormatter<CultureVariation>))]
[MessagePackFormatter(typeof(MessagePackAutoInterningStringKeyCaseInsensitiveDictionaryFormatter<CultureVariation>))]
public Dictionary<string, CultureVariation>? CultureData { get; set; }
[DataMember(Order = 2)]
[JsonProperty("us")]
[JsonPropertyName("us")]
public string? UrlSegment { get; set; }
// Legacy properties used to deserialize existing nucache db entries
[IgnoreDataMember]
[JsonProperty("properties")]
[JsonPropertyName("properties")]
[JsonConverter(typeof(CaseInsensitiveDictionaryConverter<PropertyData[]>))]
private Dictionary<string, PropertyData[]> LegacyPropertyData { set => PropertyData = value; }
[IgnoreDataMember]
[JsonProperty("cultureData")]
[JsonPropertyName("cultureData")]
[JsonConverter(typeof(CaseInsensitiveDictionaryConverter<CultureVariation>))]
private Dictionary<string, CultureVariation> LegacyCultureData { set => CultureData = value; }
[IgnoreDataMember]
[JsonProperty("urlSegment")]
[JsonPropertyName("urlSegment")]
private string LegacyUrlSegment { set => UrlSegment = value; }
}

View File

@@ -1,5 +1,6 @@
using System.Runtime.Serialization;
using Newtonsoft.Json;
using System.Text.Json.Serialization;
using Umbraco.Cms.Infrastructure.Serialization;
namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource;
@@ -10,35 +11,37 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource;
public class CultureVariation
{
[DataMember(Order = 0)]
[JsonProperty("nm")]
[JsonPropertyName("nm")]
public string? Name { get; set; }
[DataMember(Order = 1)]
[JsonProperty("us")]
[JsonPropertyName("us")]
public string? UrlSegment { get; set; }
[DataMember(Order = 2)]
[JsonProperty("dt")]
[JsonPropertyName("dt")]
[JsonConverter(typeof(ForceUtcDateTimeConverter))]
public DateTime Date { get; set; }
[DataMember(Order = 3)]
[JsonProperty("isd")]
[JsonPropertyName("isd")]
public bool IsDraft { get; set; }
// Legacy properties used to deserialize existing nucache db entries
[IgnoreDataMember]
[JsonProperty("name")]
[JsonPropertyName("nam")]
private string LegacyName { set => Name = value; }
[IgnoreDataMember]
[JsonProperty("urlSegment")]
[JsonPropertyName("urlSegment")]
private string LegacyUrlSegment { set => UrlSegment = value; }
[IgnoreDataMember]
[JsonProperty("date")]
[JsonPropertyName("date")]
[JsonConverter(typeof(ForceUtcDateTimeConverter))]
private DateTime LegacyDate { set => Date = value; }
[IgnoreDataMember]
[JsonProperty("isDraft")]
[JsonPropertyName("isDraft")]
private bool LegacyIsDraft { set => IsDraft = value; }
}

View File

@@ -1,28 +1,22 @@
using System.Buffers;
using Newtonsoft.Json;
using System.Text.Json;
using System.Text.Json.Serialization;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Infrastructure.Serialization;
namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource;
public class JsonContentNestedDataSerializer : IContentCacheDataSerializer
{
// by default JsonConvert will deserialize our numeric values as Int64
// which is bad, because they were Int32 in the database - take care
private readonly JsonSerializerSettings _jsonSerializerSettings = new()
private static readonly JsonSerializerOptions _jsonSerializerOptions = new()
{
Converters = new List<JsonConverter> { new ForceInt32Converter() },
// Explicitly specify date handling so that it's consistent and follows the same date handling as MessagePack
DateParseHandling = DateParseHandling.DateTime,
DateFormatHandling = DateFormatHandling.IsoDateFormat,
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
DateFormatString = "o",
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
private readonly JsonNameTable _propertyNameTable = new DefaultJsonNameTable();
public ContentCacheDataModel? Deserialize(IReadOnlyContentBase content, string? stringData, byte[]? byteData, bool published)
/// <inheritdoc />
public ContentCacheDataModel? Deserialize(
IReadOnlyContentBase content,
string? stringData,
byte[]? byteData,
bool published)
{
if (stringData == null && byteData != null)
{
@@ -30,62 +24,16 @@ public class JsonContentNestedDataSerializer : IContentCacheDataSerializer
$"{typeof(JsonContentNestedDataSerializer)} does not support byte[] serialization");
}
var serializer = JsonSerializer.Create(_jsonSerializerSettings);
using (var reader = new JsonTextReader(new StringReader(stringData!)))
{
// reader will get buffer from array pool
reader.ArrayPool = JsonArrayPool.Instance;
reader.PropertyNameTable = _propertyNameTable;
return serializer.Deserialize<ContentCacheDataModel>(reader);
}
return JsonSerializer.Deserialize<ContentCacheDataModel>(stringData!, _jsonSerializerOptions);
}
public ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model, bool published)
/// <inheritdoc />
public ContentCacheDataSerializationResult Serialize(
IReadOnlyContentBase content,
ContentCacheDataModel model,
bool published)
{
// note that numeric values (which are Int32) are serialized without their
// type (eg "value":1234) and JsonConvert by default deserializes them as Int64
var json = JsonConvert.SerializeObject(model);
var json = JsonSerializer.Serialize(model, _jsonSerializerOptions);
return new ContentCacheDataSerializationResult(json, null);
}
}
public class JsonArrayPool : IArrayPool<char>
{
public static readonly JsonArrayPool Instance = new();
public char[] Rent(int minimumLength) =>
// get char array from System.Buffers shared pool
ArrayPool<char>.Shared.Rent(minimumLength);
public void Return(char[]? array)
{
// return char array to System.Buffers shared pool
if (array is not null)
{
ArrayPool<char>.Shared.Return(array);
}
}
}
public class AutomaticJsonNameTable : DefaultJsonNameTable
{
private readonly int maxToAutoAdd;
private int nAutoAdded;
public AutomaticJsonNameTable(int maxToAdd) => maxToAutoAdd = maxToAdd;
public override string? Get(char[] key, int start, int length)
{
var s = base.Get(key, start, length);
if (s == null && nAutoAdded < maxToAutoAdd)
{
s = new string(key, start, length);
Add(s);
nAutoAdded++;
}
return s;
}
}

View File

@@ -1,6 +1,6 @@
using System.ComponentModel;
using System.Runtime.Serialization;
using Newtonsoft.Json;
using System.Text.Json.Serialization;
using Umbraco.Cms.Infrastructure.Serialization;
namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource;
@@ -14,7 +14,7 @@ public class PropertyData
[DataMember(Order = 0)]
[JsonConverter(typeof(AutoInterningStringConverter))]
[DefaultValue("")]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate, PropertyName = "c")]
[JsonPropertyName("c")]
public string? Culture
{
get => _culture;
@@ -26,7 +26,7 @@ public class PropertyData
[DataMember(Order = 1)]
[JsonConverter(typeof(AutoInterningStringConverter))]
[DefaultValue("")]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate, PropertyName = "s")]
[JsonPropertyName("s")]
public string? Segment
{
get => _segment;
@@ -36,26 +36,25 @@ public class PropertyData
}
[DataMember(Order = 2)]
[JsonProperty("v")]
[JsonPropertyName("v")]
public object? Value { get; set; }
// Legacy properties used to deserialize existing nucache db entries
[IgnoreDataMember]
[JsonProperty("culture")]
private string LegacyCulture
{
set => Culture = value;
}
[IgnoreDataMember]
[JsonProperty("seg")]
[JsonPropertyName("seg")]
private string LegacySegment
{
set => Segment = value;
}
[IgnoreDataMember]
[JsonProperty("val")]
[JsonPropertyName("val")]
private object LegacyValue
{
set => Value = value;

View File

@@ -10,7 +10,6 @@
<PackageReference Include="Umbraco.CSharpTest.Net.Collections" />
<PackageReference Include="MessagePack" />
<PackageReference Include="K4os.Compression.LZ4" />
<PackageReference Include="Newtonsoft.Json"/>
</ItemGroup>
<ItemGroup>