Skip to content

Commit 4efe1f3

Browse files
author
Brian Mgbeokwere
committed
feat: add netstandard2.0 target and flattening cache
Add netstandard2.0 as a second target framework alongside net10.0, making the library consumable from older runtimes. Fixes all API incompatibilities introduced by the broader language and runtime surface of net10.0, and adds a memoisation cache for flattened property accessor resolution. - Add IsExternalInit polyfill to support record struct on netstandard2.0 - Replace ArgumentNullException.ThrowIfNull (.NET 6+) with explicit null guards - Replace index/range syntax (^, ..) with Length-1 and Substring() calls - Replace Dictionary.TryAdd with ContainsKey + Add for netstandard2.0 - Use #if NET5_0_OR_GREATER to select the correct GetMethod overload per target - Fix erroneous static modifier on MapWithDepthTracking - Cache TryBuildFlattenedGetter results keyed on (destPropertyName, sourceType) to eliminate redundant reflection on repeated configuration or validation calls - Add three new flattening cache tests covering hit, key collision, and null miss - Mark all private Profile subclasses in tests as sealed
1 parent c18fd90 commit 4efe1f3

28 files changed

Lines changed: 201 additions & 62 deletions

PanoramicData.Mapper.Test/AfterMapTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public void Process(SimpleSource source, SimpleDestination destination, Resoluti
5151
}
5252
}
5353

