diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Internal/Util/ObjectToDictionaryConverterTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Internal/Util/ObjectToDictionaryConverterTests.cs new file mode 100644 index 000000000..2d83db26d --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Internal/Util/ObjectToDictionaryConverterTests.cs @@ -0,0 +1,366 @@ +// Copyright (c) "Neo4j" +// Neo4j Sweden AB [https://neo4j.com] +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections; +using System.Collections.Generic; +using FluentAssertions; +using Neo4j.Driver.Internal; +using Neo4j.Driver.Internal.Util; +using Xunit; + +namespace Neo4j.Driver.Tests.Internal.Util; + +public class ObjectToDictionaryConverterTests +{ + private readonly ObjectToDictionaryConverter _converter = new(); + + [Fact] + public void ShouldReturnNullGivenNull() + { + var dict = _converter.Convert(null); + dict.Should().BeNull(); + } + + [Theory] + [InlineData((sbyte)0)] + [InlineData((byte)0)] + [InlineData((short)0)] + [InlineData((ushort)0)] + [InlineData(0)] + [InlineData((uint)0)] + [InlineData((long)0)] + [InlineData((ulong)0)] + [InlineData((char)0)] + [InlineData((float)0)] + [InlineData((double)0)] + [InlineData(true)] + public void ShouldHandleSimpleTypes(object value) + { + var dict = _converter.Convert(new { key = value }); + dict.Should().NotBeNull(); + dict.Should().HaveCount(1); + dict.Should().ContainKey("key"); + dict.Should().ContainValue(value); + } + + [Fact] + public void ShouldHandleString() + { + var dict = _converter.Convert(new { key = "value" }); + dict.Should().NotBeNull(); + dict.Should().HaveCount(1); + dict.Should().ContainKey("key"); + dict.Should().ContainValue("value"); + } + + [Fact] + public void ShouldHandleArray() + { + var array = new byte[2]; + var dict = _converter.Convert(new { key = array }); + dict.Should().NotBeNull(); + dict.Should().HaveCount(1); + dict.Should().ContainKey("key"); + dict.Should().ContainValue(array); + } + + [Fact] + public void ShouldHandleAnonymousObjects() + { + var dict = _converter.Convert(new { key1 = "value1", key2 = "value2" }); + dict.Should().NotBeNull(); + dict.Should().HaveCount(2); + dict.Should().Contain( + new KeyValuePair("key1", "value1"), + new KeyValuePair("key2", "value2")); + } + + [Fact] + public void ShouldHandleVectors() + { + var vector = Vector.Create([1.0, 2.0, 3.0]); + var dict = _converter.Convert(new { vector }); + dict.Should().NotBeNull(); + dict.Should().HaveCount(1); + dict.Should().ContainKey("vector"); + dict["vector"].Should().BeOfType>(); + ((Vector)dict["vector"]).Values.Should().BeEquivalentTo([1.0, 2.0, 3.0]); + } + + [Fact] + public void ShouldHandlePoco() + { + var dict = _converter.Convert(new MyPOCO { Key1 = "value1", Key2 = "value2" }); + dict.Should().NotBeNull(); + dict.Should().HaveCount(2); + dict.Should().Contain( + new KeyValuePair("Key1", "value1"), + new KeyValuePair("Key2", "value2")); + } + + [Fact] + public void ShouldHandleDeeperObjects() + { + var dict = _converter.Convert(new { InnerObject = new { Key1 = 1, Key2 = "a", Key3 = 0L } }); + dict.Should().NotBeNull(); + dict.Should().HaveCount(1); + dict.Should().ContainKey("InnerObject"); + var innerObjectObject = dict["InnerObject"]; + innerObjectObject.Should().NotBeNull(); + innerObjectObject.Should().BeAssignableTo>(); + var innerObject = (IDictionary)innerObjectObject; + innerObject.Should().Contain( + new KeyValuePair("Key1", 1), + new KeyValuePair("Key2", "a"), + new KeyValuePair("Key3", 0L)); + } + + [Fact] + public void ShouldHandleDictionary() + { + var dict = _converter.Convert(new + { + InnerDictionary = new Dictionary + { + { "Key1", 1 }, + { "Key2", "a" }, + { "Key3", 0L } + } + }); + dict.Should().NotBeNull(); + dict.Should().HaveCount(1); + dict.Should().ContainKey("InnerDictionary"); + var innerDictionaryObject = dict["InnerDictionary"]; + innerDictionaryObject.Should().NotBeNull(); + innerDictionaryObject.Should().BeAssignableTo>(); + var innerDictionary = (IDictionary)innerDictionaryObject; + innerDictionary.Should().Contain( + new KeyValuePair("Key1", 1), + new KeyValuePair("Key2", "a"), + new KeyValuePair("Key3", 0L)); + } + + [Fact] + public void ShouldHandleCollections() + { + var dict = _converter.Convert(new { InnerCollection = new List { 1, 2, 3 } }); + dict.Should().NotBeNull(); + dict.Should().HaveCount(1); + dict.Should().ContainKey("InnerCollection"); + var innerCollectionObject = dict["InnerCollection"]; + innerCollectionObject.Should().NotBeNull(); + innerCollectionObject.Should().BeAssignableTo>(); + var innerCollection = (IList)innerCollectionObject; + innerCollection.Should().Contain(new[] { 1, 2, 3 }); + } + + [Fact] + public void ShouldHandleCollectionsOfArbitraryObjects() + { + var dict = _converter.Convert(new + { + InnerCollection = new List + { + new { a = "a" }, + 3, + new MyPOCO { Key1 = "value1" } + } + }); + dict.Should().NotBeNull(); + dict.Should().HaveCount(1); + dict.Should().ContainKey("InnerCollection"); + var innerCollectionObject = dict["InnerCollection"]; + innerCollectionObject.Should().NotBeNull(); + innerCollectionObject.Should().BeAssignableTo>(); + var innerCollection = (IList)innerCollectionObject; + innerCollection.Should().HaveCount(3); + innerCollection.Should().Contain( + o => o is IDictionary && + ((IDictionary)o).Contains(new KeyValuePair("a", "a"))); + innerCollection.Should().Contain(3); + innerCollection.Should().Contain( + o => o is IDictionary && + ((IDictionary)o).Contains(new KeyValuePair("Key1", "value1"))); + } + + [Fact] + public void ShouldHandleDictionaryOfArbitraryObjects() + { + var dict = _converter.Convert(new + { + InnerDictionary = new Dictionary + { + { "a", new { a = "a" } }, + { "b", "b" }, + { "c", 3 } + } + }); + dict.Should().NotBeNull(); + dict.Should().HaveCount(1); + dict.Should().ContainKey("InnerDictionary"); + var innerDictionaryObject = dict["InnerDictionary"]; + innerDictionaryObject.Should().NotBeNull(); + innerDictionaryObject.Should().BeAssignableTo>(); + var innerDictionary = (IDictionary)innerDictionaryObject; + innerDictionary.Should().HaveCount(3); + innerDictionary.Should().ContainKey("a"); + innerDictionary["a"].Should().BeAssignableTo>(); + innerDictionary["a"].As>().Should().Contain(new KeyValuePair("a", "a")); + innerDictionary.Should().Contain(new KeyValuePair("b", "b")); + innerDictionary.Should().Contain(new KeyValuePair("c", 3)); + } + + [Fact] + public void ShouldRaiseExceptionWhenDictionaryKeysAreNotStrings() + { + var ex = Record.Exception( + () => _converter.Convert(new + { + InnerDictionary = new Dictionary + { + { 1, new { a = "a" } }, + { 2, "b" }, + { 3, 3 } + } + })); + ex.Should().NotBeNull(); + ex.Should().BeOfType(); + ex.Message.Should().Contain("string keys"); + } + + [Fact] + public void ShouldHandleListOfArbitraryObjects() + { + var dict = _converter.Convert(new + { + InnerList = new List + { + new { a = "a" }, + "b", + 3 + } + }); + dict.Should().NotBeNull(); + dict.Should().HaveCount(1); + dict.Should().ContainKey("InnerList"); + var innerListObject = dict["InnerList"]; + innerListObject.Should().NotBeNull(); + innerListObject.Should().BeAssignableTo>(); + var innerList = (IList)innerListObject; + innerList.Should().HaveCount(3); + innerList[0].Should().BeAssignableTo>(); + innerList[0].As>().Should().Contain(new KeyValuePair("a", "a")); + innerList[1].Should().Be("b"); + innerList[2].As().Should().Be(3); + } + + public class Person + { + public string Name { get; set; } + public int Age { get; set; } + } + + [Fact] + public void ToDictionary_ShouldHandleEmptyDictionary() + { + var emptyDictionary = new Dictionary(); + var result = _converter.Convert(emptyDictionary); + result.Should().BeEmpty(); + } + + [Fact] + public void ToDictionary_ShouldConvertDictionaryWithSimpleObjectsCorrectly() + { + var sourceDictionary = new Dictionary + { + { "Key1", new Person { Name = "John", Age = 30 } }, + { "Key2", new Person { Name = "Jane", Age = 25 } } + }; + var result = _converter.Convert(sourceDictionary); + result.Should().HaveCount(2); + result["Key1"].Should().BeEquivalentTo(sourceDictionary["Key1"]); + result["Key2"].Should().BeEquivalentTo(sourceDictionary["Key2"]); + } + + [Fact] + public void ToDictionary_ShouldReturnNullForNullDictionary() + { + Dictionary nullDictionary = null; + var actual = _converter.Convert(nullDictionary); + actual.Should().BeNull(); + } + + [Fact] + public void ToDictionary_ShouldHandleNestedDictionaryCorrectly() + { + var nestedDictionary = new Dictionary> + { + { + "Nested", new Dictionary + { + { "InnerKey", new Person { Name = "Doe", Age = 40 } } + } + } + }; + var result = _converter.Convert(nestedDictionary); + result.Should().ContainKey("Nested"); + var innerDict = result["Nested"].As>(); + innerDict.Should().ContainKey("InnerKey"); + innerDict["InnerKey"].Should().BeEquivalentTo(new Person { Name = "Doe", Age = 40 }); + } + + [Fact] + public void ShouldHandleEnumerable() + { + var array = new[] { 1, 2, 3 }; + var value = new MyCollection(array); + var dict = _converter.Convert(new { key = value }); + dict.Should().NotBeNull(); + dict.Should().HaveCount(1); + dict.Should().ContainKey("key"); + var s = dict["key"].ToContentString(); + s.Should().Be("[1, 2, 3]"); + } + + [Fact] + public void ShouldHandleEnumerableofEnumerable() + { + var array = new[] { 1, 2, 3 }; + IEnumerable element = new MyCollection(array); + var value = new MyCollection(new[] { element, "a" }); + var dict = _converter.Convert(new { key = value }); + dict.Should().NotBeNull(); + dict.Should().HaveCount(1); + dict.Should().ContainKey("key"); + var s = dict["key"].ToContentString(); + s.Should().Be("[[1, 2, 3], a]"); + } + + private class MyPOCO + { + public string Key1 { get; set; } + public string Key2 { get; set; } + } + + public class MyCollection : IEnumerable + { + private readonly IEnumerable _values; + public MyCollection(IEnumerable values) { _values = values; } + public string Name => "My Collection implements IEnumerable"; + public IEnumerator GetEnumerator() => _values.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Internal/Util/ParameterValueTransformerTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Internal/Util/ParameterValueTransformerTests.cs new file mode 100644 index 000000000..705515d85 --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Internal/Util/ParameterValueTransformerTests.cs @@ -0,0 +1,126 @@ +// Copyright (c) "Neo4j" +// Neo4j Sweden AB [https://neo4j.com] +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Neo4j.Driver.Tests.Internal.Util; + +using System; +using System.Collections.Generic; +using FluentAssertions; +using Neo4j.Driver.Internal.Util; +using Xunit; + +public class ParameterValueTransformerTests +{ + private readonly ParameterValueTransformer _transformer = new(); + + [Fact] + public void Transform_Null_ReturnsNull() + { + _transformer.Transform(null).Should().BeNull(); + } + + [Fact] + public void Transform_String_ReturnsSameString() + { + var input = "hello"; + var result = _transformer.Transform(input); + result.Should().Be(input); + } + + [Fact] + public void Transform_IntArray_ReturnsIntArray() + { + var input = new[] { 1, 2, 3 }; + var result = _transformer.Transform(input); + result.Should() + .BeOfType() + .Which.Should() + .BeEquivalentTo(1, 2, 3); + } + + [Fact] + public void Transform_ListOfStrings_ReturnsListOfStrings() + { + var input = new List { "a", "b" }; + var result = _transformer.Transform(input); + result.Should() + .BeOfType>() + .Which.Should() + .BeEquivalentTo("a", "b"); + } + + [Fact] + public void Transform_DictionaryStringInt_ReturnsDictionaryStringObject() + { + var input = new Dictionary { { "x", 1 }, { "y", 2 } }; + var result = _transformer.Transform(input); + result.Should() + .BeOfType>() + .Which.Should() + .BeEquivalentTo(new Dictionary { { "x", 1 }, { "y", 2 } }); + } + + [Fact] + public void Transform_DictionaryWithNonStringKey_Throws() + { + var input = new Dictionary { { 1, "a" } }; + Action act = () => _transformer.Transform(input); + act.Should() + .Throw() + .WithMessage("*string keys*"); + } + + private class TestObject + { + public int A { get; set; } + public string B { get; set; } + } + + [Fact] + public void Transform_Object_ReturnsDictionaryOfProperties() + { + var input = new TestObject { A = 42, B = "foo" }; + var result = _transformer.Transform(input); + result.Should() + .BeOfType>() + .Which.Should() + .Contain(new KeyValuePair("A", 42)) + .And.Contain(new KeyValuePair("B", "foo")); + } + + [Fact] + public void Transform_ListOfObjects_ReturnsListOfDictionaries() + { + var input = new List + { + new TestObject { A = 1, B = "x" }, + new TestObject { A = 2, B = "y" } + }; + + var result = _transformer.Transform(input); + result.Should().BeOfType>(); + var list = result as List; + list.Should().AllBeOfType>(); + } + + [Fact] + public void Transform_Vector_ReturnsSameVector() + { + var input = Vector.Create([1.0, 2.0, 3.0]); + var result = _transformer.Transform(input); + result.Should().BeOfType>(); + ((Vector) result).Values.Should().BeEquivalentTo([1.0, 2.0, 3.0]); + } +} diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/TestUtil/CollectionExtensionsTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/TestUtil/CollectionExtensionsTests.cs index 74ae22771..2a40274ab 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/TestUtil/CollectionExtensionsTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/TestUtil/CollectionExtensionsTests.cs @@ -13,14 +13,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; using System.Collections; using System.Collections.Generic; using System.Linq; using FluentAssertions; using Neo4j.Driver.Internal; using Xunit; -using CollectionExtensions = Neo4j.Driver.Internal.CollectionExtensions; namespace Neo4j.Driver.Tests.TestUtil; @@ -111,400 +109,6 @@ public void ShouldGetValueCorrectlyWhenExpectingMap() } } - public class ToDictionaryMethod - { - [Fact] - public void ShouldReturnNullGivenNull() - { - var dict = CollectionExtensions.ToDictionary(null); - - dict.Should().BeNull(); - } - - [Theory] - [InlineData((sbyte)0)] - [InlineData((byte)0)] - [InlineData((short)0)] - [InlineData((ushort)0)] - [InlineData(0)] - [InlineData((uint)0)] - [InlineData((long)0)] - [InlineData((ulong)0)] - [InlineData((char)0)] - [InlineData((float)0)] - [InlineData((double)0)] - [InlineData(true)] - public void ShouldHandleSimpleTypes(object value) - { - var dict = new - { - key = value - }.ToDictionary(); - - dict.Should().NotBeNull(); - dict.Should().HaveCount(1); - dict.Should().ContainKey("key"); - dict.Should().ContainValue(value); - } - - [Fact] - public void ShouldHandleString() - { - var dict = new - { - key = "value" - }.ToDictionary(); - - dict.Should().NotBeNull(); - dict.Should().HaveCount(1); - dict.Should().ContainKey("key"); - dict.Should().ContainValue("value"); - } - - [Fact] - public void ShouldHandleArray() - { - var array = new byte[2]; - - var dict = new - { - key = array - }.ToDictionary(); - - dict.Should().NotBeNull(); - dict.Should().HaveCount(1); - dict.Should().ContainKey("key"); - dict.Should().ContainValue(array); - } - - [Fact] - public void ShouldHandleAnonymousObjects() - { - var dict = new { key1 = "value1", key2 = "value2" }.ToDictionary(); - - dict.Should().NotBeNull(); - dict.Should().HaveCount(2); - dict.Should() - .Contain( - new KeyValuePair("key1", "value1"), - new KeyValuePair("key2", "value2")); - } - - [Fact] - public void ShouldHandlePoco() - { - var dict = new MyPOCO { Key1 = "value1", Key2 = "value2" }.ToDictionary(); - - dict.Should().NotBeNull(); - dict.Should().HaveCount(2); - dict.Should() - .Contain( - new KeyValuePair("Key1", "value1"), - new KeyValuePair("Key2", "value2")); - } - - [Fact] - public void ShouldHandleDeeperObjects() - { - var dict = new - { - InnerObject = new { Key1 = 1, Key2 = "a", Key3 = 0L } - }.ToDictionary(); - - dict.Should().NotBeNull(); - dict.Should().HaveCount(1); - dict.Should().ContainKey("InnerObject"); - - var innerObjectObject = dict["InnerObject"]; - innerObjectObject.Should().NotBeNull(); - innerObjectObject.Should().BeAssignableTo>(); - - var innerObject = (IDictionary)innerObjectObject; - innerObject.Should() - .Contain( - new KeyValuePair("Key1", 1), - new KeyValuePair("Key2", "a"), - new KeyValuePair("Key3", 0L)); - } - - [Fact] - public void ShouldHandleDictionary() - { - var dict = new - { - InnerDictionary = new Dictionary - { - { "Key1", 1 }, - { "Key2", "a" }, - { "Key3", 0L } - } - }.ToDictionary(); - - dict.Should().NotBeNull(); - dict.Should().HaveCount(1); - dict.Should().ContainKey("InnerDictionary"); - - var innerDictionaryObject = dict["InnerDictionary"]; - innerDictionaryObject.Should().NotBeNull(); - innerDictionaryObject.Should().BeAssignableTo>(); - - var innerDictionary = (IDictionary)innerDictionaryObject; - innerDictionary.Should() - .Contain( - new KeyValuePair("Key1", 1), - new KeyValuePair("Key2", "a"), - new KeyValuePair("Key3", 0L)); - } - - [Fact] - public void ShouldHandleCollections() - { - var dict = new - { - InnerCollection = new List { 1, 2, 3 } - }.ToDictionary(); - - dict.Should().NotBeNull(); - dict.Should().HaveCount(1); - dict.Should().ContainKey("InnerCollection"); - - var innerCollectionObject = dict["InnerCollection"]; - innerCollectionObject.Should().NotBeNull(); - innerCollectionObject.Should().BeAssignableTo>(); - - var innerCollection = (IList)innerCollectionObject; - innerCollection.Should().Contain(new[] { 1, 2, 3 }); - } - - [Fact] - public void ShouldHandleCollectionsOfArbitraryObjects() - { - var dict = new - { - InnerCollection = new List - { - new { a = "a" }, - 3, - new MyPOCO { Key1 = "value1" } - } - }.ToDictionary(); - - dict.Should().NotBeNull(); - dict.Should().HaveCount(1); - dict.Should().ContainKey("InnerCollection"); - - var innerCollectionObject = dict["InnerCollection"]; - innerCollectionObject.Should().NotBeNull(); - innerCollectionObject.Should().BeAssignableTo>(); - - var innerCollection = (IList)innerCollectionObject; - innerCollection.Should().HaveCount(3); - innerCollection.Should() - .Contain( - o => o is IDictionary && - ((IDictionary)o).Contains(new KeyValuePair("a", "a"))); - - innerCollection.Should().Contain(3); - innerCollection.Should() - .Contain( - o => o is IDictionary && - ((IDictionary)o).Contains(new KeyValuePair("Key1", "value1"))); - } - - [Fact] - public void ShouldHandleDictionaryOfArbitraryObjects() - { - var dict = new - { - InnerDictionary = new Dictionary - { - { "a", new { a = "a" } }, - { "b", "b" }, - { "c", 3 } - } - }.ToDictionary(); - - dict.Should().NotBeNull(); - dict.Should().HaveCount(1); - dict.Should().ContainKey("InnerDictionary"); - - var innerDictionaryObject = dict["InnerDictionary"]; - innerDictionaryObject.Should().NotBeNull(); - innerDictionaryObject.Should().BeAssignableTo>(); - - var innerDictionary = (IDictionary)innerDictionaryObject; - innerDictionary.Should().HaveCount(3); - innerDictionary.Should().ContainKey("a"); - innerDictionary["a"].Should().BeAssignableTo>(); - innerDictionary["a"] - .As>() - .Should() - .Contain(new KeyValuePair("a", "a")); - - innerDictionary.Should().Contain(new KeyValuePair("b", "b")); - innerDictionary.Should().Contain(new KeyValuePair("c", 3)); - } - - [Fact] - public void ShouldRaiseExceptionWhenDictionaryKeysAreNotStrings() - { - var ex = Record.Exception( - () => new - { - InnerDictionary = new Dictionary - { - { 1, new { a = "a" } }, - { 2, "b" }, - { 3, 3 } - } - }.ToDictionary()); - - ex.Should().NotBeNull(); - ex.Should().BeOfType(); - ex.Message.Should().Contain("string keys"); - } - - [Fact] - public void ShouldHandleListOfArbitraryObjects() - { - var dict = new - { - InnerList = new List - { - new { a = "a" }, - "b", - 3 - } - }.ToDictionary(); - - dict.Should().NotBeNull(); - dict.Should().HaveCount(1); - dict.Should().ContainKey("InnerList"); - - var innerListObject = dict["InnerList"]; - innerListObject.Should().NotBeNull(); - innerListObject.Should().BeAssignableTo>(); - - var innerList = (IList)innerListObject; - innerList.Should().HaveCount(3); - innerList[0].Should().BeAssignableTo>(); - innerList[0] - .As>() - .Should() - .Contain(new KeyValuePair("a", "a")); - - innerList[1].Should().Be("b"); - innerList[2].As().Should().Be(3); - } - - // Simple two-property class - public class Person - { - public string Name { get; set; } - public int Age { get; set; } - } - - [Fact] - public void ToDictionary_ShouldHandleEmptyDictionary() - { - var emptyDictionary = new Dictionary(); - var result = emptyDictionary.ToDictionary(); - result.Should().BeEmpty(); - } - - [Fact] - public void ToDictionary_ShouldConvertDictionaryWithSimpleObjectsCorrectly() - { - var sourceDictionary = new Dictionary - { - { "Key1", new Person { Name = "John", Age = 30 } }, - { "Key2", new Person { Name = "Jane", Age = 25 } } - }; - - var result = sourceDictionary.ToDictionary(); - - result.Should().HaveCount(2); - result["Key1"].Should().BeEquivalentTo(sourceDictionary["Key1"]); - result["Key2"].Should().BeEquivalentTo(sourceDictionary["Key2"]); - } - - [Fact] - public void ToDictionary_ShouldReturnNullForNullDictionary() - { - Dictionary nullDictionary = null; - // ReSharper disable once ExpressionIsAlwaysNull - var actual = nullDictionary.ToDictionary(); - actual.Should().BeNull(); - } - - [Fact] - public void ToDictionary_ShouldHandleNestedDictionaryCorrectly() - { - var nestedDictionary = new Dictionary> - { - { - "Nested", new Dictionary - { - { "InnerKey", new Person { Name = "Doe", Age = 40 } } - } - } - }; - - var result = nestedDictionary.ToDictionary(); - - result.Should().ContainKey("Nested"); - - // Validate nested dictionary - var innerDict = result["Nested"].As>(); - innerDict.Should().ContainKey("InnerKey"); - innerDict["InnerKey"].Should().BeEquivalentTo(new Person { Name = "Doe", Age = 40 }); - } - - [Fact] - public void ShouldHandleEnumerable() - { - var array = new[] { 1, 2, 3 }; - var value = new MyCollection(array); - - var dict = new - { - key = value - }.ToDictionary(); - - dict.Should().NotBeNull(); - dict.Should().HaveCount(1); - dict.Should().ContainKey("key"); - var s = dict["key"].ToContentString(); - s.Should().Be("[1, 2, 3]"); // GetEnumerator rather than the Name field - } - - [Fact] - public void ShouldHandleEnumerableofEnumerable() - { - var array = new[] { 1, 2, 3 }; - IEnumerable element = new MyCollection(array); - var value = new MyCollection(new[] { element, "a" }); - - var dict = new - { - key = value - }.ToDictionary(); - - dict.Should().NotBeNull(); - dict.Should().HaveCount(1); - dict.Should().ContainKey("key"); - var s = dict["key"].ToContentString(); - s.Should().Be("[[1, 2, 3], a]"); // GetEnumerator rather than the Name field - } - - private class MyPOCO - { - public string Key1 { get; set; } - - public string Key2 { get; set; } - } - } - public class MyCollection : IEnumerable { private readonly IEnumerable _values; diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Extensions/CollectionExtensions.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Extensions/CollectionExtensions.cs index baf49789b..ed7a312a6 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/Extensions/CollectionExtensions.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Extensions/CollectionExtensions.cs @@ -19,13 +19,20 @@ using System.Linq; using System.Reflection; using Neo4j.Driver.Internal.Types; +using Neo4j.Driver.Internal.Util; namespace Neo4j.Driver.Internal; -internal static class CollectionExtensions +internal static partial class CollectionExtensions { private const string DefaultItemSeparator = ", "; private static readonly TypeInfo NeoValueTypeInfo = typeof(IValue).GetTypeInfo(); + + private static readonly IParameterValueTransformer _parameterValueTransformer = + new ParameterValueTransformer(); + + private static readonly IObjectToDictionaryConverter _objectToDictionaryConverter = + new ObjectToDictionaryConverter(); public static T GetMandatoryValue( this IDictionary dictionary, @@ -106,72 +113,7 @@ public static string ToContentString(this object o, string separator = DefaultIt public static IDictionary ToDictionary(this object o) { - if (o == null) - { - return null; - } - - if (o is Dictionary dict) - { - return dict; - } - - if (o is IDictionary dictInt) - { - return new Dictionary(dictInt); - } - - if (o is IReadOnlyDictionary dictIntRo) - { - return dictIntRo.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); - } - - if (TryGetDictionaryOfStringKeys(o, out var dictStr)) - { - return dictStr; - } - - if (o is IEnumerable> kvpSeq) - { - return kvpSeq.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); - } - - return FillDictionary(o, new Dictionary()); - } - - public static Type GetItemType(this IList list) - { - // Check if the list is a generic type - var type = list.GetType(); - if (type.IsGenericType) - { - // Get the generic type argument (e.g., T in List) - return type.GetGenericArguments()[0]; - } - - // If not generic, then object will do - return typeof(object); - } - - private static bool TryGetDictionaryOfStringKeys(object o, out IDictionary dictionary) - { - dictionary = null; - - var typeInfo = o.GetType().GetTypeInfo(); - - // get all the interfaces implemented by the type and make sure that one of them is - // IDictionary - var interfaces = typeInfo.ImplementedInterfaces; - var canUse = interfaces.Any(i => i.IsGenericType && - i.GetGenericTypeDefinition() == typeof(IDictionary<,>) && - i.GenericTypeArguments[0] == typeof(string)); - - if (canUse) - { - dictionary = new DictionaryAccessWrapper((IDictionary)o); - } - - return canUse; + return _objectToDictionaryConverter.Convert(o); } private static IDictionary FillDictionary(object o, IDictionary dict) @@ -180,7 +122,7 @@ private static IDictionary FillDictionary(object o, IDictionary< { var name = propInfo.Name; var value = propInfo.GetValue(o); - var valueTransformed = Transform(value); + var valueTransformed = _parameterValueTransformer.Transform(value); dict.Add(name, valueTransformed); } @@ -188,112 +130,6 @@ private static IDictionary FillDictionary(object o, IDictionary< return dict; } - private static object Transform(object value) - { - if (value == null) - { - return null; - } - - var valueType = value.GetType(); - - if (value is Array) - { - var elementType = valueType.GetElementType(); - - if (elementType.NeedsConversion()) - { - var convertedList = new List(((IList)value).Count); - foreach (var element in (IEnumerable)value) - { - convertedList.Add(Transform(element)); - } - - value = convertedList; - } - } - else if (value is IList) - { - var valueTypeInfo = valueType.GetTypeInfo(); - var elementType = (Type)null; - - if (valueTypeInfo.IsGenericType && valueTypeInfo.GetGenericTypeDefinition() == typeof(List<>)) - { - elementType = valueTypeInfo.GenericTypeArguments[0]; - } - - if (elementType == null || elementType.NeedsConversion()) - { - var convertedList = new List(((IList)value).Count); - foreach (var element in (IEnumerable)value) - { - convertedList.Add(Transform(element)); - } - - value = convertedList; - } - } - else if (value is IDictionary) - { - var valueTypeInfo = valueType.GetTypeInfo(); - var elementType = (Type)null; - - if (valueTypeInfo.IsGenericType && valueTypeInfo.GetGenericTypeDefinition() == typeof(IDictionary<,>)) - { - elementType = valueTypeInfo.GenericTypeArguments[1]; - } - - if (elementType == null || elementType.NeedsConversion()) - { - var dict = (IDictionary)value; - - var convertedDict = new Dictionary(dict.Count); - foreach (var key in dict.Keys) - { - if (!(key is string)) - { - throw new InvalidOperationException( - "dictionaries passed as part of a parameter to cypher queries should have string keys!"); - } - - convertedDict.Add((string)key, Transform(dict[key])); - } - - value = convertedDict; - } - } - else if (value is IEnumerable && !(value is string)) - { - var valueTypeInfo = valueType.GetTypeInfo(); - var elementType = (Type)null; - - if (valueTypeInfo.IsGenericType && valueTypeInfo.GetGenericTypeDefinition() == typeof(List<>)) - { - elementType = valueTypeInfo.GenericTypeArguments[0]; - } - - if (elementType == null || elementType.NeedsConversion()) - { - var convertedList = new List(); - foreach (var element in (IEnumerable)value) - { - convertedList.Add(Transform(element)); - } - - value = convertedList; - } - } - else - { - if (valueType.NeedsConversion()) - { - value = FillDictionary(value, new Dictionary()); - } - } - - return value; - } - private static bool NeedsConversion(this Type type) { if (type == typeof(string)) @@ -349,73 +185,4 @@ public static void OverwriteFrom( } } } - - private struct DictionaryAccessWrapper(IDictionary dictionary) : IDictionary - { - public object this[string key] - { - get => dictionary[key]; - set => throw new NotSupportedException("This dictionary is read-only."); - } - - public ICollection Keys => dictionary.Keys.Cast().ToList(); - public ICollection Values => dictionary.Values.Cast().ToList(); - - /// - bool ICollection>.Remove(KeyValuePair item) - { - throw new NotSupportedException("This dictionary is read-only."); - } - - public int Count => dictionary.Count; - public bool IsReadOnly => true; - - public void Add(string key, object value) => throw new NotSupportedException("This dictionary is read-only."); - - public bool ContainsKey(string key) - { - return dictionary.Contains(key); - } - - public bool Remove(string key) => throw new NotSupportedException("This dictionary is read-only."); - - public bool TryGetValue(string key, out object value) - { - if (dictionary.Contains(key)) - { - value = dictionary[key]; - return true; - } - - value = null; - return false; - } - - public void Add(KeyValuePair item) => - throw new NotSupportedException("This dictionary is read-only."); - - public void Clear() => throw new NotSupportedException("This dictionary is read-only."); - - public bool Contains(KeyValuePair item) - { - return TryGetValue(item.Key, out var value) && Equals(value, item.Value); - } - - public void CopyTo(KeyValuePair[] array, int arrayIndex) => throw new NotSupportedException(); - - /// - IEnumerator> IEnumerable>.GetEnumerator() - { - foreach (DictionaryEntry entry in dictionary) - { - yield return new KeyValuePair((string)entry.Key, entry.Value); - } - } - - /// - IEnumerator IEnumerable.GetEnumerator() - { - return ((IEnumerable>)this).GetEnumerator(); - } - } } diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Util/ObjectToDictionary/DictionaryAccessWrapper.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Util/ObjectToDictionary/DictionaryAccessWrapper.cs new file mode 100644 index 000000000..5c4bfe2de --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Util/ObjectToDictionary/DictionaryAccessWrapper.cs @@ -0,0 +1,90 @@ +// Copyright (c) "Neo4j" +// Neo4j Sweden AB [https://neo4j.com] +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Neo4j.Driver.Internal.Util; + +internal readonly struct DictionaryAccessWrapper(IDictionary dictionary) : IDictionary +{ + public object this[string key] + { + get => dictionary[key]; + set => throw new NotSupportedException("This dictionary is read-only."); + } + + public ICollection Keys => dictionary.Keys.Cast().ToList(); + public ICollection Values => dictionary.Values.Cast().ToList(); + + /// + bool ICollection>.Remove(KeyValuePair item) + { + throw new NotSupportedException("This dictionary is read-only."); + } + + public int Count => dictionary.Count; + public bool IsReadOnly => true; + + public void Add(string key, object value) => throw new NotSupportedException("This dictionary is read-only."); + + public bool ContainsKey(string key) + { + return dictionary.Contains(key); + } + + public bool Remove(string key) => throw new NotSupportedException("This dictionary is read-only."); + + public bool TryGetValue(string key, out object value) + { + if (dictionary.Contains(key)) + { + value = dictionary[key]; + return true; + } + + value = null; + return false; + } + + public void Add(KeyValuePair item) => + throw new NotSupportedException("This dictionary is read-only."); + + public void Clear() => throw new NotSupportedException("This dictionary is read-only."); + + public bool Contains(KeyValuePair item) + { + return TryGetValue(item.Key, out var value) && Equals(value, item.Value); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) => throw new NotSupportedException(); + + /// + IEnumerator> IEnumerable>.GetEnumerator() + { + foreach (DictionaryEntry entry in dictionary) + { + yield return new KeyValuePair((string)entry.Key, entry.Value); + } + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable>)this).GetEnumerator(); + } +} diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Util/ObjectToDictionary/IObjectToDictionaryConverter.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Util/ObjectToDictionary/IObjectToDictionaryConverter.cs new file mode 100644 index 000000000..c6c284926 --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Util/ObjectToDictionary/IObjectToDictionaryConverter.cs @@ -0,0 +1,23 @@ +// Copyright (c) "Neo4j" +// Neo4j Sweden AB [https://neo4j.com] +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Generic; + +namespace Neo4j.Driver.Internal.Util; + +internal interface IObjectToDictionaryConverter +{ + IDictionary Convert(object obj); +} diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Util/ObjectToDictionary/IParameterValueTransformer.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Util/ObjectToDictionary/IParameterValueTransformer.cs new file mode 100644 index 000000000..727d2150e --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Util/ObjectToDictionary/IParameterValueTransformer.cs @@ -0,0 +1,21 @@ +// Copyright (c) "Neo4j" +// Neo4j Sweden AB [https://neo4j.com] +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Neo4j.Driver.Internal.Util; + +internal interface IParameterValueTransformer +{ + object Transform(object value); +} diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Util/ObjectToDictionary/ObjectToDictionaryConverter.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Util/ObjectToDictionary/ObjectToDictionaryConverter.cs new file mode 100644 index 000000000..672c2c73b --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Util/ObjectToDictionary/ObjectToDictionaryConverter.cs @@ -0,0 +1,78 @@ +// Copyright (c) "Neo4j" +// Neo4j Sweden AB [https://neo4j.com] +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Neo4j.Driver.Internal.Util; + +internal class ObjectToDictionaryConverter(IParameterValueTransformer parameterValueTransformer = null) + : IObjectToDictionaryConverter +{ + private IParameterValueTransformer _parameterValueTransformer = + parameterValueTransformer ?? new ParameterValueTransformer(); + + public IDictionary Convert(object o) + { + switch (o) + { + case null: return null; + case Dictionary dict: return dict; + case IDictionary dictInt: return new Dictionary(dictInt); + case IReadOnlyDictionary dictIntRo: return dictIntRo.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + case var _ when TryGetDictionaryOfStringKeys(o, out var dictStr): return dictStr; + case IEnumerable> kvpSeq: return kvpSeq.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + default: return FillDictionary(o, new Dictionary()); + } + } + + private static bool TryGetDictionaryOfStringKeys(object o, out IDictionary dictionary) + { + dictionary = null; + + var typeInfo = o.GetType().GetTypeInfo(); + + // get all the interfaces implemented by the type and make sure that one of them is + // IDictionary + var interfaces = typeInfo.ImplementedInterfaces; + var canUse = interfaces.Any(i => i.IsGenericType && + i.GetGenericTypeDefinition() == typeof(IDictionary<,>) && + i.GenericTypeArguments[0] == typeof(string)); + + if (canUse) + { + dictionary = new DictionaryAccessWrapper((IDictionary)o); + } + + return canUse; + } + + private IDictionary FillDictionary(object o, IDictionary dict) + { + foreach (var propInfo in o.GetType().GetRuntimeProperties()) + { + var name = propInfo.Name; + var value = propInfo.GetValue(o); + var valueTransformed = _parameterValueTransformer.Transform(value); + + dict.Add(name, valueTransformed); + } + + return dict; + } +} diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Util/ObjectToDictionary/ParameterValueTransformer.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Util/ObjectToDictionary/ParameterValueTransformer.cs new file mode 100644 index 000000000..62288aa73 --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Util/ObjectToDictionary/ParameterValueTransformer.cs @@ -0,0 +1,180 @@ +// Copyright (c) "Neo4j" +// Neo4j Sweden AB [https://neo4j.com] +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Neo4j.Driver.Internal.Types; + +namespace Neo4j.Driver.Internal.Util; + +internal class ParameterValueTransformer : IParameterValueTransformer +{ + private static readonly TypeInfo NeoValueTypeInfo = typeof(IValue).GetTypeInfo(); + + public object Transform(object value) + { + var valueType = value?.GetType(); + if (valueType == null || !NeedsConversion(valueType)) + { + return value; + } + + switch (value) + { + case Array array: + { + var elementType = valueType.GetElementType(); + + if (NeedsConversion(elementType)) + { + value = array.Cast().Select(Transform).ToList(); + } + + break; + } + + case IList list: + { + var valueTypeInfo = valueType.GetTypeInfo(); + Type elementType = null; + + if (valueTypeInfo.IsGenericType && valueTypeInfo.GetGenericTypeDefinition() == typeof(List<>)) + { + elementType = valueTypeInfo.GenericTypeArguments[0]; + } + + if (elementType == null || NeedsConversion(elementType)) + { + var convertedList = new List(list.Count); + foreach (var element in list) + { + convertedList.Add(Transform(element)); + } + + value = convertedList; + } + + break; + } + + case IDictionary dictionary: + { + var valueTypeInfo = valueType.GetTypeInfo(); + var elementType = (Type)null; + + if (valueTypeInfo.IsGenericType && valueTypeInfo.GetGenericTypeDefinition() == typeof(IDictionary<,>)) + { + elementType = valueTypeInfo.GenericTypeArguments[1]; + } + + if (elementType == null || NeedsConversion(elementType)) + { + var dict = dictionary; + + var convertedDict = new Dictionary(dict.Count); + foreach (var key in dict.Keys) + { + if (key is not string str) + { + throw new InvalidOperationException( + "dictionaries passed as part of a parameter to cypher queries should have string keys!"); + } + + convertedDict.Add(str, Transform(dict[str])); + } + + value = convertedDict; + } + + break; + } + + default: + { + if (value is IEnumerable enumerable and not string) + { + var valueTypeInfo = valueType.GetTypeInfo(); + var elementType = (Type)null; + + if (valueTypeInfo.IsGenericType && valueTypeInfo.GetGenericTypeDefinition() == typeof(List<>)) + { + elementType = valueTypeInfo.GenericTypeArguments[0]; + } + + if (elementType == null || NeedsConversion(elementType)) + { + var convertedList = new List(); + foreach (var element in enumerable) + { + convertedList.Add(Transform(element)); + } + + value = convertedList; + } + } + else + { + if (NeedsConversion(valueType)) + { + value = FillDictionary(value, new Dictionary()); + } + } + + break; + } + } + + return value; + } + + private bool NeedsConversion(Type type) + { + if (type == typeof(string)) + { + return false; + } + + var typeInfo = type.GetTypeInfo(); + + if (typeInfo.IsValueType) + { + return false; + } + + if (NeoValueTypeInfo.IsAssignableFrom(typeInfo)) + { + return false; + } + + return true; + } + + private IDictionary FillDictionary(object o, IDictionary dict) + { + foreach (var propInfo in o.GetType().GetRuntimeProperties()) + { + var name = propInfo.Name; + var value = propInfo.GetValue(o); + var valueTransformed = Transform(value); + + dict.Add(name, valueTransformed); + } + + return dict; + } +} diff --git a/Neo4j.Driver/Neo4j.Driver/Neo4j.Driver.csproj.DotSettings b/Neo4j.Driver/Neo4j.Driver/Neo4j.Driver.csproj.DotSettings index da8e3f423..638e431ad 100644 --- a/Neo4j.Driver/Neo4j.Driver/Neo4j.Driver.csproj.DotSettings +++ b/Neo4j.Driver/Neo4j.Driver/Neo4j.Driver.csproj.DotSettings @@ -12,6 +12,7 @@ True True True + True True True True