Skip to content

Commit 4bcf97f

Browse files
Fix parameter bug and enhance documentation for WithTlsNegotiator method (#851)
* Fix parameter bug and enhance documentation for WithTlsNegotiator method * Update Neo4j.Driver/Neo4j.Driver/Internal/Extensions/CollectionExtensions.cs ---------
1 parent 404334e commit 4bcf97f

File tree

4 files changed

+232
-2
lines changed

4 files changed

+232
-2
lines changed

Neo4j.Driver/Neo4j.Driver.Tests/ExecutableQuery/ExecutableQueryTests.cs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,4 +391,72 @@ async IAsyncEnumerable<int> GetInts(int start, int count)
391391

392392
queryExecution.Result.Should().Be(45 + rnd);
393393
}
394+
395+
[Fact]
396+
public async Task ShouldSetParametersWithObjectContainingPerson()
397+
{
398+
var person = new Person { Name = "Test Person No Work", Age = 22 };
399+
var anonParams = new { Shaken = true, Stirred = false, Agent = person };
400+
401+
var autoMock = new AutoMocker(MockBehavior.Loose);
402+
var driverMock = autoMock.GetMock<IDriverRowSource<int>>();
403+
404+
object capturedParams = null;
405+
406+
driverMock
407+
.Setup(x => x.SetParameters(It.IsAny<object>()))
408+
.Callback<object>(p => capturedParams = p);
409+
410+
driverMock
411+
.Setup(x => x.GetRowsAsync(It.IsAny<Action<int>>(), It.IsAny<CancellationToken>()))
412+
.ReturnsAsync(new ExecutionSummary(null, null));
413+
414+
var subject = new ExecutableQuery<int, int>(driverMock.Object, i => i);
415+
416+
await subject
417+
.WithParameters(anonParams)
418+
.ExecuteAsync();
419+
420+
capturedParams.Should().BeEquivalentTo(anonParams);
421+
}
422+
423+
[Fact]
424+
public async Task ShouldSetParametersWithDictionaryContainingPerson()
425+
{
426+
var person = new Person { Name = "Test Person No Work", Age = 22 };
427+
var parameters = new Dictionary<string, object> { ["agent"] = person };
428+
429+
var autoMock = new AutoMocker(MockBehavior.Loose);
430+
var driverMock = autoMock.GetMock<IDriverRowSource<int>>();
431+
432+
Dictionary<string, object> capturedParams = null;
433+
434+
driverMock
435+
.Setup(x => x.SetParameters(It.IsAny<Dictionary<string, object>>()))
436+
.Callback<Dictionary<string, object>>(p => capturedParams = p);
437+
438+
driverMock
439+
.Setup(x => x.GetRowsAsync(It.IsAny<Action<int>>(), It.IsAny<CancellationToken>()))
440+
.ReturnsAsync(new ExecutionSummary(null, null));
441+
442+
var subject = new ExecutableQuery<int, int>(driverMock.Object, i => i);
443+
444+
await subject
445+
.WithParameters(parameters)
446+
.ExecuteAsync();
447+
448+
capturedParams.Should().ContainKey("agent");
449+
capturedParams["agent"]
450+
.Should()
451+
.BeOfType<Person>()
452+
.Which.Should()
453+
.BeEquivalentTo(person);
454+
}
455+
456+
457+
public class Person
458+
{
459+
public string Name { get; set; }
460+
public int Age { get; set; }
461+
}
394462
}

Neo4j.Driver/Neo4j.Driver.Tests/TestUtil/CollectionExtensionsTests.cs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,69 @@ public void ShouldHandleListOfArbitraryObjects()
397397
innerList[2].As<int>().Should().Be(3);
398398
}
399399