54-
private class AfterMapGenericProfile : Profile
54+
private sealed class AfterMapGenericProfile : Profile
5555
{
5656
public AfterMapGenericProfile()
5757
{

PanoramicData.Mapper.Test/BeforeMapTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public void BeforeMap_MappingAction_ExecutesBeforeMapping()
3434
dest.Tag.Should().Be("action-tag");
3535
}
3636

37-
private class BeforeMapLambdaProfile : Profile
37+
private sealed class BeforeMapLambdaProfile : Profile
3838
{
3939
public BeforeMapLambdaProfile()
4040
{
@@ -51,7 +51,7 @@ public void Process(BeforeMapSource _source, BeforeMapDest destination, Resoluti
5151
}
5252
}
5353

54-
private class BeforeMapActionProfile : Profile
54+
private sealed class BeforeMapActionProfile : Profile
5555
{
5656
public BeforeMapActionProfile()
5757
{

PanoramicData.Mapper.Test/CollectionMappingTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ public void Map_NoElementTypeMap_ThrowsAutoMapperMappingException()
122122
act.Should().Throw<AutoMapperMappingException>();
123123
}
124124

125-
private class ElementProfile : Profile
125+
private sealed class ElementProfile : Profile
126126
{
127127
public ElementProfile()
128128
{

PanoramicData.Mapper.Test/ConditionalMappingTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public void PreCondition_WhenTrue_MapsValue()
5656
dest.Age.Should().Be(30);
5757
}
5858

59-
private class ConditionProfile : Profile
59+
private sealed class ConditionProfile : Profile
6060
{
6161
public ConditionProfile()
6262
{
@@ -69,7 +69,7 @@ public ConditionProfile()
6969
}
7070
}
7171

72-
private class PreConditionProfile : Profile
72+
private sealed class PreConditionProfile : Profile
7373
{
7474
public PreConditionProfile()
7575
{

PanoramicData.Mapper.Test/ConstructionTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public void ForCtorParam_MapsConstructorParameters()
3232
dest.Age.Should().Be(30);
3333
}
3434

35-
private class ConstructUsingProfile : Profile
35+
private sealed class ConstructUsingProfile : Profile
3636
{
3737
public ConstructUsingProfile()
3838
{
@@ -41,7 +41,7 @@ public ConstructUsingProfile()
4141
}
4242
}
4343

44-
private class CtorParamProfile : Profile
44+
private sealed class CtorParamProfile : Profile
4545
{
4646
public CtorParamProfile()
4747
{

PanoramicData.Mapper.Test/FlatteningTests.cs

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,27 +114,114 @@ public void AssertConfigurationIsValid_DeepFlatten_DoesNotThrow()
114114
act.Should().NotThrow();
115115
}
116116

117-
private class FlattenProfile : Profile
117+
[Fact]
118+
public void Map_Flattening_CalledTwice_ReturnsSameResult()
119+
{
120+
// Maps the same type pair twice; the second call hits the flattened accessor cache.
121+
var config = new MapperConfiguration(cfg =>
122+
{
123+
cfg.AddProfile(new FlattenProfile());
124+
});
125+
var mapper = config.CreateMapper();
126+
127+
var source = new CustomerSource { Id = 7, Customer = new CustomerNameSource { Name = "Bob", Age = 25 } };
128+
129+
var first = mapper.Map<FlatCustomerDest>(source);
130+
var second = mapper.Map<FlatCustomerDest>(source);
131+
132+
second.CustomerName.Should().Be(first.CustomerName);
133+
second.CustomerAge.Should().Be(first.CustomerAge);
134+
}
135+
136+
[Fact]
137+
public void Map_Flattening_DifferentSourceTypes_SameDestPropertyName_MapsIndependently()
138+
{
139+
// CustomerSourceA and CustomerSourceB both flatten to CustomerName,
140+
// verifying the cache key is (destPropName, sourceType) not just destPropName.
141+
var config = new MapperConfiguration(cfg =>
142+
{
143+
cfg.AddProfile(new FlattenAProfile());
144+
cfg.AddProfile(new FlattenBProfile());
145+
});
146+
var mapper = config.CreateMapper();
147+
148+
var sourceA = new CustomerSourceA { Id = 1, Customer = new CustomerNameSourceA { Name = "Alpha" } };
149+
var sourceB = new CustomerSourceB { Id = 2, Customer = new CustomerNameSourceB { Name = "Beta" } };
150+
151+
var destA = mapper.Map<FlatCustomerDestAB>(sourceA);
152+
var destB = mapper.Map<FlatCustomerDestAB>(sourceB);
153+
154+
destA.CustomerName.Should().Be("Alpha");
155+
destB.CustomerName.Should().Be("Beta");
156+
}
157+
158+
[Fact]
159+
public void Map_Flattening_NoMatchingNestedProperty_LeavesDestinationDefault()
160+
{
161+
// Destination has a property that has no flattened match; result should be default.
162+
// Also verifies a null/miss is cached and doesn't cause a second lookup to misbehave.
163+
var config = new MapperConfiguration(cfg =>
164+
{
165+
cfg.AddProfile(new FlattenIgnoredProfile());
166+
});
167+
var mapper = config.CreateMapper();
168+
169+
var source = new CustomerSource { Id = 3, Customer = new CustomerNameSource { Name = "Carol", Age = 40 } };
170+
171+
var first = mapper.Map<FlatCustomerDest>(source);
172+
var second = mapper.Map<FlatCustomerDest>(source);
173+
174+
first.CustomerName.Should().BeNullOrEmpty();
175+
second.CustomerName.Should().BeNullOrEmpty();
176+
}
177+
178+
private sealed class FlattenProfile : Profile
118179
{
119180
public FlattenProfile()
120181
{
121182
CreateMap<CustomerSource, FlatCustomerDest>();
122183
}
123184
}
124185

125-
private class DeepFlattenProfile : Profile
186+
private sealed class DeepFlattenProfile : Profile
126187
{
127188
public DeepFlattenProfile()
128189
{
129190
CreateMap<DeepSource, DeepFlatDest>();
130191
}
131192
}
132193

133-
private class GetterProfile : Profile
194+
private sealed class GetterProfile : Profile
134195
{
135196
public GetterProfile()
136197
{
137198
CreateMap<GetterSource, GetterDest>();
138199
}
139200
}
201+
202+
private sealed class FlattenAProfile : Profile
203+
{
204+
public FlattenAProfile()
205+
{
206+
CreateMap<CustomerSourceA, FlatCustomerDestAB>();
207+
}
208+
}
209+
210+
private sealed class FlattenBProfile : Profile
211+
{
212+
public FlattenBProfile()
213+
{
214+
CreateMap<CustomerSourceB, FlatCustomerDestAB>();
215+
}
216+
}
217+
218+
private sealed class FlattenIgnoredProfile : Profile
219+
{
220+
public FlattenIgnoredProfile()
221+
{
222+
CreateMap<CustomerSource, FlatCustomerDest>()
223+
.ForMember(d => d.CustomerName, opt => opt.Ignore())
224+
.ForMember(d => d.CustomerAge, opt => opt.Ignore());
225+
}
226+
}
140227
}

PanoramicData.Mapper.Test/ForPathTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public void ForPath_ConfigurationIsValid_DoesNotThrow()
4545
config.AssertConfigurationIsValid();
4646
}
4747

48-
private class ForPathProfile : Profile
48+
private sealed class ForPathProfile : Profile
4949
{
5050
public ForPathProfile()
5151
{

PanoramicData.Mapper.Test/IgnoreInaccessibleSetterTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ private class InaccessibleDest
5858
public int Value { get; set; }
5959
}
6060

61-
private class InaccessibleSetterProfile : Profile
61+
private sealed class InaccessibleSetterProfile : Profile
6262
{
6363
public InaccessibleSetterProfile()
6464
{
@@ -80,7 +80,7 @@ private class InitOnlyDest
8080
public string InitOnly { get; init; } = "init-default";
8181
}
8282

83-
private class InitOnlyProfile : Profile
83+
private sealed class InitOnlyProfile : Profile
8484
{
8585
public InitOnlyProfile()
8686
{

PanoramicData.Mapper.Test/IgnoreTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public void IgnoreAttribute_SkipsProperty()
3535
dest.Secret.Should().Be("original");
3636
}
3737

38-
private class TestProfile : Profile
38+
private sealed class TestProfile : Profile
3939
{
4040
public TestProfile()
4141
{

PanoramicData.Mapper.Test/InheritanceTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public void IncludeAllDerived_AutomaticallyMapsDerivedTypes()
4949
dest.Breed.Should().Be("Poodle");
5050
}
5151

52-
private class IncludeProfile : Profile
52+
private sealed class IncludeProfile : Profile
5353
{
5454
public IncludeProfile()
5555
{
@@ -60,7 +60,7 @@ public IncludeProfile()
6060
}
6161
}
6262

63-
private class IncludeBaseProfile : Profile
63+
private sealed class IncludeBaseProfile : Profile
6464
{
6565
public IncludeBaseProfile()
6666
{
@@ -71,7 +71,7 @@ public IncludeBaseProfile()
7171
}
7272
}
7373

74-
private class IncludeAllDerivedProfile : Profile
74+
private sealed class IncludeAllDerivedProfile : Profile
7575
{
7676
public IncludeAllDerivedProfile()
7777
{

0 commit comments

Comments
 (0)