From 0f5531af4daae463900d129088d981e83f13582f Mon Sep 17 00:00:00 2001 From: ta264 Date: Thu, 20 Aug 2020 21:31:45 +0100 Subject: [PATCH] Fixed: Error deserializing preferred words with dapper Fixes Sentry LIDARR-106 Fixes Sentry LIDARR-10B --- .../KeyValuePairConverterFixture.cs | 47 ++++++ .../Converters/EmbeddedDocumentConverter.cs | 1 + .../Converters/KeyValuePairConverter.cs | 149 ++++++++++++++++++ 3 files changed, 197 insertions(+) create mode 100644 src/NzbDrone.Core.Test/Datastore/Converters/KeyValuePairConverterFixture.cs create mode 100644 src/NzbDrone.Core/Datastore/Converters/KeyValuePairConverter.cs diff --git a/src/NzbDrone.Core.Test/Datastore/Converters/KeyValuePairConverterFixture.cs b/src/NzbDrone.Core.Test/Datastore/Converters/KeyValuePairConverterFixture.cs new file mode 100644 index 000000000..401f97233 --- /dev/null +++ b/src/NzbDrone.Core.Test/Datastore/Converters/KeyValuePairConverterFixture.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Data.SQLite; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Datastore.Converters; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.Datastore.Converters +{ + [TestFixture] + public class KeyValuePairConverterFixture : CoreTest>>> + { + private SQLiteParameter _param; + + [SetUp] + public void Setup() + { + _param = new SQLiteParameter(); + } + + [Test] + public void should_serialize_in_camel_case() + { + var items = new List> + { + new KeyValuePair("word", 1) + }; + + Subject.SetValue(_param, items); + + var result = (string)_param.Value; + result.Should().Be(@"[ + { + ""key"": ""word"", + ""value"": 1 + } +]"); + } + + [TestCase(@"[{""key"": ""deluxe"", ""value"": 10 }]")] + [TestCase(@"[{""Key"": ""deluxe"", ""Value"": 10 }]")] + public void should_deserialize_case_insensitive(string input) + { + Subject.Parse(input).Should().BeEquivalentTo(new List> { new KeyValuePair("deluxe", 10) }); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Converters/EmbeddedDocumentConverter.cs b/src/NzbDrone.Core/Datastore/Converters/EmbeddedDocumentConverter.cs index 6b6744110..d3ac86fe6 100644 --- a/src/NzbDrone.Core/Datastore/Converters/EmbeddedDocumentConverter.cs +++ b/src/NzbDrone.Core/Datastore/Converters/EmbeddedDocumentConverter.cs @@ -22,6 +22,7 @@ namespace NzbDrone.Core.Datastore.Converters }; serializerSettings.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, true)); + serializerSettings.Converters.Add(new KeyValuePairConverter()); /* Remove in .NET 5 */ serializerSettings.Converters.Add(new TimeSpanConverter()); serializerSettings.Converters.Add(new UtcConverter()); diff --git a/src/NzbDrone.Core/Datastore/Converters/KeyValuePairConverter.cs b/src/NzbDrone.Core/Datastore/Converters/KeyValuePairConverter.cs new file mode 100644 index 000000000..b4cbf765f --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Converters/KeyValuePairConverter.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace NzbDrone.Core.Datastore.Converters +{ + /* See https://github.com/dotnet/runtime/issues/1197 + Can be removed once we switch to .NET 5 + Based on https://github.com/layomia/dotnet_runtime/blob/master/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/KeyValuePairConverter.cs + and https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to + */ + + public class KeyValuePairConverter : JsonConverterFactory + { + public override bool CanConvert(Type typeToConvert) + { + if (!typeToConvert.IsGenericType) + { + return false; + } + + if (typeToConvert.GetGenericTypeDefinition() != typeof(KeyValuePair<,>)) + { + return false; + } + + return true; + } + + public override JsonConverter CreateConverter( + Type type, + JsonSerializerOptions options) + { + var keyType = type.GetGenericArguments()[0]; + var valueType = type.GetGenericArguments()[1]; + + var converter = (JsonConverter)Activator.CreateInstance( + typeof(KeyValuePairConverterInner<,>).MakeGenericType( + new Type[] { keyType, valueType }), + BindingFlags.Instance | BindingFlags.Public, + binder: null, + args: null, + culture: null); + + return converter; + } + + private class KeyValuePairConverterInner : + JsonConverter> + { + public KeyValuePairConverterInner() + { + } + + public override KeyValuePair Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException(); + } + + TKey k = default; + var keySet = false; + + TValue v = default; + var valueSet = false; + + reader.Read(); + + // Get the first property. + if (reader.TokenType != JsonTokenType.PropertyName) + { + throw new JsonException(); + } + + var propertyName = reader.GetString(); + if (string.Equals(propertyName, "Key", StringComparison.OrdinalIgnoreCase)) + { + reader.Read(); + k = JsonSerializer.Deserialize(ref reader, options); + keySet = true; + } + else if (string.Equals(propertyName, "Value", StringComparison.OrdinalIgnoreCase)) + { + reader.Read(); + v = JsonSerializer.Deserialize(ref reader, options); + valueSet = true; + } + else + { + throw new JsonException(); + } + + // Get the second property. + reader.Read(); + if (reader.TokenType != JsonTokenType.PropertyName) + { + throw new JsonException(); + } + + propertyName = reader.GetString(); + if (!keySet && string.Equals(propertyName, "Key", StringComparison.OrdinalIgnoreCase)) + { + reader.Read(); + k = JsonSerializer.Deserialize(ref reader, options); + } + else if (!valueSet && string.Equals(propertyName, "Value", StringComparison.OrdinalIgnoreCase)) + { + reader.Read(); + v = JsonSerializer.Deserialize(ref reader, options); + } + else + { + throw new JsonException(); + } + + reader.Read(); + + if (reader.TokenType != JsonTokenType.EndObject) + { + throw new JsonException(); + } + + return new KeyValuePair(k, v); + } + + public override void Write( + Utf8JsonWriter writer, + KeyValuePair kvp, + JsonSerializerOptions options) + { + writer.WriteStartObject(); + + writer.WritePropertyName("key"); + JsonSerializer.Serialize(writer, kvp.Key, options); + + writer.WritePropertyName("value"); + JsonSerializer.Serialize(writer, kvp.Value, options); + + writer.WriteEndObject(); + } + } + } +}