diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/ExecutableQuery/ExecutableQueryTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/ExecutableQuery/ExecutableQueryTests.cs index c7977789e..1dc3b3379 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/ExecutableQuery/ExecutableQueryTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/ExecutableQuery/ExecutableQueryTests.cs @@ -391,4 +391,72 @@ async IAsyncEnumerable GetInts(int start, int count) queryExecution.Result.Should().Be(45 + rnd); } + + [Fact] + public async Task ShouldSetParametersWithObjectContainingPerson() + { + var person = new Person { Name = "Test Person No Work", Age = 22 }; + var anonParams = new { Shaken = true, Stirred = false, Agent = person }; + + var autoMock = new AutoMocker(MockBehavior.Loose); + var driverMock = autoMock.GetMock>(); + + object capturedParams = null; + + driverMock + .Setup(x => x.SetParameters(It.IsAny())) + .Callback(p => capturedParams = p); + + driverMock + .Setup(x => x.GetRowsAsync(It.IsAny>(), It.IsAny())) + .ReturnsAsync(new ExecutionSummary(null, null)); + + var subject = new ExecutableQuery(driverMock.Object, i => i); + + await subject + .WithParameters(anonParams) + .ExecuteAsync(); + + capturedParams.Should().BeEquivalentTo(anonParams); + } + + [Fact] + public async Task ShouldSetParametersWithDictionaryContainingPerson() + { + var person = new Person { Name = "Test Person No Work", Age = 22 }; + var parameters = new Dictionary { ["agent"] = person }; + + var autoMock = new AutoMocker(MockBehavior.Loose); + var driverMock = autoMock.GetMock>(); + + Dictionary capturedParams = null; + + driverMock + .Setup(x => x.SetParameters(It.IsAny>())) + .Callback>(p => capturedParams = p); + + driverMock + .Setup(x => x.GetRowsAsync(It.IsAny>(), It.IsAny())) + .ReturnsAsync(new ExecutionSummary(null, null)); + + var subject = new ExecutableQuery(driverMock.Object, i => i); + + await subject + .WithParameters(parameters) + .ExecuteAsync(); + + capturedParams.Should().ContainKey("agent"); + capturedParams["agent"] + .Should() + .BeOfType() + .Which.Should() + .BeEquivalentTo(person); + } + + + public class Person + { + public string Name { get; set; } + public int Age { get; set; } + } } diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/TestUtil/CollectionExtensionsTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/TestUtil/CollectionExtensionsTests.cs index 23dc54651..74ae22771 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/TestUtil/CollectionExtensionsTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/TestUtil/CollectionExtensionsTests.cs @@ -397,6 +397,69 @@ public void ShouldHandleListOfArbitraryObjects() 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() { diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Extensions/CollectionExtensions.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Extensions/CollectionExtensions.cs index f836d0914..4918be423 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/Extensions/CollectionExtensions.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Extensions/CollectionExtensions.cs @@ -126,6 +126,11 @@ public static IDictionary ToDictionary(this object o) 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); @@ -134,6 +139,28 @@ public static IDictionary ToDictionary(this object o) 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 static IDictionary FillDictionary(object o, IDictionary dict) { foreach (var propInfo in o.GetType().GetRuntimeProperties()) @@ -309,4 +336,72 @@ 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/Public/ConfigBuilder.cs b/Neo4j.Driver/Neo4j.Driver/Public/ConfigBuilder.cs index 1b1874d3a..c9c45bc1a 100644 --- a/Neo4j.Driver/Neo4j.Driver/Public/ConfigBuilder.cs +++ b/Neo4j.Driver/Neo4j.Driver/Public/ConfigBuilder.cs @@ -533,7 +533,9 @@ public ConfigBuilder WithTls13() } #endif - /// Sets a custom to use when establishing a TLS connection. + /// Sets a custom to use when establishing a TLS connection. Note + /// that this overrides the default TLS negotiator, which handles certificate-based trust, so if you + /// use this method you should implement certificate validation yourself. /// The to use. /// A instance for further configuration options. /// @@ -559,7 +561,9 @@ public ConfigBuilder WithTlsNegotiator(NegotiateTlsDelegate negotiateTls) return this; } - /// Sets the type of custom to use when establishing a TLS connection. + /// Sets the type of custom to use when establishing a TLS connection. Note + /// that this overrides the default TLS negotiator, which handles certificate-based trust, so if you + /// use this method you should implement certificate validation yourself. /// The to use. /// A instance for further configuration options. ///