Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add DictionaryKeyPolicy support for EnumConverter [#47765] #54429

Merged
merged 5 commits into from
Aug 3, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,14 @@ internal sealed class EnumConverter<T> : JsonConverter<T>

private readonly ConcurrentDictionary<ulong, JsonEncodedText> _nameCache;

private readonly Lazy<ConcurrentDictionary<ulong, JsonEncodedText>> _dictionaryKeyPolicyCache = new Lazy<ConcurrentDictionary<ulong, JsonEncodedText>>();
SkiFoD marked this conversation as resolved.
Show resolved Hide resolved

// This is used to prevent flooding the cache due to exponential bitwise combinations of flags.
// Since multiple threads can add to the cache, a few more values might be added.
private const int NameCacheSizeSoftLimit = 64;

private const int DictionaryKeyPolicyCacheSizeSoftLimit = 64;
SkiFoD marked this conversation as resolved.
Show resolved Hide resolved

public override bool CanConvert(Type type)
{
return type.IsEnum;
Expand Down Expand Up @@ -325,13 +329,56 @@ internal override void WriteWithQuotes(Utf8JsonWriter writer, T value, JsonSeria

ulong key = ConvertToUInt64(value);

if (_nameCache.TryGetValue(key, out JsonEncodedText formatted))
JsonEncodedText formatted;
string original;

if (options.DictionaryKeyPolicy != null && !state.Current.IgnoreDictionaryKeyPolicy)
{
if (_dictionaryKeyPolicyCache.Value.TryGetValue(key, out formatted))
SkiFoD marked this conversation as resolved.
Show resolved Hide resolved
{
writer.WritePropertyName(formatted);
return;
}

original = value.ToString();

if (IsValidIdentifier(original))
{
original = options.DictionaryKeyPolicy.ConvertName(original);

if (original == null)
{
ThrowHelper.ThrowInvalidOperationException_NamingPolicyReturnNull(options.DictionaryKeyPolicy);
}

if (_dictionaryKeyPolicyCache.Value.Count < DictionaryKeyPolicyCacheSizeSoftLimit)
{
JavaScriptEncoder? encoder = options.Encoder;

formatted = JsonEncodedText.Encode(original, encoder);

writer.WritePropertyName(formatted);

_dictionaryKeyPolicyCache.Value.TryAdd(key, formatted);
}
else
{
// We also do not create a JsonEncodedText instance here because passing the string
// directly to the writer is cheaper than creating one and not caching it for reuse.
writer.WritePropertyName(original);
}

return;
}
}

if (_nameCache.TryGetValue(key, out formatted))
{
writer.WritePropertyName(formatted);
return;
}

string original = value.ToString();
original = value.ToString();
SkiFoD marked this conversation as resolved.
Show resolved Hide resolved
if (IsValidIdentifier(original))
{
// We are dealing with a combination of flag constants since
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,101 @@ public static void CustomNameSerialize()
Assert.Equal(JsonCustomKey, json);
}

public enum ETestEnum
{
TestValue1 = 1,
TestValue2 = 2,
}

[Fact]
public static void EnumSerialization_DictionaryPolicy_Honored_CamelCase()
{
var options = new JsonSerializerOptions
{
DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
};

Dictionary<ETestEnum, ETestEnum> dict = new Dictionary<ETestEnum, ETestEnum> { [ETestEnum.TestValue1] = ETestEnum.TestValue1 };
string value = JsonSerializer.Serialize(dict, options);
Assert.Equal("{\"testValue1\":1}", value);

dict = new Dictionary<ETestEnum, ETestEnum> { [ETestEnum.TestValue2] = ETestEnum.TestValue2 };
value = JsonSerializer.Serialize(dict, options);
Assert.Equal("{\"testValue2\":2}", value);

dict = new Dictionary<ETestEnum, ETestEnum> { [ETestEnum.TestValue1] = ETestEnum.TestValue1, [ETestEnum.TestValue2] = ETestEnum.TestValue2 };
value = JsonSerializer.Serialize(dict, options);
Assert.Equal("{\"testValue1\":1,\"testValue2\":2}", value);
}

[Fact]
public static void EnumSerialization_DictionaryPolicy_Honored_None()
SkiFoD marked this conversation as resolved.
Show resolved Hide resolved
{
Dictionary<ETestEnum, ETestEnum> dict = new Dictionary<ETestEnum, ETestEnum> { [ETestEnum.TestValue1] = ETestEnum.TestValue1 };
string value = JsonSerializer.Serialize(dict);
Assert.Equal("{\"TestValue1\":1}", value);

dict = new Dictionary<ETestEnum, ETestEnum> { [ETestEnum.TestValue2] = ETestEnum.TestValue2 };
value = JsonSerializer.Serialize(dict);
Assert.Equal("{\"TestValue2\":2}", value);

dict = new Dictionary<ETestEnum, ETestEnum> { [ETestEnum.TestValue1] = ETestEnum.TestValue1, [ETestEnum.TestValue2] = ETestEnum.TestValue2 };
value = JsonSerializer.Serialize(dict);
Assert.Equal("{\"TestValue1\":1,\"TestValue2\":2}", value);
}

public class ClassWithEnumProperties
{
public ETestEnum TestEnumProperty1 { get; } = ETestEnum.TestValue2;
public DayOfWeek TestEnumProperty2 { get; } = DayOfWeek.Monday;
}

[Fact]
public static void EnumSerialization_DictionaryPolicy_NotApplied_WhenEnumsAreSerialized()
{
var options = new JsonSerializerOptions
{
DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
};

string value = JsonSerializer.Serialize(DayOfWeek.Friday, options);

Assert.Equal("5", value);

value = JsonSerializer.Serialize(ETestEnum.TestValue2, options);

Assert.Equal("2", value);


value = JsonSerializer.Serialize(new ClassWithEnumProperties(), options);

Assert.Equal("{\"TestEnumProperty1\":2,\"TestEnumProperty2\":1}", value);

value = JsonSerializer.Serialize(new List<DayOfWeek> { DayOfWeek.Sunday, DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday}, options);

Assert.Equal("[0,1,2,3,4,5,6]", value);
}

public class CustomJsonNamingPolicy : JsonNamingPolicy
{
public override string ConvertName(string name) => null;
}

[Fact]
public static void EnumSerialization_DictionaryPolicy_ThrowsException_WhenNamingPolicyReturnsNull()
{
var options = new JsonSerializerOptions
{
DictionaryKeyPolicy = new CustomJsonNamingPolicy(),
};

Dictionary<ETestEnum, ETestEnum> dict = new Dictionary<ETestEnum, ETestEnum> { [ETestEnum.TestValue1] = ETestEnum.TestValue1 };

InvalidOperationException ex = Assert.Throws<InvalidOperationException>(() => JsonSerializer.Serialize(dict, options));

Assert.Contains(typeof(CustomJsonNamingPolicy).ToString(), ex.Message);
}

[Fact]
public static void NullNamePolicy()
{
Expand Down