Skip to content

Commit 88dd410

Browse files
committed
Fix: Deserialize multi type objects from the most restrictive type to the least restrictive
1 parent 80daa31 commit 88dd410

14 files changed

+513
-208
lines changed

WebExtensions.Net.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Icon", "Icon", "{12B00534-9
3939
EndProject
4040
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebExtensions.Net.Extensions.DependencyInjection", "src\WebExtensions.Net.Extensions.DependencyInjection\WebExtensions.Net.Extensions.DependencyInjection.csproj", "{2C1ED236-60A4-47D5-8467-33A456F5B458}"
4141
EndProject
42+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebExtensions.Net.Test", "test\WebExtensions.Net.Test\WebExtensions.Net.Test.csproj", "{404F2BE4-2762-471B-894F-B9282EEE22E6}"
43+
EndProject
4244
Global
4345
GlobalSection(SolutionConfigurationPlatforms) = preSolution
4446
Debug|Any CPU = Debug|Any CPU
@@ -65,6 +67,10 @@ Global
6567
{2C1ED236-60A4-47D5-8467-33A456F5B458}.Debug|Any CPU.Build.0 = Debug|Any CPU
6668
{2C1ED236-60A4-47D5-8467-33A456F5B458}.Release|Any CPU.ActiveCfg = Release|Any CPU
6769
{2C1ED236-60A4-47D5-8467-33A456F5B458}.Release|Any CPU.Build.0 = Release|Any CPU
70+
{404F2BE4-2762-471B-894F-B9282EEE22E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
71+
{404F2BE4-2762-471B-894F-B9282EEE22E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
72+
{404F2BE4-2762-471B-894F-B9282EEE22E6}.Release|Any CPU.ActiveCfg = Release|Any CPU
73+
{404F2BE4-2762-471B-894F-B9282EEE22E6}.Release|Any CPU.Build.0 = Release|Any CPU
6874
EndGlobalSection
6975
GlobalSection(SolutionProperties) = preSolution
7076
HideSolutionNode = FALSE
@@ -76,6 +82,7 @@ Global
7682
{6F6D0020-DE47-47F5-8D85-25A4D328B0A1} = {402C7B21-F25A-4EB2-AAAE-A8F827CD0ED4}
7783
{12B00534-99DE-4E2A-AD2B-C73285B6541D} = {37728F80-FC95-4451-B92C-3D915C0D11B6}
7884
{2C1ED236-60A4-47D5-8467-33A456F5B458} = {37728F80-FC95-4451-B92C-3D915C0D11B6}
85+
{404F2BE4-2762-471B-894F-B9282EEE22E6} = {402C7B21-F25A-4EB2-AAAE-A8F827CD0ED4}
7986
EndGlobalSection
8087
GlobalSection(ExtensibilityGlobals) = postSolution
8188
SolutionGuid = {500E229A-A512-45E3-B66E-46EB228929D3}

src/WebExtensions.Net/BaseStringFormat.cs

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Reflection;
23
using System.Text.RegularExpressions;
34

45
namespace WebExtensions.Net
@@ -18,12 +19,12 @@ public BaseStringFormat(string value, string format, string pattern)
1819
{
1920
Value = value;
2021

21-
if (!string.IsNullOrEmpty(pattern) && !Regex.IsMatch(value, pattern, RegexOptions.None, TimeSpan.FromSeconds(30)))
22+
if (!string.IsNullOrEmpty(pattern) && !IsValidPattern(value, pattern))
2223
{
2324
throw new ArgumentException($"The value '{value}' does not match the pattern '{pattern}' specified for type {GetType().Name}.");
2425
}
2526

26-
if (!string.IsNullOrEmpty(format) && !IsValid(value, format))
27+
if (!string.IsNullOrEmpty(format) && !IsValidFormat(value, format))
2728
{
2829
throw new ArgumentException($"The value '{value}' does not match the format '{format}' specified for type {GetType().Name}.");
2930
}
@@ -40,7 +41,42 @@ public override string ToString()
4041
return Value;
4142
}
4243

43-
private static bool IsValid(string value, string format)
44+
internal static bool IsValid(string value, Type type)
45+
{
46+
#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
47+
var fields = type.GetFields(BindingFlags.NonPublic | BindingFlags.Static);
48+
#pragma warning restore S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
49+
string pattern = null;
50+
string format = null;
51+
52+
foreach (var field in fields)
53+
{
54+
if (field.Name == "FORMAT")
55+
{
56+
format = (string)field.GetValue(null);
57+
}
58+
else if (field.Name == "PATTERN")
59+
{
60+
pattern = (string)field.GetValue(null);
61+
}
62+
}
63+
64+
return
65+
(!string.IsNullOrEmpty(pattern) && IsValidPattern(value, pattern)) ||
66+
(!string.IsNullOrEmpty(format) && IsValidFormat(value, format));
67+
}
68+
69+
internal static object TryCreate(string value, Type type)
70+
{
71+
if (IsValid(value, type))
72+
{
73+
return Activator.CreateInstance(type, value);
74+
}
75+
76+
return null;
77+
}
78+
79+
private static bool IsValidFormat(string value, string format)
4480
{
4581
if (format.Contains("url", StringComparison.OrdinalIgnoreCase))
4682
{
@@ -64,5 +100,10 @@ private static bool IsValid(string value, string format)
64100

65101
return true;
66102
}
103+
104+
private static bool IsValidPattern(string value, string pattern)
105+
{
106+
return Regex.IsMatch(value, pattern, RegexOptions.None, TimeSpan.FromSeconds(30));
107+
}
67108
}
68109
}
Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using System;
22
using System.Linq;
3-
using System.Reflection;
43
using System.Text.Json;
54
using System.Text.Json.Serialization;
65

@@ -12,29 +11,22 @@ namespace WebExtensions.Net
1211
/// <typeparam name="EnumType"></typeparam>
1312
public class EnumStringConverter<EnumType> : JsonConverter<EnumType>
1413
{
15-
private readonly EnumValueMapping[] enumValueMappings = GetEnumValueMappings();
16-
1714
/// <inheritdoc/>
1815
public override EnumType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
1916
{
2017
var stringValue = reader.GetString();
21-
var enumValue = enumValueMappings.SingleOrDefault(mapping => mapping.StringValue.Equals(stringValue))?.EnumValue;
22-
if (Enum.TryParse(typeof(EnumType), enumValue, true, out var result))
18+
if (stringValue is not null && EnumValueAttribute.GetEnumValues(typeof(EnumType)).TryGetValue(stringValue, out var enumValue))
2319
{
24-
return (EnumType)result;
20+
return (EnumType)enumValue;
2521
}
22+
2623
throw new JsonException($"Invalid enum value of '{stringValue}' for type '{typeof(EnumType).Name}'.");
2724
}
2825

2926
/// <inheritdoc/>
3027
public override void Write(Utf8JsonWriter writer, EnumType value, JsonSerializerOptions options)
3128
{
32-
writer.WriteStringValue(enumValueMappings.SingleOrDefault(mapping => mapping.EnumValue.Equals(value?.ToString()))?.StringValue);
33-
}
34-
35-
private static EnumValueMapping[] GetEnumValueMappings()
36-
{
37-
return typeof(EnumType).GetMembers().Select(member => new EnumValueMapping(member.Name, member.GetCustomAttribute<EnumValueAttribute>()?.Value ?? member.Name)).ToArray();
29+
writer.WriteStringValue(EnumValueAttribute.GetEnumValues(typeof(EnumType)).SingleOrDefault(mapping => mapping.Value.Equals(value)).Key);
3830
}
3931
}
4032
}

src/WebExtensions.Net/Common/EnumSerialization/EnumValueAttribute.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Reflection;
25

36
namespace WebExtensions.Net
47
{
@@ -10,5 +13,21 @@ public EnumValueAttribute(string value)
1013
{
1114
Value = value;
1215
}
16+
17+
public static Dictionary<string, object> GetEnumValues(Type type)
18+
{
19+
if (cachedAttributes.TryGetValue(type, out var cached))
20+
{
21+
return cached;
22+
}
23+
24+
cached = type.GetFields(BindingFlags.Public | BindingFlags.Static)
25+
.Select(enumField => KeyValuePair.Create(enumField.GetCustomAttribute<EnumValueAttribute>()?.Value ?? enumField.Name, enumField.GetValue(null)!))
26+
.ToDictionary();
27+
cachedAttributes[type] = cached;
28+
return cached;
29+
}
30+
31+
private static Dictionary<Type, Dictionary<string, object>> cachedAttributes = new();
1332
}
1433
}

