Interns strings for aliases, etc... for when content is deserialized from the contentNu table so we aren't duplicating strings when cold booting.
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Umbraco.Core.Serialization
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// When applied to a string or string collection field will ensure the deserialized strings are interned
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Borrowed from https://stackoverflow.com/a/34906004/694494
|
||||
/// On the same page an interesting approach of using a local intern pool https://stackoverflow.com/a/39605620/694494 which re-uses .NET System.Xml.NameTable
|
||||
/// </remarks>
|
||||
internal class AutoInterningStringConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
// CanConvert is not called when a converter is applied directly to a property.
|
||||
throw new NotImplementedException($"{nameof(AutoInterningStringConverter)} should not be used globally");
|
||||
}
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.TokenType == JsonToken.Null)
|
||||
return null;
|
||||
// Check is in case the value is a non-string literal such as an integer.
|
||||
var s = reader.TokenType == JsonToken.String
|
||||
? string.Intern((string)reader.Value)
|
||||
: string.Intern((string)JToken.Load(reader));
|
||||
return s;
|
||||
}
|
||||
|
||||
public override bool CanWrite => false;
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace Umbraco.Core.Serialization
|
||||
{
|
||||
/// <summary>
|
||||
/// When applied to a dictionary with a string key, will ensure the deserialized string keys are interned
|
||||
/// </summary>
|
||||
/// <typeparam name="TValue"></typeparam>
|
||||
/// <remarks>
|
||||
/// borrowed from https://stackoverflow.com/a/36116462/694494
|
||||
/// </remarks>
|
||||
internal class AutoInterningStringKeyCaseInsensitiveDictionaryConverter<TValue> : CaseInsensitiveDictionaryConverter<TValue>
|
||||
{
|
||||
public AutoInterningStringKeyCaseInsensitiveDictionaryConverter()
|
||||
{
|
||||
}
|
||||
public AutoInterningStringKeyCaseInsensitiveDictionaryConverter(StringComparer comparer) : base(comparer)
|
||||
{
|
||||
}
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.TokenType == JsonToken.StartObject)
|
||||
{
|
||||
var dictionary = new Dictionary<string, TValue>();
|
||||
while (reader.Read())
|
||||
{
|
||||
switch (reader.TokenType)
|
||||
{
|
||||
case JsonToken.PropertyName:
|
||||
var key = string.Intern(reader.Value.ToString());
|
||||
|
||||
if (!reader.Read())
|
||||
throw new Exception("Unexpected end when reading object.");
|
||||
|
||||
var v = serializer.Deserialize<TValue>(reader);
|
||||
dictionary[key] = v;
|
||||
break;
|
||||
case JsonToken.Comment:
|
||||
break;
|
||||
case JsonToken.EndObject:
|
||||
return dictionary;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -14,12 +14,24 @@ namespace Umbraco.Core.Serialization
|
||||
/// </example>
|
||||
public class CaseInsensitiveDictionaryConverter<T> : CustomCreationConverter<IDictionary>
|
||||
{
|
||||
private readonly StringComparer _comparer;
|
||||
|
||||
public CaseInsensitiveDictionaryConverter()
|
||||
: this(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
}
|
||||
|
||||
public CaseInsensitiveDictionaryConverter(StringComparer comparer)
|
||||
{
|
||||
_comparer = comparer ?? throw new ArgumentNullException(nameof(comparer));
|
||||
}
|
||||
|
||||
public override bool CanWrite => false;
|
||||
|
||||
public override bool CanRead => true;
|
||||
|
||||
public override bool CanConvert(Type objectType) => typeof(IDictionary<string,T>).IsAssignableFrom(objectType);
|
||||
|
||||
public override IDictionary Create(Type objectType) => new Dictionary<string, T>(StringComparer.OrdinalIgnoreCase);
|
||||
public override IDictionary Create(Type objectType) => new Dictionary<string, T>(_comparer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,6 +139,8 @@
|
||||
<Compile Include="Models\RelationTypeExtensions.cs" />
|
||||
<Compile Include="Persistence\Repositories\IInstallationRepository.cs" />
|
||||
<Compile Include="Persistence\Repositories\Implement\InstallationRepository.cs" />
|
||||
<Compile Include="Serialization\AutoInterningStringConverter.cs" />
|
||||
<Compile Include="Serialization\AutoInterningStringKeyCaseInsensitiveDictionaryConverter.cs" />
|
||||
<Compile Include="Services\Implement\InstallationService.cs" />
|
||||
<Compile Include="Migrations\Upgrade\V_8_6_0\AddMainDomLock.cs" />
|
||||
<Compile Include="Models\UpgradeResult.cs" />
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
using Newtonsoft.Json;
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Serialization;
|
||||
using Umbraco.Core.Serialization;
|
||||
|
||||
namespace Umbraco.Tests.Serialization
|
||||
{
|
||||
[TestFixture]
|
||||
public class AutoInterningStringConverterTests
|
||||
{
|
||||
[Test]
|
||||
public void Intern_Property_String()
|
||||
{
|
||||
var str1 = "Hello";
|
||||
var obj = new Test
|
||||
{
|
||||
Name = str1 + " " + "there"
|
||||
};
|
||||
|
||||
// ensure the raw value is not interned
|
||||
Assert.IsNull(string.IsInterned(obj.Name));
|
||||
|
||||
var serialized = JsonConvert.SerializeObject(obj);
|
||||
obj = JsonConvert.DeserializeObject<Test>(serialized);
|
||||
|
||||
Assert.IsNotNull(string.IsInterned(obj.Name));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Intern_Property_Dictionary()
|
||||
{
|
||||
var str1 = "key";
|
||||
var obj = new Test
|
||||
{
|
||||
Values = new Dictionary<string, int>
|
||||
{
|
||||
[str1 + "1"] = 0,
|
||||
[str1 + "2"] = 1
|
||||
}
|
||||
};
|
||||
|
||||
// ensure the raw value is not interned
|
||||
Assert.IsNull(string.IsInterned(obj.Values.Keys.First()));
|
||||
Assert.IsNull(string.IsInterned(obj.Values.Keys.Last()));
|
||||
|
||||
var serialized = JsonConvert.SerializeObject(obj);
|
||||
obj = JsonConvert.DeserializeObject<Test>(serialized);
|
||||
|
||||
Assert.IsNotNull(string.IsInterned(obj.Values.Keys.First()));
|
||||
Assert.IsNotNull(string.IsInterned(obj.Values.Keys.Last()));
|
||||
}
|
||||
|
||||
public class Test
|
||||
{
|
||||
[JsonConverter(typeof(AutoInterningStringConverter))]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonConverter(typeof(AutoInterningStringKeyCaseInsensitiveDictionaryConverter<int>))]
|
||||
public Dictionary<string, int> Values = new Dictionary<string, int>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,6 +158,7 @@
|
||||
<Compile Include="Routing\RoutableDocumentFilterTests.cs" />
|
||||
<Compile Include="Runtimes\StandaloneTests.cs" />
|
||||
<Compile Include="Routing\GetContentUrlsTests.cs" />
|
||||
<Compile Include="Serialization\AutoInterningStringConverterTests.cs" />
|
||||
<Compile Include="Services\AmbiguousEventTests.cs" />
|
||||
<Compile Include="Services\ContentServiceEventTests.cs" />
|
||||
<Compile Include="Services\ContentServicePublishBranchTests.cs" />
|
||||
|
||||
@@ -18,8 +18,13 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource
|
||||
var dict = new Dictionary<string, CultureVariation>(StringComparer.InvariantCultureIgnoreCase);
|
||||
for (var i = 0; i < pcount; i++)
|
||||
{
|
||||
var languageId = PrimitiveSerializer.String.ReadFrom(stream);
|
||||
var cultureVariation = new CultureVariation { Name = ReadStringObject(stream), UrlSegment = ReadStringObject(stream), Date = ReadDateTime(stream) };
|
||||
var languageId = string.Intern(PrimitiveSerializer.String.ReadFrom(stream));
|
||||
var cultureVariation = new CultureVariation
|
||||
{
|
||||
Name = ReadStringObject(stream),
|
||||
UrlSegment = ReadStringObject(stream),
|
||||
Date = ReadDateTime(stream)
|
||||
};
|
||||
dict[languageId] = cultureVariation;
|
||||
}
|
||||
return dict;
|
||||
|
||||
@@ -38,8 +38,8 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource
|
||||
// the 'current' value, and string.Empty should be used to represent the invariant or
|
||||
// neutral values - PropertyData throws when getting nulls, so falling back to
|
||||
// string.Empty here - what else?
|
||||
pdata.Culture = string.Intern(ReadStringObject(stream)) ?? string.Empty;
|
||||
pdata.Segment = string.Intern(ReadStringObject(stream)) ?? string.Empty;
|
||||
pdata.Culture = ReadStringObject(stream, true) ?? string.Empty;
|
||||
pdata.Segment = ReadStringObject(stream, true) ?? string.Empty;
|
||||
pdata.Value = ReadObject(stream);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,11 +11,11 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource
|
||||
{
|
||||
//dont serialize empty properties
|
||||
[JsonProperty("pd")]
|
||||
[JsonConverter(typeof(CaseInsensitiveDictionaryConverter<PropertyData[]>))]
|
||||
[JsonConverter(typeof(AutoInterningStringKeyCaseInsensitiveDictionaryConverter<PropertyData[]>))]
|
||||
public Dictionary<string, PropertyData[]> PropertyData { get; set; }
|
||||
|
||||
[JsonProperty("cd")]
|
||||
[JsonConverter(typeof(CaseInsensitiveDictionaryConverter<CultureVariation>))]
|
||||
[JsonConverter(typeof(AutoInterningStringKeyCaseInsensitiveDictionaryConverter<CultureVariation>))]
|
||||
public Dictionary<string, CultureVariation> CultureData { get; set; }
|
||||
|
||||
[JsonProperty("us")]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using Newtonsoft.Json;
|
||||
using Umbraco.Core.Serialization;
|
||||
|
||||
namespace Umbraco.Web.PublishedCache.NuCache.DataSource
|
||||
{
|
||||
@@ -9,6 +10,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource
|
||||
private string _culture;
|
||||
private string _segment;
|
||||
|
||||
[JsonConverter(typeof(AutoInterningStringConverter))]
|
||||
[DefaultValue("")]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate, PropertyName = "c")]
|
||||
public string Culture
|
||||
@@ -17,6 +19,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource
|
||||
set => _culture = value ?? throw new ArgumentNullException(nameof(value)); // TODO: or fallback to string.Empty? CANNOT be null
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(AutoInterningStringConverter))]
|
||||
[DefaultValue("")]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate, PropertyName = "s")]
|
||||
public string Segment
|
||||
@@ -28,7 +31,6 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource
|
||||
[JsonProperty("v")]
|
||||
public object Value { get; set; }
|
||||
|
||||
|
||||
//Legacy properties used to deserialize existing nucache db entries
|
||||
[JsonProperty("culture")]
|
||||
private string LegacyCulture
|
||||
|
||||
@@ -23,13 +23,15 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource
|
||||
return read(stream);
|
||||
}
|
||||
|
||||
protected string ReadStringObject(Stream stream) // required 'cos string is not a struct
|
||||
protected string ReadStringObject(Stream stream, bool intern = false) // required 'cos string is not a struct
|
||||
{
|
||||
var type = PrimitiveSerializer.Char.ReadFrom(stream);
|
||||
if (type == 'N') return null;
|
||||
if (type != 'S')
|
||||
throw new NotSupportedException($"Cannot deserialize type '{type}', expected 'S'.");
|
||||
return PrimitiveSerializer.String.ReadFrom(stream);
|
||||
return intern
|
||||
? string.Intern(PrimitiveSerializer.String.ReadFrom(stream))
|
||||
: PrimitiveSerializer.String.ReadFrom(stream);
|
||||
}
|
||||
|
||||
protected int? ReadIntObject(Stream stream) => ReadObject(stream, 'I', ReadInt);
|
||||
|
||||
Reference in New Issue
Block a user