From edc0d212f12c833f58e5d37540c47ce023248276 Mon Sep 17 00:00:00 2001 From: Richard Irons Date: Thu, 25 Sep 2025 15:20:44 +0100 Subject: [PATCH 01/11] Add support for UnsupportedType --- .../Internal/IO/Utils/Machines.cs | 6 +- .../UnsupportedTypeSerializerTests.cs | 168 ++++++++++++++++++ .../UnsupportedTypeSerializer.cs | 85 +++++++++ .../Internal/Protocol/MessageFormat.cs | 5 + .../Public/Types/UnsupportedType.cs | 57 ++++++ 5 files changed, 318 insertions(+), 3 deletions(-) create mode 100644 Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/UnsupportedTypeSerializerTests.cs create mode 100644 Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/UnsupportedTypeSerializer.cs create mode 100644 Neo4j.Driver/Neo4j.Driver/Public/Types/UnsupportedType.cs diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/Utils/Machines.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/Utils/Machines.cs index 5266f410f..63bb07acc 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/Utils/Machines.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/Utils/Machines.cs @@ -16,6 +16,7 @@ using System; using System.IO; using Neo4j.Driver.Internal.IO; +using Neo4j.Driver.Internal.Protocol; namespace Neo4j.Driver.Tests.Internal.IO.Utils; @@ -44,13 +45,12 @@ public byte[] GetOutput() public class PackStreamReaderMachine { - private readonly MemoryStream _input; private readonly PackStreamReader _reader; internal PackStreamReaderMachine(byte[] bytes, Func readerFactory) { - _input = new MemoryStream(bytes); - _reader = readerFactory(_input); + var input = new MemoryStream(bytes); + _reader = readerFactory(input); } internal PackStreamReader Reader() diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/UnsupportedTypeSerializerTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/UnsupportedTypeSerializerTests.cs new file mode 100644 index 000000000..2cfa5491e --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/UnsupportedTypeSerializerTests.cs @@ -0,0 +1,168 @@ +// 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.Generic; +using FluentAssertions; +using Neo4j.Driver; +using Neo4j.Driver.Internal.IO; +using Neo4j.Driver.Internal.IO.ValueSerializers; +using Neo4j.Driver.Internal.Protocol; +using Neo4j.Driver.Tests.Internal.IO; +using Xunit; + +public class UnsupportedTypeSerializerTests : PackStreamSerializerTests +{ + internal override IPackStreamSerializer SerializerUnderTest { get; } = new UnsupportedTypeSerializer(); + + [Fact] + public void ShouldDeserialize() + { + var writerMachine = CreateWriterMachine(); + var writer = writerMachine.Writer; + + writer.WriteStructHeader(4, (byte)'?'); + writer.WriteString("the_type"); + writer.WriteByte(42); + writer.WriteByte(69); + writer.WriteMapHeader(1); + writer.WriteString("message"); + writer.WriteString("This is the message"); + + var readerMachine = CreateReaderMachine(writerMachine.GetOutput()); + var reader = readerMachine.Reader(); + var value = reader.Read(); + + value.Should().BeOfType(); + var unsupported = (UnsupportedType)value; + unsupported.Name.Should().Be("the_type"); + unsupported.MinimumProtocolVersion.Should().Be("42.69"); + unsupported.Message.Should().Be("This is the message"); + } + + [Fact] + public void ShouldThrowOnWrongSignature() + { + var writerMachine = CreateWriterMachine(); + var writer = writerMachine.Writer; + + writer.WriteStructHeader(4, 0x01); // Wrong signature + writer.WriteString("the_type"); + writer.WriteByte(6); + writer.WriteByte(0); + writer.WriteMapHeader(1); + writer.WriteString("message"); + writer.WriteString("msg"); + + var readerMachine = CreateReaderMachine(writerMachine.GetOutput()); + var reader = readerMachine.Reader(); + + FluentActions.Invoking(() => + SerializerUnderTest.Deserialize(BoltProtocolVersion.V6_0, reader, 0x01, 4)) + .Should().Throw(); + } + + [Fact] + public void ShouldThrowOnWrongStructSize() + { + var writerMachine = CreateWriterMachine(); + var writer = writerMachine.Writer; + + writer.WriteStructHeader(3, (byte)'?'); // Wrong size + writer.WriteString("the_type"); + writer.WriteByte(6); + writer.WriteByte(0); + + var readerMachine = CreateReaderMachine(writerMachine.GetOutput()); + var reader = readerMachine.Reader(); + + FluentActions.Invoking(() => + SerializerUnderTest.Deserialize(BoltProtocolVersion.V6_0, reader, (byte)'?', 3)) + .Should().Throw(); + } + + [Fact] + public void ShouldThrowIfMessageMissing() + { + var writerMachine = CreateWriterMachine(); + var writer = writerMachine.Writer; + + writer.WriteStructHeader(4, (byte)'?'); + writer.WriteString("the_type"); + writer.WriteByte(6); + writer.WriteByte(0); + writer.WriteMapHeader(0); // No "message" field + + var readerMachine = CreateReaderMachine(writerMachine.GetOutput()); + var reader = readerMachine.Reader(); + + FluentActions.Invoking(() => + SerializerUnderTest.Deserialize(BoltProtocolVersion.V6_0, reader, (byte)'?', 4)) + .Should().Throw(); + } + + [Fact] + public void ShouldThrowIfMessageNotString() + { + var writerMachine = CreateWriterMachine(); + var writer = writerMachine.Writer; + + writer.WriteStructHeader(4, (byte)'?'); + writer.WriteString("the_type"); + writer.WriteByte(6); + writer.WriteByte(0); + writer.WriteMapHeader(1); + writer.WriteString("message"); + writer.WriteByte(123); // Not a string + + var readerMachine = CreateReaderMachine(writerMachine.GetOutput()); + var reader = readerMachine.Reader(); + + FluentActions.Invoking(() => + SerializerUnderTest.Deserialize(BoltProtocolVersion.V6_0, reader, (byte)'?', 4)) + .Should().Throw(); + } + + [Fact] + public void ShouldThrowOnSerialize() + { + FluentActions.Invoking(() => + SerializerUnderTest.Serialize(BoltProtocolVersion.V6_0, null, new object())) + .Should().Throw(); + } + + [Fact] + public void DeserializeSpanShouldMatchDeserialize() + { + var writerMachine = CreateWriterMachine(); + var writer = writerMachine.Writer; + + writer.WriteStructHeader(4, (byte)'?'); + writer.WriteString("Vector"); + writer.WriteByte(6); + writer.WriteByte(0); + writer.WriteMapHeader(1); + writer.WriteString("message"); + writer.WriteString("A"); + + var reader = CreateSpanReader(writerMachine.GetOutput()); + var result = reader.Read(); + result.Should().BeOfType(); + var unsupported = (UnsupportedType)result; + unsupported.Name.Should().Be("Vector"); + unsupported.MinimumProtocolVersion.Should().Be("6.0"); + unsupported.Message.Should().Be("A"); + } +} diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/UnsupportedTypeSerializer.cs b/Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/UnsupportedTypeSerializer.cs new file mode 100644 index 000000000..4121aee2f --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/UnsupportedTypeSerializer.cs @@ -0,0 +1,85 @@ +// 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.Generic; +using Neo4j.Driver.Internal.Protocol; + +namespace Neo4j.Driver.Internal.IO.ValueSerializers; + +internal class UnsupportedTypeSerializer: IPackStreamSerializer +{ + private const byte UnsupportedTypeStructType = (byte)'?'; + private const int UnsupportedTypeStructSize = 4; + + /// + public byte[] ReadableStructs => [UnsupportedTypeStructType]; + + // we don't write unknown data + public IEnumerable WritableTypes { get; } = []; + + /// + public (object, int) DeserializeSpan(BoltProtocolVersion version, SpanPackStreamReader reader, byte signature, int size) + { + if (signature != UnsupportedTypeStructType) + { + throw new ProtocolException( + $"Unsupported struct signature {signature} passed to {nameof(UnsupportedTypeSerializer)}!"); + } + + PackStream.EnsureStructSize("UnsupportedType", UnsupportedTypeStructSize, size); + + var name = reader.ReadString(); + var minProtocolMajor = reader.ReadInteger(); + var minProtocolMinor = reader.ReadInteger(); + var extra = reader.ReadMap(); + if (!extra.TryGetValue("message", out var messageObj) || messageObj is not string message) + { + throw new ProtocolException("UnsupportedType struct is missing the 'message' field."); + } + + var result = new UnsupportedType(name, minProtocolMajor, minProtocolMinor, message); + return (result, reader.Index); + } + + public object Deserialize(BoltProtocolVersion version, PackStreamReader reader, byte signature, long size) + { + if (signature != UnsupportedTypeStructType) + { + throw new ProtocolException( + $"Unsupported struct signature {signature} passed to {nameof(UnsupportedTypeSerializer)}!"); + } + + PackStream.EnsureStructSize("UnsupportedType", UnsupportedTypeStructSize, size); + + var name = reader.ReadString(); + var minProtocolMajor = reader.ReadInteger(); + var minProtocolMinor = reader.ReadInteger(); + var extra = reader.ReadMap(); + if (!extra.TryGetValue("message", out var messageObj) || messageObj is not string message) + { + throw new ProtocolException("UnsupportedType struct is missing the 'message' field."); + } + + var result = new UnsupportedType(name, minProtocolMajor, minProtocolMinor, message); + return result; + } + + /// + public void Serialize(BoltProtocolVersion version, PackStreamWriter writer, object value) + { + throw new NotImplementedException("UnsupportedType cannot be serialized."); + } +} diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/MessageFormat.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/MessageFormat.cs index 9d23111c4..b506ab6eb 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/MessageFormat.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/MessageFormat.cs @@ -125,6 +125,11 @@ internal MessageFormat(BoltProtocolVersion version, DriverContext context) // Test code. internal MessageFormat(IEnumerable serializers = null) { + if(serializers == null) + { + return; + } + foreach (var packStreamSerializer in serializers) { AddHandler(packStreamSerializer); diff --git a/Neo4j.Driver/Neo4j.Driver/Public/Types/UnsupportedType.cs b/Neo4j.Driver/Neo4j.Driver/Public/Types/UnsupportedType.cs new file mode 100644 index 000000000..dcd9200eb --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver/Public/Types/UnsupportedType.cs @@ -0,0 +1,57 @@ +// 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.Dynamic; +using Neo4j.Driver.Internal.Protocol; + +namespace Neo4j.Driver; + +/// +/// Represents a type unknown to the driver, received from the server. +/// This type is used for instance when a newer DBMS produces a result containing a type that the current version of the driver does not yet understand. +/// +/// Note that this type may only be received from the server, but cannot be sent to the server (e.g., as a query parameter). +/// +/// The attributes exposed by this type are meant for displaying and debugging purposes. +/// They may change in future versions of the server, and should not be relied upon for any logic in your application. +/// If your application requires handling this type, you must upgrade your driver to a version that supports it. +/// +public class UnsupportedType +{ + /// + /// Gets the name of the unsupported type as provided by the server. + /// For example, "UUID" or "Vector". + /// + public string Name { get; } + + /// + /// Returns the minimum required Bolt protocol version that supports this type. + /// To understand which driver version this corresponds to, refer to the driver's release notes or documentation. + /// + public string MinimumProtocolVersion { get; } + + /// + /// Gets an optional message from the server with additional information about the unsupported type. + /// This may include hints, migration paths, or required configuration options. May be null. + /// + public string Message { get; } + + internal UnsupportedType(string name, int minimumProtocolMajor, int minimumProtocolMinor, string message) + { + Name = name; + MinimumProtocolVersion = $"{minimumProtocolMajor}.{minimumProtocolMinor}"; + Message = message; + } +} From 023ebbb2358b9f7a6c65779a2351aa255dcfdbd2 Mon Sep 17 00:00:00 2001 From: Richard Irons Date: Fri, 26 Sep 2025 10:59:50 +0100 Subject: [PATCH 02/11] Enable serialization and get TestKit tests working --- .../SupportedFeatures.cs | 1 + .../Types/NativeToCypher.cs | 27 +++++++++++++++++++ .../UnsupportedTypeSerializerTests.cs | 15 +++++------ .../UnsupportedTypeSerializer.cs | 17 +++++++----- .../Internal/Protocol/MessageFormat.cs | 8 +++--- 5 files changed, 51 insertions(+), 17 deletions(-) diff --git a/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/SupportedFeatures.cs b/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/SupportedFeatures.cs index eff1bbae4..203e4d89f 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/SupportedFeatures.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/SupportedFeatures.cs @@ -51,6 +51,7 @@ static SupportedFeatures() "Feature:API:Summary:GqlStatusObjects", "Feature:API:Type.Temporal", "Feature:API:Type.Vector", + "Feature:API:Type.UnsupportedType", "Feature:Auth:Bearer", "Feature:Auth:Custom", "Feature:Auth:Kerberos", diff --git a/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/Types/NativeToCypher.cs b/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/Types/NativeToCypher.cs index 20a4266c9..a25df51e7 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/Types/NativeToCypher.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/Types/NativeToCypher.cs @@ -43,6 +43,7 @@ internal static class NativeToCypher { typeof(List), CypherList }, { typeof(Dictionary), CypherMap }, { typeof(IVector), CypherVector }, + { typeof(UnsupportedType), CypherUnsupportedType }, { typeof(bool), CypherSimple }, { typeof(long), CypherSimple }, @@ -74,6 +75,11 @@ public static object Convert(object sourceObject) { return FunctionMap[typeof(IVector)]("CypherVector", sourceObject); } + + if(sourceObject is UnsupportedType) + { + return FunctionMap[typeof(UnsupportedType)]("CypherUnsupportedType", sourceObject); + } if (sourceObject is List) { @@ -220,6 +226,27 @@ public static NativeToCypherObject CypherVector(string cypherType, object obj) name = cypherType }; } + + public static NativeToCypherObject CypherUnsupportedType(string cypherType, object obj) + { + var unsupported = (UnsupportedType)obj; + var data = new Dictionary + { + ["name"] = unsupported.Name, + ["minimumProtocol"] = unsupported.MinimumProtocolVersion + }; + + if (!string.IsNullOrEmpty(unsupported.Message)) + { + data["message"] = unsupported.Message; + } + + return new NativeToCypherObject + { + name = cypherType, + data = data + }; + } private static string ByteStreamToHexString(byte[] byteStream) { diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/UnsupportedTypeSerializerTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/UnsupportedTypeSerializerTests.cs index 2cfa5491e..18b6b3af9 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/UnsupportedTypeSerializerTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/UnsupportedTypeSerializerTests.cs @@ -14,15 +14,14 @@ // limitations under the License. using System; -using System.Collections.Generic; using FluentAssertions; -using Neo4j.Driver; using Neo4j.Driver.Internal.IO; using Neo4j.Driver.Internal.IO.ValueSerializers; using Neo4j.Driver.Internal.Protocol; -using Neo4j.Driver.Tests.Internal.IO; using Xunit; +namespace Neo4j.Driver.Tests.Internal.IO.ValueSerializers; + public class UnsupportedTypeSerializerTests : PackStreamSerializerTests { internal override IPackStreamSerializer SerializerUnderTest { get; } = new UnsupportedTypeSerializer(); @@ -70,7 +69,7 @@ public void ShouldThrowOnWrongSignature() var reader = readerMachine.Reader(); FluentActions.Invoking(() => - SerializerUnderTest.Deserialize(BoltProtocolVersion.V6_0, reader, 0x01, 4)) + SerializerUnderTest.Deserialize(BoltProtocolVersion.V6_0, reader, 0x01, 4)) .Should().Throw(); } @@ -89,7 +88,7 @@ public void ShouldThrowOnWrongStructSize() var reader = readerMachine.Reader(); FluentActions.Invoking(() => - SerializerUnderTest.Deserialize(BoltProtocolVersion.V6_0, reader, (byte)'?', 3)) + SerializerUnderTest.Deserialize(BoltProtocolVersion.V6_0, reader, (byte)'?', 3)) .Should().Throw(); } @@ -109,7 +108,7 @@ public void ShouldThrowIfMessageMissing() var reader = readerMachine.Reader(); FluentActions.Invoking(() => - SerializerUnderTest.Deserialize(BoltProtocolVersion.V6_0, reader, (byte)'?', 4)) + SerializerUnderTest.Deserialize(BoltProtocolVersion.V6_0, reader, (byte)'?', 4)) .Should().Throw(); } @@ -131,7 +130,7 @@ public void ShouldThrowIfMessageNotString() var reader = readerMachine.Reader(); FluentActions.Invoking(() => - SerializerUnderTest.Deserialize(BoltProtocolVersion.V6_0, reader, (byte)'?', 4)) + SerializerUnderTest.Deserialize(BoltProtocolVersion.V6_0, reader, (byte)'?', 4)) .Should().Throw(); } @@ -139,7 +138,7 @@ public void ShouldThrowIfMessageNotString() public void ShouldThrowOnSerialize() { FluentActions.Invoking(() => - SerializerUnderTest.Serialize(BoltProtocolVersion.V6_0, null, new object())) + SerializerUnderTest.Serialize(BoltProtocolVersion.V6_0, null, new object())) .Should().Throw(); } diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/UnsupportedTypeSerializer.cs b/Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/UnsupportedTypeSerializer.cs index 4121aee2f..cfc5cbb96 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/UnsupportedTypeSerializer.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/UnsupportedTypeSerializer.cs @@ -29,6 +29,8 @@ internal class UnsupportedTypeSerializer: IPackStreamSerializer // we don't write unknown data public IEnumerable WritableTypes { get; } = []; + + public static UnsupportedTypeSerializer Instance { get; } = new(); /// public (object, int) DeserializeSpan(BoltProtocolVersion version, SpanPackStreamReader reader, byte signature, int size) @@ -45,9 +47,11 @@ internal class UnsupportedTypeSerializer: IPackStreamSerializer var minProtocolMajor = reader.ReadInteger(); var minProtocolMinor = reader.ReadInteger(); var extra = reader.ReadMap(); - if (!extra.TryGetValue("message", out var messageObj) || messageObj is not string message) + var message = ""; + + if (extra.TryGetValue("message", out var messageObj) && messageObj is string foundMessage) { - throw new ProtocolException("UnsupportedType struct is missing the 'message' field."); + message = foundMessage; } var result = new UnsupportedType(name, minProtocolMajor, minProtocolMinor, message); @@ -68,15 +72,16 @@ public object Deserialize(BoltProtocolVersion version, PackStreamReader reader, var minProtocolMajor = reader.ReadInteger(); var minProtocolMinor = reader.ReadInteger(); var extra = reader.ReadMap(); - if (!extra.TryGetValue("message", out var messageObj) || messageObj is not string message) + var message = ""; + + if (extra.TryGetValue("message", out var messageObj) && messageObj is string foundMessage) { - throw new ProtocolException("UnsupportedType struct is missing the 'message' field."); + message = foundMessage; } - var result = new UnsupportedType(name, minProtocolMajor, minProtocolMinor, message); return result; } - + /// public void Serialize(BoltProtocolVersion version, PackStreamWriter writer, object value) { diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/MessageFormat.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/MessageFormat.cs index b506ab6eb..80f11a716 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/MessageFormat.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/MessageFormat.cs @@ -115,11 +115,13 @@ internal MessageFormat(BoltProtocolVersion version, DriverContext context) AddHandler(ElementUnboundRelationshipSerializer.Instance); } - if(Version >= BoltProtocolVersion.V6_0) + if (Version < BoltProtocolVersion.V6_0) { - // vectors in 6.0 + - AddHandler(VectorSerializer.Instance); + return; } + + AddHandler(VectorSerializer.Instance); + AddHandler(UnsupportedTypeSerializer.Instance); } // Test code. From 5fffe23df97c4e95f836eeadc0c87ad7f1a1aab6 Mon Sep 17 00:00:00 2001 From: Richard Irons Date: Fri, 26 Sep 2025 13:00:27 +0100 Subject: [PATCH 03/11] Slight tidyup to internal vector namespaces --- .../Protocol/Session/SessionRun.cs | 11 ++-------- .../Types/NativeToCypher.cs | 3 +-- .../ValueSerializers/VectorSerializerTests.cs | 2 +- .../VectorSerializer.cs | 4 ++-- .../ITypedVectorSerialisationHelper.cs | 22 ------------------- .../Internal/Protocol/MessageFormat.cs | 1 - Neo4j.Driver/Neo4j.Driver/Neo4j.Driver.csproj | 3 +++ 7 files changed, 9 insertions(+), 37 deletions(-) rename Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/{VectorSerializers => }/VectorSerializer.cs (97%) delete mode 100644 Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/VectorSerializers/ITypedVectorSerialisationHelper.cs diff --git a/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/Protocol/Session/SessionRun.cs b/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/Protocol/Session/SessionRun.cs index caf94aa3f..6772def08 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/Protocol/Session/SessionRun.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/Protocol/Session/SessionRun.cs @@ -40,15 +40,8 @@ public override async Task Process() .ConfigureAwait(false); Result.Result result = null; - try - { - result = ProtocolObjectFactory.CreateObject(); - result.ResultCursor = cursor; - } - catch(Exception ex) - { - throw; - } + result = ProtocolObjectFactory.CreateObject(); + result.ResultCursor = cursor; ResultId = result.uniqueId; } diff --git a/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/Types/NativeToCypher.cs b/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/Types/NativeToCypher.cs index 20a4266c9..0f9db1a8f 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/Types/NativeToCypher.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/Types/NativeToCypher.cs @@ -17,8 +17,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using Neo4j.Driver.Internal.IO.ValueSerializers.VectorSerializers; -using Neo4j.Driver.Tests.TestBackend.Protocol.Result; +using Neo4j.Driver.Internal.IO.ValueSerializers; #pragma warning disable CS0618 // Type or member is obsolete - but still needs to be handled diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/VectorSerializerTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/VectorSerializerTests.cs index ed90bc268..29ac0655a 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/VectorSerializerTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/VectorSerializerTests.cs @@ -16,7 +16,7 @@ using System.Linq; using FluentAssertions; using Neo4j.Driver.Internal.IO; -using Neo4j.Driver.Internal.IO.ValueSerializers.VectorSerializers; +using Neo4j.Driver.Internal.IO.ValueSerializers; using Xunit; namespace Neo4j.Driver.Tests.Internal.IO.ValueSerializers; diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/VectorSerializers/VectorSerializer.cs b/Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/VectorSerializer.cs similarity index 97% rename from Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/VectorSerializers/VectorSerializer.cs rename to Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/VectorSerializer.cs index c48f9324e..6ec16bbd5 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/VectorSerializers/VectorSerializer.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/VectorSerializer.cs @@ -19,13 +19,13 @@ using Neo4j.Driver.Internal.Protocol; using Neo4j.Driver.Internal.Util; -namespace Neo4j.Driver.Internal.IO.ValueSerializers.VectorSerializers; +namespace Neo4j.Driver.Internal.IO.ValueSerializers; internal class VectorSerializer : IPackStreamSerializer { public static VectorSerializer Instance { get; } = new (); - public const byte VectorStructType = (byte)'V'; + private const byte VectorStructType = (byte)'V'; private const int VectorStructSize = 2; /// diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/VectorSerializers/ITypedVectorSerialisationHelper.cs b/Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/VectorSerializers/ITypedVectorSerialisationHelper.cs deleted file mode 100644 index c830380fb..000000000 --- a/Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/VectorSerializers/ITypedVectorSerialisationHelper.cs +++ /dev/null @@ -1,22 +0,0 @@ -// 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.IO.ValueSerializers.VectorSerializers; - -public interface ITypedVectorSerialisationHelper -{ - byte TypeMarker { get; } - -} diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/MessageFormat.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/MessageFormat.cs index 9d23111c4..b270bbd22 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/MessageFormat.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/MessageFormat.cs @@ -20,7 +20,6 @@ using Neo4j.Driver.Internal.IO.MessageSerializers; using Neo4j.Driver.Internal.IO.ValueSerializers; using Neo4j.Driver.Internal.IO.ValueSerializers.Temporal; -using Neo4j.Driver.Internal.IO.ValueSerializers.VectorSerializers; using Neo4j.Driver.Internal.Messaging; namespace Neo4j.Driver.Internal.Protocol; diff --git a/Neo4j.Driver/Neo4j.Driver/Neo4j.Driver.csproj b/Neo4j.Driver/Neo4j.Driver/Neo4j.Driver.csproj index 63d95eeea..40ebbddc1 100644 --- a/Neo4j.Driver/Neo4j.Driver/Neo4j.Driver.csproj +++ b/Neo4j.Driver/Neo4j.Driver/Neo4j.Driver.csproj @@ -51,4 +51,7 @@ + + + From a1cec627bb326c08756a6f5bdea2320d74e9702d Mon Sep 17 00:00:00 2001 From: Richard Irons <115992270+RichardIrons-neo4j@users.noreply.github.com> Date: Fri, 26 Sep 2025 13:32:42 +0100 Subject: [PATCH 04/11] Update Neo4j.Driver/Neo4j.Driver/Neo4j.Driver.csproj Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Neo4j.Driver/Neo4j.Driver/Neo4j.Driver.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/Neo4j.Driver/Neo4j.Driver/Neo4j.Driver.csproj b/Neo4j.Driver/Neo4j.Driver/Neo4j.Driver.csproj index 40ebbddc1..c8f8ee4c0 100644 --- a/Neo4j.Driver/Neo4j.Driver/Neo4j.Driver.csproj +++ b/Neo4j.Driver/Neo4j.Driver/Neo4j.Driver.csproj @@ -52,6 +52,5 @@ - From f4bc6a6fc59c627a99d69e56d1d5ff49154a5e09 Mon Sep 17 00:00:00 2001 From: Richard Irons Date: Fri, 26 Sep 2025 15:25:15 +0100 Subject: [PATCH 05/11] Fix broken test --- .../UnsupportedTypeSerializerTests.cs | 20 ------------------- .../UnsupportedTypeSerializer.cs | 1 + 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/UnsupportedTypeSerializerTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/UnsupportedTypeSerializerTests.cs index 18b6b3af9..559889fe0 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/UnsupportedTypeSerializerTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/UnsupportedTypeSerializerTests.cs @@ -92,26 +92,6 @@ public void ShouldThrowOnWrongStructSize() .Should().Throw(); } - [Fact] - public void ShouldThrowIfMessageMissing() - { - var writerMachine = CreateWriterMachine(); - var writer = writerMachine.Writer; - - writer.WriteStructHeader(4, (byte)'?'); - writer.WriteString("the_type"); - writer.WriteByte(6); - writer.WriteByte(0); - writer.WriteMapHeader(0); // No "message" field - - var readerMachine = CreateReaderMachine(writerMachine.GetOutput()); - var reader = readerMachine.Reader(); - - FluentActions.Invoking(() => - SerializerUnderTest.Deserialize(BoltProtocolVersion.V6_0, reader, (byte)'?', 4)) - .Should().Throw(); - } - [Fact] public void ShouldThrowIfMessageNotString() { diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/UnsupportedTypeSerializer.cs b/Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/UnsupportedTypeSerializer.cs index cfc5cbb96..8dd6f3fad 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/UnsupportedTypeSerializer.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/UnsupportedTypeSerializer.cs @@ -78,6 +78,7 @@ public object Deserialize(BoltProtocolVersion version, PackStreamReader reader, { message = foundMessage; } + var result = new UnsupportedType(name, minProtocolMajor, minProtocolMinor, message); return result; } From 661849e23356419e03516d4df1577eef1a01db84 Mon Sep 17 00:00:00 2001 From: Richard Irons Date: Mon, 29 Sep 2025 17:32:56 +0100 Subject: [PATCH 06/11] Add string representation and tests --- .../Types/UnsupportedTypeTests.cs | 47 +++++++++++++++++++ .../Public/Types/UnsupportedType.cs | 5 ++ 2 files changed, 52 insertions(+) create mode 100644 Neo4j.Driver/Neo4j.Driver.Tests/Types/UnsupportedTypeTests.cs diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Types/UnsupportedTypeTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Types/UnsupportedTypeTests.cs new file mode 100644 index 000000000..50ca4520b --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Types/UnsupportedTypeTests.cs @@ -0,0 +1,47 @@ +// 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 FluentAssertions; +using Xunit; + +namespace Neo4j.Driver.Tests.Types; + +public class UnsupportedTypeTests +{ + [Theory] + [InlineData("QuantumEntity", 9, 1, "QuantumEntity type not supported", "9.1")] + [InlineData("TemporalVortex", 7, 3, "TemporalVortex type not supported", "7.3")] + [InlineData("HyperEdge", 8, 0, "HyperEdge type not supported", "8.0")] + [InlineData("MetaNode", 10, 2, "MetaNode type not supported", "10.2")] + public void ShouldCreate( + string name, + int minimumProtocolMajor, + int minimumProtocolMinor, + string message, + string expectedMinimumProtocolVersion) + { + var unsupportedType = new UnsupportedType(name, minimumProtocolMajor, minimumProtocolMinor, message); + unsupportedType.Message.Should().Be(message); + unsupportedType.Name.Should().Be(name); + unsupportedType.MinimumProtocolVersion.Should().Be(expectedMinimumProtocolVersion); + } + + [Fact] + public void ShouldGiveCorrectStringRepresentation() + { + var unsupportedType = new UnsupportedType("QuantumEntity", 9, 1, "QuantumEntity type not supported"); + unsupportedType.ToString().Should().Be("UnsupportedType(QuantumEntity)"); + } +} diff --git a/Neo4j.Driver/Neo4j.Driver/Public/Types/UnsupportedType.cs b/Neo4j.Driver/Neo4j.Driver/Public/Types/UnsupportedType.cs index dcd9200eb..75bd8a4d9 100644 --- a/Neo4j.Driver/Neo4j.Driver/Public/Types/UnsupportedType.cs +++ b/Neo4j.Driver/Neo4j.Driver/Public/Types/UnsupportedType.cs @@ -54,4 +54,9 @@ internal UnsupportedType(string name, int minimumProtocolMajor, int minimumProto MinimumProtocolVersion = $"{minimumProtocolMajor}.{minimumProtocolMinor}"; Message = message; } + + public override string ToString() + { + return $"UnsupportedType({Name})"; + } } From b9e31d74009e9ca2de1e80d77fac0c3fd4832c60 Mon Sep 17 00:00:00 2001 From: Richard Irons Date: Tue, 30 Sep 2025 09:28:12 +0100 Subject: [PATCH 07/11] Remove redundant ItemGroup in Neo4j.Driver.csproj --- Neo4j.Driver/Neo4j.Driver/Neo4j.Driver.csproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/Neo4j.Driver/Neo4j.Driver/Neo4j.Driver.csproj b/Neo4j.Driver/Neo4j.Driver/Neo4j.Driver.csproj index c8f8ee4c0..63d95eeea 100644 --- a/Neo4j.Driver/Neo4j.Driver/Neo4j.Driver.csproj +++ b/Neo4j.Driver/Neo4j.Driver/Neo4j.Driver.csproj @@ -51,6 +51,4 @@ - - From 720b1f5a34846f2b2456cc4af85ba385144a5623 Mon Sep 17 00:00:00 2001 From: Richard Irons Date: Tue, 30 Sep 2025 09:53:37 +0100 Subject: [PATCH 08/11] Review notes addressed --- .../UnsupportedTypeSerializerTests.cs | 60 +++++++++---------- .../UnsupportedTypeSerializer.cs | 4 +- .../Public/Types/UnsupportedType.cs | 6 ++ 3 files changed, 38 insertions(+), 32 deletions(-) diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/UnsupportedTypeSerializerTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/UnsupportedTypeSerializerTests.cs index 559889fe0..325ff11ba 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/UnsupportedTypeSerializerTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Internal/IO/ValueSerializers/UnsupportedTypeSerializerTests.cs @@ -32,13 +32,13 @@ public void ShouldDeserialize() var writerMachine = CreateWriterMachine(); var writer = writerMachine.Writer; - writer.WriteStructHeader(4, (byte)'?'); + writer.WriteStructHeader(4, (byte)'?'); // '?' is the signature for UnsupportedType writer.WriteString("the_type"); - writer.WriteByte(42); - writer.WriteByte(69); - writer.WriteMapHeader(1); - writer.WriteString("message"); - writer.WriteString("This is the message"); + writer.WriteByte(42); // Major version + writer.WriteByte(69); // Minor version + writer.WriteMapHeader(1); // One key-value pair in the map + writer.WriteString("message"); // Key + writer.WriteString("This is the message"); // Value var readerMachine = CreateReaderMachine(writerMachine.GetOutput()); var reader = readerMachine.Reader(); @@ -57,13 +57,13 @@ public void ShouldThrowOnWrongSignature() var writerMachine = CreateWriterMachine(); var writer = writerMachine.Writer; - writer.WriteStructHeader(4, 0x01); // Wrong signature - writer.WriteString("the_type"); - writer.WriteByte(6); - writer.WriteByte(0); - writer.WriteMapHeader(1); - writer.WriteString("message"); - writer.WriteString("msg"); + writer.WriteStructHeader(4, 0x01); // Invalid signature + writer.WriteString("the_type"); // Name + writer.WriteByte(6); // Major version + writer.WriteByte(0); // Minor version + writer.WriteMapHeader(1); // One key-value pair in the map + writer.WriteString("message"); // Key + writer.WriteString("msg"); // Value var readerMachine = CreateReaderMachine(writerMachine.GetOutput()); var reader = readerMachine.Reader(); @@ -79,10 +79,10 @@ public void ShouldThrowOnWrongStructSize() var writerMachine = CreateWriterMachine(); var writer = writerMachine.Writer; - writer.WriteStructHeader(3, (byte)'?'); // Wrong size - writer.WriteString("the_type"); - writer.WriteByte(6); - writer.WriteByte(0); + writer.WriteStructHeader(3, (byte)'?'); // Wrong size, should be 4 + writer.WriteString("the_type"); // Name + writer.WriteByte(6); // Major version + writer.WriteByte(0); // Minor version var readerMachine = CreateReaderMachine(writerMachine.GetOutput()); var reader = readerMachine.Reader(); @@ -98,12 +98,12 @@ public void ShouldThrowIfMessageNotString() var writerMachine = CreateWriterMachine(); var writer = writerMachine.Writer; - writer.WriteStructHeader(4, (byte)'?'); - writer.WriteString("the_type"); - writer.WriteByte(6); - writer.WriteByte(0); - writer.WriteMapHeader(1); - writer.WriteString("message"); + writer.WriteStructHeader(4, (byte)'?'); // '?' is the signature for UnsupportedType + writer.WriteString("the_type"); // Name + writer.WriteByte(6);// Major version + writer.WriteByte(0); // Minor version + writer.WriteMapHeader(1); // One key-value pair in the map + writer.WriteString("message"); // Key writer.WriteByte(123); // Not a string var readerMachine = CreateReaderMachine(writerMachine.GetOutput()); @@ -128,13 +128,13 @@ public void DeserializeSpanShouldMatchDeserialize() var writerMachine = CreateWriterMachine(); var writer = writerMachine.Writer; - writer.WriteStructHeader(4, (byte)'?'); - writer.WriteString("Vector"); - writer.WriteByte(6); - writer.WriteByte(0); - writer.WriteMapHeader(1); - writer.WriteString("message"); - writer.WriteString("A"); + writer.WriteStructHeader(4, (byte)'?'); // '?' is the signature for UnsupportedType + writer.WriteString("Vector"); // Name + writer.WriteByte(6); // Major version + writer.WriteByte(0); // Minor version + writer.WriteMapHeader(1); // One key-value pair in the map + writer.WriteString("message"); // Key + writer.WriteString("A"); // Value var reader = CreateSpanReader(writerMachine.GetOutput()); var result = reader.Read(); diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/UnsupportedTypeSerializer.cs b/Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/UnsupportedTypeSerializer.cs index 8dd6f3fad..100ba74e1 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/UnsupportedTypeSerializer.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/IO/ValueSerializers/UnsupportedTypeSerializer.cs @@ -38,7 +38,7 @@ internal class UnsupportedTypeSerializer: IPackStreamSerializer if (signature != UnsupportedTypeStructType) { throw new ProtocolException( - $"Unsupported struct signature {signature} passed to {nameof(UnsupportedTypeSerializer)}!"); + $"Unknown struct signature {signature} passed to {nameof(UnsupportedTypeSerializer)}!"); } PackStream.EnsureStructSize("UnsupportedType", UnsupportedTypeStructSize, size); @@ -63,7 +63,7 @@ public object Deserialize(BoltProtocolVersion version, PackStreamReader reader, if (signature != UnsupportedTypeStructType) { throw new ProtocolException( - $"Unsupported struct signature {signature} passed to {nameof(UnsupportedTypeSerializer)}!"); + $"Unknown struct signature {signature} passed to {nameof(UnsupportedTypeSerializer)}!"); } PackStream.EnsureStructSize("UnsupportedType", UnsupportedTypeStructSize, size); diff --git a/Neo4j.Driver/Neo4j.Driver/Public/Types/UnsupportedType.cs b/Neo4j.Driver/Neo4j.Driver/Public/Types/UnsupportedType.cs index 75bd8a4d9..f25497952 100644 --- a/Neo4j.Driver/Neo4j.Driver/Public/Types/UnsupportedType.cs +++ b/Neo4j.Driver/Neo4j.Driver/Public/Types/UnsupportedType.cs @@ -38,7 +38,13 @@ public class UnsupportedType /// /// Returns the minimum required Bolt protocol version that supports this type. + /// /// To understand which driver version this corresponds to, refer to the driver's release notes or documentation. + /// + /// Note: Bolt version does not correlate directly with Driver version. See + /// for which driver version is + /// required for new Types. + /// /// public string MinimumProtocolVersion { get; } From 8468542b2d18dee2deb5b5d914106512231d1c3b Mon Sep 17 00:00:00 2001 From: Richard Irons Date: Fri, 3 Oct 2025 11:14:05 +0100 Subject: [PATCH 09/11] Add object-to-dictionary conversion utilities and tests for parameter value transformation, to fix a bug with serializing parameters. --- .../Util/ParameterValueTransformerTests.cs | 126 +++++++++ .../TestUtil/CollectionExtensionsTests.cs | 17 ++ .../Extensions/CollectionExtensions.cs | 253 +----------------- .../DictionaryAccessWrapper.cs | 90 +++++++ .../IObjectToDictionaryConverter.cs | 23 ++ .../IParameterValueTransformer.cs | 21 ++ .../ObjectToDictionaryConverter.cs | 78 ++++++ .../ParameterValueTransformer.cs | 186 +++++++++++++ .../Neo4j.Driver.csproj.DotSettings | 1 + 9 files changed, 552 insertions(+), 243 deletions(-) create mode 100644 Neo4j.Driver/Neo4j.Driver.Tests/Internal/Util/ParameterValueTransformerTests.cs create mode 100644 Neo4j.Driver/Neo4j.Driver/Internal/Util/ObjectToDictionary/DictionaryAccessWrapper.cs create mode 100644 Neo4j.Driver/Neo4j.Driver/Internal/Util/ObjectToDictionary/IObjectToDictionaryConverter.cs create mode 100644 Neo4j.Driver/Neo4j.Driver/Internal/Util/ObjectToDictionary/IParameterValueTransformer.cs create mode 100644 Neo4j.Driver/Neo4j.Driver/Internal/Util/ObjectToDictionary/ObjectToDictionaryConverter.cs create mode 100644 Neo4j.Driver/Neo4j.Driver/Internal/Util/ObjectToDictionary/ParameterValueTransformer.cs 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..88df708cb 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/TestUtil/CollectionExtensionsTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/TestUtil/CollectionExtensionsTests.cs @@ -189,6 +189,23 @@ public void ShouldHandleAnonymousObjects() new KeyValuePair("key1", "value1"), new KeyValuePair("key2", "value2")); } + + [Fact] + public void ShouldHandleVectors() + { + var vector = Vector.Create([1.0, 2.0, 3.0]); + + var dict = new + { + vector + }.ToDictionary(); + + 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() 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..c60cb3627 --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Util/ObjectToDictionary/ParameterValueTransformer.cs @@ -0,0 +1,186 @@ +// 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) + { + if (value == null) + { + return null; + } + + var valueType = value.GetType(); + + if(!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 From a4b63c47cee5612e84a6d78b54866412ce906d44 Mon Sep 17 00:00:00 2001 From: Richard Irons Date: Fri, 3 Oct 2025 12:13:56 +0100 Subject: [PATCH 10/11] Simplify quick exit code --- .../ObjectToDictionary/ParameterValueTransformer.cs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Util/ObjectToDictionary/ParameterValueTransformer.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Util/ObjectToDictionary/ParameterValueTransformer.cs index c60cb3627..62288aa73 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/Util/ObjectToDictionary/ParameterValueTransformer.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Util/ObjectToDictionary/ParameterValueTransformer.cs @@ -25,17 +25,11 @@ namespace Neo4j.Driver.Internal.Util; internal class ParameterValueTransformer : IParameterValueTransformer { private static readonly TypeInfo NeoValueTypeInfo = typeof(IValue).GetTypeInfo(); - + public object Transform(object value) { - if (value == null) - { - return null; - } - - var valueType = value.GetType(); - - if(!NeedsConversion(valueType)) + var valueType = value?.GetType(); + if (valueType == null || !NeedsConversion(valueType)) { return value; } From fe01b3a5349fcdc2b6b61cfad94829505a6bdc8a Mon Sep 17 00:00:00 2001 From: Richard Irons Date: Mon, 6 Oct 2025 14:32:25 +0100 Subject: [PATCH 11/11] Moving unit tests around --- .../Util/ObjectToDictionaryConverterTests.cs | 366 ++++++++++++++++ .../TestUtil/CollectionExtensionsTests.cs | 413 ------------------ 2 files changed, 366 insertions(+), 413 deletions(-) create mode 100644 Neo4j.Driver/Neo4j.Driver.Tests/Internal/Util/ObjectToDictionaryConverterTests.cs 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/TestUtil/CollectionExtensionsTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/TestUtil/CollectionExtensionsTests.cs index 88df708cb..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,417 +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 ShouldHandleVectors() - { - var vector = Vector.Create([1.0, 2.0, 3.0]); - - var dict = new - { - vector - }.ToDictionary(); - - 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 = 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;