src/WebExtensions.Net/Common/EnumSerialization/EnumValueMapping.cs

Lines changed: 0 additions & 13 deletions
This file was deleted.
Lines changed: 1 addition & 180 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11
using System;
2-
using System.Collections.Generic;
3-
using System.Linq;
4-
using System.Reflection;
52
using System.Text.Json;
63
using System.Text.Json.Serialization;
74

@@ -23,190 +20,14 @@ public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerial
2320
return null;
2421
}
2522

26-
var typeChoices = GetTypeChoices(typeToConvert);
2723
var jsonElement = JsonSerializer.Deserialize<JsonElement>(ref reader, options);
28-
29-
foreach (var typeChoice in typeChoices)
30-
{
31-
if (IsMatchingType(typeChoice.Key, jsonElement, options, out var value))
32-
{
33-
return CreateFromConstructor(typeChoice.Value, value);
34-
}
35-
}
36-
37-
return null;
24+
return TypeConstructor.CreateInstance(typeToConvert, jsonElement, options) as T;
3825
}
3926

4027
/// <inheritdoc/>
4128
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
4229
{
4330
JsonSerializer.Serialize(writer, value?.Value, options);
4431
}
45-
46-
private static KeyValuePair<Type, ConstructorInfo>[] GetTypeChoices(Type type)
47-
{
48-
return type
49-
.GetConstructors(BindingFlags.Public | BindingFlags.Instance)
50-
.Select(constructor =>
51-
{
52-
var parameterInfo = constructor.GetParameters().SingleOrDefault();
53-
return KeyValuePair.Create(parameterInfo?.ParameterType, constructor);
54-
})
55-
.Where(typeChoice => typeChoice.Key is not null)
56-
.OrderBy(typeChoice => GetOrderForType(typeChoice.Key))
57-
.ToArray();
58-
}
59-
60-
private static int GetOrderForType(Type type)
61-
{
62-
if (type.IsPrimitive)
63-
{
64-
return 0;
65-
}
66-
67-
if (IsBoolType(type) || IsIntType(type) || IsDoubleType(type))
68-
{
69-
return 1;
70-
}
71-
72-
if (IsStringType(type))
73-
{
74-
return 2;
75-
}
76-
77-
if (IsObjectType(type))
78-
{
79-
return 10;
80-
}
81-
82-
if (IsArrayType(type))
83-
{
84-
var arrayItemType = type.GenericTypeArguments[0];
85-
return 20 + GetOrderForType(arrayItemType);
86-
}
87-
88-
return 2;
89-
}
90-
91-
private static bool IsBoolType(Type type)
92-
{
93-
return type == typeof(bool);
94-
}
95-
96-
private static bool IsIntType(Type type)
97-
{
98-
return type == typeof(int);
99-
}
100-
101-
private static bool IsDoubleType(Type type)
102-
{
103-
return type == typeof(double);
104-
}
105-
106-
private static bool IsStringType(Type type)
107-
{
108-
return type == typeof(string) || typeof(BaseStringFormat).IsAssignableFrom(type);
109-
}
110-
111-
private static bool IsObjectType(Type type)
112-
{
113-
return !IsBoolType(type) && !IsIntType(type) && !IsDoubleType(type) && !IsStringType(type) && !IsArrayType(type);
114-
}
115-
116-
private static bool IsArrayType(Type type)
117-
{
118-
return type.IsGenericType && typeof(IEnumerable<>).IsAssignableFrom(type.GetGenericTypeDefinition());
119-
}
120-
121-
private static bool IsMatchingType(Type type, JsonElement jsonElement, JsonSerializerOptions jsonSerializerOptions, out object value)
122-
{
123-
if (IsMatchingBoolean(type, jsonElement))
124-
{
125-
value = jsonElement.GetBoolean();
126-
return true;
127-
}
128-
129-
if (IsMatchingInteger(type, jsonElement))
130-
{
131-
value = jsonElement.GetInt32();
132-
return true;
133-
}
134-
135-
if (IsMatchingDouble(type, jsonElement))
136-
{
137-
value = jsonElement.GetDouble();
138-
return true;
139-
}
140-
141-
if (IsMatchingString(type, jsonElement))
142-
{
143-
value = jsonElement.GetString();
144-
return true;
145-
}
146-
147-
if (IsMatchingObject(type, jsonElement))
148-
{
149-
try
150-
{
151-
value = JsonSerializer.Deserialize(jsonElement.GetRawText(), type, jsonSerializerOptions);
152-
return true;
153-
}
154-
catch (JsonException)
155-
{
156-
// Ignore if there is an error deserializing into this object type
157-
}
158-
}
159-
160-
if (IsMatchingArray(type, jsonElement))
161-
{
162-
try
163-
{
164-
value = JsonSerializer.Deserialize(jsonElement.GetRawText(), type, jsonSerializerOptions);
165-
return true;
166-
}
167-
catch (JsonException)
168-
{
169-
// Ignore if there is an error deserializing into this array type
170-
}
171-
}
172-
173-
value = null;
174-
return false;
175-
}
176-
177-
private static bool IsMatchingBoolean(Type type, JsonElement jsonElement)
178-
{
179-
return (jsonElement.ValueKind == JsonValueKind.True || jsonElement.ValueKind == JsonValueKind.False) && IsBoolType(type);
180-
}
181-
182-
private static bool IsMatchingInteger(Type type, JsonElement jsonElement)
183-
{
184-
return jsonElement.ValueKind == JsonValueKind.Number && IsIntType(type);
185-
}
186-
187-
private static bool IsMatchingDouble(Type type, JsonElement jsonElement)
188-
{
189-
return jsonElement.ValueKind == JsonValueKind.Number && IsDoubleType(type);
190-
}
191-
192-
private static bool IsMatchingString(Type type, JsonElement jsonElement)
193-
{
194-
return jsonElement.ValueKind == JsonValueKind.String && IsStringType(type);
195-
}
196-
197-
private static bool IsMatchingObject(Type type, JsonElement jsonElement)
198-
{
199-
return jsonElement.ValueKind == JsonValueKind.Object && IsObjectType(type);
200-
}
201-
202-
private static bool IsMatchingArray(Type type, JsonElement jsonElement)
203-
{
204-
return jsonElement.ValueKind == JsonValueKind.Array && IsArrayType(type);
205-
}
206-
207-
private static T CreateFromConstructor(ConstructorInfo constructorInfo, object value)
208-
{
209-
return (T)constructorInfo.Invoke(new[] { value });
210-
}
21132
}
21233
}

0 commit comments

Comments
 (0)