From 7e8cd40f5e68778c8e67e6aa8d3f3d773a2979a3 Mon Sep 17 00:00:00 2001 From: Richard Irons Date: Tue, 1 Jul 2025 12:29:58 +0100 Subject: [PATCH 1/2] Fix parameter bug and enhance documentation for WithTlsNegotiator method --- .../ExecutableQuery/ExecutableQueryTests.cs | 68 ++++++++++++++ .../TestUtil/CollectionExtensionsTests.cs | 63 +++++++++++++ .../Extensions/CollectionExtensions.cs | 92 +++++++++++++++++++ .../Neo4j.Driver/Public/ConfigBuilder.cs | 8 +- 4 files changed, 229 insertions(+), 2 deletions(-) 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..672e647a0 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,69 @@ 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() + { + yield break; + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + yield break; + } + } } 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. /// From 55fabf7f42027a7111b5532241dc240519bd0907 Mon Sep 17 00:00:00 2001 From: Richard Irons <115992270+RichardIrons-neo4j@users.noreply.github.com> Date: Tue, 1 Jul 2025 15:46:59 +0100 Subject: [PATCH 2/2] Update Neo4j.Driver/Neo4j.Driver/Internal/Extensions/CollectionExtensions.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Internal/Extensions/CollectionExtensions.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Extensions/CollectionExtensions.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Extensions/CollectionExtensions.cs index 672e647a0..4918be423 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/Extensions/CollectionExtensions.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Extensions/CollectionExtensions.cs @@ -392,13 +392,16 @@ public bool Contains(KeyValuePair item) /// IEnumerator> IEnumerable>.GetEnumerator() { - yield break; + foreach (DictionaryEntry entry in dictionary) + { + yield return new KeyValuePair((string)entry.Key, entry.Value); + } } /// IEnumerator IEnumerable.GetEnumerator() { - yield break; + return ((IEnumerable>)this).GetEnumerator(); } } }