Add support for NamingStrategy to StringEnumConverter (#1885)
This commit is contained in:
@@ -82,7 +82,9 @@ namespace Newtonsoft.Json.Tests.Converters
|
||||
|
||||
string json = JsonConvert.SerializeObject(regex, Formatting.Indented, new JsonSerializerSettings
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
Converters = { new RegexConverter(), new StringEnumConverter() { CamelCaseText = true } },
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver()
|
||||
});
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ using System.IO;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
#if DNXCORE50
|
||||
using Xunit;
|
||||
using Test = Xunit.FactAttribute;
|
||||
@@ -132,39 +133,173 @@ namespace Newtonsoft.Json.Tests.Converters
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter), true)]
|
||||
public enum CamelCaseEnum
|
||||
public enum CamelCaseEnumObsolete
|
||||
{
|
||||
This,
|
||||
Is,
|
||||
CamelCase
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter), true, false)]
|
||||
[JsonConverter(typeof(StringEnumConverter), typeof(CamelCaseNamingStrategy))]
|
||||
public enum CamelCaseEnumNew
|
||||
{
|
||||
This,
|
||||
Is,
|
||||
CamelCase
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter), typeof(SnakeCaseNamingStrategy))]
|
||||
public enum SnakeCaseEnumNew
|
||||
{
|
||||
This,
|
||||
Is,
|
||||
SnakeCase
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter), typeof(CamelCaseNamingStrategy), new object[0], false)]
|
||||
public enum NotAllowIntegerValuesEnum
|
||||
{
|
||||
Foo = 0,
|
||||
Bar = 1
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter), true, true)]
|
||||
[JsonConverter(typeof(StringEnumConverter), typeof(CamelCaseNamingStrategy))]
|
||||
public enum AllowIntegerValuesEnum
|
||||
{
|
||||
Foo = 0,
|
||||
Bar = 1
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter), typeof(CamelCaseNamingStrategy), null)]
|
||||
public enum NullArgumentInAttribute
|
||||
{
|
||||
Foo = 0,
|
||||
Bar = 1
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Serialize_CamelCaseFromAttribute_Obsolete()
|
||||
{
|
||||
string json = JsonConvert.SerializeObject(CamelCaseEnumObsolete.CamelCase);
|
||||
Assert.AreEqual(@"""camelCase""", json);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NamingStrategyAndCamelCaseText()
|
||||
{
|
||||
StringEnumConverter converter = new StringEnumConverter();
|
||||
Assert.IsNull(converter.NamingStrategy);
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
converter.CamelCaseText = true;
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
Assert.IsNotNull(converter.NamingStrategy);
|
||||
Assert.AreEqual(typeof(CamelCaseNamingStrategy), converter.NamingStrategy.GetType());
|
||||
|
||||
var camelCaseInstance = converter.NamingStrategy;
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
converter.CamelCaseText = true;
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
Assert.AreEqual(camelCaseInstance, converter.NamingStrategy);
|
||||
|
||||
converter.NamingStrategy = null;
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
Assert.IsFalse(converter.CamelCaseText);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
|
||||
converter.NamingStrategy = new CamelCaseNamingStrategy();
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
Assert.IsTrue(converter.CamelCaseText);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
|
||||
converter.NamingStrategy = new SnakeCaseNamingStrategy();
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
Assert.IsFalse(converter.CamelCaseText);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
converter.CamelCaseText = false;
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
Assert.IsNotNull(converter.NamingStrategy);
|
||||
Assert.AreEqual(typeof(SnakeCaseNamingStrategy), converter.NamingStrategy.GetType());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StringEnumConverter_CamelCaseTextCtor()
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
StringEnumConverter converter = new StringEnumConverter(true);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
|
||||
Assert.IsNotNull(converter.NamingStrategy);
|
||||
Assert.AreEqual(typeof(CamelCaseNamingStrategy), converter.NamingStrategy.GetType());
|
||||
Assert.AreEqual(true, converter.AllowIntegerValues);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StringEnumConverter_NamingStrategyTypeCtor()
|
||||
{
|
||||
StringEnumConverter converter = new StringEnumConverter(typeof(CamelCaseNamingStrategy), new object[] { true, true, true }, false);
|
||||
|
||||
Assert.IsNotNull(converter.NamingStrategy);
|
||||
Assert.AreEqual(typeof(CamelCaseNamingStrategy), converter.NamingStrategy.GetType());
|
||||
Assert.AreEqual(false, converter.AllowIntegerValues);
|
||||
Assert.AreEqual(true, converter.NamingStrategy.OverrideSpecifiedNames);
|
||||
Assert.AreEqual(true, converter.NamingStrategy.ProcessDictionaryKeys);
|
||||
Assert.AreEqual(true, converter.NamingStrategy.ProcessExtensionDataNames);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StringEnumConverter_NamingStrategyTypeCtor_Null()
|
||||
{
|
||||
ExceptionAssert.Throws<ArgumentNullException>(
|
||||
() => new StringEnumConverter(null),
|
||||
@"Value cannot be null.
|
||||
Parameter name: namingStrategyType");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StringEnumConverter_NamingStrategyTypeWithArgsCtor_Null()
|
||||
{
|
||||
ExceptionAssert.Throws<ArgumentNullException>(
|
||||
() => new StringEnumConverter(null, new object[] { true, true, true }, false),
|
||||
@"Value cannot be null.
|
||||
Parameter name: namingStrategyType");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Deserialize_CamelCaseFromAttribute_Obsolete()
|
||||
{
|
||||
CamelCaseEnumObsolete e = JsonConvert.DeserializeObject<CamelCaseEnumObsolete>(@"""camelCase""");
|
||||
Assert.AreEqual(CamelCaseEnumObsolete.CamelCase, e);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Serialize_CamelCaseFromAttribute()
|
||||
{
|
||||
string json = JsonConvert.SerializeObject(CamelCaseEnum.CamelCase);
|
||||
string json = JsonConvert.SerializeObject(CamelCaseEnumNew.CamelCase);
|
||||
Assert.AreEqual(@"""camelCase""", json);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Deserialize_CamelCaseFromAttribute()
|
||||
{
|
||||
CamelCaseEnum e = JsonConvert.DeserializeObject<CamelCaseEnum>(@"""camelCase""");
|
||||
Assert.AreEqual(CamelCaseEnum.CamelCase, e);
|
||||
CamelCaseEnumNew e = JsonConvert.DeserializeObject<CamelCaseEnumNew>(@"""camelCase""");
|
||||
Assert.AreEqual(CamelCaseEnumNew.CamelCase, e);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Serialize_SnakeCaseFromAttribute()
|
||||
{
|
||||
string json = JsonConvert.SerializeObject(SnakeCaseEnumNew.SnakeCase);
|
||||
Assert.AreEqual(@"""snake_case""", json);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Deserialize_SnakeCaseFromAttribute()
|
||||
{
|
||||
SnakeCaseEnumNew e = JsonConvert.DeserializeObject<SnakeCaseEnumNew>(@"""snake_case""");
|
||||
Assert.AreEqual(SnakeCaseEnumNew.SnakeCase, e);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -176,6 +311,17 @@ namespace Newtonsoft.Json.Tests.Converters
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CannotPassNullArgumentToConverter()
|
||||
{
|
||||
var ex = ExceptionAssert.Throws<JsonException>(() =>
|
||||
{
|
||||
JsonConvert.DeserializeObject<NullArgumentInAttribute>(@"""9""");
|
||||
});
|
||||
|
||||
Assert.AreEqual("Cannot pass a null parameter to the constructor.", ex.InnerException.Message);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Deserialize_AllowIntegerValuesAttribute()
|
||||
{
|
||||
@@ -311,7 +457,9 @@ namespace Newtonsoft.Json.Tests.Converters
|
||||
NullableStoreColor2 = null
|
||||
};
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
string json = JsonConvert.SerializeObject(enumClass, Formatting.Indented, new StringEnumConverter { CamelCaseText = true });
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
|
||||
StringAssert.AreEqual(@"{
|
||||
""StoreColor"": ""red"",
|
||||
@@ -474,7 +622,9 @@ namespace Newtonsoft.Json.Tests.Converters
|
||||
Enum = FlagsTestEnum.First | FlagsTestEnum.Second
|
||||
};
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
string json = JsonConvert.SerializeObject(c, Formatting.Indented, new StringEnumConverter { CamelCaseText = true });
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
StringAssert.AreEqual(@"{
|
||||
""Enum"": ""first, second""
|
||||
}", json);
|
||||
@@ -487,7 +637,9 @@ namespace Newtonsoft.Json.Tests.Converters
|
||||
""Enum"": ""first, second""
|
||||
}";
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
EnumContainer<FlagsTestEnum> c = JsonConvert.DeserializeObject<EnumContainer<FlagsTestEnum>>(json, new StringEnumConverter { CamelCaseText = true });
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
Assert.AreEqual(FlagsTestEnum.First | FlagsTestEnum.Second, c.Enum);
|
||||
}
|
||||
|
||||
@@ -565,7 +717,9 @@ namespace Newtonsoft.Json.Tests.Converters
|
||||
(Foo)int.MaxValue
|
||||
};
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
string json1 = JsonConvert.SerializeObject(lfoo, Formatting.Indented, new StringEnumConverter { CamelCaseText = true });
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
|
||||
StringAssert.AreEqual(@"[
|
||||
""Bat, baz"",
|
||||
@@ -588,7 +742,9 @@ namespace Newtonsoft.Json.Tests.Converters
|
||||
|
||||
List<Bar> lbar = new List<Bar>() { Bar.FooBar, Bar.Bat, Bar.SerializeAsBaz };
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
string json2 = JsonConvert.SerializeObject(lbar, Formatting.Indented, new StringEnumConverter { CamelCaseText = true });
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
|
||||
StringAssert.AreEqual(@"[
|
||||
""foo_bar"",
|
||||
|
||||
@@ -51,7 +51,7 @@ namespace Newtonsoft.Json.Tests.Utilities
|
||||
{
|
||||
Type enumType = expected.GetType();
|
||||
|
||||
Enum result = (Enum)EnumUtils.ParseEnum(enumType, value, false);
|
||||
Enum result = (Enum)EnumUtils.ParseEnum(enumType, null, value, false);
|
||||
Assert.AreEqual(expected, result);
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ namespace Newtonsoft.Json.Tests.Utilities
|
||||
{
|
||||
try
|
||||
{
|
||||
EnumUtils.ParseEnum(enumType, value, false);
|
||||
EnumUtils.ParseEnum(enumType, null, value, false);
|
||||
}
|
||||
catch (Exception ex) when (ex.GetType() == exceptionType)
|
||||
{
|
||||
@@ -80,7 +80,7 @@ namespace Newtonsoft.Json.Tests.Utilities
|
||||
[TestCaseSource(nameof(ToString_Format_TestData))]
|
||||
public static void ToString_Format(Enum e, string expected)
|
||||
{
|
||||
EnumUtils.TryToString(e.GetType(), e, false, out string result);
|
||||
EnumUtils.TryToString(e.GetType(), e, null, out string result);
|
||||
|
||||
Assert.AreEqual(expected, result);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ using System.Globalization;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Serialization;
|
||||
using Newtonsoft.Json.Utilities;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
#if !HAVE_LINQ
|
||||
using Newtonsoft.Json.Utilities.LinqBridge;
|
||||
#else
|
||||
@@ -50,40 +51,121 @@ namespace Newtonsoft.Json.Converters
|
||||
/// The default value is <c>false</c>.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if the written enum text will be camel case; otherwise, <c>false</c>.</value>
|
||||
public bool CamelCaseText { get; set; }
|
||||
[Obsolete("StringEnumConverter.CamelCaseText is obsolete. Set StringEnumConverter.NamingStrategy with CamelCaseNamingStrategy instead.")]
|
||||
public bool CamelCaseText
|
||||
{
|
||||
get => NamingStrategy is CamelCaseNamingStrategy ? true : false;
|
||||
set
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
if (NamingStrategy is CamelCaseNamingStrategy)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
NamingStrategy = new CamelCaseNamingStrategy();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!(NamingStrategy is CamelCaseNamingStrategy))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
NamingStrategy = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the naming strategy used to resolve how enum text is written.
|
||||
/// </summary>
|
||||
/// <value>The naming strategy used to resolve how enum text is written.</value>
|
||||
public NamingStrategy NamingStrategy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether integer values are allowed when serializing and deserializing.
|
||||
/// The default value is <c>true</c>.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if integers are allowed when serializing and deserializing; otherwise, <c>false</c>.</value>
|
||||
public bool AllowIntegerValues { get; set; }
|
||||
public bool AllowIntegerValues { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StringEnumConverter"/> class.
|
||||
/// </summary>
|
||||
public StringEnumConverter()
|
||||
{
|
||||
AllowIntegerValues = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StringEnumConverter"/> class.
|
||||
/// </summary>
|
||||
/// <param name="camelCaseText"><c>true</c> if the written enum text will be camel case; otherwise, <c>false</c>.</param>
|
||||
[Obsolete("StringEnumConverter(bool) is obsolete. Create a converter with StringEnumConverter(NamingStrategy, bool) instead.")]
|
||||
public StringEnumConverter(bool camelCaseText)
|
||||
: this()
|
||||
{
|
||||
CamelCaseText = camelCaseText;
|
||||
if (camelCaseText)
|
||||
{
|
||||
NamingStrategy = new CamelCaseNamingStrategy();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StringEnumConverter"/> class.
|
||||
/// </summary>
|
||||
/// <param name="camelCaseText"><c>true</c> if the written enum text will be camel case; otherwise, <c>false</c>.</param>
|
||||
/// <param name="namingStrategy">The naming strategy used to resolve how enum text is written.</param>
|
||||
/// <param name="allowIntegerValues"><c>true</c> if integers are allowed when serializing and deserializing; otherwise, <c>false</c>.</param>
|
||||
public StringEnumConverter(bool camelCaseText, bool allowIntegerValues) : this(camelCaseText)
|
||||
public StringEnumConverter(NamingStrategy namingStrategy, bool allowIntegerValues = true)
|
||||
{
|
||||
NamingStrategy = namingStrategy;
|
||||
AllowIntegerValues = allowIntegerValues;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StringEnumConverter"/> class.
|
||||
/// </summary>
|
||||
/// <param name="namingStrategyType">The <see cref="System.Type"/> of the <see cref="Newtonsoft.Json.Serialization.NamingStrategy"/> used to write enum text.</param>
|
||||
public StringEnumConverter(Type namingStrategyType)
|
||||
{
|
||||
ValidationUtils.ArgumentNotNull(namingStrategyType, nameof(namingStrategyType));
|
||||
|
||||
NamingStrategy = JsonTypeReflector.CreateNamingStrategyInstance(namingStrategyType, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StringEnumConverter"/> class.
|
||||
/// </summary>
|
||||
/// <param name="namingStrategyType">The <see cref="System.Type"/> of the <see cref="Newtonsoft.Json.Serialization.NamingStrategy"/> used to write enum text.</param>
|
||||
/// <param name="namingStrategyParameters">
|
||||
/// The parameter list to use when constructing the <see cref="Newtonsoft.Json.Serialization.NamingStrategy"/> described by <paramref name="namingStrategyType"/>.
|
||||
/// If <c>null</c>, the default constructor is used.
|
||||
/// When non-<c>null</c>, there must be a constructor defined in the <see cref="Newtonsoft.Json.Serialization.NamingStrategy"/> that exactly matches the number,
|
||||
/// order, and type of these parameters.
|
||||
/// </param>
|
||||
public StringEnumConverter(Type namingStrategyType, object[] namingStrategyParameters)
|
||||
{
|
||||
ValidationUtils.ArgumentNotNull(namingStrategyType, nameof(namingStrategyType));
|
||||
|
||||
NamingStrategy = JsonTypeReflector.CreateNamingStrategyInstance(namingStrategyType, namingStrategyParameters);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StringEnumConverter"/> class.
|
||||
/// </summary>
|
||||
/// <param name="namingStrategyType">The <see cref="System.Type"/> of the <see cref="Newtonsoft.Json.Serialization.NamingStrategy"/> used to write enum text.</param>
|
||||
/// <param name="namingStrategyParameters">
|
||||
/// The parameter list to use when constructing the <see cref="Newtonsoft.Json.Serialization.NamingStrategy"/> described by <paramref name="namingStrategyType"/>.
|
||||
/// If <c>null</c>, the default constructor is used.
|
||||
/// When non-<c>null</c>, there must be a constructor defined in the <see cref="Newtonsoft.Json.Serialization.NamingStrategy"/> that exactly matches the number,
|
||||
/// order, and type of these parameters.
|
||||
/// </param>
|
||||
/// <param name="allowIntegerValues"><c>true</c> if integers are allowed when serializing and deserializing; otherwise, <c>false</c>.</param>
|
||||
public StringEnumConverter(Type namingStrategyType, object[] namingStrategyParameters, bool allowIntegerValues)
|
||||
{
|
||||
ValidationUtils.ArgumentNotNull(namingStrategyType, nameof(namingStrategyType));
|
||||
|
||||
NamingStrategy = JsonTypeReflector.CreateNamingStrategyInstance(namingStrategyType, namingStrategyParameters);
|
||||
AllowIntegerValues = allowIntegerValues;
|
||||
}
|
||||
|
||||
@@ -103,7 +185,7 @@ namespace Newtonsoft.Json.Converters
|
||||
|
||||
Enum e = (Enum)value;
|
||||
|
||||
if (!EnumUtils.TryToString(e.GetType(), value, CamelCaseText, out string enumName))
|
||||
if (!EnumUtils.TryToString(e.GetType(), value, NamingStrategy, out string enumName))
|
||||
{
|
||||
if (!AllowIntegerValues)
|
||||
{
|
||||
@@ -153,7 +235,7 @@ namespace Newtonsoft.Json.Converters
|
||||
return null;
|
||||
}
|
||||
|
||||
return EnumUtils.ParseEnum(t, enumText, !AllowIntegerValues);
|
||||
return EnumUtils.ParseEnum(t, NamingStrategy, enumText, !AllowIntegerValues);
|
||||
}
|
||||
|
||||
if (reader.TokenType == JsonToken.Integer)
|
||||
|
||||
@@ -49,9 +49,9 @@ namespace Newtonsoft.Json
|
||||
internal TypeNameHandling? _itemTypeNameHandling;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="JsonConverter"/> used when serializing the property's collection items.
|
||||
/// Gets or sets the <see cref="JsonConverter"/> type used when serializing the property's collection items.
|
||||
/// </summary>
|
||||
/// <value>The collection's items <see cref="JsonConverter"/>.</value>
|
||||
/// <value>The collection's items <see cref="JsonConverter"/> type.</value>
|
||||
public Type ItemConverterType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -30,38 +30,6 @@ using Newtonsoft.Json.Utilities;
|
||||
|
||||
namespace Newtonsoft.Json.Serialization
|
||||
{
|
||||
internal readonly struct ResolverContractKey : IEquatable<ResolverContractKey>
|
||||
{
|
||||
private readonly Type _resolverType;
|
||||
private readonly Type _contractType;
|
||||
|
||||
public ResolverContractKey(Type resolverType, Type contractType)
|
||||
{
|
||||
_resolverType = resolverType;
|
||||
_contractType = contractType;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return _resolverType.GetHashCode() ^ _contractType.GetHashCode();
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (!(obj is ResolverContractKey key))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Equals(key);
|
||||
}
|
||||
|
||||
public bool Equals(ResolverContractKey other)
|
||||
{
|
||||
return (_resolverType == other._resolverType && _contractType == other._contractType);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves member mappings for a type, camel casing property names.
|
||||
/// </summary>
|
||||
@@ -69,7 +37,7 @@ namespace Newtonsoft.Json.Serialization
|
||||
{
|
||||
private static readonly object TypeContractCacheLock = new object();
|
||||
private static readonly DefaultJsonNameTable NameTable = new DefaultJsonNameTable();
|
||||
private static Dictionary<ResolverContractKey, JsonContract> _contractCache;
|
||||
private static Dictionary<StructMultiKey<Type, Type>, JsonContract> _contractCache;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CamelCasePropertyNamesContractResolver"/> class.
|
||||
@@ -96,8 +64,8 @@ namespace Newtonsoft.Json.Serialization
|
||||
}
|
||||
|
||||
// for backwards compadibility the CamelCasePropertyNamesContractResolver shares contracts between instances
|
||||
ResolverContractKey key = new ResolverContractKey(GetType(), type);
|
||||
Dictionary<ResolverContractKey, JsonContract> cache = _contractCache;
|
||||
StructMultiKey<Type, Type> key = new StructMultiKey<Type, Type>(GetType(), type);
|
||||
Dictionary<StructMultiKey<Type, Type>, JsonContract> cache = _contractCache;
|
||||
if (cache == null || !cache.TryGetValue(key, out JsonContract contract))
|
||||
{
|
||||
contract = CreateContract(type);
|
||||
@@ -106,9 +74,9 @@ namespace Newtonsoft.Json.Serialization
|
||||
lock (TypeContractCacheLock)
|
||||
{
|
||||
cache = _contractCache;
|
||||
Dictionary<ResolverContractKey, JsonContract> updatedCache = (cache != null)
|
||||
? new Dictionary<ResolverContractKey, JsonContract>(cache)
|
||||
: new Dictionary<ResolverContractKey, JsonContract>();
|
||||
Dictionary<StructMultiKey<Type, Type>, JsonContract> updatedCache = (cache != null)
|
||||
? new Dictionary<StructMultiKey<Type, Type>, JsonContract>(cache)
|
||||
: new Dictionary<StructMultiKey<Type, Type>, JsonContract>();
|
||||
updatedCache[key] = contract;
|
||||
|
||||
_contractCache = updatedCache;
|
||||
|
||||
@@ -43,20 +43,20 @@ namespace Newtonsoft.Json.Serialization
|
||||
{
|
||||
internal static readonly DefaultSerializationBinder Instance = new DefaultSerializationBinder();
|
||||
|
||||
private readonly ThreadSafeStore<TypeNameKey, Type> _typeCache;
|
||||
private readonly ThreadSafeStore<StructMultiKey<string, string>, Type> _typeCache;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DefaultSerializationBinder"/> class.
|
||||
/// </summary>
|
||||
public DefaultSerializationBinder()
|
||||
{
|
||||
_typeCache = new ThreadSafeStore<TypeNameKey, Type>(GetTypeFromTypeNameKey);
|
||||
_typeCache = new ThreadSafeStore<StructMultiKey<string, string>, Type>(GetTypeFromTypeNameKey);
|
||||
}
|
||||
|
||||
private Type GetTypeFromTypeNameKey(TypeNameKey typeNameKey)
|
||||
private Type GetTypeFromTypeNameKey(StructMultiKey<string, string> typeNameKey)
|
||||
{
|
||||
string assemblyName = typeNameKey.AssemblyName;
|
||||
string typeName = typeNameKey.TypeName;
|
||||
string assemblyName = typeNameKey.Value1;
|
||||
string typeName = typeNameKey.Value2;
|
||||
|
||||
if (assemblyName != null)
|
||||
{
|
||||
@@ -159,7 +159,7 @@ namespace Newtonsoft.Json.Serialization
|
||||
{
|
||||
string typeArgAssemblyQualifiedName = typeName.Substring(typeArgStartIndex, i - typeArgStartIndex);
|
||||
|
||||
TypeNameKey typeNameKey = ReflectionUtils.SplitFullyQualifiedTypeName(typeArgAssemblyQualifiedName);
|
||||
StructMultiKey<string, string> typeNameKey = ReflectionUtils.SplitFullyQualifiedTypeName(typeArgAssemblyQualifiedName);
|
||||
genericTypeArguments.Add(GetTypeByName(typeNameKey));
|
||||
}
|
||||
break;
|
||||
@@ -173,7 +173,7 @@ namespace Newtonsoft.Json.Serialization
|
||||
return type;
|
||||
}
|
||||
|
||||
private Type GetTypeByName(TypeNameKey typeNameKey)
|
||||
private Type GetTypeByName(StructMultiKey<string, string> typeNameKey)
|
||||
{
|
||||
return _typeCache.Get(typeNameKey);
|
||||
}
|
||||
@@ -188,7 +188,7 @@ namespace Newtonsoft.Json.Serialization
|
||||
/// </returns>
|
||||
public override Type BindToType(string assemblyName, string typeName)
|
||||
{
|
||||
return GetTypeByName(new TypeNameKey(assemblyName, typeName));
|
||||
return GetTypeByName(new StructMultiKey<string, string>(assemblyName, typeName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -777,12 +777,12 @@ namespace Newtonsoft.Json.Serialization
|
||||
|
||||
if (resolvedTypeNameHandling != TypeNameHandling.None)
|
||||
{
|
||||
TypeNameKey typeNameKey = ReflectionUtils.SplitFullyQualifiedTypeName(qualifiedTypeName);
|
||||
StructMultiKey<string, string> typeNameKey = ReflectionUtils.SplitFullyQualifiedTypeName(qualifiedTypeName);
|
||||
|
||||
Type specifiedType;
|
||||
try
|
||||
{
|
||||
specifiedType = Serializer._serializationBinder.BindToType(typeNameKey.AssemblyName, typeNameKey.TypeName);
|
||||
specifiedType = Serializer._serializationBinder.BindToType(typeNameKey.Value1, typeNameKey.Value2);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -955,7 +955,7 @@ namespace Newtonsoft.Json.Serialization
|
||||
{
|
||||
if (value is string s)
|
||||
{
|
||||
return EnumUtils.ParseEnum(contract.NonNullableUnderlyingType, s, false);
|
||||
return EnumUtils.ParseEnum(contract.NonNullableUnderlyingType, null, s, false);
|
||||
}
|
||||
if (ConvertUtils.IsInteger(primitiveContract.TypeCode))
|
||||
{
|
||||
|
||||
@@ -1150,7 +1150,7 @@ namespace Newtonsoft.Json.Serialization
|
||||
{
|
||||
escape = true;
|
||||
|
||||
if (primitiveContract.IsEnum && EnumUtils.TryToString(primitiveContract.NonNullableUnderlyingType, name, false, out string enumName))
|
||||
if (primitiveContract.IsEnum && EnumUtils.TryToString(primitiveContract.NonNullableUnderlyingType, name, null, out string enumName))
|
||||
{
|
||||
return enumName;
|
||||
}
|
||||
|
||||
@@ -198,18 +198,18 @@ namespace Newtonsoft.Json.Serialization
|
||||
/// Lookup and create an instance of the <see cref="JsonConverter"/> type described by the argument.
|
||||
/// </summary>
|
||||
/// <param name="converterType">The <see cref="JsonConverter"/> type to create.</param>
|
||||
/// <param name="converterArgs">Optional arguments to pass to an initializing constructor of the JsonConverter.
|
||||
/// <param name="args">Optional arguments to pass to an initializing constructor of the JsonConverter.
|
||||
/// If <c>null</c>, the default constructor is used.</param>
|
||||
public static JsonConverter CreateJsonConverterInstance(Type converterType, object[] converterArgs)
|
||||
public static JsonConverter CreateJsonConverterInstance(Type converterType, object[] args)
|
||||
{
|
||||
Func<object[], object> converterCreator = CreatorCache.Get(converterType);
|
||||
return (JsonConverter)converterCreator(converterArgs);
|
||||
return (JsonConverter)converterCreator(args);
|
||||
}
|
||||
|
||||
public static NamingStrategy CreateNamingStrategyInstance(Type namingStrategyType, object[] converterArgs)
|
||||
public static NamingStrategy CreateNamingStrategyInstance(Type namingStrategyType, object[] args)
|
||||
{
|
||||
Func<object[], object> converterCreator = CreatorCache.Get(namingStrategyType);
|
||||
return (NamingStrategy)converterCreator(converterArgs);
|
||||
return (NamingStrategy)converterCreator(args);
|
||||
}
|
||||
|
||||
public static NamingStrategy GetContainerNamingStrategy(JsonContainerAttribute containerAttribute)
|
||||
@@ -239,10 +239,18 @@ namespace Newtonsoft.Json.Serialization
|
||||
{
|
||||
if (parameters != null)
|
||||
{
|
||||
Type[] paramTypes = parameters.Select(param => param.GetType()).ToArray();
|
||||
Type[] paramTypes = parameters.Select(param =>
|
||||
{
|
||||
if (param == null)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot pass a null parameter to the constructor.");
|
||||
}
|
||||
|
||||
return param.GetType();
|
||||
}).ToArray();
|
||||
ConstructorInfo parameterizedConstructorInfo = type.GetConstructor(paramTypes);
|
||||
|
||||
if (null != parameterizedConstructorInfo)
|
||||
if (parameterizedConstructorInfo != null)
|
||||
{
|
||||
ObjectConstructor<object> parameterizedConstructor = ReflectionDelegateFactory.CreateParameterizedConstructor(parameterizedConstructorInfo);
|
||||
return parameterizedConstructor(parameters);
|
||||
|
||||
@@ -248,46 +248,15 @@ namespace Newtonsoft.Json.Utilities
|
||||
#endif
|
||||
}
|
||||
|
||||
internal readonly struct TypeConvertKey : IEquatable<TypeConvertKey>
|
||||
private static readonly ThreadSafeStore<StructMultiKey<Type, Type>, Func<object, object>> CastConverters =
|
||||
new ThreadSafeStore<StructMultiKey<Type, Type>, Func<object, object>>(CreateCastConverter);
|
||||
|
||||
private static Func<object, object> CreateCastConverter(StructMultiKey<Type, Type> t)
|
||||
{
|
||||
public Type InitialType { get; }
|
||||
|
||||
public Type TargetType { get; }
|
||||
|
||||
public TypeConvertKey(Type initialType, Type targetType)
|
||||
{
|
||||
InitialType = initialType;
|
||||
TargetType = targetType;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return InitialType.GetHashCode() ^ TargetType.GetHashCode();
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (!(obj is TypeConvertKey key))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Equals(key);
|
||||
}
|
||||
|
||||
public bool Equals(TypeConvertKey other)
|
||||
{
|
||||
return (InitialType == other.InitialType && TargetType == other.TargetType);
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly ThreadSafeStore<TypeConvertKey, Func<object, object>> CastConverters =
|
||||
new ThreadSafeStore<TypeConvertKey, Func<object, object>>(CreateCastConverter);
|
||||
|
||||
private static Func<object, object> CreateCastConverter(TypeConvertKey t)
|
||||
{
|
||||
MethodInfo castMethodInfo = t.TargetType.GetMethod("op_Implicit", new[] { t.InitialType })
|
||||
?? t.TargetType.GetMethod("op_Explicit", new[] { t.InitialType });
|
||||
Type initialType = t.Value1;
|
||||
Type targetType = t.Value2;
|
||||
MethodInfo castMethodInfo = targetType.GetMethod("op_Implicit", new[] { initialType })
|
||||
?? targetType.GetMethod("op_Explicit", new[] { initialType });
|
||||
|
||||
if (castMethodInfo == null)
|
||||
{
|
||||
@@ -630,7 +599,7 @@ namespace Newtonsoft.Json.Utilities
|
||||
return value;
|
||||
}
|
||||
|
||||
Func<object, object> castConverter = CastConverters.Get(new TypeConvertKey(valueType, targetType));
|
||||
Func<object, object> castConverter = CastConverters.Get(new StructMultiKey<Type, Type>(valueType, targetType));
|
||||
if (castConverter != null)
|
||||
{
|
||||
return castConverter(value);
|
||||
|
||||
@@ -35,6 +35,7 @@ using System.Linq;
|
||||
#endif
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace Newtonsoft.Json.Utilities
|
||||
{
|
||||
@@ -43,10 +44,11 @@ namespace Newtonsoft.Json.Utilities
|
||||
private const char EnumSeparatorChar = ',';
|
||||
private const string EnumSeparatorString = ", ";
|
||||
|
||||
private static readonly ThreadSafeStore<Type, EnumInfo> ValuesAndNamesPerEnum = new ThreadSafeStore<Type, EnumInfo>(InitializeValuesAndNames);
|
||||
private static readonly ThreadSafeStore<StructMultiKey<Type, NamingStrategy>, EnumInfo> ValuesAndNamesPerEnum = new ThreadSafeStore<StructMultiKey<Type, NamingStrategy>, EnumInfo>(InitializeValuesAndNames);
|
||||
|
||||
private static EnumInfo InitializeValuesAndNames(Type enumType)
|
||||
private static EnumInfo InitializeValuesAndNames(StructMultiKey<Type, NamingStrategy> key)
|
||||
{
|
||||
Type enumType = key.Value1;
|
||||
string[] names = Enum.GetNames(enumType);
|
||||
string[] resolvedNames = new string[names.Length];
|
||||
ulong[] values = new ulong[names.Length];
|
||||
@@ -72,7 +74,9 @@ namespace Newtonsoft.Json.Utilities
|
||||
resolvedName = name;
|
||||
#endif
|
||||
|
||||
resolvedNames[i] = resolvedName;
|
||||
resolvedNames[i] = key.Value2 != null
|
||||
? key.Value2.GetPropertyName(resolvedName, false)
|
||||
: resolvedName;
|
||||
}
|
||||
|
||||
bool isFlags = enumType.IsDefined(typeof(FlagsAttribute), false);
|
||||
@@ -113,9 +117,9 @@ namespace Newtonsoft.Json.Utilities
|
||||
return selectedFlagsValues;
|
||||
}
|
||||
|
||||
public static bool TryToString(Type enumType, object value, bool camelCaseText, out string name)
|
||||
public static bool TryToString(Type enumType, object value, NamingStrategy namingStrategy, out string name)
|
||||
{
|
||||
EnumInfo enumInfo = ValuesAndNamesPerEnum.Get(enumType);
|
||||
EnumInfo enumInfo = ValuesAndNamesPerEnum.Get(new StructMultiKey<Type, NamingStrategy>(enumType, namingStrategy));
|
||||
ulong v = ToUInt64(value);
|
||||
|
||||
if (!enumInfo.IsFlags)
|
||||
@@ -124,11 +128,6 @@ namespace Newtonsoft.Json.Utilities
|
||||
if (index >= 0)
|
||||
{
|
||||
name = enumInfo.ResolvedNames[index];
|
||||
if (camelCaseText)
|
||||
{
|
||||
name = StringUtils.ToCamelCase(name);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -138,12 +137,12 @@ namespace Newtonsoft.Json.Utilities
|
||||
}
|
||||
else // These are flags OR'ed together (We treat everything as unsigned types)
|
||||
{
|
||||
name = InternalFlagsFormat(enumInfo, v, camelCaseText);
|
||||
name = InternalFlagsFormat(enumInfo, v);
|
||||
return name != null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String InternalFlagsFormat(EnumInfo entry, ulong result, bool camelCaseText)
|
||||
private static string InternalFlagsFormat(EnumInfo entry, ulong result)
|
||||
{
|
||||
string[] resolvedNames = entry.ResolvedNames;
|
||||
ulong[] values = entry.Values;
|
||||
@@ -172,7 +171,7 @@ namespace Newtonsoft.Json.Utilities
|
||||
}
|
||||
|
||||
string resolvedName = resolvedNames[index];
|
||||
sb.Insert(0, camelCaseText ? StringUtils.ToCamelCase(resolvedName) : resolvedName);
|
||||
sb.Insert(0, resolvedName);
|
||||
firstTime = false;
|
||||
}
|
||||
|
||||
@@ -191,10 +190,6 @@ namespace Newtonsoft.Json.Utilities
|
||||
if (values.Length > 0 && values[0] == 0)
|
||||
{
|
||||
returnString = resolvedNames[0]; // Zero was one of the enum values.
|
||||
if (camelCaseText)
|
||||
{
|
||||
returnString = StringUtils.ToCamelCase(returnString);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -211,7 +206,7 @@ namespace Newtonsoft.Json.Utilities
|
||||
|
||||
public static EnumInfo GetEnumValuesAndNames(Type enumType)
|
||||
{
|
||||
return ValuesAndNamesPerEnum.Get(enumType);
|
||||
return ValuesAndNamesPerEnum.Get(new StructMultiKey<Type, NamingStrategy>(enumType, null));
|
||||
}
|
||||
|
||||
private static ulong ToUInt64(object value)
|
||||
@@ -247,7 +242,7 @@ namespace Newtonsoft.Json.Utilities
|
||||
}
|
||||
}
|
||||
|
||||
public static object ParseEnum(Type enumType, string value, bool disallowNumber)
|
||||
public static object ParseEnum(Type enumType, NamingStrategy namingStrategy, string value, bool disallowNumber)
|
||||
{
|
||||
ValidationUtils.ArgumentNotNull(enumType, nameof(enumType));
|
||||
ValidationUtils.ArgumentNotNull(value, nameof(value));
|
||||
@@ -257,7 +252,7 @@ namespace Newtonsoft.Json.Utilities
|
||||
throw new ArgumentException("Type provided must be an Enum.", nameof(enumType));
|
||||
}
|
||||
|
||||
EnumInfo entry = ValuesAndNamesPerEnum.Get(enumType);
|
||||
EnumInfo entry = ValuesAndNamesPerEnum.Get(new StructMultiKey<Type, NamingStrategy>(enumType, namingStrategy));
|
||||
string[] enumNames = entry.Names;
|
||||
string[] resolvedNames = entry.ResolvedNames;
|
||||
ulong[] enumValues = entry.Values;
|
||||
|
||||
@@ -824,7 +824,7 @@ namespace Newtonsoft.Json.Utilities
|
||||
}
|
||||
#endif
|
||||
|
||||
public static TypeNameKey SplitFullyQualifiedTypeName(string fullyQualifiedTypeName)
|
||||
public static StructMultiKey<string, string> SplitFullyQualifiedTypeName(string fullyQualifiedTypeName)
|
||||
{
|
||||
int? assemblyDelimiterIndex = GetAssemblyDelimiterIndex(fullyQualifiedTypeName);
|
||||
|
||||
@@ -842,7 +842,7 @@ namespace Newtonsoft.Json.Utilities
|
||||
assemblyName = null;
|
||||
}
|
||||
|
||||
return new TypeNameKey(assemblyName, typeName);
|
||||
return new StructMultiKey<string, string>(assemblyName, typeName);
|
||||
}
|
||||
|
||||
private static int? GetAssemblyDelimiterIndex(string fullyQualifiedTypeName)
|
||||
@@ -1094,36 +1094,4 @@ namespace Newtonsoft.Json.Utilities
|
||||
return Activator.CreateInstance(type);
|
||||
}
|
||||
}
|
||||
|
||||
internal readonly struct TypeNameKey : IEquatable<TypeNameKey>
|
||||
{
|
||||
internal readonly string AssemblyName;
|
||||
internal readonly string TypeName;
|
||||
|
||||
public TypeNameKey(string assemblyName, string typeName)
|
||||
{
|
||||
AssemblyName = assemblyName;
|
||||
TypeName = typeName;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return (AssemblyName?.GetHashCode() ?? 0) ^ (TypeName?.GetHashCode() ?? 0);
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (!(obj is TypeNameKey key))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Equals(key);
|
||||
}
|
||||
|
||||
public bool Equals(TypeNameKey other)
|
||||
{
|
||||
return (AssemblyName == other.AssemblyName && TypeName == other.TypeName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
#region License
|
||||
// Copyright (c) 2007 James Newton-King
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person
|
||||
// obtaining a copy of this software and associated documentation
|
||||
// files (the "Software"), to deal in the Software without
|
||||
// restriction, including without limitation the rights to use,
|
||||
// copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following
|
||||
// conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
// OTHER DEALINGS IN THE SOFTWARE.
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
|
||||
namespace Newtonsoft.Json.Utilities
|
||||
{
|
||||
internal readonly struct StructMultiKey<T1, T2> : IEquatable<StructMultiKey<T1, T2>>
|
||||
{
|
||||
public readonly T1 Value1;
|
||||
public readonly T2 Value2;
|
||||
|
||||
public StructMultiKey(T1 v1, T2 v2)
|
||||
{
|
||||
Value1 = v1;
|
||||
Value2 = v2;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return (Value1?.GetHashCode() ?? 0) ^ (Value2?.GetHashCode() ?? 0);
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (!(obj is StructMultiKey<T1, T2> key))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Equals(key);
|
||||
}
|
||||
|
||||
public bool Equals(StructMultiKey<T1, T2> other)
|
||||
{
|
||||
return (Equals(Value1, other.Value1) && Equals(Value2, other.Value2));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user