diff --git a/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary.Tests/SerializationTests.cs b/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary.Tests/SerializationTests.cs index a8dae554..2ef950a6 100644 --- a/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary.Tests/SerializationTests.cs +++ b/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary.Tests/SerializationTests.cs @@ -14,28 +14,27 @@ namespace Squidex.ClientLibrary.Tests { public class SerializationTests { - public sealed class MyClass + public sealed record MyClass { [JsonConverter(typeof(InvariantConverter))] public T Value { get; set; } } - public sealed class MyCamelClass + public sealed record MyWriteClass { + [JsonConverter(typeof(InvariantWriteConverter))] public T Value { get; set; } } - [KeepCasing] - public sealed class MyPascalClass + public sealed record MyCamelClass { public T Value { get; set; } } - private readonly JsonSerializerSettings settings = new JsonSerializerSettings(); - - public SerializationTests() + [KeepCasing] + public sealed record MyPascalClass { - settings.Converters.Add(new InvariantConverter()); + public T Value { get; set; } } [Fact] @@ -153,46 +152,58 @@ public void Should_serialize_invariant() } [Fact] - public void Should_serialize_dynamic_properties_with_original_casing() + public void Should_serialize_invariant_jsonnull() { - var source = new DynamicData + var source = new MyClass> { - ["Property1"] = new JObject() + Value = "hello" }; var serialized = source.ToJson(); - Assert.Contains("\"Property1\": {}", serialized, StringComparison.Ordinal); + Assert.Contains("\"iv\": \"hello\"", serialized, StringComparison.Ordinal); } [Fact] - public void Should_deserialize_invariant() + public void Should_serialize_write_invariant_jsonull() { - var json = "{ 'value': { 'iv': 'hello'} }"; + var source = new MyWriteClass> + { + Value = "hello" + }; - var result = JsonConvert.DeserializeObject>(json, settings); + var serialized = source.ToJson(); - Assert.Equal("hello", result?.Value); + Assert.Contains("\"iv\": \"hello\"", serialized, StringComparison.Ordinal); } [Fact] - public void Should_deserialize_invariant_null_value() + public void Should_serialize_localized_jsonnull() { - var json = "{ 'value': null }"; + var source = new + { + value = new + { + en = new JsonNull("hello") + } + }; - var result = JsonConvert.DeserializeObject>(json, settings); + var serialized = source.ToJson(); - Assert.Null(result?.Value); + Assert.Contains("\"en\": \"hello\"", serialized, StringComparison.Ordinal); } [Fact] - public void Should_deserialize_invariant_empty_value() + public void Should_serialize_dynamic_properties_with_original_casing() { - var json = "{ 'value': {} }"; + var source = new DynamicData + { + ["Property1"] = new JObject() + }; - var result = JsonConvert.DeserializeObject>(json, settings); + var serialized = source.ToJson(); - Assert.Null(result?.Value); + Assert.Contains("\"Property1\": {}", serialized, StringComparison.Ordinal); } [Fact] @@ -220,5 +231,75 @@ public void Should_serialize_with_pascal_case() Assert.Contains("\"Value\": \"hello\"", serialized, StringComparison.Ordinal); } + + [Fact] + public void Should_deserialize_invariant() + { + var json = "{ 'value': { 'iv': 'hello'} }"; + + var result = json.FromJson>(); + + Assert.Equal("hello", result?.Value); + } + + [Fact] + public void Should_deserialize_invariant_null_value() + { + var json = "{ 'value': null }"; + + var result = json.FromJson>(); + + Assert.Null(result?.Value); + } + + [Fact] + public void Should_deserialize_invariant_empty_value() + { + var json = "{ 'value': {} }"; + + var result = json.FromJson>(); + + Assert.Null(result?.Value); + } + + [Fact] + public void Should_deserialize_invariant_jsonnull() + { + var json = "{ 'value': { 'iv': 'hello'} }"; + + var result = json.FromJson>>(); + + Assert.Equal("hello", result?.Value.Value); + } + + [Fact] + public void Should_deserialize_invariant_jsonnull_ull_value() + { + var json = "{ 'value': null }"; + + var result = json.FromJson>>(); + + Assert.Null(result?.Value.Value); + } + + [Fact] + public void Should_deserialize_invariant_jsonnull_empty_value() + { + var json = "{ 'value': {} }"; + + var result = json.FromJson>>(); + + Assert.Null(result?.Value.Value); + } + + [Fact] + public void Should_deserialize_localized_jsonnull() + { + var json = "{ 'value': { 'en': 'hello'} }"; + + var result = json.FromJson>>>(); + + Assert.Equal("hello", result?.Value["en"].Value); + } } } diff --git a/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/JsonNull.cs b/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/JsonNull.cs new file mode 100644 index 00000000..1a1ae2f8 --- /dev/null +++ b/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/JsonNull.cs @@ -0,0 +1,42 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +#pragma warning disable SA1313 // Parameter names should begin with lower-case letter + +namespace Squidex.ClientLibrary +{ + /// + /// A value that can be null. + /// + /// The actual value. + public record struct JsonNull(T Value) + { + /// + /// Operator to compare the value to the wrapper. + /// + /// The value to convert. + public static implicit operator JsonNull(T value) + { + return new JsonNull(value); + } + + /// + /// Operator to compare the wrapper to the actual value. + /// + /// The wrapper to convert. + public static implicit operator T(JsonNull wrapper) + { + return wrapper.Value; + } + + /// + public override readonly string ToString() + { + return Value?.ToString() ?? string.Empty; + } + } +} diff --git a/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/Squidex.ClientLibrary.csproj b/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/Squidex.ClientLibrary.csproj index 43341beb..4f22eb89 100644 --- a/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/Squidex.ClientLibrary.csproj +++ b/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/Squidex.ClientLibrary.csproj @@ -13,7 +13,7 @@ https://github.com/Squidex/squidex/ Squidex HeadlessCMS netstandard2.0;netcoreapp3.1;net5.0;net6.0 - 8.18.0 + 8.19.0 diff --git a/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/Utils/ActorConverter.cs b/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/Utils/ActorConverter.cs index f013e338..539caa1a 100644 --- a/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/Utils/ActorConverter.cs +++ b/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/Utils/ActorConverter.cs @@ -16,28 +16,28 @@ namespace Squidex.ClientLibrary.Utils public class ActorConverter : JsonConverter { /// - public override Actor? ReadJson(JsonReader reader, Type objectType, Actor? existingValue, bool hasExistingValue, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, Actor? value, JsonSerializer serializer) { - if (reader.TokenType == JsonToken.Null) + if (value == null) { - return null; + writer.WriteNull(); + return; } - var s = ((string)reader.Value!).Split(':'); - - return new Actor { Id = s[1], Type = s[0] }; + serializer.Serialize(writer, $"{value.Type}:{value.Id}"); } /// - public override void WriteJson(JsonWriter writer, Actor? value, JsonSerializer serializer) + public override Actor? ReadJson(JsonReader reader, Type objectType, Actor? existingValue, bool hasExistingValue, JsonSerializer serializer) { - if (value == null) + if (reader.TokenType == JsonToken.Null) { - writer.WriteNull(); - return; + return null; } - serializer.Serialize(writer, $"{value.Type}:{value.Id}"); + var s = ((string)reader.Value!).Split(':'); + + return new Actor { Id = s[1], Type = s[0] }; } } } diff --git a/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/Utils/HttpClientExtensions.cs b/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/Utils/HttpClientExtensions.cs index f4fc16c3..ec085584 100644 --- a/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/Utils/HttpClientExtensions.cs +++ b/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/Utils/HttpClientExtensions.cs @@ -24,7 +24,7 @@ static HttpClientExtensions() { SerializerSettings = new JsonSerializerSettings { - ContractResolver = new CamelCasePropertyNamesContractResolver() + ContractResolver = new JsonNullContractResolver() }; SerializerSettings.Converters.Add(new StringEnumConverter()); @@ -63,6 +63,13 @@ public static string ToJson(this T value) return json; } + public static T FromJson(this string value) + { + var json = JsonConvert.DeserializeObject(value, SerializerSettings)!; + + return json; + } + public static async Task ReadAsJsonAsync(this HttpContent content) { #if NET5_0_OR_GREATER diff --git a/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/Utils/JsonNullContractResolver.cs b/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/Utils/JsonNullContractResolver.cs new file mode 100644 index 00000000..6f3b7085 --- /dev/null +++ b/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/Utils/JsonNullContractResolver.cs @@ -0,0 +1,74 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using System.Reflection; + +namespace Squidex.ClientLibrary.Utils +{ + internal class JsonNullContractResolver : CamelCasePropertyNamesContractResolver + { + /* + protected override JsonContract CreateContract(Type objectType) + { + var contract = base.CreateContract(objectType); + + if (contract.UnderlyingType.IsGenericType && contract.UnderlyingType.GetGenericTypeDefinition() == typeof(JsonNull<>)) + { + var converterType = typeof(JsonNullConverter<>).MakeGenericType(contract.UnderlyingType.GetGenericArguments()[0]); + var converterObj = Activator.CreateInstance(converterType) as JsonConverter; + + contract.Converter = converterObj; + } + + return contract; + } + */ + + protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) + { + var property = base.CreateProperty(member, memberSerialization); + + var objectType = (member as PropertyInfo)?.PropertyType; + + if (objectType?.IsGenericType == true && objectType.GetGenericTypeDefinition() == typeof(JsonNull<>)) + { + var baseType = typeof(JsonNullConverter<>); + + if (property.Converter is InvariantConverter) + { + baseType = typeof(JsonNullInvariantConverter<>); + } + else if (property.Converter is InvariantWriteConverter) + { + baseType = typeof(JsonNullInvariantWriteConverter<>); + } + + var converterType = baseType.MakeGenericType(objectType.GetGenericArguments()[0]); + var converterObj = Activator.CreateInstance(converterType) as JsonConverter; + + property.Converter = converterObj; + } + + return property; + } + + protected override JsonConverter? ResolveContractConverter(Type objectType) + { + if (objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(JsonNull<>)) + { + var converterType = typeof(JsonNullConverter<>).MakeGenericType(objectType.GetGenericArguments()[0]); + var converterObj = Activator.CreateInstance(converterType) as JsonConverter; + + return converterObj; + } + + return base.ResolveContractConverter(objectType); + } + } +} diff --git a/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/Utils/JsonNullConverter.cs b/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/Utils/JsonNullConverter.cs new file mode 100644 index 00000000..e63e52ff --- /dev/null +++ b/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/Utils/JsonNullConverter.cs @@ -0,0 +1,32 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Newtonsoft.Json; + +namespace Squidex.ClientLibrary.Utils +{ + /// + /// Convert JsonNull to its actual value. + /// + /// The wrapped type. + public class JsonNullConverter : JsonConverter> + { + /// + public override void WriteJson(JsonWriter writer, JsonNull value, JsonSerializer serializer) + { + serializer.Serialize(writer, value.Value, typeof(T)); + } + + /// + public override JsonNull ReadJson(JsonReader reader, Type objectType, JsonNull existingValue, bool hasExistingValue, JsonSerializer serializer) + { + var value = serializer.Deserialize(reader); + + return new JsonNull(value!); + } + } +} diff --git a/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/Utils/JsonNullInvariantConverter.cs b/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/Utils/JsonNullInvariantConverter.cs new file mode 100644 index 00000000..06e0a787 --- /dev/null +++ b/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/Utils/JsonNullInvariantConverter.cs @@ -0,0 +1,54 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Newtonsoft.Json; + +namespace Squidex.ClientLibrary.Utils +{ + internal sealed class JsonNullInvariantConverter : JsonConverter> + { + /// + public override void WriteJson(JsonWriter writer, JsonNull value, JsonSerializer serializer) + { + writer.WriteStartObject(); + writer.WritePropertyName("iv"); + + serializer.Serialize(writer, value.Value, typeof(T)); + + writer.WriteEndObject(); + } + + /// + public override JsonNull ReadJson(JsonReader reader, Type objectType, JsonNull existingValue, bool hasExistingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + { + return default; + } + + reader.Read(); + + if (reader.TokenType == JsonToken.EndObject) + { + // empty object + return default; + } + else if (reader.TokenType != JsonToken.PropertyName || !string.Equals(reader.Value?.ToString(), "iv", StringComparison.OrdinalIgnoreCase)) + { + throw new JsonSerializationException("Property must have a invariant language property."); + } + + reader.Read(); + + var result = serializer.Deserialize>(reader)!; + + reader.Read(); + + return result; + } + } +} diff --git a/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/Utils/JsonNullInvariantWriteConverter.cs b/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/Utils/JsonNullInvariantWriteConverter.cs new file mode 100644 index 00000000..e7877183 --- /dev/null +++ b/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/Utils/JsonNullInvariantWriteConverter.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Newtonsoft.Json; + +namespace Squidex.ClientLibrary.Utils +{ + internal sealed class JsonNullInvariantWriteConverter : JsonNullConverter + { + /// + public override void WriteJson(JsonWriter writer, JsonNull value, JsonSerializer serializer) + { + writer.WriteStartObject(); + writer.WritePropertyName("iv"); + + serializer.Serialize(writer, value.Value, typeof(T)); + + writer.WriteEndObject(); + } + } +} diff --git a/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/Utils/NamedIdConverter.cs b/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/Utils/NamedIdConverter.cs index 2527da9a..d134a3f0 100644 --- a/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/Utils/NamedIdConverter.cs +++ b/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/Utils/NamedIdConverter.cs @@ -10,34 +10,36 @@ namespace Squidex.ClientLibrary.Utils { /// - /// Convert comma separated string to NamedId - /// Example of input: "00000000-0000-0000-0000-000000000000,name". + /// Convert comma separated string to NamedId. /// + /// + /// Example of input: "00000000-0000-0000-0000-000000000000,name". + /// public class NamedIdConverter : JsonConverter { /// - public override NamedId? ReadJson(JsonReader reader, Type objectType, NamedId? existingValue, bool hasExistingValue, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, NamedId? value, JsonSerializer serializer) { - if (reader.TokenType == JsonToken.Null) + if (value == null) { - return null; + writer.WriteNull(); + return; } - var s = ((string)reader.Value!).Split(','); - - return new NamedId { Id = s[0], Name = s[1] }; + serializer.Serialize(writer, $"{value.Id},{value.Name}"); } /// - public override void WriteJson(JsonWriter writer, NamedId? value, JsonSerializer serializer) + public override NamedId? ReadJson(JsonReader reader, Type objectType, NamedId? existingValue, bool hasExistingValue, JsonSerializer serializer) { - if (value == null) + if (reader.TokenType == JsonToken.Null) { - writer.WriteNull(); - return; + return null; } - serializer.Serialize(writer, $"{value.Id},{value.Name}"); + var s = ((string)reader.Value!).Split(','); + + return new NamedId { Id = s[0], Name = s[1] }; } } }