Skip to content

Commit c826f5f

Browse files
Fix default mapper bug (#821)
* Fix default mapper bug * remove unused using * Tests * Reset default mapper now needed when resetting the default mapper config * wrong exception type in test
1 parent dae40e6 commit c826f5f

File tree

8 files changed

+118
-8
lines changed

8 files changed

+118
-8
lines changed

Neo4j.Driver/Neo4j.Driver.Tests/Mapping/DelegateAsyncEnumerableExtensionsTests.cs renamed to Neo4j.Driver/Neo4j.Driver.Tests/Mapping/AsyncEnumerableExtensionsTests.cs

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,17 @@
1414
// limitations under the License.
1515

1616
using System.Collections.Generic;
17+
using System.Linq;
1718
using System.Threading.Tasks;
1819
using FluentAssertions;
20+
using Neo4j.Driver.Mapping;
1921
using Neo4j.Driver.Preview.Mapping;
2022
using Neo4j.Driver.Tests.TestUtil;
2123
using Xunit;
2224

2325
namespace Neo4j.Driver.Tests.Mapping;
2426

25-
public class DelegateAsyncEnumerableExtensionsTests
27+
public class AsyncEnumerableExtensionsTests
2628
{
2729
[Fact]
2830
public async Task ShouldMapAllRecordsFromCursor_01_Property()
@@ -444,4 +446,52 @@ async IAsyncEnumerable<IRecord> GetRecordsAsync()
444446
pet = "Cat", car = "BMW", food = "Burger", sport = "Football"
445447
});
446448
}
449+
450+
[Fact]
451+
public async Task AsObjectsFromBlueprintAsync_ShouldMapRecordsToObjects()
452+
{
453+
async IAsyncEnumerable<IRecord> GetRecordsAsync()
454+
{
455+
var record1 = TestRecord.Create(
456+
("name", "Alice"),
457+
("age", 25),
458+
("city", "New York"));
459+
460+
var record2 = TestRecord.Create(
461+
("name", "Bob"),
462+
("age", 30),
463+
("city", "Los Angeles"));
464+
465+
await Task.Yield();
466+
yield return record1;
467+
yield return record2;
468+
}
469+
470+
var result = await GetRecordsAsync()
471+
.AsObjectsFromBlueprintAsync(new { name = string.Empty, age = 0, city = string.Empty })
472+
.ToArrayAsync();
473+
474+
result.Should()
475+
.BeEquivalentTo(
476+
[
477+
new { name = "Alice", age = 25, city = "New York" },
478+
new { name = "Bob", age = 30, city = "Los Angeles" }
479+
]);
480+
}
481+
482+
[Fact]
483+
public async Task AsObjectsFromBlueprintAsync_ShouldHandleEmptyEnumerable()
484+
{
485+
async IAsyncEnumerable<IRecord> GetRecordsAsync()
486+
{
487+
await Task.Yield();
488+
yield break;
489+
}
490+
491+
var result = await GetRecordsAsync()
492+
.AsObjectsFromBlueprintAsync(new { name = string.Empty, age = 0, city = string.Empty })
493+
.ToArrayAsync();
494+
495+
result.Should().BeEmpty();
496+
}
447497
}

Neo4j.Driver/Neo4j.Driver.Tests/Mapping/DefaultMapperTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,7 @@ private NoConstructors()
355355
[Fact]
356356
public void ShouldThrowIfClassHasNoConstructors()
357357
{
358-
var act = DefaultMapper.Get<NoConstructors>;
358+
var act = () => DefaultMapper.Get<NoConstructors>(null);
359359
act.Should().Throw<InvalidOperationException>();
360360
}
361361
}

Neo4j.Driver/Neo4j.Driver.Tests/Mapping/MappingProviderTests.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,4 +201,52 @@ public void ShouldUseCustomMapper()
201201
obj2.JobTitle.Should().Be("developer");
202202
obj2.YearsOfService.Should().Be(5);
203203
}
204+
205+
private class NameAndGuid
206+
{
207+
public string Name { get; set; } = null!;
208+
public Guid Guid { get; set; }
209+
}
210+
211+
private class MappingProviderThatUsesDefaultMappingAndOverridesAGuidProperty(bool overrideGuid): IMappingProvider
212+
{
213+
public void CreateMappers(IMappingRegistry registry)
214+
{
215+
registry.RegisterMapping<NameAndGuid>(
216+
b =>
217+
{
218+
b.UseDefaultMapping();
219+
if(overrideGuid)
220+
{
221+
b.Map(x => x.Guid, "Guid", converter: x => Guid.Parse(x.As<string>()));
222+
}
223+
});
224+
}
225+
}
226+
227+
[Fact]
228+
public void ShouldNotFailWhenUsingDefaultMapperButMappingSomePropertiesExplicitly()
229+
{
230+
var guid = Guid.NewGuid();
231+
var testRecord = TestRecord.Create(("Name", "Alice"), ("Guid", guid.ToString()));
232+
RecordObjectMapping.RegisterProvider(
233+
new MappingProviderThatUsesDefaultMappingAndOverridesAGuidProperty(true));
234+
235+
var obj = testRecord.AsObject<NameAndGuid>();
236+
237+
obj.Name.Should().Be("Alice");
238+
obj.Guid.Should().Be(guid);
239+
}
240+
241+
[Fact]
242+
public void ShouldFailWhenUsingDefaultMapperWithoutOverriding()
243+
{
244+
var guid = Guid.NewGuid();
245+
var testRecord = TestRecord.Create(("Name", "Alice"), ("Guid", guid.ToString()));
246+
RecordObjectMapping.RegisterProvider(
247+
new MappingProviderThatUsesDefaultMappingAndOverridesAGuidProperty(false));
248+
249+
var act = () => testRecord.AsObject<NameAndGuid>();
250+
act.Should().Throw<MappingFailedException>();
251+
}
204252
}