400+
// Simple two-property class
401+
public class Person
402+
{
403+
public string Name { get; set; }
404+
public int Age { get; set; }
405+
}
406+
407+
[Fact]
408+
public void ToDictionary_ShouldHandleEmptyDictionary()
409+
{
410+
var emptyDictionary = new Dictionary<string, Person>();
411+
var result = emptyDictionary.ToDictionary();
412+
result.Should().BeEmpty();
413+
}
414+
415+
[Fact]
416+
public void ToDictionary_ShouldConvertDictionaryWithSimpleObjectsCorrectly()
417+
{
418+
var sourceDictionary = new Dictionary<string, Person>
419+
{
420+
{ "Key1", new Person { Name = "John", Age = 30 } },
421+
{ "Key2", new Person { Name = "Jane", Age = 25 } }
422+
};
423+
424+
var result = sourceDictionary.ToDictionary();
425+
426+
result.Should().HaveCount(2);
427+
result["Key1"].Should().BeEquivalentTo(sourceDictionary["Key1"]);
428+
result["Key2"].Should().BeEquivalentTo(sourceDictionary["Key2"]);
429+
}
430+
431+
[Fact]
432+
public void ToDictionary_ShouldReturnNullForNullDictionary()
433+
{
434+
Dictionary<string, Person> nullDictionary = null;
435+
// ReSharper disable once ExpressionIsAlwaysNull
436+
var actual = nullDictionary.ToDictionary();
437+
actual.Should().BeNull();
438+
}
439+
440+
[Fact]
441+
public void ToDictionary_ShouldHandleNestedDictionaryCorrectly()
442+
{
443+
var nestedDictionary = new Dictionary<string, Dictionary<string, Person>>
444+
{
445+
{
446+
"Nested", new Dictionary<string, Person>
447+
{
448+
{ "InnerKey", new Person { Name = "Doe", Age = 40 } }
449+
}
450+
}
451+
};
452+
453+
var result = nestedDictionary.ToDictionary();
454+
455+
result.Should().ContainKey("Nested");
456+
457+
// Validate nested dictionary
458+
var innerDict = result["Nested"].As<Dictionary<string, Person>>();
459+
innerDict.Should().ContainKey("InnerKey");
460+
innerDict["InnerKey"].Should().BeEquivalentTo(new Person { Name = "Doe", Age = 40 });
461+
}
462+
400463
[Fact]
401464
public void ShouldHandleEnumerable()
402465
{

Neo4j.Driver/Neo4j.Driver/Internal/Extensions/CollectionExtensions.cs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,11 @@ public static IDictionary<string, object> ToDictionary(this object o)
126126
return dictIntRo.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
127127
}
128128

129+
if (TryGetDictionaryOfStringKeys(o, out var dictStr))
130+
{
131+
return dictStr;
132+
}
133+
129134
if (o is IEnumerable<KeyValuePair<string, object>> kvpSeq)
130135
{
131136
return kvpSeq.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
@@ -134,6 +139,28 @@ public static IDictionary<string, object> ToDictionary(this object o)
134139
return FillDictionary(o, new Dictionary<string, object>());
135140
}
136141

