v14: Refactor and enhance System.Text.Json converters (#15960)
* Update JsonUdiConverter to support Udi, GuidUdi and StringUdi types * Require boolean (like) value and rename to JsonFuzzyBooleanConverter * Add read/write only JsonConverters and align naming * Rename SystemTextJsonSerializer to DefaultJsonSerializer * Rename SystemTextConfigurationEditorJsonSerializer to DefaultConfigurationEditorJsonSerializer * Add JsonUdiRangeConverter * Rename JsonFuzzyBooleanConverter back to JsonBooleanConverter * Fix value type check in JsonObjectConverter * Revert class names * Updated tests * Post fix after merge. --------- Co-authored-by: Bjarke Berg <mail@bergmania.dk>
This commit is contained in:
@@ -32,7 +32,7 @@ public class ConfigureUmbracoBackofficeJsonOptions : IConfigureNamedOptions<Json
|
||||
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
|
||||
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
|
||||
options.JsonSerializerOptions.Converters.Add(new JsonUdiConverter());
|
||||
options.JsonSerializerOptions.Converters.Add(new JsonGuidUdiConverter());
|
||||
options.JsonSerializerOptions.Converters.Add(new JsonUdiRangeConverter());
|
||||
options.JsonSerializerOptions.Converters.Add(new JsonObjectConverter());
|
||||
|
||||
options.JsonSerializerOptions.TypeInfoResolver = _umbracoJsonTypeInfoResolver;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
namespace Umbraco.Cms.Core.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Provides functionality to serialize objects or value types to JSON and to deserialize JSON into objects or value types, used for data type configuration.
|
||||
/// </summary>
|
||||
public interface IConfigurationEditorJsonSerializer : IJsonSerializer
|
||||
{
|
||||
}
|
||||
{ }
|
||||
|
||||
@@ -1,8 +1,27 @@
|
||||
namespace Umbraco.Cms.Core.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Provides functionality to serialize objects or value types to JSON and to deserialize JSON into objects or value types.
|
||||
/// </summary>
|
||||
public interface IJsonSerializer
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts the specified <paramref name="input" /> into a JSON string.
|
||||
/// </summary>
|
||||
/// <param name="input">The input.</param>
|
||||
/// <returns>
|
||||
/// A JSON string representation of the value.
|
||||
/// </returns>
|
||||
string Serialize(object? input);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Parses the text representing a single JSON value into an instance of the type specified by a generic type parameter.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The target type of the JSON value.</typeparam>
|
||||
/// <param name="input">The JSON input to parse.</param>
|
||||
/// <returns>
|
||||
/// A <typeparamref name="T" /> representation of the JSON value.
|
||||
/// </returns>
|
||||
T? Deserialize<T>(string input);
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Serialization;
|
||||
|
||||
public class AutoInterningStringConverter : JsonConverter<string>
|
||||
{
|
||||
// This is a hacky workaround to creating a "read only converter", since System.Text.Json doesn't support it.
|
||||
// Taken from https://github.com/dotnet/runtime/issues/46372#issuecomment-1660515178
|
||||
private readonly JsonConverter<string> _fallbackConverter = (JsonConverter<string>)JsonSerializerOptions.Default.GetConverter(typeof(string));
|
||||
|
||||
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
|
||||
reader.TokenType switch
|
||||
{
|
||||
JsonTokenType.Null => null,
|
||||
JsonTokenType.String =>
|
||||
// It's safe to ignore nullability here, because according to the docs:
|
||||
// Returns null when TokenType is JsonTokenType.Null
|
||||
// https://learn.microsoft.com/en-us/dotnet/api/system.text.json.utf8jsonreader.getstring?view=net-8.0#remarks
|
||||
string.Intern(reader.GetString()!),
|
||||
_ => throw new InvalidOperationException($"{nameof(AutoInterningStringConverter)} only supports strings."),
|
||||
};
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
|
||||
=> _fallbackConverter.Write(writer, value, options);
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Serialization;
|
||||
|
||||
public class AutoInterningStringKeyCaseInsensitiveDictionaryConverter<TValue> : JsonConverter<IDictionary<string, TValue>>
|
||||
{
|
||||
// This is a hacky workaround to creating a "read only converter", since System.Text.Json doesn't support it.
|
||||
// Taken from https://github.com/dotnet/runtime/issues/46372#issuecomment-1660515178
|
||||
private readonly JsonConverter<IDictionary<string, TValue>> _fallbackConverter = (JsonConverter<IDictionary<string, TValue>>)JsonSerializerOptions.Default.GetConverter(typeof(IDictionary<string, TValue>));
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type typeToConvert) => typeof(IDictionary<string, TValue>).IsAssignableFrom(typeToConvert);
|
||||
|
||||
public override Dictionary<string, TValue>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.StartObject)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var dictionary = new Dictionary<string, TValue>(StringComparer.OrdinalIgnoreCase);
|
||||
while (reader.Read())
|
||||
{
|
||||
switch (reader.TokenType)
|
||||
{
|
||||
case JsonTokenType.PropertyName:
|
||||
var key = string.Intern(reader.GetString()!);
|
||||
|
||||
if (reader.Read() is false)
|
||||
{
|
||||
throw new JsonException();
|
||||
}
|
||||
|
||||
TValue? value = JsonSerializer.Deserialize<TValue>(ref reader, options);
|
||||
dictionary[key] = value!;
|
||||
break;
|
||||
case JsonTokenType.Comment:
|
||||
break;
|
||||
case JsonTokenType.EndObject:
|
||||
return dictionary;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, IDictionary<string, TValue> value, JsonSerializerOptions options) => _fallbackConverter.Write(writer, value, options);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Serialization;
|
||||
|
||||
public class CaseInsensitiveDictionaryConverter<TValue> : JsonConverter<IDictionary<string, TValue>>
|
||||
{
|
||||
// This is a hacky workaround to creating a "read only converter", since System.Text.Json doesn't support it.
|
||||
// Taken from https://github.com/dotnet/runtime/issues/46372#issuecomment-1660515178
|
||||
private readonly JsonConverter<IDictionary<string, TValue>> _fallbackConverter = (JsonConverter<IDictionary<string, TValue>>)JsonSerializerOptions.Default.GetConverter(typeof(IDictionary<string, TValue>));
|
||||
|
||||
public override IDictionary<string, TValue>? Read(
|
||||
ref Utf8JsonReader reader,
|
||||
Type typeToConvert,
|
||||
JsonSerializerOptions options)
|
||||
{
|
||||
IDictionary<string, TValue>? defaultDictionary = JsonSerializer.Deserialize<IDictionary<string, TValue>>(ref reader, options);
|
||||
return defaultDictionary is null
|
||||
? null
|
||||
: new Dictionary<string, TValue>(defaultDictionary, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, IDictionary<string, TValue> value, JsonSerializerOptions options) => _fallbackConverter.Write(writer, value, options);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// In order to match the existing behaviour, and that os messagepack, we need to ensure that DateTimes are always read as UTC.
|
||||
/// This is not the case by default for System.Text.Json, see: https://github.com/dotnet/runtime/issues/1566
|
||||
/// </summary>
|
||||
public class ForceUtcDateTimeConverter : JsonConverter<DateTime>
|
||||
{
|
||||
private readonly JsonConverter<DateTime> _fallBackConverter = (JsonConverter<DateTime>)JsonSerializerOptions.Default.GetConverter(typeof(DateTime));
|
||||
|
||||
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
=> reader.GetDateTime().ToUniversalTime();
|
||||
|
||||
// The existing behaviour is fine for writing, it's only reading that's an issue.
|
||||
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
|
||||
=> _fallBackConverter.Write(writer, value, options);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Serialization;
|
||||
|
||||
public class JsonBoolConverter : JsonConverter<bool>
|
||||
{
|
||||
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options) =>
|
||||
writer.WriteBooleanValue(value);
|
||||
|
||||
public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
|
||||
reader.TokenType switch
|
||||
{
|
||||
JsonTokenType.True => true,
|
||||
JsonTokenType.False => false,
|
||||
JsonTokenType.String => ParseString(ref reader),
|
||||
JsonTokenType.Number => reader.TryGetInt64(out long l) ? Convert.ToBoolean(l) : reader.TryGetDouble(out double d) ? Convert.ToBoolean(d) : false,
|
||||
_ => throw new JsonException(),
|
||||
};
|
||||
|
||||
private bool ParseString(ref Utf8JsonReader reader)
|
||||
{
|
||||
var value = reader.GetString();
|
||||
if (bool.TryParse(value, out var b))
|
||||
{
|
||||
return b;
|
||||
}
|
||||
|
||||
if (int.TryParse(value, out var i))
|
||||
{
|
||||
return Convert.ToBoolean(i);
|
||||
}
|
||||
|
||||
throw new JsonException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Converts a boolean value to or from JSON, always converting a boolean like value (like <c>1</c> or <c>0</c>) to a boolean.
|
||||
/// </summary>
|
||||
public sealed class JsonBooleanConverter : JsonConverter<bool>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
=> reader.TokenType switch
|
||||
{
|
||||
JsonTokenType.String => ParseString(ref reader),
|
||||
JsonTokenType.Number => ParseNumber(ref reader),
|
||||
JsonTokenType.True => true,
|
||||
JsonTokenType.False => false,
|
||||
_ => throw new JsonException(),
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
|
||||
=> writer.WriteBooleanValue(value);
|
||||
|
||||
private static bool ParseString(ref Utf8JsonReader reader)
|
||||
{
|
||||
var value = reader.GetString();
|
||||
|
||||
if (bool.TryParse(value, out var boolValue))
|
||||
{
|
||||
return boolValue;
|
||||
}
|
||||
|
||||
if (long.TryParse(value, out var longValue))
|
||||
{
|
||||
return Convert.ToBoolean(longValue);
|
||||
}
|
||||
|
||||
if (double.TryParse(value, out double doubleValue))
|
||||
{
|
||||
return Convert.ToBoolean(doubleValue);
|
||||
}
|
||||
|
||||
throw new JsonException();
|
||||
}
|
||||
|
||||
private static bool ParseNumber(ref Utf8JsonReader reader)
|
||||
{
|
||||
if (reader.TryGetInt64(out long longValue))
|
||||
{
|
||||
return Convert.ToBoolean(longValue);
|
||||
}
|
||||
|
||||
if (reader.TryGetDouble(out double doubleValue))
|
||||
{
|
||||
return Convert.ToBoolean(doubleValue);
|
||||
}
|
||||
|
||||
throw new JsonException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Converts a dictionary with a string key to or from JSON, using the <see cref="StringComparer.OrdinalIgnoreCase" /> comparer.
|
||||
/// </summary>
|
||||
/// <typeparam name="TValue">The type of the dictionary value.</typeparam>
|
||||
public sealed class JsonDictionaryStringIgnoreCaseConverter<TValue> : ReadOnlyJsonConverter<Dictionary<string, TValue>>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override Dictionary<string, TValue>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.StartObject)
|
||||
{
|
||||
throw new JsonException();
|
||||
}
|
||||
|
||||
var dictionary = new Dictionary<string, TValue>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
while (reader.Read())
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.EndObject)
|
||||
{
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
// Get key
|
||||
if (reader.TokenType != JsonTokenType.PropertyName)
|
||||
{
|
||||
throw new JsonException();
|
||||
}
|
||||
|
||||
string propertyName = reader.GetString() ?? throw new JsonException();
|
||||
|
||||
// Get value
|
||||
reader.Read();
|
||||
TValue? value = JsonSerializer.Deserialize<TValue>(ref reader, options);
|
||||
if (value is not null)
|
||||
{
|
||||
dictionary[propertyName] = value;
|
||||
}
|
||||
}
|
||||
|
||||
throw new JsonException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Converts a dictionary with a string key to or from JSON, using the <see cref="StringComparer.OrdinalIgnoreCase" /> comparer and interning the string key when reading.
|
||||
/// </summary>
|
||||
/// <typeparam name="TValue">The type of the dictionary value.</typeparam>
|
||||
public sealed class JsonDictionaryStringInternIgnoreCaseConverter<TValue> : ReadOnlyJsonConverter<Dictionary<string, TValue>>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override Dictionary<string, TValue>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.StartObject)
|
||||
{
|
||||
throw new JsonException();
|
||||
}
|
||||
|
||||
var dictionary = new Dictionary<string, TValue>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
while (reader.Read())
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.EndObject)
|
||||
{
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
// Get key
|
||||
if (reader.TokenType != JsonTokenType.PropertyName)
|
||||
{
|
||||
throw new JsonException();
|
||||
}
|
||||
|
||||
string propertyName = reader.GetString() ?? throw new JsonException();
|
||||
|
||||
// Get value
|
||||
reader.Read();
|
||||
TValue? value = JsonSerializer.Deserialize<TValue>(ref reader, options);
|
||||
if (value is not null)
|
||||
{
|
||||
dictionary[string.Intern(propertyName)] = value;
|
||||
}
|
||||
}
|
||||
|
||||
throw new JsonException();
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Serialization;
|
||||
|
||||
public class JsonGuidUdiConverter : JsonConverter<GuidUdi>
|
||||
{
|
||||
public override GuidUdi? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var stringValue = reader.GetString();
|
||||
return stringValue.IsNullOrWhiteSpace() == false && UdiParser.TryParse(stringValue, out Udi? udi) && udi is GuidUdi guidUdi
|
||||
? guidUdi
|
||||
: null;
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, GuidUdi value, JsonSerializerOptions options)
|
||||
=> writer.WriteStringValue(value.ToString());
|
||||
}
|
||||
@@ -1,43 +1,43 @@
|
||||
using System.Collections;
|
||||
using System.Collections;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Serialization;
|
||||
|
||||
public class JsonObjectConverter : JsonConverter<object>
|
||||
/// <summary>
|
||||
/// Converts an object to or from JSON.
|
||||
/// </summary>
|
||||
public sealed class JsonObjectConverter : JsonConverter<object>
|
||||
{
|
||||
public override object? Read(
|
||||
ref Utf8JsonReader reader,
|
||||
Type typeToConvert,
|
||||
JsonSerializerOptions options) =>
|
||||
ParseObject(ref reader);
|
||||
/// <inheritdoc />
|
||||
public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
=> ParseObject(ref reader);
|
||||
|
||||
public override void Write(
|
||||
Utf8JsonWriter writer,
|
||||
object objectToWrite,
|
||||
JsonSerializerOptions options)
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
|
||||
{
|
||||
if (objectToWrite is null)
|
||||
if (value is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If an object is equals "new object()", Json.Serialize would recurse forever and cause a stack overflow
|
||||
// If the value equals an empty 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())
|
||||
Type inputType = value.GetType();
|
||||
if (inputType == typeof(object) && inputType.GetProperties().Length == 0)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
else
|
||||
{
|
||||
JsonSerializer.Serialize(writer, objectToWrite, objectToWrite.GetType(), options);
|
||||
JsonSerializer.Serialize(writer, value, inputType, options);
|
||||
}
|
||||
}
|
||||
|
||||
private object? ParseObject(ref Utf8JsonReader reader)
|
||||
private static object? ParseObject(ref Utf8JsonReader reader)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.StartArray)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,20 +1,25 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Serialization;
|
||||
|
||||
public class JsonUdiConverter : JsonConverter<Udi>
|
||||
/// <summary>
|
||||
/// Converts an <see cref="Udi" /> to or from JSON.
|
||||
/// </summary>
|
||||
public sealed class JsonUdiConverter : JsonConverter<Udi>
|
||||
{
|
||||
public override Udi? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var stringValue = reader.GetString();
|
||||
return stringValue.IsNullOrWhiteSpace() == false && UdiParser.TryParse(stringValue, out Udi? udi)
|
||||
? udi
|
||||
: null;
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type typeToConvert)
|
||||
=> typeof(Udi).IsAssignableFrom(typeToConvert);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Udi? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
=> reader.GetString() is string value
|
||||
? UdiParser.Parse(value)
|
||||
: null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, Udi value, JsonSerializerOptions options)
|
||||
=> writer.WriteStringValue(value.ToString());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Umbraco.Cms.Core;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Converts an <see cref="UdiRange" /> to or from JSON.
|
||||
/// </summary>
|
||||
public sealed class JsonUdiRangeConverter : JsonConverter<UdiRange>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override UdiRange? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
=> reader.GetString() is string value
|
||||
? UdiRange.Parse(value)
|
||||
: null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, UdiRange value, JsonSerializerOptions options)
|
||||
=> writer.WriteStringValue(value.ToString());
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Converts a DateTime value to or from JSON, always converting the value to Coordinated Universal Time (UTC) when reading.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// In order to match the existing behaviour, and that of MessagePack, we need to ensure that DateTimes are always read as UTC.
|
||||
/// This is not the case by default for System.Text.Json, see: https://github.com/dotnet/runtime/issues/1566.
|
||||
/// </remarks>
|
||||
public sealed class JsonUniversalDateTimeConverter : ReadOnlyJsonConverter<DateTime>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
=> reader.GetDateTime().ToUniversalTime();
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Converts an object or value from JSON.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of object or value handled by the converter.</typeparam>
|
||||
public abstract class ReadOnlyJsonConverter<T> : JsonConverter<T>
|
||||
{
|
||||
private readonly JsonConverter<T> _fallbackConverter = (JsonConverter<T>)JsonSerializerOptions.Default.GetConverter(typeof(T));
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
||||
=> _fallbackConverter.Write(writer, value, options);
|
||||
}
|
||||
@@ -1,36 +1,38 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Serialization;
|
||||
|
||||
// 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)
|
||||
|
||||
public class SystemTextConfigurationEditorJsonSerializer : IConfigurationEditorJsonSerializer
|
||||
/// <inheritdoc />
|
||||
public sealed class SystemTextConfigurationEditorJsonSerializer : IConfigurationEditorJsonSerializer
|
||||
{
|
||||
private JsonSerializerOptions _jsonSerializerOptions;
|
||||
private readonly JsonSerializerOptions _jsonSerializerOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SystemTextConfigurationEditorJsonSerializer" /> class.
|
||||
/// </summary>
|
||||
public SystemTextConfigurationEditorJsonSerializer()
|
||||
{
|
||||
_jsonSerializerOptions = new JsonSerializerOptions
|
||||
=> _jsonSerializerOptions = new JsonSerializerOptions()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
// 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 = JsonNumberHandling.AllowReadingFromString
|
||||
NumberHandling = JsonNumberHandling.AllowReadingFromString,
|
||||
Converters =
|
||||
{
|
||||
new JsonStringEnumConverter(),
|
||||
new JsonObjectConverter(),
|
||||
new JsonUdiConverter(),
|
||||
new JsonUdiRangeConverter(),
|
||||
new JsonBooleanConverter()
|
||||
}
|
||||
};
|
||||
_jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
|
||||
_jsonSerializerOptions.Converters.Add(new JsonObjectConverter());
|
||||
_jsonSerializerOptions.Converters.Add(new JsonUdiConverter());
|
||||
_jsonSerializerOptions.Converters.Add(new JsonGuidUdiConverter());
|
||||
_jsonSerializerOptions.Converters.Add(new JsonBoolConverter());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Serialize(object? input) => JsonSerializer.Serialize(input, _jsonSerializerOptions);
|
||||
|
||||
/// <inheritdoc />
|
||||
public T? Deserialize<T>(string input) => JsonSerializer.Deserialize<T>(input, _jsonSerializerOptions);
|
||||
}
|
||||
|
||||
@@ -1,24 +1,34 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Serialization;
|
||||
|
||||
public class SystemTextJsonSerializer : IJsonSerializer
|
||||
/// <inheritdoc />
|
||||
public sealed class SystemTextJsonSerializer : IJsonSerializer
|
||||
{
|
||||
private readonly JsonSerializerOptions _jsonSerializerOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SystemTextJsonSerializer" /> class.
|
||||
/// </summary>
|
||||
public SystemTextJsonSerializer()
|
||||
{
|
||||
_jsonSerializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
|
||||
_jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
|
||||
_jsonSerializerOptions.Converters.Add(new JsonUdiConverter());
|
||||
_jsonSerializerOptions.Converters.Add(new JsonGuidUdiConverter());
|
||||
// we may need to add JsonObjectConverter at some point, but for the time being things work fine without
|
||||
// _jsonSerializerOptions.Converters.Add(new JsonObjectConverter());
|
||||
}
|
||||
=> _jsonSerializerOptions = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
Converters =
|
||||
{
|
||||
new JsonStringEnumConverter(),
|
||||
new JsonUdiConverter(),
|
||||
new JsonUdiRangeConverter(),
|
||||
// We may need to add JsonObjectConverter at some point, but for the time being things work fine without
|
||||
//new JsonObjectConverter()
|
||||
}
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Serialize(object? input) => JsonSerializer.Serialize(input, _jsonSerializerOptions);
|
||||
|
||||
/// <inheritdoc />
|
||||
public T? Deserialize<T>(string input) => JsonSerializer.Deserialize<T>(input, _jsonSerializerOptions);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Converts an object or value to JSON.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of object or value handled by the converter.</typeparam>
|
||||
public abstract class WriteOnlyJsonConverter<T> : JsonConverter<T>
|
||||
{
|
||||
private readonly JsonConverter<T> _fallbackConverter = (JsonConverter<T>)JsonSerializerOptions.Default.GetConverter(typeof(T));
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
=> _fallbackConverter.Read(ref reader, typeToConvert, options);
|
||||
}
|
||||
@@ -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[]>))]
|
||||
[MessagePackFormatter(typeof(MessagePackDictionaryStringInternIgnoreCaseFormatter<PropertyData[]>))]
|
||||
public Dictionary<string, PropertyData[]>? PropertyData { get; set; }
|
||||
|
||||
[DataMember(Order = 1)]
|
||||
[JsonPropertyName("cd")]
|
||||
[JsonConverter(typeof(AutoInterningStringKeyCaseInsensitiveDictionaryConverter<CultureVariation>))]
|
||||
[MessagePackFormatter(typeof(MessagePackAutoInterningStringKeyCaseInsensitiveDictionaryFormatter<CultureVariation>))]
|
||||
[JsonConverter(typeof(JsonDictionaryStringInternIgnoreCaseConverter<CultureVariation>))]
|
||||
[MessagePackFormatter(typeof(MessagePackDictionaryStringInternIgnoreCaseFormatter<CultureVariation>))]
|
||||
public Dictionary<string, CultureVariation>? CultureData { get; set; }
|
||||
|
||||
[DataMember(Order = 2)]
|
||||
@@ -32,12 +32,12 @@ public class ContentCacheDataModel
|
||||
// Legacy properties used to deserialize existing nucache db entries
|
||||
[IgnoreDataMember]
|
||||
[JsonPropertyName("properties")]
|
||||
[JsonConverter(typeof(CaseInsensitiveDictionaryConverter<PropertyData[]>))]
|
||||
[JsonConverter(typeof(JsonDictionaryStringIgnoreCaseConverter<PropertyData[]>))]
|
||||
private Dictionary<string, PropertyData[]> LegacyPropertyData { set => PropertyData = value; }
|
||||
|
||||
[IgnoreDataMember]
|
||||
[JsonPropertyName("cultureData")]
|
||||
[JsonConverter(typeof(CaseInsensitiveDictionaryConverter<CultureVariation>))]
|
||||
[JsonConverter(typeof(JsonDictionaryStringIgnoreCaseConverter<CultureVariation>))]
|
||||
private Dictionary<string, CultureVariation> LegacyCultureData { set => CultureData = value; }
|
||||
|
||||
[IgnoreDataMember]
|
||||
|
||||
@@ -20,7 +20,7 @@ public class CultureVariation
|
||||
|
||||
[DataMember(Order = 2)]
|
||||
[JsonPropertyName("dt")]
|
||||
[JsonConverter(typeof(ForceUtcDateTimeConverter))]
|
||||
[JsonConverter(typeof(JsonUniversalDateTimeConverter))]
|
||||
public DateTime Date { get; set; }
|
||||
|
||||
[DataMember(Order = 3)]
|
||||
@@ -38,7 +38,7 @@ public class CultureVariation
|
||||
|
||||
[IgnoreDataMember]
|
||||
[JsonPropertyName("date")]
|
||||
[JsonConverter(typeof(ForceUtcDateTimeConverter))]
|
||||
[JsonConverter(typeof(JsonUniversalDateTimeConverter))]
|
||||
private DateTime LegacyDate { set => Date = value; }
|
||||
|
||||
[IgnoreDataMember]
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
using MessagePack;
|
||||
using MessagePack.Formatters;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource;
|
||||
|
||||
/// <summary>
|
||||
/// A MessagePack formatter (deserializer) for a string key dictionary that uses
|
||||
/// <see cref="StringComparer.OrdinalIgnoreCase" /> for the key string comparison.
|
||||
/// </summary>
|
||||
/// <typeparam name="TValue">The type of the value.</typeparam>
|
||||
/// <seealso
|
||||
/// cref="DictionaryFormatterBase<string, TValue, Dictionary<string, TValue>, Dictionary<string, TValue>.Enumerator, Dictionary<string, TValue>>" />
|
||||
public sealed class MessagePackAutoInterningStringKeyCaseInsensitiveDictionaryFormatter<TValue> :
|
||||
DictionaryFormatterBase<string, TValue, Dictionary<string, TValue>, Dictionary<string, TValue>.Enumerator,
|
||||
Dictionary<string, TValue>>
|
||||
{
|
||||
protected override void Add(Dictionary<string, TValue> collection, int index, string key, TValue value,
|
||||
MessagePackSerializerOptions options)
|
||||
{
|
||||
string.Intern(key);
|
||||
collection.Add(key, value);
|
||||
}
|
||||
|
||||
protected override Dictionary<string, TValue> Complete(Dictionary<string, TValue> intermediateCollection) =>
|
||||
intermediateCollection;
|
||||
|
||||
protected override Dictionary<string, TValue>.Enumerator GetSourceEnumerator(Dictionary<string, TValue> source) =>
|
||||
source.GetEnumerator();
|
||||
|
||||
protected override Dictionary<string, TValue> Create(int count, MessagePackSerializerOptions options) =>
|
||||
new(count, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using MessagePack;
|
||||
using MessagePack.Formatters;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource;
|
||||
|
||||
/// <summary>
|
||||
/// A MessagePack formatter (deserializer) for a string key dictionary that uses <see cref="StringComparer.OrdinalIgnoreCase" /> for the key string comparison and interns the string.
|
||||
/// </summary>
|
||||
/// <typeparam name="TValue">The type of the value.</typeparam>
|
||||
public sealed class MessagePackDictionaryStringInternIgnoreCaseFormatter<TValue> : DictionaryFormatterBase<string, TValue, Dictionary<string, TValue>, Dictionary<string, TValue>.Enumerator, Dictionary<string, TValue>>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Add(Dictionary<string, TValue> collection, int index, string key, TValue value, MessagePackSerializerOptions options)
|
||||
=> collection.Add(string.Intern(key), value);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Dictionary<string, TValue> Complete(Dictionary<string, TValue> intermediateCollection)
|
||||
=> intermediateCollection;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Dictionary<string, TValue>.Enumerator GetSourceEnumerator(Dictionary<string, TValue> source)
|
||||
=> source.GetEnumerator();
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Dictionary<string, TValue> Create(int count, MessagePackSerializerOptions options)
|
||||
=> new(count, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
@@ -12,7 +12,7 @@ public class PropertyData
|
||||
private string? _segment;
|
||||
|
||||
[DataMember(Order = 0)]
|
||||
[JsonConverter(typeof(AutoInterningStringConverter))]
|
||||
[JsonConverter(typeof(JsonStringInternConverter))]
|
||||
[DefaultValue("")]
|
||||
[JsonPropertyName("c")]
|
||||
public string? Culture
|
||||
@@ -24,7 +24,7 @@ public class PropertyData
|
||||
}
|
||||
|
||||
[DataMember(Order = 1)]
|
||||
[JsonConverter(typeof(AutoInterningStringConverter))]
|
||||
[JsonConverter(typeof(JsonStringInternConverter))]
|
||||
[DefaultValue("")]
|
||||
[JsonPropertyName("s")]
|
||||
public string? Segment
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Moq;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
@@ -23,7 +23,6 @@ public class ArtifactBaseTests
|
||||
Converters =
|
||||
{
|
||||
new JsonUdiConverter(),
|
||||
new JsonGuidUdiConverter()
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Infrastructure.Serialization;
|
||||
|
||||
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Serialization;
|
||||
|
||||
[TestFixture]
|
||||
public class AutoInterningStringConverterTests
|
||||
{
|
||||
[Test]
|
||||
public void Intern_Property_String()
|
||||
{
|
||||
var str1 = "Hello";
|
||||
var obj = new Test { Name = str1 + Guid.NewGuid() };
|
||||
|
||||
// ensure the raw value is not interned
|
||||
Assert.IsNull(string.IsInterned(obj.Name));
|
||||
|
||||
var serialized = JsonSerializer.Serialize(obj);
|
||||
obj = JsonSerializer.Deserialize<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 + Guid.NewGuid()] = 0, [str1 + Guid.NewGuid()] = 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 = JsonSerializer.Serialize(obj);
|
||||
obj = JsonSerializer.Deserialize<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 { get; set; } = new();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Infrastructure.Serialization;
|
||||
|
||||
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Serialization;
|
||||
|
||||
[TestFixture]
|
||||
public class JsonStringInternConverterTests
|
||||
{
|
||||
[Test]
|
||||
public void Intern_Property_String()
|
||||
{
|
||||
var obj = new Test
|
||||
{
|
||||
Name = Guid.NewGuid().ToString(),
|
||||
};
|
||||
|
||||
// Ensure the raw value is not interned
|
||||
Assert.IsNull(string.IsInterned(obj.Name));
|
||||
|
||||
// Ensure the value is interned when deserializing using converter
|
||||
var json = JsonSerializer.Serialize(obj);
|
||||
obj = JsonSerializer.Deserialize<Test>(json);
|
||||
|
||||
Assert.IsNotNull(string.IsInterned(obj.Name));
|
||||
|
||||
// Ensure the value is interned when deserializing using options
|
||||
json = JsonSerializer.Serialize(Guid.NewGuid().ToString());
|
||||
var str = JsonSerializer.Deserialize<string>(json, new JsonSerializerOptions()
|
||||
{
|
||||
Converters =
|
||||
{
|
||||
new JsonStringInternConverter(),
|
||||
},
|
||||
});
|
||||
|
||||
Assert.IsNotNull(string.IsInterned(str));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Intern_Property_Dictionary()
|
||||
{
|
||||
var obj = new Test
|
||||
{
|
||||
Values = new Dictionary<string, int>
|
||||
{
|
||||
[Guid.NewGuid().ToString()] = 0,
|
||||
[Guid.NewGuid().ToString()] = 1,
|
||||
},
|
||||
};
|
||||
|
||||
// Ensure the raw values are not interned
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (string key in obj.Values.Keys)
|
||||
{
|
||||
Assert.IsNull(string.IsInterned(key));
|
||||
}
|
||||
});
|
||||
|
||||
// Add value to dictionary to test case-insensitivity
|
||||
obj.Values.Add("Test", 3);
|
||||
|
||||
// Ensure the value is interned when deserializing using converter
|
||||
var json = JsonSerializer.Serialize(obj);
|
||||
obj = JsonSerializer.Deserialize<Test>(json);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (string key in obj.Values.Keys)
|
||||
{
|
||||
Assert.IsNotNull(string.IsInterned(key));
|
||||
}
|
||||
});
|
||||
|
||||
// Check that the dictionary is case-insensitive
|
||||
Assert.IsTrue(obj.Values.ContainsKey("Test"));
|
||||
Assert.IsTrue(obj.Values.ContainsKey("test"));
|
||||
|
||||
// Ensure the value is interned when deserializing using options
|
||||
json = JsonSerializer.Serialize(new Dictionary<string, int>
|
||||
{
|
||||
[Guid.NewGuid().ToString()] = 0,
|
||||
[Guid.NewGuid().ToString()] = 1,
|
||||
["Test"] = 3
|
||||
});
|
||||
var dictionary = JsonSerializer.Deserialize<Dictionary<string, int>>(json, new JsonSerializerOptions()
|
||||
{
|
||||
Converters =
|
||||
{
|
||||
new JsonDictionaryStringInternIgnoreCaseConverter<int>(),
|
||||
},
|
||||
});
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (string key in dictionary.Keys)
|
||||
{
|
||||
Assert.IsNotNull(string.IsInterned(key));
|
||||
}
|
||||
});
|
||||
|
||||
// Check that the dictionary is case-insensitive
|
||||
Assert.IsTrue(dictionary.ContainsKey("Test"));
|
||||
Assert.IsTrue(dictionary.ContainsKey("test"));
|
||||
}
|
||||
|
||||
public class Test
|
||||
{
|
||||
[JsonConverter(typeof(JsonStringInternConverter))]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonConverter(typeof(JsonDictionaryStringInternIgnoreCaseConverter<int>))]
|
||||
public Dictionary<string, int> Values { get; set; } = new();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user