Neo4j.Driver/Neo4j.Driver.Tests/Neo4j.Driver.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
<PackageReference Include="Microsoft.Reactive.Testing" Version="5.0.0" />
2323
<PackageReference Include="Moq" Version="4.20.69" />
2424
<PackageReference Include="Moq.AutoMock" Version="3.5.0" />
25+
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
2526
<PackageReference Include="System.Runtime.InteropServices.RuntimeInformation" Version="4.3.0" />
2627
<PackageReference Include="TeamCity.VSTest.TestAdapter" Version="1.0.23" />
2728
<PackageReference Include="xunit" Version="$(XunitVersion)" />

Neo4j.Driver/Neo4j.Driver/Public/Mapping/BuiltMapper.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ internal class BuiltMapper<T> : IRecordMapper<T>
2828

2929
private Func<IRecord, T> _wholeObjectMapping;
3030
private readonly List<Action<T, IRecord>> _propertyMappings = new();
31+
public HashSet<MethodInfo> MappedSetters { get; } = [];
3132

3233
public T Map(IRecord record)
3334
{
@@ -139,6 +140,7 @@ public void AddMapping(
139140
{
140141
// this part only happens once, at the time of building the mapper
141142
_propertyMappings.Add(MapFromRecord);
143+
MappedSetters.Add(propertySetter);
142144
return;
143145

144146
// this part happens every time a record is mapped
@@ -152,7 +154,7 @@ void MapFromRecord(T obj, IRecord record)
152154

153155
if (mappableValueFound)
154156
{
155-
propertySetter.Invoke(obj, new[] { mappableValue });
157+
propertySetter.Invoke(obj, [mappableValue]);
156158
}
157159
else if(!optional)
158160
{

Neo4j.Driver/Neo4j.Driver/Public/Mapping/DefaultMapper.cs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,14 @@ internal static class DefaultMapper
2323
{
2424
private static readonly Dictionary<Type, object> Mappers = new();
2525

26-
public static IRecordMapper<T> Get<T>()
26+
public static void Reset()
2727
{
28+
Mappers.Clear();
29+
}
30+
31+
public static IRecordMapper<T> Get<T>(HashSet<MethodInfo> mappedSetters = null)
32+
{
33+
mappedSetters ??= [];
2834
var type = typeof(T);
2935

3036
// if we already have a mapper for this type, return it
@@ -45,9 +51,11 @@ public static IRecordMapper<T> Get<T>()
4551
var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
4652
foreach (var property in properties)
4753
{
48-
// ignore properties without a setter or with MappingIgnoredAttribute, or compiler generated
54+
// ignore properties without a setter or with MappingIgnoredAttribute, or compiler generated,
55+
// or if the setter has already been mapped elsewhere (e.g. custom mapping config)
4956
if (property.SetMethod is null ||
50-
property.GetCustomAttribute<MappingIgnoredAttribute>() is not null)
57+
property.GetCustomAttribute<MappingIgnoredAttribute>() is not null ||
58+
mappedSetters.Contains(property.SetMethod))
5159
{
5260
continue;
5361
}

Neo4j.Driver/Neo4j.Driver/Public/Mapping/MappingBuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ internal IMappingBuilder<T> UseConstructor(ConstructorInfo constructor)
7474
/// <inheritdoc />
7575
public IMappingBuilder<T> UseDefaultMapping()
7676
{
77-
_builtMapper.AddWholeObjectMapping(r => DefaultMapper.Get<T>().Map(r));
77+
_builtMapper.AddWholeObjectMapping(r => DefaultMapper.Get<T>(_builtMapper.MappedSetters).Map(r));
7878
return this;
7979
}
8080

Neo4j.Driver/Neo4j.Driver/Public/Mapping/RecordObjectMapping.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ internal static void Reset()
6060
{
6161
// discard the current instance and create a new one, which will have no mappers registered
6262
Instance = new RecordObjectMapping();
63+
DefaultMapper.Reset();
6364
}
6465

6566
/// <summary>
@@ -84,7 +85,7 @@ private static object GetMapperForType(Type type)
8485
// no mapper registered for this type, so use the default mapper
8586
var openGenericMethod = typeof(DefaultMapper).GetMethod(nameof(DefaultMapper.Get));
8687
var closedGenericMethod = openGenericMethod!.MakeGenericMethod(type);
87-
return closedGenericMethod.Invoke(null, null);
88+
return closedGenericMethod.Invoke(null, [null]);
8889
}
8990

9091
/// <summary>

0 commit comments

Comments
 (0)