142+
private static bool TryGetDictionaryOfStringKeys(object o, out IDictionary<string, object> dictionary)
143+
{
144+
dictionary = null;
145+
146+
var typeInfo = o.GetType().GetTypeInfo();
147+
148+
// get all the interfaces implemented by the type and make sure that one of them is
149+
// IDictionary<string,?>
150+
var interfaces = typeInfo.ImplementedInterfaces;
151+
var canUse = interfaces.Any(
152+
i => i.IsGenericType &&
153+
i.GetGenericTypeDefinition() == typeof(IDictionary<,>) &&
154+
i.GenericTypeArguments[0] == typeof(string));
155+
156+
if (canUse)
157+
{
158+
dictionary = new DictionaryAccessWrapper((IDictionary)o);
159+
}
160+
161+
return canUse;
162+
}
163+
137164
private static IDictionary<string, object> FillDictionary(object o, IDictionary<string, object> dict)
138165
{
139166
foreach (var propInfo in o.GetType().GetRuntimeProperties())
@@ -309,4 +336,72 @@ public static void OverwriteFrom<TKey, TValue>(
309336
}
310337
}
311338
}
339+
340+
private struct DictionaryAccessWrapper(IDictionary dictionary) : IDictionary<string, object>
341+
{
342+
public object this[string key]
343+
{
344+
get => dictionary[key];
345+
set => throw new NotSupportedException("This dictionary is read-only.");
346+
}
347+
348+
public ICollection<string> Keys => dictionary.Keys.Cast<string>().ToList();
349+
public ICollection<object> Values => dictionary.Values.Cast<object>().ToList();
350+
351+
/// <inheritdoc />
352+
bool ICollection<KeyValuePair<string, object>>.Remove(KeyValuePair<string, object> item)
353+
{
354+
throw new NotSupportedException("This dictionary is read-only.");
355+
}
356+
357+
public int Count => dictionary.Count;
358+
public bool IsReadOnly => true;
359+
360+
public void Add(string key, object value) => throw new NotSupportedException("This dictionary is read-only.");
361+
362+
public bool ContainsKey(string key)
363+
{
364+
return dictionary.Contains(key);
365+
}
366+
367+
public bool Remove(string key) => throw new NotSupportedException("This dictionary is read-only.");
368+
369+
public bool TryGetValue(string key, out object value)
370+
{
371+
if (dictionary.Contains(key))
372+
{
373+
value = dictionary[key];
374+
return true;
375+
}
376+
377+
value = null;
378+
return false;
379+
}
380+
381+
public void Add(KeyValuePair<string, object> item) => throw new NotSupportedException("This dictionary is read-only.");
382+
383+
public void Clear() => throw new NotSupportedException("This dictionary is read-only.");
384+
385+
public bool Contains(KeyValuePair<string, object> item)
386+
{
387+
return TryGetValue(item.Key, out var value) && Equals(value, item.Value);
388+
}
389+
390+
public void CopyTo(KeyValuePair<string, object>[] array, int arrayIndex) => throw new NotSupportedException();
391+
392+
/// <inheritdoc />
393+
IEnumerator<KeyValuePair<string, object>> IEnumerable<KeyValuePair<string, object>>.GetEnumerator()
394+
{
395+
foreach (DictionaryEntry entry in dictionary)
396+
{
397+
yield return new KeyValuePair<string, object>((string)entry.Key, entry.Value);
398+
}
399+
}
400+
401+
/// <inheritdoc />
402+
IEnumerator IEnumerable.GetEnumerator()
403+
{
404+
return ((IEnumerable<KeyValuePair<string, object>>)this).GetEnumerator();
405+
}
406+
}
312407
}

Neo4j.Driver/Neo4j.Driver/Public/ConfigBuilder.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -533,7 +533,9 @@ public ConfigBuilder WithTls13()
533533
}
534534
#endif
535535

536-
/// <summary>Sets a custom <see cref="ITlsNegotiator"/> to use when establishing a TLS connection.</summary>
536+
/// <summary>Sets a custom <see cref="ITlsNegotiator"/> to use when establishing a TLS connection. Note
537+
/// that this overrides the default TLS negotiator, which handles certificate-based trust, so if you
538+
/// use this method you should implement certificate validation yourself.</summary>
537539
/// <param name="tlsNegotiator">The <see cref="ITlsNegotiator"/> to use.</param>
538540
/// <returns>A <see cref="ConfigBuilder"/> instance for further configuration options.</returns>
539541
/// <warning>
@@ -559,7 +561,9 @@ public ConfigBuilder WithTlsNegotiator(NegotiateTlsDelegate negotiateTls)
559561
return this;
560562
}
561563

562-
/// <summary>Sets the type of custom <see cref="ITlsNegotiator"/> to use when establishing a TLS connection.</summary>
564+
/// <summary>Sets the type of custom <see cref="ITlsNegotiator"/> to use when establishing a TLS connection. Note
565+
/// that this overrides the default TLS negotiator, which handles certificate-based trust, so if you
566+
/// use this method you should implement certificate validation yourself.</summary>
563567
/// <typeparam name="T">The <see cref="ITlsNegotiator"/> to use.</typeparam>
564568
/// <returns>A <see cref="ConfigBuilder"/> instance for further configuration options.</returns>
565569
/// <warning>

0 commit comments

Comments
 (0)