diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 78beba5780..0b24a474b4 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -117,7 +117,7 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddScoped(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/src/Umbraco.Infrastructure/Serialization/ContextualJsonSerializer.cs b/src/Umbraco.Infrastructure/Serialization/ContextualJsonSerializer.cs new file mode 100644 index 0000000000..4583c502ab --- /dev/null +++ b/src/Umbraco.Infrastructure/Serialization/ContextualJsonSerializer.cs @@ -0,0 +1,49 @@ +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Web; + +namespace Umbraco.Cms.Infrastructure.Serialization; + +// FIXME: move away from Json.NET; this is a temporary fix that attempts to use System.Text.Json for management API operations, Json.NET for other operations +public class ContextualJsonSerializer : IJsonSerializer +{ + private readonly IRequestAccessor _requestAccessor; + private readonly IJsonSerializer _jsonNetSerializer; + private readonly IJsonSerializer _systemTextSerializer; + + public ContextualJsonSerializer(IRequestAccessor requestAccessor) + { + _requestAccessor = requestAccessor; + _jsonNetSerializer = new JsonNetSerializer(); + _systemTextSerializer = new SystemTextJsonSerializer(); + } + + public string Serialize(object? input) => ContextualizedSerializer().Serialize(input); + + public T? Deserialize(string input) => ContextualizedSerializer().Deserialize(input); + + public T? DeserializeSubset(string input, string key) => throw new NotSupportedException(); + + private IJsonSerializer ContextualizedSerializer() + { + try + { + var requestedPath = _requestAccessor.GetRequestUrl()?.AbsolutePath; + if (requestedPath != null) + { + // add white listed paths for the System.Text.Json config serializer here + // - always use it for the new management API + if (requestedPath.Contains("/umbraco/management/api/")) + { + return _systemTextSerializer; + } + } + } + catch (Exception ex) + { + // ignore - this whole thing is a temporary workaround, let's not make a fuss + } + + return _jsonNetSerializer; + } +} + diff --git a/src/Umbraco.Infrastructure/Serialization/SystemTextConfigurationEditorJsonSerializer.cs b/src/Umbraco.Infrastructure/Serialization/SystemTextConfigurationEditorJsonSerializer.cs index 088a16be88..c7fcd456fa 100644 --- a/src/Umbraco.Infrastructure/Serialization/SystemTextConfigurationEditorJsonSerializer.cs +++ b/src/Umbraco.Infrastructure/Serialization/SystemTextConfigurationEditorJsonSerializer.cs @@ -1,10 +1,10 @@ using System.Text.Json; -using System.Text.Json.Nodes; +using System.Text.Json.Serialization; using Umbraco.Cms.Core.Serialization; namespace Umbraco.Cms.Infrastructure.Serialization; -// TODO: clean up all config editor serializers when we can migrate fully to System.Text.Json +// FIXME: clean up all config editor serializers when we can migrate fully to System.Text.Json // - move this implementation to ConfigurationEditorJsonSerializer (delete the old implementation) // - use this implementation as the registered singleton (delete ContextualConfigurationEditorJsonSerializer) // - reuse the JsonObjectConverter implementation from management API (delete the local implementation - pending V12 branch update) @@ -21,9 +21,9 @@ public class SystemTextConfigurationEditorJsonSerializer : IConfigurationEditorJ // in some cases, configs aren't camel cased in the DB, so we have to resort to case insensitive // property name resolving when creating configuration objects (deserializing DB configs) PropertyNameCaseInsensitive = true, - NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString + NumberHandling = JsonNumberHandling.AllowReadingFromString }; - _jsonSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter()); + _jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); _jsonSerializerOptions.Converters.Add(new JsonObjectConverter()); } @@ -32,73 +32,4 @@ public class SystemTextConfigurationEditorJsonSerializer : IConfigurationEditorJ public T? Deserialize(string input) => JsonSerializer.Deserialize(input, _jsonSerializerOptions); public T? DeserializeSubset(string input, string key) => throw new NotSupportedException(); - - // TODO: reuse the JsonObjectConverter implementation from management API - private class JsonObjectConverter : System.Text.Json.Serialization.JsonConverter - { - public override object Read( - ref Utf8JsonReader reader, - Type typeToConvert, - JsonSerializerOptions options) => - ParseObject(ref reader); - - public override void Write( - Utf8JsonWriter writer, - object objectToWrite, - JsonSerializerOptions options) - { - if (objectToWrite is null) - { - 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()) - { - writer.WriteStartObject(); - writer.WriteEndObject(); - } - else - { - JsonSerializer.Serialize(writer, objectToWrite, objectToWrite.GetType(), options); - } - } - - private object ParseObject(ref Utf8JsonReader reader) - { - if (reader.TokenType == JsonTokenType.StartArray) - { - var items = new List(); - while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) - { - items.Add(ParseObject(ref reader)); - } - - return items.ToArray(); - } - - if (reader.TokenType == JsonTokenType.StartObject) - { - var jsonNode = JsonNode.Parse(ref reader); - if (jsonNode is JsonObject jsonObject) - { - return jsonObject; - } - } - - return reader.TokenType switch - { - JsonTokenType.True => true, - JsonTokenType.False => false, - JsonTokenType.Number when reader.TryGetInt32(out int i) => i, - JsonTokenType.Number when reader.TryGetInt64(out long l) => l, - JsonTokenType.Number => reader.GetDouble(), - JsonTokenType.String when reader.TryGetDateTime(out DateTime datetime) => datetime, - JsonTokenType.String => reader.GetString()!, - _ => JsonDocument.ParseValue(ref reader).RootElement.Clone() - }; - } - } } diff --git a/src/Umbraco.Infrastructure/Serialization/SystemTextJsonSerializer.cs b/src/Umbraco.Infrastructure/Serialization/SystemTextJsonSerializer.cs new file mode 100644 index 0000000000..17a4e0fc1a --- /dev/null +++ b/src/Umbraco.Infrastructure/Serialization/SystemTextJsonSerializer.cs @@ -0,0 +1,24 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Umbraco.Cms.Core.Serialization; + +namespace Umbraco.Cms.Infrastructure.Serialization; + +public class SystemTextJsonSerializer : IJsonSerializer +{ + private readonly JsonSerializerOptions _jsonSerializerOptions; + + public SystemTextJsonSerializer() + { + _jsonSerializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + _jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + // we may need to add JsonObjectConverter at some point, but for the time being things work fine without + // _jsonSerializerOptions.Converters.Add(new JsonObjectConverter()); + } + + public string Serialize(object? input) => JsonSerializer.Serialize(input, _jsonSerializerOptions); + + public T? Deserialize(string input) => JsonSerializer.Deserialize(input, _jsonSerializerOptions); + + public T? DeserializeSubset(string input, string key) => throw new NotSupportedException(); +}