From f128fd1e907222e7683f7fb60b1e5e54c26579c7 Mon Sep 17 00:00:00 2001 From: Richard Irons Date: Thu, 20 Mar 2025 09:54:00 +0000 Subject: [PATCH 1/3] Move internal items to internal folder and adjust namespaces accordingly --- .../Mapping/BuiltMapperTests.cs | 1 + .../Mapping/DefaultMapperTests.cs | 1 + .../Mapping/DictAsRecordTests.cs | 1 + .../Mapping/MappableValueProviderTests.cs | 1 + .../Mapping/MappedListCreatorTests.cs | 1 + .../Mapping/MappingBuilderTests.cs | 1 + .../MappingSourceDelegateBuilderTests.cs | 1 + .../Mapping/RecordPathFinderTests.cs | 1 + .../Extensions}/MappingExtensions.cs | 2 +- .../Mapping/BuiltMapper.cs | 3 ++- .../Mapping/DefaultMapper.cs | 3 ++- .../Mapping/DelegateMapper.cs | 3 ++- .../Mapping/DictAsRecord.cs | 2 +- .../Mapping/EntityMappingInfo.cs | 3 ++- .../Mapping/MappableValueProvider.cs | 3 ++- .../Mapping/MappedListCreator.cs | 3 ++- .../Mapping/MappingBuilder.cs | 3 ++- .../Mapping/MappingSourceDelegateBuilder.cs | 4 +++- .../Mapping/RecordPathFinder.cs | 2 +- .../IMappingTypeConversionManager.cs | 21 +++++++++++++++++++ Neo4j.Driver/Neo4j.Driver/Neo4j.Driver.csproj | 3 +++ .../DelegateAsyncEnumerableExtensions.cs | 1 + .../DelegateMappingRecordExtensions.cs | 1 + .../Public/Mapping/EntityExtensions.cs | 2 ++ .../Public/Mapping/MappingSourceAttribute.cs | 1 + .../Public/Mapping/RecordObjectMapping.cs | 1 + 26 files changed, 58 insertions(+), 11 deletions(-) rename Neo4j.Driver/Neo4j.Driver/{Public/Mapping => Internal/Extensions}/MappingExtensions.cs (97%) rename Neo4j.Driver/Neo4j.Driver/{Public => Internal}/Mapping/BuiltMapper.cs (98%) rename Neo4j.Driver/Neo4j.Driver/{Public => Internal}/Mapping/DefaultMapper.cs (98%) rename Neo4j.Driver/Neo4j.Driver/{Public => Internal}/Mapping/DelegateMapper.cs (97%) rename Neo4j.Driver/Neo4j.Driver/{Public => Internal}/Mapping/DictAsRecord.cs (98%) rename Neo4j.Driver/Neo4j.Driver/{Public => Internal}/Mapping/EntityMappingInfo.cs (97%) rename Neo4j.Driver/Neo4j.Driver/{Public => Internal}/Mapping/MappableValueProvider.cs (98%) rename Neo4j.Driver/Neo4j.Driver/{Public => Internal}/Mapping/MappedListCreator.cs (96%) rename Neo4j.Driver/Neo4j.Driver/{Public => Internal}/Mapping/MappingBuilder.cs (98%) rename Neo4j.Driver/Neo4j.Driver/{Public => Internal}/Mapping/MappingSourceDelegateBuilder.cs (96%) rename Neo4j.Driver/Neo4j.Driver/{Public => Internal}/Mapping/RecordPathFinder.cs (98%) create mode 100644 Neo4j.Driver/Neo4j.Driver/Internal/Mapping/TypeConversion/IMappingTypeConversionManager.cs diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/BuiltMapperTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/BuiltMapperTests.cs index 7c0c4a2ee..16d6fd530 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/BuiltMapperTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/BuiltMapperTests.cs @@ -14,6 +14,7 @@ // limitations under the License. using FluentAssertions; +using Neo4j.Driver.Internal.Mapping; using Neo4j.Driver.Mapping; using Neo4j.Driver.Tests.TestUtil; using Xunit; diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/DefaultMapperTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/DefaultMapperTests.cs index 3b1ac52db..b01209c96 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/DefaultMapperTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/DefaultMapperTests.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; using FluentAssertions; +using Neo4j.Driver.Internal.Mapping; using Neo4j.Driver.Internal.Types; using Neo4j.Driver.Mapping; using Neo4j.Driver.Tests.TestUtil; diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/DictAsRecordTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/DictAsRecordTests.cs index cf3c0b01c..46b8ad262 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/DictAsRecordTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/DictAsRecordTests.cs @@ -16,6 +16,7 @@ using System.Collections; using System.Collections.Generic; using FluentAssertions; +using Neo4j.Driver.Internal.Mapping; using Neo4j.Driver.Internal.Types; using Neo4j.Driver.Mapping; using Neo4j.Driver.Tests.TestUtil; diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappableValueProviderTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappableValueProviderTests.cs index 8fdab357f..89ce9b7d0 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappableValueProviderTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappableValueProviderTests.cs @@ -17,6 +17,7 @@ using FluentAssertions; using Moq; using Moq.AutoMock; +using Neo4j.Driver.Internal.Mapping; using Neo4j.Driver.Internal.Types; using Neo4j.Driver.Mapping; using Xunit; diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappedListCreatorTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappedListCreatorTests.cs index 4dc92ea1d..ef737515c 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappedListCreatorTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappedListCreatorTests.cs @@ -18,6 +18,7 @@ using FluentAssertions; using Moq; using Moq.AutoMock; +using Neo4j.Driver.Internal.Mapping; using Neo4j.Driver.Mapping; using Neo4j.Driver.Tests.TestUtil; using Xunit; diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappingBuilderTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappingBuilderTests.cs index bf538f0ba..ad0412341 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappingBuilderTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappingBuilderTests.cs @@ -15,6 +15,7 @@ using System; using FluentAssertions; +using Neo4j.Driver.Internal.Mapping; using Neo4j.Driver.Mapping; using Xunit; diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappingSourceDelegateBuilderTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappingSourceDelegateBuilderTests.cs index 385a154a2..26c4ebf59 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappingSourceDelegateBuilderTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappingSourceDelegateBuilderTests.cs @@ -15,6 +15,7 @@ using System.Collections.Generic; using FluentAssertions; +using Neo4j.Driver.Internal.Mapping; using Neo4j.Driver.Internal.Types; using Neo4j.Driver.Mapping; using Neo4j.Driver.Tests.TestUtil; diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/RecordPathFinderTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/RecordPathFinderTests.cs index 283375ffc..2f766e7cd 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/RecordPathFinderTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/RecordPathFinderTests.cs @@ -15,6 +15,7 @@ using System.Collections.Generic; using FluentAssertions; +using Neo4j.Driver.Internal.Mapping; using Neo4j.Driver.Mapping; using Neo4j.Driver.Tests.TestUtil; using Xunit; diff --git a/Neo4j.Driver/Neo4j.Driver/Public/Mapping/MappingExtensions.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Extensions/MappingExtensions.cs similarity index 97% rename from Neo4j.Driver/Neo4j.Driver/Public/Mapping/MappingExtensions.cs rename to Neo4j.Driver/Neo4j.Driver/Internal/Extensions/MappingExtensions.cs index b3d1657d5..15998e875 100644 --- a/Neo4j.Driver/Neo4j.Driver/Public/Mapping/MappingExtensions.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Extensions/MappingExtensions.cs @@ -17,7 +17,7 @@ using System.Collections.Generic; using System.Reflection; -namespace Neo4j.Driver.Mapping; +namespace Neo4j.Driver.Internal; internal static class MappingExtensions { diff --git a/Neo4j.Driver/Neo4j.Driver/Public/Mapping/BuiltMapper.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/BuiltMapper.cs similarity index 98% rename from Neo4j.Driver/Neo4j.Driver/Public/Mapping/BuiltMapper.cs rename to Neo4j.Driver/Neo4j.Driver/Internal/Mapping/BuiltMapper.cs index 3b42f24f8..6379039e1 100644 --- a/Neo4j.Driver/Neo4j.Driver/Public/Mapping/BuiltMapper.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/BuiltMapper.cs @@ -17,8 +17,9 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using Neo4j.Driver.Mapping; -namespace Neo4j.Driver.Mapping; +namespace Neo4j.Driver.Internal.Mapping; internal class BuiltMapper : IRecordMapper { diff --git a/Neo4j.Driver/Neo4j.Driver/Public/Mapping/DefaultMapper.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/DefaultMapper.cs similarity index 98% rename from Neo4j.Driver/Neo4j.Driver/Public/Mapping/DefaultMapper.cs rename to Neo4j.Driver/Neo4j.Driver/Internal/Mapping/DefaultMapper.cs index 832dd5267..5ac8b7233 100644 --- a/Neo4j.Driver/Neo4j.Driver/Public/Mapping/DefaultMapper.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/DefaultMapper.cs @@ -16,8 +16,9 @@ using System; using System.Collections.Generic; using System.Reflection; +using Neo4j.Driver.Mapping; -namespace Neo4j.Driver.Mapping; +namespace Neo4j.Driver.Internal.Mapping; internal static class DefaultMapper { diff --git a/Neo4j.Driver/Neo4j.Driver/Public/Mapping/DelegateMapper.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/DelegateMapper.cs similarity index 97% rename from Neo4j.Driver/Neo4j.Driver/Public/Mapping/DelegateMapper.cs rename to Neo4j.Driver/Neo4j.Driver/Internal/Mapping/DelegateMapper.cs index a15e4cc62..9a5afb301 100644 --- a/Neo4j.Driver/Neo4j.Driver/Public/Mapping/DelegateMapper.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/DelegateMapper.cs @@ -16,8 +16,9 @@ using System; using System.Collections.Generic; using System.Reflection; +using Neo4j.Driver.Mapping; -namespace Neo4j.Driver.Mapping; +namespace Neo4j.Driver.Internal.Mapping; internal class DelegateMapper { diff --git a/Neo4j.Driver/Neo4j.Driver/Public/Mapping/DictAsRecord.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/DictAsRecord.cs similarity index 98% rename from Neo4j.Driver/Neo4j.Driver/Public/Mapping/DictAsRecord.cs rename to Neo4j.Driver/Neo4j.Driver/Internal/Mapping/DictAsRecord.cs index 8e6ed6ca9..5d7b8bdf7 100644 --- a/Neo4j.Driver/Neo4j.Driver/Public/Mapping/DictAsRecord.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/DictAsRecord.cs @@ -18,7 +18,7 @@ using System.Collections.Generic; using System.Linq; -namespace Neo4j.Driver.Mapping; +namespace Neo4j.Driver.Internal.Mapping; internal sealed class DictAsRecord : IRecord { diff --git a/Neo4j.Driver/Neo4j.Driver/Public/Mapping/EntityMappingInfo.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/EntityMappingInfo.cs similarity index 97% rename from Neo4j.Driver/Neo4j.Driver/Public/Mapping/EntityMappingInfo.cs rename to Neo4j.Driver/Neo4j.Driver/Internal/Mapping/EntityMappingInfo.cs index 747f1d38f..e2ac4d360 100644 --- a/Neo4j.Driver/Neo4j.Driver/Public/Mapping/EntityMappingInfo.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/EntityMappingInfo.cs @@ -15,8 +15,9 @@ using System.Linq; using System.Reflection; +using Neo4j.Driver.Mapping; -namespace Neo4j.Driver.Mapping; +namespace Neo4j.Driver.Internal.Mapping; internal record EntityMappingInfo( string Path, diff --git a/Neo4j.Driver/Neo4j.Driver/Public/Mapping/MappableValueProvider.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/MappableValueProvider.cs similarity index 98% rename from Neo4j.Driver/Neo4j.Driver/Public/Mapping/MappableValueProvider.cs rename to Neo4j.Driver/Neo4j.Driver/Internal/Mapping/MappableValueProvider.cs index 69fabdabf..d5f5fe1ab 100644 --- a/Neo4j.Driver/Neo4j.Driver/Public/Mapping/MappableValueProvider.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/MappableValueProvider.cs @@ -17,8 +17,9 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using Neo4j.Driver.Mapping; -namespace Neo4j.Driver.Mapping; +namespace Neo4j.Driver.Internal.Mapping; internal interface IMappableValueProvider { diff --git a/Neo4j.Driver/Neo4j.Driver/Public/Mapping/MappedListCreator.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/MappedListCreator.cs similarity index 96% rename from Neo4j.Driver/Neo4j.Driver/Public/Mapping/MappedListCreator.cs rename to Neo4j.Driver/Neo4j.Driver/Internal/Mapping/MappedListCreator.cs index 3d36e5743..ae00b9c71 100644 --- a/Neo4j.Driver/Neo4j.Driver/Public/Mapping/MappedListCreator.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/MappedListCreator.cs @@ -16,8 +16,9 @@ using System; using System.Collections; using System.Collections.Generic; +using Neo4j.Driver.Mapping; -namespace Neo4j.Driver.Mapping; +namespace Neo4j.Driver.Internal.Mapping; internal interface IMappedListCreator { diff --git a/Neo4j.Driver/Neo4j.Driver/Public/Mapping/MappingBuilder.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/MappingBuilder.cs similarity index 98% rename from Neo4j.Driver/Neo4j.Driver/Public/Mapping/MappingBuilder.cs rename to Neo4j.Driver/Neo4j.Driver/Internal/Mapping/MappingBuilder.cs index 1a6885f68..12b57e53d 100644 --- a/Neo4j.Driver/Neo4j.Driver/Public/Mapping/MappingBuilder.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/MappingBuilder.cs @@ -16,8 +16,9 @@ using System; using System.Linq.Expressions; using System.Reflection; +using Neo4j.Driver.Mapping; -namespace Neo4j.Driver.Mapping; +namespace Neo4j.Driver.Internal.Mapping; /// This class wraps the and exposes a fluent API for building the mapper. internal class MappingBuilder : IMappingBuilder diff --git a/Neo4j.Driver/Neo4j.Driver/Public/Mapping/MappingSourceDelegateBuilder.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/MappingSourceDelegateBuilder.cs similarity index 96% rename from Neo4j.Driver/Neo4j.Driver/Public/Mapping/MappingSourceDelegateBuilder.cs rename to Neo4j.Driver/Neo4j.Driver/Internal/Mapping/MappingSourceDelegateBuilder.cs index 633037133..82b30c12f 100644 --- a/Neo4j.Driver/Neo4j.Driver/Public/Mapping/MappingSourceDelegateBuilder.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/MappingSourceDelegateBuilder.cs @@ -13,7 +13,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Neo4j.Driver.Mapping; +using Neo4j.Driver.Mapping; + +namespace Neo4j.Driver.Internal.Mapping; internal delegate bool MappingValueDelegate(IRecord record, out object value); diff --git a/Neo4j.Driver/Neo4j.Driver/Public/Mapping/RecordPathFinder.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/RecordPathFinder.cs similarity index 98% rename from Neo4j.Driver/Neo4j.Driver/Public/Mapping/RecordPathFinder.cs rename to Neo4j.Driver/Neo4j.Driver/Internal/Mapping/RecordPathFinder.cs index 32ec7c522..af24692be 100644 --- a/Neo4j.Driver/Neo4j.Driver/Public/Mapping/RecordPathFinder.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/RecordPathFinder.cs @@ -15,7 +15,7 @@ using System.Collections.Generic; -namespace Neo4j.Driver.Mapping; +namespace Neo4j.Driver.Internal.Mapping; internal interface IRecordPathFinder { diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/TypeConversion/IMappingTypeConversionManager.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/TypeConversion/IMappingTypeConversionManager.cs new file mode 100644 index 000000000..8c30644c8 --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/TypeConversion/IMappingTypeConversionManager.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.Mapping.TypeConversion; + +internal interface IMappingTypeConversionManager +{ + TResult Convert(TSource value); +} diff --git a/Neo4j.Driver/Neo4j.Driver/Neo4j.Driver.csproj b/Neo4j.Driver/Neo4j.Driver/Neo4j.Driver.csproj index 4d5b8fa9c..0469063a4 100644 --- a/Neo4j.Driver/Neo4j.Driver/Neo4j.Driver.csproj +++ b/Neo4j.Driver/Neo4j.Driver/Neo4j.Driver.csproj @@ -51,4 +51,7 @@ + + + diff --git a/Neo4j.Driver/Neo4j.Driver/Public/Mapping/DelegateAsyncEnumerableExtensions.cs b/Neo4j.Driver/Neo4j.Driver/Public/Mapping/DelegateAsyncEnumerableExtensions.cs index 1ca53e624..05efca7d2 100644 --- a/Neo4j.Driver/Neo4j.Driver/Public/Mapping/DelegateAsyncEnumerableExtensions.cs +++ b/Neo4j.Driver/Neo4j.Driver/Public/Mapping/DelegateAsyncEnumerableExtensions.cs @@ -18,6 +18,7 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; +using Neo4j.Driver.Internal.Mapping; namespace Neo4j.Driver.Mapping; diff --git a/Neo4j.Driver/Neo4j.Driver/Public/Mapping/DelegateMappingRecordExtensions.cs b/Neo4j.Driver/Neo4j.Driver/Public/Mapping/DelegateMappingRecordExtensions.cs index e9f990e0c..a20b79944 100644 --- a/Neo4j.Driver/Neo4j.Driver/Public/Mapping/DelegateMappingRecordExtensions.cs +++ b/Neo4j.Driver/Neo4j.Driver/Public/Mapping/DelegateMappingRecordExtensions.cs @@ -14,6 +14,7 @@ // limitations under the License. using System; +using Neo4j.Driver.Internal.Mapping; namespace Neo4j.Driver.Mapping; diff --git a/Neo4j.Driver/Neo4j.Driver/Public/Mapping/EntityExtensions.cs b/Neo4j.Driver/Neo4j.Driver/Public/Mapping/EntityExtensions.cs index 284a74c97..167a87868 100644 --- a/Neo4j.Driver/Neo4j.Driver/Public/Mapping/EntityExtensions.cs +++ b/Neo4j.Driver/Neo4j.Driver/Public/Mapping/EntityExtensions.cs @@ -13,6 +13,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Neo4j.Driver.Internal.Mapping; + namespace Neo4j.Driver.Mapping; /// Contains extensions for entities such as nodes and relationships. diff --git a/Neo4j.Driver/Neo4j.Driver/Public/Mapping/MappingSourceAttribute.cs b/Neo4j.Driver/Neo4j.Driver/Public/Mapping/MappingSourceAttribute.cs index 313a9c3bb..8eca52fa6 100644 --- a/Neo4j.Driver/Neo4j.Driver/Public/Mapping/MappingSourceAttribute.cs +++ b/Neo4j.Driver/Neo4j.Driver/Public/Mapping/MappingSourceAttribute.cs @@ -14,6 +14,7 @@ // limitations under the License. using System; +using Neo4j.Driver.Internal.Mapping; namespace Neo4j.Driver.Mapping; diff --git a/Neo4j.Driver/Neo4j.Driver/Public/Mapping/RecordObjectMapping.cs b/Neo4j.Driver/Neo4j.Driver/Public/Mapping/RecordObjectMapping.cs index e84903cd1..01c1b4411 100644 --- a/Neo4j.Driver/Neo4j.Driver/Public/Mapping/RecordObjectMapping.cs +++ b/Neo4j.Driver/Neo4j.Driver/Public/Mapping/RecordObjectMapping.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; using System.Reflection; +using Neo4j.Driver.Internal.Mapping; namespace Neo4j.Driver.Mapping; From 7654c0b40f98719dcac488242b57c8c09abe8d31 Mon Sep 17 00:00:00 2001 From: Richard Irons Date: Tue, 25 Mar 2025 14:25:37 +0000 Subject: [PATCH 2/3] Added type conversion to object mapping --- .../Internal/Util/VarLongTests.cs | 1 - .../Mapping/DelegateMapperTests.cs | 2 +- .../Mapping/DictAsRecordTests.cs | 1 - .../Mapping/LabelCaptureTests.cs | 7 +- .../Mapping/MappableValueProviderTests.cs | 28 ++++ .../Mapping/MappingBuilderTests.cs | 1 - .../Mapping/MappingProviderTests.cs | 7 +- .../Mapping/MappingTestWithGlobalState.cs | 33 +++++ .../Mapping/RecordMappingTests.cs | 4 +- .../Mapping/RecordPathFinderTests.cs | 1 - .../TypeConversionManagerTests.cs | 80 +++++++++++ .../TypeConversion/TypeConversionTests.cs | 133 ++++++++++++++++++ .../Neo4j.Driver.Tests/TransactionTests.cs | 1 - .../Internal/Connector/IConnection.cs | 1 - .../Internal/Extensions/MappingExtensions.cs | 12 +- .../Internal/Mapping/BuiltMapper.cs | 65 ++++----- .../Internal/Mapping/DelegateMapper.cs | 48 +------ .../Internal/Mapping/MappableValueProvider.cs | 36 ++++- .../Internal/Mapping/MappedListCreator.cs | 16 ++- .../Internal/Mapping/ParameterMapper.cs | 85 +++++++++++ .../IMappingTypeConversionManager.cs | 7 +- .../MappingTypeConversionManager.cs | 61 ++++++++ .../MessageHandling/RouteResponseHandler.cs | 1 - .../Protocol/BoltProtocolHandlerFactory.cs | 1 - .../Internal/Protocol/IBoltProtocol.cs | 1 - .../Internal/Routing/IDiscovery.cs | 2 - Neo4j.Driver/Neo4j.Driver/Neo4j.Driver.csproj | 3 - .../Public/Mapping/RecordObjectMapping.cs | 37 +++-- .../Neo4j.Driver/Public/SessionConfig.cs | 2 - 29 files changed, 544 insertions(+), 133 deletions(-) create mode 100644 Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappingTestWithGlobalState.cs create mode 100644 Neo4j.Driver/Neo4j.Driver.Tests/Mapping/TypeConversion/TypeConversionManagerTests.cs create mode 100644 Neo4j.Driver/Neo4j.Driver.Tests/Mapping/TypeConversion/TypeConversionTests.cs create mode 100644 Neo4j.Driver/Neo4j.Driver/Internal/Mapping/ParameterMapper.cs create mode 100644 Neo4j.Driver/Neo4j.Driver/Internal/Mapping/TypeConversion/MappingTypeConversionManager.cs diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Internal/Util/VarLongTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Internal/Util/VarLongTests.cs index e108d1d77..ef2b9caa2 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Internal/Util/VarLongTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Internal/Util/VarLongTests.cs @@ -15,7 +15,6 @@ using System; using FluentAssertions; -using Neo4j.Driver.Internal.IO; using Neo4j.Driver.Internal.Util; using Xunit; diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/DelegateMapperTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/DelegateMapperTests.cs index 6317a89da..9869f4d40 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/DelegateMapperTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/DelegateMapperTests.cs @@ -355,7 +355,7 @@ public void ShouldFailIfDelegateThrowsException() { var record = TestRecord.Create(("x", 69)); - Action act = () => record.AsObject( + var act = () => record.AsObject( (int x) => { if (x == 69) diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/DictAsRecordTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/DictAsRecordTests.cs index 46b8ad262..b9a0ce5c2 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/DictAsRecordTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/DictAsRecordTests.cs @@ -18,7 +18,6 @@ using FluentAssertions; using Neo4j.Driver.Internal.Mapping; using Neo4j.Driver.Internal.Types; -using Neo4j.Driver.Mapping; using Neo4j.Driver.Tests.TestUtil; using Xunit; using InvalidOperationException = System.InvalidOperationException; diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/LabelCaptureTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/LabelCaptureTests.cs index 88f39a6d4..3102dc642 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/LabelCaptureTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/LabelCaptureTests.cs @@ -23,13 +23,8 @@ namespace Neo4j.Driver.Tests.Mapping; -public class LabelCaptureTests +public class LabelCaptureTests : MappingTestWithGlobalState { - public LabelCaptureTests() - { - RecordObjectMapping.Reset(); - } - [Fact] public void ShouldCaptureSingleNodeLabel() { diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappableValueProviderTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappableValueProviderTests.cs index 89ce9b7d0..cafe82019 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappableValueProviderTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappableValueProviderTests.cs @@ -13,11 +13,13 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System; using System.Collections.Generic; using FluentAssertions; using Moq; using Moq.AutoMock; using Neo4j.Driver.Internal.Mapping; +using Neo4j.Driver.Internal.Mapping.TypeConversion; using Neo4j.Driver.Internal.Types; using Neo4j.Driver.Mapping; using Xunit; @@ -129,6 +131,32 @@ public void GetConvertedValueShouldReturnNullWhenFieldNotFound() result.Should().BeNull(); } + [Fact] + public void GetConvertedValueShouldUseTypeConversion() + { + var expected = new Guid("b19f766c-4f74-41a9-a73d-6298697d81a7"); + var entityMappingInfo = new EntityMappingInfo("field-name", EntityMappingSource.Property); + _mocker.GetMock() + .Setup(x => x.GetMappingDelegate(entityMappingInfo)) + .Returns(GetMockMappingDelegate(expected.ToString(), true)); + + object expectedObj = expected; + _mocker.GetMock() + .Setup(x => x.TryConvert(typeof(string), typeof(Guid), expected.ToString(), out expectedObj)) + .Returns(true); + + var subject = _mocker.CreateInstance(true); + + var result = subject.GetConvertedValue( + Mock.Of(), + entityMappingInfo, + typeof(Guid)); + + result.Should().Be(expected); + } + + + [Fact] public void ShouldReturnNullWhenValueIsNull() { diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappingBuilderTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappingBuilderTests.cs index ad0412341..83f48db4a 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappingBuilderTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappingBuilderTests.cs @@ -16,7 +16,6 @@ using System; using FluentAssertions; using Neo4j.Driver.Internal.Mapping; -using Neo4j.Driver.Mapping; using Xunit; namespace Neo4j.Driver.Tests.Mapping; diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappingProviderTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappingProviderTests.cs index 80c1af5cd..911cbd030 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappingProviderTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappingProviderTests.cs @@ -21,13 +21,8 @@ namespace Neo4j.Driver.Tests.Mapping; -public class MappingProviderTests +public class MappingProviderTests : MappingTestWithGlobalState { - public MappingProviderTests() - { - RecordObjectMapping.Reset(); - } - [Fact] public void ShouldOverrideDefaultMapping() { diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappingTestWithGlobalState.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappingTestWithGlobalState.cs new file mode 100644 index 000000000..91ae22775 --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappingTestWithGlobalState.cs @@ -0,0 +1,33 @@ +// 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 Neo4j.Driver.Mapping; +using Xunit; + +namespace Neo4j.Driver.Tests.Mapping; + +[CollectionDefinition("UsesMappingGlobalState", DisableParallelization = true)] +public class UsesMappingGlobalState +{ +} + +[Collection("UsesMappingGlobalState")] +public class MappingTestWithGlobalState +{ + public MappingTestWithGlobalState() + { + RecordObjectMapping.Reset(); + } +} diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/RecordMappingTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/RecordMappingTests.cs index 6ef9b5155..8c1f087cf 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/RecordMappingTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/RecordMappingTests.cs @@ -676,7 +676,5 @@ private class ClassWithDefaultConstructorWithAttributes([MappingSource("forename public int Age => age; } - private record TestXY(int X, string Y) - { - } + private record TestXY(int X, string Y); } diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/RecordPathFinderTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/RecordPathFinderTests.cs index 2f766e7cd..2a009bfa7 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/RecordPathFinderTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/RecordPathFinderTests.cs @@ -16,7 +16,6 @@ using System.Collections.Generic; using FluentAssertions; using Neo4j.Driver.Internal.Mapping; -using Neo4j.Driver.Mapping; using Neo4j.Driver.Tests.TestUtil; using Xunit; diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/TypeConversion/TypeConversionManagerTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/TypeConversion/TypeConversionManagerTests.cs new file mode 100644 index 000000000..b787a9441 --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/TypeConversion/TypeConversionManagerTests.cs @@ -0,0 +1,80 @@ +// 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 Neo4j.Driver.Internal.Mapping.TypeConversion; +using Xunit; + +namespace Neo4j.Driver.Tests.Mapping.TypeConversion; + +public class TypeConversionManagerTests +{ + [Fact] + public void ShouldConvertTypes() + { + var manager = new MappingTypeConversionManager(); + manager.RegisterConverter(i => i.ToString()); + manager.RegisterConverter(int.Parse); + + manager.TryConvert(42, out string str).Should().BeTrue(); + str.Should().Be("42"); + + manager.TryConvert("42", out int i).Should().BeTrue(); + i.Should().Be(42); + } + + [Fact] + public void ShouldNotConvertTypes() + { + var manager = new MappingTypeConversionManager(); + manager.RegisterConverter(i => i.ToString()); + manager.RegisterConverter(int.Parse); + + manager.TryConvert(42, out int _).Should().BeFalse(); + + manager.TryConvert("42", out string _).Should().BeFalse(); + } + + [Fact] + public void ShouldNotConvertTypesWhenNoConverterRegistered() + { + var manager = new MappingTypeConversionManager(); + manager.TryConvert(42, out string _).Should().BeFalse(); + manager.TryConvert("42", out int _).Should().BeFalse(); + } + + [Fact] + public void ShouldNotConvertTypesWhenConverterIsNotRegistered() + { + var manager = new MappingTypeConversionManager(); + manager.RegisterConverter(i => i.ToString()); + + manager.TryConvert(42, out int _).Should().BeFalse(); + } + + [Fact] + public void ShouldConvertTypesCalledWithDifferentTypes() + { + var manager = new MappingTypeConversionManager(); + manager.RegisterConverter(i => i.ToString()); + manager.RegisterConverter(int.Parse); + + manager.TryConvert(typeof(int), typeof(string), 42, out var str).Should().BeTrue(); + str.Should().Be("42"); + + manager.TryConvert(typeof(string), typeof(int), "42", out var i).Should().BeTrue(); + i.Should().Be(42); + } +} diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/TypeConversion/TypeConversionTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/TypeConversion/TypeConversionTests.cs new file mode 100644 index 000000000..0fdb2044a --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/TypeConversion/TypeConversionTests.cs @@ -0,0 +1,133 @@ +// 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 System.Linq; +using FluentAssertions; +using Neo4j.Driver.Internal.Types; +using Neo4j.Driver.Mapping; +using Neo4j.Driver.Tests.TestUtil; +using Xunit; + +namespace Neo4j.Driver.Tests.Mapping.TypeConversion; + +public class TypeConversionTests : MappingTestWithGlobalState +{ + private class WebSite + { + public string Name { get; set; } + public Uri Uri { get; set; } + } + + private class User + { + public string Name { get; set; } + public Guid Id { get; set; } + } + + private class UserAndWebSite + { + public User User { get; set; } + public WebSite WebSite { get; set; } + } + + [Fact] + public void ShouldFailToMapRecordToClassWithoutConversion() + { + var record = TestRecord.Create(("Name", "Neo4j"), ("Uri", "http://neo4j.com")); + + var act = () => { _ = record.AsObject(); }; + + act.Should().Throw(); + } + + [Fact] + public void ShouldMapRecordToClassWithConversion() + { + var record = TestRecord.Create(("Name", "Neo4j"), ("Uri", "http://neo4j.com")); + RecordObjectMapping.RegisterTypeConverter((string s) => new Uri(s)); + + var website = record.AsObject(); + + website.Name.Should().Be("Neo4j"); + website.Uri.Should().Be(new Uri("http://neo4j.com")); + } + + [Fact] + public void ShouldMapWhenUsingBlueprintMapping() + { + const string guidStr = "123e4567-e89b-12d3-a456-426614174000"; + var record = TestRecord.Create(("Name", "Neo4j"), ("Id", guidStr)); + RecordObjectMapping.RegisterTypeConverter((string s) => Guid.Parse(s)); + var user = record.AsObjectFromBlueprint(new { Name = default(string), Id = default(Guid) }); + user.Name.Should().Be("Neo4j"); + user.Id.Should().Be(Guid.Parse(guidStr)); + } + + [Fact] + public void ShouldMapWhenUsingDelegateMapping() + { + const string guidStr = "123e4567-e89b-12d3-a456-426614174999"; + var record = TestRecord.Create(("name", "Neo4j"), ("id", guidStr)); + RecordObjectMapping.RegisterTypeConverter((string s) => Guid.Parse(s)); + + var user = record.AsObject((string name, Guid id) => new User { Name = name, Id = id }); + + user.Name.Should().Be("Neo4j"); + user.Id.Should().Be(Guid.Parse(guidStr)); + } + + [Fact] + public void ShouldUseConverterDuringNestedMapping() + { + var websiteEntity = new Node(1, new[] {"WebSite"}, new Dictionary + { + {"Name", "Neo4j"}, + {"Uri", "http://neo4j.com"} + }); + + var userId = Guid.NewGuid(); + var userEntity = new Node(2, new[] {"User"}, new Dictionary + { + {"Name", "John"}, + {"Id", userId.ToString()} + }); + + var testRecord = TestRecord.Create(("WebSite", websiteEntity), ("User", userEntity)); + RecordObjectMapping.RegisterTypeConverter((string s) => Guid.Parse(s)); + RecordObjectMapping.RegisterTypeConverter((string s) => new Uri(s)); + + var userAndWebSite = testRecord.AsObject(); + + userAndWebSite.User.Name.Should().Be("John"); + userAndWebSite.User.Id.Should().Be(userId); + userAndWebSite.WebSite.Name.Should().Be("Neo4j"); + userAndWebSite.WebSite.Uri.Should().Be(new Uri("http://neo4j.com")); + } + + [Fact] + public void ShouldUseConverterWhenMappingLists() + { + var uriStringList = new List {"http://neo4j.com", "http://google.com", "http://bing.com", "http://yahoo.com"}; + var expected = uriStringList.Select(s => new Uri(s)); + var record = TestRecord.Create(("UriList", uriStringList)); + RecordObjectMapping.RegisterTypeConverter((string s) => new Uri(s)); + + var mapped = record.AsObjectFromBlueprint(new { UriList = default(List) }); + + mapped.UriList.Should().BeEquivalentTo(expected); + } +} diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/TransactionTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/TransactionTests.cs index f6edbdc31..bc72136fa 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/TransactionTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/TransactionTests.cs @@ -14,7 +14,6 @@ // limitations under the License. using System; -using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; using Moq; diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Connector/IConnection.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Connector/IConnection.cs index 61424b36a..4c238d03c 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/Connector/IConnection.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Connector/IConnection.cs @@ -13,7 +13,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Extensions/MappingExtensions.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Extensions/MappingExtensions.cs index 15998e875..f188ec1ec 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/Extensions/MappingExtensions.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Extensions/MappingExtensions.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; using System.Reflection; +using Neo4j.Driver.Mapping; namespace Neo4j.Driver.Internal; @@ -38,9 +39,16 @@ public static object AsType(this object obj, Type type) { return asMethod.Invoke(null, [obj]); } - catch (TargetInvocationException tie) + catch (Exception ex) { - throw tie.InnerException!; + var inner = ex switch + { + TargetInvocationException tie => tie.InnerException, + _ => ex + }; + + throw new MappingFailedException( + $"Failed to map value to type {type.Name}.", inner); } } } diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/BuiltMapper.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/BuiltMapper.cs index 6379039e1..ceba0cbec 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/BuiltMapper.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/BuiltMapper.cs @@ -15,7 +15,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Reflection; using Neo4j.Driver.Mapping; @@ -24,6 +23,7 @@ namespace Neo4j.Driver.Internal.Mapping; internal class BuiltMapper : IRecordMapper { private readonly IMappableValueProvider _mappableValueProvider = new MappableValueProvider(); + private readonly IParameterMapper _parameterMapper = new ParameterMapper(); private readonly List> _propertyMappings = new(); private Func _wholeObjectMapping; @@ -43,14 +43,24 @@ public T Map(IRecord record) { throw new MappingFailedException( $"Cannot map record to type {typeof(T).Name} " + - $"because the mapping function threw an exception.", + $"because the mapping function threw an exception ({ex.Message}).", ex); } // if there are individual mappings for the properties, apply them foreach (var mapping in _propertyMappings) { - mapping(result, record); + try + { + mapping(result, record); + } + catch (Exception ex) + { + throw new MappingFailedException( + $"Cannot map record to type {typeof(T).Name} " + + $"because the mapping function threw an exception ({ex.Message}).", + ex.InnerException); + } } return result; @@ -77,42 +87,7 @@ public void AddWholeObjectMapping(Func mappingFunction) public void AddConstructorMapping(ConstructorInfo constructorInfo) { - // this part only happens once, at the time of building the mapper - var parameters = constructorInfo.GetParameters(); - var paramMappings = parameters.Select( - parameter => new - { - parameter, - mapping = parameter.GetEntityMappingInfo() - }); - - _wholeObjectMapping = MapFromRecord; - return; - - // this part happens every time a record is mapped - T MapFromRecord(IRecord record) - { - var args = new List(); - foreach (var p in paramMappings) - { - var success = _mappableValueProvider.TryGetMappableValue( - record, - r => _mappableValueProvider.GetConvertedValue(r, p.mapping, p.parameter.ParameterType, null), - p.parameter.ParameterType, - out var mappable); - - if (!success) - { - throw new MappingFailedException( - $"Cannot map record to type {typeof(T).Name} because the record does not " + - $"contain a value for the constructor parameter '{p.parameter.Name}'."); - } - - args.Add(mappable); - } - - return (T)constructorInfo.Invoke(args.ToArray()); - } + _wholeObjectMapping = _parameterMapper.GetParameterMappedCall(constructorInfo); } public void AddMappingBySetter( @@ -153,7 +128,17 @@ void MapFromRecord(T obj, IRecord record) if (mappableValueFound) { - propertySetter.Invoke(obj, [mappableValue]); + try + { + propertySetter.Invoke(obj, [mappableValue]); + } + catch (Exception ex) + { + throw new MappingFailedException( + $"Cannot map record to type {typeof(T).Name} " + + $"because the property setter '{propertySetter.Name}' threw an exception.", + ex); + } } else if (!optional) { diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/DelegateMapper.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/DelegateMapper.cs index 9a5afb301..f75915300 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/DelegateMapper.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/DelegateMapper.cs @@ -13,57 +13,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; -using System.Collections.Generic; using System.Reflection; -using Neo4j.Driver.Mapping; namespace Neo4j.Driver.Internal.Mapping; internal class DelegateMapper { + private static readonly IParameterMapper ParameterMapper = new ParameterMapper(); + internal static T MapWithMethodInfo(IRecord record, MethodInfo mapFunction, object target) { - var paramValues = new List(); - foreach (var param in mapFunction.GetParameters()) - { - if (record.TryGet(param.Name, out object value)) - { - object valueToUse; - try - { - valueToUse = value.AsType(param.ParameterType); - } - catch (InvalidCastException) when (value is IEntity or IReadOnlyDictionary) - { - var objToMap = new DictAsRecord(value, null); - valueToUse = RecordObjectMapping.Map(objToMap, param.ParameterType); - } - catch (Exception ex) - { - throw new MappingFailedException( - $"Failed to map parameter {param.Name}: " + - $"Could not convert value of field {param.Name} to required type.", - ex); - } - - paramValues.Add(valueToUse); - } - else - { - throw new MappingFailedException($"Failed to map parameter {param.Name}: No such key in record."); - } - } - - try - { - return (T) - mapFunction.Invoke(target, paramValues.ToArray()); - } - catch (Exception ex) - { - var inner = ex is TargetInvocationException tie ? tie.InnerException : ex; - throw new MappingFailedException("Failed to map record.", inner); - } + var mapMethod = ParameterMapper.GetParameterMappedCall(mapFunction, target); + return mapMethod(record); } } diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/MappableValueProvider.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/MappableValueProvider.cs index d5f5fe1ab..1e2217bca 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/MappableValueProvider.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/MappableValueProvider.cs @@ -16,7 +16,9 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; +using Neo4j.Driver.Internal.Mapping.TypeConversion; using Neo4j.Driver.Mapping; namespace Neo4j.Driver.Internal.Mapping; @@ -41,15 +43,18 @@ internal class MappableValueProvider : IMappableValueProvider private readonly IMappedListCreator _mappedListCreator; private readonly IMappingSourceDelegateBuilder _mappingSourceDelegateBuilder; private readonly IRecordObjectMapping _recordObjectMapping; + private readonly IMappingTypeConversionManager _typeConversionManager; internal MappableValueProvider( IMappedListCreator mappedListCreator = null, IMappingSourceDelegateBuilder mappingSourceDelegateBuilder = null, - IRecordObjectMapping recordObjectMapping = null) + IRecordObjectMapping recordObjectMapping = null, + IMappingTypeConversionManager typeConversionManager = null) { _mappedListCreator = mappedListCreator ?? new MappedListCreator(); _mappingSourceDelegateBuilder = mappingSourceDelegateBuilder ?? new MappingSourceDelegateBuilder(); - _recordObjectMapping = recordObjectMapping ?? RecordObjectMapping.Instance; + _recordObjectMapping = recordObjectMapping; + _typeConversionManager = typeConversionManager; } public bool TryGetMappableValue( @@ -75,7 +80,8 @@ public bool TryGetMappableValue( // if the value is an entity or dictionary, make it into a fake record and map that (indirectly recursive) case IEntity or IDictionary: var dictAsRecord = new DictAsRecord(value, record); - result = _recordObjectMapping.Map(dictAsRecord, desiredType); + var mapper = _recordObjectMapping ?? RecordObjectMapping.Instance; + result = mapper.Map(dictAsRecord, desiredType); return true; // otherwise, just return the value @@ -111,7 +117,29 @@ public object GetConvertedValue( ICollection list => _mappedListCreator.CreateMappedList(list, propertyType, record), // otherwise, convert the value to the type of the property - _ => value.AsType(propertyType) + _ => ConvertToType(value, propertyType) }; } + + private object ConvertToType(object value, Type propertyType) + { + if (value is null) + { + return null; + } + + if (propertyType.IsInstanceOfType(value)) + { + return value; + } + + var mapper = _recordObjectMapping ?? RecordObjectMapping.Instance; + var converter = _typeConversionManager ?? mapper.TypeConversionManager; + if (converter.TryConvert(value.GetType(), propertyType, value, out var convertedValue)) + { + return convertedValue; + } + + return value.AsType(propertyType); + } } diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/MappedListCreator.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/MappedListCreator.cs index ae00b9c71..6c31cb467 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/MappedListCreator.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/MappedListCreator.cs @@ -16,6 +16,7 @@ using System; using System.Collections; using System.Collections.Generic; +using Neo4j.Driver.Internal.Mapping.TypeConversion; using Neo4j.Driver.Mapping; namespace Neo4j.Driver.Internal.Mapping; @@ -28,10 +29,14 @@ internal interface IMappedListCreator internal class MappedListCreator : IMappedListCreator { private readonly IRecordObjectMapping _recordObjectMapping; + private readonly IMappingTypeConversionManager _typeConversionManager; - internal MappedListCreator(IRecordObjectMapping recordObjectMapping = null) + internal MappedListCreator( + IRecordObjectMapping recordObjectMapping = null, + IMappingTypeConversionManager typeConversionManager = null) { _recordObjectMapping = recordObjectMapping ?? RecordObjectMapping.Instance; + _typeConversionManager = typeConversionManager ?? _recordObjectMapping.TypeConversionManager; } public IList CreateMappedList(IEnumerable list, Type desiredListType, IRecord record) @@ -51,7 +56,14 @@ public IList CreateMappedList(IEnumerable list, Type desiredListType, IRecord re else { // otherwise, just convert the item to the type of the list - newList!.Add(item.AsType(desiredItemType)); + if (_typeConversionManager.TryConvert(item.GetType(), desiredItemType, item, out var convertedValue)) + { + newList!.Add(convertedValue); + } + else + { + newList!.Add(item.AsType(desiredItemType)); + } } } diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/ParameterMapper.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/ParameterMapper.cs new file mode 100644 index 000000000..ac4302eae --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/ParameterMapper.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 System.Linq; +using System.Reflection; +using Neo4j.Driver.Mapping; + +namespace Neo4j.Driver.Internal.Mapping; + +internal interface IParameterMapper +{ + Func GetParameterMappedCall(MethodBase methodBase, object target = null); +} + +internal class ParameterMapper : IParameterMapper +{ + private readonly IMappableValueProvider _mappableValueProvider = new MappableValueProvider(); + + public Func GetParameterMappedCall(MethodBase method, object target = null) + { + // this part only happens once, at the time of building the mapper + var parameters = method.GetParameters(); + var paramMappings = parameters.Select( + parameter => new + { + parameter, + mapping = parameter.GetEntityMappingInfo() + }); + + return MapFromRecord; + + // this part happens every time a record is mapped + T MapFromRecord(IRecord record) + { + var args = new List(); + foreach (var p in paramMappings) + { + var success = _mappableValueProvider.TryGetMappableValue( + record, + r => _mappableValueProvider.GetConvertedValue(r, p.mapping, p.parameter.ParameterType, null), + p.parameter.ParameterType, + out var mappable); + + if (!success) + { + throw new MappingFailedException( + $"Cannot map record to type {typeof(T).Name} because the record does not " + + $"contain a value for the parameter '{p.parameter.Name}'."); + } + + args.Add(mappable); + } + + try + { + return method switch + { + ConstructorInfo constructorInfo => (T)constructorInfo.Invoke(args.ToArray()), + MethodInfo methodInfo => (T)methodInfo.Invoke(target, args.ToArray()), + _ => throw new NotSupportedException($"Unsupported method type: {method.GetType()}") + }; + } + catch (TargetInvocationException tie) + { + throw new MappingFailedException( + $"Cannot map record to type {typeof(T).Name} because the method '{method.Name}' threw an exception.", + tie.InnerException); + } + } + } +} diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/TypeConversion/IMappingTypeConversionManager.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/TypeConversion/IMappingTypeConversionManager.cs index 8c30644c8..0b28e52df 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/TypeConversion/IMappingTypeConversionManager.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/TypeConversion/IMappingTypeConversionManager.cs @@ -13,9 +13,14 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System; + namespace Neo4j.Driver.Internal.Mapping.TypeConversion; internal interface IMappingTypeConversionManager { - TResult Convert(TSource value); + bool TryConvert(TFrom from, out TTo to); + void RegisterConverter(Func converter); + bool TryConvert(Type fromType, Type toType, object from, out object to); + void Clear(); } diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/TypeConversion/MappingTypeConversionManager.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/TypeConversion/MappingTypeConversionManager.cs new file mode 100644 index 000000000..743872a9c --- /dev/null +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Mapping/TypeConversion/MappingTypeConversionManager.cs @@ -0,0 +1,61 @@ +// 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.Concurrent; +using System.Diagnostics; + +namespace Neo4j.Driver.Internal.Mapping.TypeConversion; + +internal class MappingTypeConversionManager : IMappingTypeConversionManager +{ + private readonly ConcurrentDictionary<(Type From, Type To), Func> _converters = new(); + + public void Clear() => _converters.Clear(); + + public bool TryConvert(Type fromType, Type toType, object from, out object to) + { + if (_converters.TryGetValue((fromType, toType), out var converter)) + { + to = converter(from); + return true; + } + + to = default!; + return false; + } + + /// + public bool TryConvert(TFrom from, out TTo to) + { + var success = TryConvert(typeof(TFrom), typeof(TTo), from, out var result); + if (success) + { + to = (TTo)result; + return true; + } + + to = default!; + return false; + } + + /// + public void RegisterConverter(Func converter) + { + Trace.WriteLine("Registering converter in " + GetHashCode()); + Func func = o => converter((TFrom)o); + _converters.AddOrUpdate((typeof(TFrom), typeof(TTo)), func, (_, _) => func); + } +} diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/MessageHandling/RouteResponseHandler.cs b/Neo4j.Driver/Neo4j.Driver/Internal/MessageHandling/RouteResponseHandler.cs index a04f91d95..095db8ff8 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/MessageHandling/RouteResponseHandler.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/MessageHandling/RouteResponseHandler.cs @@ -13,7 +13,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; using System.Collections.Generic; using Neo4j.Driver.Internal.HomeDbCaching; using Neo4j.Driver.Internal.MessageHandling.Metadata; diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/BoltProtocolHandlerFactory.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/BoltProtocolHandlerFactory.cs index ece5d4811..ff291b995 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/BoltProtocolHandlerFactory.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/BoltProtocolHandlerFactory.cs @@ -14,7 +14,6 @@ // limitations under the License. using System; -using System.Collections.Generic; using System.Threading.Tasks; using Neo4j.Driver.Internal.Connector; using Neo4j.Driver.Internal.HomeDbCaching; diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/IBoltProtocol.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/IBoltProtocol.cs index 1274507b5..5e31a0a27 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/IBoltProtocol.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Protocol/IBoltProtocol.cs @@ -13,7 +13,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; using System.Collections.Generic; using System.Threading.Tasks; using Neo4j.Driver.Internal.Connector; diff --git a/Neo4j.Driver/Neo4j.Driver/Internal/Routing/IDiscovery.cs b/Neo4j.Driver/Neo4j.Driver/Internal/Routing/IDiscovery.cs index 76217fadf..bc97b8848 100644 --- a/Neo4j.Driver/Neo4j.Driver/Internal/Routing/IDiscovery.cs +++ b/Neo4j.Driver/Neo4j.Driver/Internal/Routing/IDiscovery.cs @@ -13,8 +13,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; -using System.Collections.Generic; using System.Threading.Tasks; using Neo4j.Driver.Internal.Connector; using Neo4j.Driver.Internal.HomeDbCaching; diff --git a/Neo4j.Driver/Neo4j.Driver/Neo4j.Driver.csproj b/Neo4j.Driver/Neo4j.Driver/Neo4j.Driver.csproj index 0469063a4..4d5b8fa9c 100644 --- a/Neo4j.Driver/Neo4j.Driver/Neo4j.Driver.csproj +++ b/Neo4j.Driver/Neo4j.Driver/Neo4j.Driver.csproj @@ -51,7 +51,4 @@ - - - diff --git a/Neo4j.Driver/Neo4j.Driver/Public/Mapping/RecordObjectMapping.cs b/Neo4j.Driver/Neo4j.Driver/Public/Mapping/RecordObjectMapping.cs index 01c1b4411..b4696af6e 100644 --- a/Neo4j.Driver/Neo4j.Driver/Public/Mapping/RecordObjectMapping.cs +++ b/Neo4j.Driver/Neo4j.Driver/Public/Mapping/RecordObjectMapping.cs @@ -17,6 +17,7 @@ using System.Collections.Generic; using System.Reflection; using Neo4j.Driver.Internal.Mapping; +using Neo4j.Driver.Internal.Mapping.TypeConversion; namespace Neo4j.Driver.Mapping; @@ -33,24 +34,29 @@ public interface IMappingRegistry IMappingRegistry RegisterMapping(Action> mappingBuilder); } -internal interface IRecordObjectMapping +internal interface IRecordObjectMapping : IMappingRegistry { object Map(IRecord record, Type type); TResult MapFromBlueprint(IRecord record, TResult blueprint); + IMappingTypeConversionManager TypeConversionManager { get; } + void RegisterTypeConverter(Func converter); + MethodInfo GetMapMethodForType(Type type); } +internal delegate object MapDelegate(IRecord record); + /// Controls global record mapping configuration. -public class RecordObjectMapping : IMappingRegistry, IRecordObjectMapping +public class RecordObjectMapping : IRecordObjectMapping { private readonly Dictionary _mapMethods = new(); - private readonly Dictionary _mappers = new(); + private readonly IMappingTypeConversionManager _typeConversionManager = new MappingTypeConversionManager(); private RecordObjectMapping() { } - internal static RecordObjectMapping Instance { get; private set; } = new(); + internal static readonly RecordObjectMapping Instance = new(); IMappingRegistry IMappingRegistry.RegisterMapping(Action> mappingBuilder) { @@ -65,10 +71,11 @@ object IRecordObjectMapping.Map(IRecord record, Type type) { var mapMethod = Instance.GetMapMethodForType(type); var mapperForType = GetMapperForType(type); + var mapDelegate = (MapDelegate)mapMethod.CreateDelegate(typeof(MapDelegate), mapperForType); try { - return mapMethod.Invoke(mapperForType, [record]); + return mapDelegate(record); } catch (Exception ex) { @@ -82,10 +89,24 @@ T IRecordObjectMapping.MapFromBlueprint(IRecord record, T blueprint) return (T)Map(record, typeof(T)); } + IMappingTypeConversionManager IRecordObjectMapping.TypeConversionManager => _typeConversionManager; + + void IRecordObjectMapping.RegisterTypeConverter(Func converter) + { + _typeConversionManager.RegisterConverter(converter); + } + + public static void RegisterTypeConverter(Func converter) + { + ((IRecordObjectMapping)Instance).RegisterTypeConverter(converter); + } + internal static void Reset() { - // discard the current instance and create a new one, which will have no mappers registered - Instance = new RecordObjectMapping(); + // clear all registered mappers and type converters + Instance._mappers.Clear(); + Instance._mapMethods.Clear(); + Instance._typeConversionManager.Clear(); DefaultMapper.Reset(); } @@ -140,7 +161,7 @@ public static void RegisterProvider(IMappingProvider provider) provider.CreateMappers(Instance); } - private MethodInfo GetMapMethodForType(Type type) + public MethodInfo GetMapMethodForType(Type type) { if (_mapMethods.TryGetValue(type, out var method)) { diff --git a/Neo4j.Driver/Neo4j.Driver/Public/SessionConfig.cs b/Neo4j.Driver/Neo4j.Driver/Public/SessionConfig.cs index 81068a965..4e49b4771 100644 --- a/Neo4j.Driver/Neo4j.Driver/Public/SessionConfig.cs +++ b/Neo4j.Driver/Neo4j.Driver/Public/SessionConfig.cs @@ -16,9 +16,7 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using Neo4j.Driver.Internal; -using Neo4j.Driver.Internal.Auth; using Neo4j.Driver.Internal.Types; namespace Neo4j.Driver; From f76a5bd0e48bd951702c3b096d32ab33f165a6ef Mon Sep 17 00:00:00 2001 From: Richard Irons Date: Tue, 25 Mar 2025 15:38:02 +0000 Subject: [PATCH 3/3] Add constructor mapping test for type conversion --- .../TypeConversion/TypeConversionTests.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/TypeConversion/TypeConversionTests.cs b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/TypeConversion/TypeConversionTests.cs index 0fdb2044a..902c8f689 100644 --- a/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/TypeConversion/TypeConversionTests.cs +++ b/Neo4j.Driver/Neo4j.Driver.Tests/Mapping/TypeConversion/TypeConversionTests.cs @@ -38,6 +38,12 @@ private class User public Guid Id { get; set; } } + private class UserWithConstructor(string name, Guid id) + { + public string Name => name; + public Guid Id => id; + } + private class UserAndWebSite { public User User { get; set; } @@ -130,4 +136,17 @@ public void ShouldUseConverterWhenMappingLists() mapped.UriList.Should().BeEquivalentTo(expected); } + + [Fact] + public void ShouldUseConverterWhenUsingConstructorMapping() + { + var guid = Guid.NewGuid(); + var record = TestRecord.Create(("name", "John"), ("id", guid.ToString())); + RecordObjectMapping.RegisterTypeConverter((string s) => Guid.Parse(s)); + + var user = record.AsObject(); + + user.Name.Should().Be("John"); + user.Id.Should().Be(guid); + } }