-
Notifications
You must be signed in to change notification settings - Fork 2.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
v14: Refactor and enhance System.Text.Json
converters
#15960
Conversation
…ationEditorJsonSerializer
using System.Text.Json; | ||
|
||
namespace Umbraco.Cms.Infrastructure.Serialization; | ||
|
||
/// <summary> | ||
/// Converts a string to or from JSON, interning the string when reading. | ||
/// </summary> | ||
public sealed class JsonStringInternConverter : ReadOnlyJsonConverter<string> | ||
{ | ||
/// <inheritdoc /> | ||
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) | ||
=> reader.GetString() is string value | ||
? string.Intern(value) | ||
: null; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this entire converter should be removed? string.Intern has many downsides: dotnet/runtime#28368 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like the string interning was discussed in PR #8365 and implemented in PR #8376. This specific converter interns the culture and segment property values of the PropertyData
model, which is repeated/duplicated for each variant property value and might actually be worth this downside because of the memory savings and performance enhancements (when comparing the culture/segment values).
Maybe @nzdev can give his take on this, especially since this was added in v8 running .NET Framework and modern .NET might already have optimizations for this and/or better ways to achieve similar performance improvements?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at the code of string.Intern, it still has the same downsides in .NET 8.0. I think the recommendation there to use a custom collection would be better.
/// <summary> | ||
/// Converts a boolean (like) value to or from JSON. | ||
/// </summary> | ||
public sealed class JsonFuzzyBooleanConverter : JsonConverter<bool> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this name makes any sense. Fuzzy matching means matching something approximately, there's nothing approximately about this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This does however parse values to their Boolean equivalent in a non-strict way, so what name would you suggest?
It's also worthwhile to note that once parsed, it writes actual true
/false
values back again... Maybe JsonConvertToBooleanConverter
would be a better name?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ConvertToXConverter feels a bit repetitive. Why not just JsonBooleanConverter?
{ | ||
return; | ||
} | ||
|
||
// If an object is equals "new object()", Json.Serialize would recurse forever and cause a stack overflow | ||
// We have no good way of checking if its an empty object | ||
// which is why we try to check if the object has any properties, and thus will be empty. | ||
if (objectToWrite.GetType().Name is "Object" && !objectToWrite.GetType().GetProperties().Any()) | ||
if (value.GetType().Name is "Object" && !value.GetType().GetProperties().Any()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (value.GetType().Name is "Object" && !value.GetType().GetProperties().Any()) | |
if (value.GetType() == typeof(object) && !value.GetType().GetProperties().Any()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at the reason for having the JsonObjectConverter
in PR #13537, it seems to be because by default STJ deserializes objects to JsonElement
, which is 'hard to work with' (I assume because it's completely immutable). Is that correct @Zeegaan?
If that is the only reason, maybe we should set UnknownTypeHandling
to JsonUnknownTypeHandling.JsonNode
instead?
src/Umbraco.Infrastructure/Serialization/JsonDictionaryStringInternIgnoreCaseConverter.cs
Show resolved
Hide resolved
src/Umbraco.Infrastructure/Serialization/JsonDictionaryStringIgnoreCaseConverter.cs
Show resolved
Hide resolved
...co.PublishedCache.NuCache/DataSource/MessagePackDictionaryStringInternIgnoreCaseFormatter.cs
Show resolved
Hide resolved
@@ -15,14 +15,14 @@ public class ContentCacheDataModel | |||
// dont serialize empty properties | |||
[DataMember(Order = 0)] | |||
[JsonPropertyName("pd")] | |||
[JsonConverter(typeof(AutoInterningStringKeyCaseInsensitiveDictionaryConverter<PropertyData[]>))] | |||
[MessagePackFormatter(typeof(MessagePackAutoInterningStringKeyCaseInsensitiveDictionaryFormatter<PropertyData[]>))] | |||
[JsonConverter(typeof(JsonDictionaryStringInternIgnoreCaseConverter<PropertyData[]>))] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just commenting on this one, an alternative is to use the built-in converter and set PreferredObjectCreationHandling to JsonObjectCreationHandling.Populate so you can define the collection type (with comparer) in this file, then you don't need this custom converter.
Most parts make sense to me, but personally I do not understand why I would prefer those to keep the names. Regarding interning or not, I think that part should be kept in a separate PR. I'm sure more people have opinions about that. |
We don't ship different implementations, so prepending them with
I agree! String interning can still improve performance and lower memory usage, but maybe using a custom collection as @Nuklon suggested would be a better alternative... |
…eature/jsonconverters
…eature/jsonconverters # Conflicts: # tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/RichTextPropertyEditorHelperTests.cs
@ronaldbarendse I have reverted the renaming of these classes. SystemText* is a good name indicating what technology is named. The verbose class name is not really exposed, because people use the technology agnostic interface name, when they inject. For future PRs, I will recommend to keep to the changes you need and that blocks you, so silly things like naming are not blocking the PR :) |
This refactors and enhances the
System.Text.Json
converters provided by the CMS, most of which were migrated fromNewtonsoft.Json
in PRs #15728 and #15685. The next version of Umbraco Deploy will also use STJ and the changes in this PR enhances these built-in converters, reducing the amount of duplicate code.To summarize, I've done the following:
JsonUdiConverter
to support bothUdi
andGuidUdi
(removingJsonGuidUdiConverter
), plus now also adding support forStringUdi
(used in Deploy for languages, templates and other file based entities);JsonBoolConverter
toJsonFuzzyBooleanConverter
and update the implementation to always require a boolean (like) value (removing the fallback tofalse
for invalid values);ReadOnlyJsonConverter<T>
andWriteOnlyJsonConverter<T>
base converters;AutoInterningStringConverter
toJsonStringInternConverter
;AutoInterningStringKeyCaseInsensitiveDictionaryConverter<TValue>
toJsonDictionaryStringInternIgnoreCaseConverter<TValue>
;CaseInsensitiveDictionaryConverter<TValue>
toJsonDictionaryStringIgnoreCaseConverter<TValue>
;ForceUtcDateTimeConverter
toJsonUniversalDateTimeConverter
;MessagePackAutoInterningStringKeyCaseInsensitiveDictionaryFormatter<TValue>
toMessagePackDictionaryStringInternIgnoreCaseFormatter<TValue>
;SystemTextJsonSerializer
toDefaultJsonSerializer
andSystemTextConfigurationEditorJsonSerializer
toDefaultConfigurationEditorJsonSerializer
;JsonUdiRangeConverter
to supportUdiRange
;sealed
keyword to all converters.