Skip to content

Commit d3bfe11

Browse files
authored
Fixing population of readonly dictionary members (#99)
* Skipping target members lookup for enumerable members * Updating * Short-circuiting GetTargetMembers checks * Updating test to cover mapping to a populated, readonly Dictionary member * Stopping Dictionary from being created as fallback complex type data source / Extra dictionary mapping test coverage
1 parent 6af5cd9 commit d3bfe11

File tree

8 files changed

+114
-24
lines changed

8 files changed

+114
-24
lines changed

AgileMapper.UnitTests/Dictionaries/WhenMappingToNewDictionaryMembers.cs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,43 @@ public void ShouldMapADictionaryObjectValuesToNewDictionaryObjectValues()
184184
result.Value["key2"].ShouldNotBeSameAs(source.Value["key2"]);
185185
}
186186

187+
// See https://github.com/agileobjects/AgileMapper/issues/97
188+
[Fact]
189+
public void ShouldDeepCloneAReadOnlyDictionaryMember()
190+
{
191+
var source = new Issue97.ReadonlyDictionary();
192+
193+
source.Dictionary["Test"] = "123";
194+
195+
var cloned = Mapper.DeepClone(source);
196+
197+
cloned.Dictionary.ContainsKey("Test").ShouldBeTrue();
198+
cloned.Dictionary["Test"].ShouldBe("123");
199+
}
200+
201+
[Fact]
202+
public void ShouldUseACloneConstructorToPopulateADictionaryConstructorParameter()
203+
{
204+
var source = new PublicReadOnlyProperty<IDictionary<string, string>>(
205+
new Dictionary<string, string> { ["Test"] = "Hello!" });
206+
207+
var result = Mapper.Map(source).ToANew<PublicCtor<IDictionary<string, string>>>();
208+
209+
result.Value.ContainsKey("Test").ShouldBeTrue();
210+
result.Value["Test"].ShouldBe("Hello!");
211+
}
212+
213+
[Fact]
214+
public void ShouldNotCreateDictionaryAsFallbackComplexType()
215+
{
216+
var source = new PublicReadOnlyProperty<IDictionary<string, string>>(
217+
new Dictionary<string, string>());
218+
219+
var cloned = Mapper.DeepClone(source);
220+
221+
cloned.ShouldBeNull();
222+
}
223+
187224
[Fact]
188225
public void ShouldFlattenAComplexTypeCollectionToANestedObjectDictionaryImplementation()
189226
{
@@ -234,5 +271,22 @@ public void ShouldFlattenAComplexTypeCollectionToANestedObjectDictionaryImplemen
234271
result.Value.ShouldNotContainKey("[1].Address.Line2");
235272

236273
}
274+
275+
#region Helper Members
276+
277+
private static class Issue97
278+
{
279+
public class ReadonlyDictionary
280+
{
281+
public ReadonlyDictionary()
282+
{
283+
Dictionary = new Dictionary<string, string>();
284+
}
285+
286+
public IDictionary<string, string> Dictionary { get; }
287+
}
288+
}
289+
290+
#endregion
237291
}
238292
}

AgileMapper/DataSources/Finders/SourceMemberDataSourceFinder.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public IEnumerable<IDataSource> FindFor(DataSourceFindContext context)
2222
{
2323
if (context.DataSourceIndex == 0)
2424
{
25-
if (targetMember.IsComplex && (targetMember.Type != typeof(object)))
25+
if (UseFallbackComplexTypeMappingDataSource(targetMember))
2626
{
2727
yield return new ComplexTypeMappingDataSource(context.DataSourceIndex, context.ChildMappingData);
2828
}
@@ -61,5 +61,8 @@ private static IDataSource GetSourceMemberDataSourceOrNull(DataSourceFindContext
6161

6262
return context.GetFinalDataSource(sourceMemberDataSource, contextMappingData);
6363
}
64+
65+
private static bool UseFallbackComplexTypeMappingDataSource(QualifiedMember targetMember)
66+
=> targetMember.IsComplex && !targetMember.IsDictionary && (targetMember.Type != typeof(object));
6467
}
6568
}

AgileMapper/Extensions/Internal/EnumerableExtensions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ public static void AddRange<TContained, TItem>(this List<TContained> items, IEnu
4343
[DebuggerStepThrough]
4444
public static T First<T>(this IList<T> items) => items[0];
4545

46+
[DebuggerStepThrough]
4647
public static T First<T>(this IList<T> items, Func<T, bool> predicate)
4748
{
4849
if (TryFindMatch(items, predicate, out var match))
@@ -53,9 +54,11 @@ public static T First<T>(this IList<T> items, Func<T, bool> predicate)
5354
throw new InvalidOperationException("Sequence contains no matching element");
5455
}
5556

57+
[DebuggerStepThrough]
5658
public static T FirstOrDefault<T>(this IList<T> items, Func<T, bool> predicate)
5759
=> TryFindMatch(items, predicate, out var match) ? match : default(T);
5860

61+
[DebuggerStepThrough]
5962
public static bool TryFindMatch<T>(this IList<T> items, Func<T, bool> predicate, out T match)
6063
{
6164
for (int i = 0, n = items.Count; i < n; i++)

AgileMapper/Members/MemberCache.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ public IList<Member> GetTargetMembers(Type targetType)
4040
{
4141
return _membersCache.GetOrAdd(TypeKey.ForTargetMembers(targetType), key =>
4242
{
43+
if (key.Type.IsEnumerable())
44+
{
45+
return Enumerable<Member>.EmptyArray;
46+
}
47+
4348
var fields = GetFields(key.Type, All);
4449
var properties = GetProperties(key.Type, All);
4550
var methods = GetMethods(key.Type, OnlyCallableSetters, Member.SetMethod);

AgileMapper/Members/SourceMemberMatcher.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using System.Collections.Generic;
55
using System.Linq;
66
using Extensions;
7-
using Extensions.Internal;
87

98
internal static class SourceMemberMatcher
109
{

AgileMapper/ObjectPopulation/DictionaryMappingExpressionFactory.cs

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -277,40 +277,59 @@ protected override bool TargetCannotBeMapped(IObjectMappingData mappingData, out
277277

278278
protected override IEnumerable<Expression> GetObjectPopulation(MappingCreationContext context)
279279
{
280-
var mapperData = context.MapperData;
281-
282-
if (!mapperData.TargetMember.IsDictionary)
280+
if (!context.MapperData.TargetMember.IsDictionary)
283281
{
284282
yield return GetDictionaryPopulation(context.MappingData);
285283
yield break;
286284
}
287285

288-
Func<DictionarySourceMember, IObjectMappingData, Expression> assignmentFactory;
286+
var assignmentFactory = GetDictionaryAssignmentFactoryOrNull(context, out var useAssignmentOnly);
287+
288+
if (useAssignmentOnly)
289+
{
290+
yield return assignmentFactory.Invoke(context.MappingData);
291+
yield break;
292+
}
293+
294+
var population = GetDictionaryPopulation(context.MappingData);
295+
var assignment = assignmentFactory?.Invoke(context.MappingData);
296+
297+
yield return assignment;
298+
yield return population;
299+
}
300+
301+
private static Func<IObjectMappingData, Expression> GetDictionaryAssignmentFactoryOrNull(
302+
MappingCreationContext context,
303+
out bool useAssignmentOnly)
304+
{
305+
if (context.MapperData.TargetMember.IsReadOnly)
306+
{
307+
useAssignmentOnly = false;
308+
return null;
309+
}
310+
311+
var mapperData = context.MapperData;
289312

290313
if (SourceMemberIsDictionary(mapperData, out var sourceDictionaryMember))
291314
{
292315
if (UseDictionaryCloneConstructor(sourceDictionaryMember, mapperData, out var cloneConstructor))
293316
{
294-
yield return GetClonedDictionaryAssignment(mapperData, cloneConstructor);
295-
yield break;
317+
useAssignmentOnly = true;
318+
return md => GetClonedDictionaryAssignment(md.MapperData, cloneConstructor);
296319
}
297320

298-
assignmentFactory = GetMappedDictionaryAssignment;
299-
}
300-
else if (context.InstantiateLocalVariable)
301-
{
302-
assignmentFactory = (dsm, md) => GetParameterlessDictionaryAssignment(md);
321+
useAssignmentOnly = false;
322+
return md => GetMappedDictionaryAssignment(sourceDictionaryMember, md);
303323
}
304-
else
324+
325+
useAssignmentOnly = false;
326+
327+
if (context.InstantiateLocalVariable)
305328
{
306-
assignmentFactory = null;
329+
return GetParameterlessDictionaryAssignment;
307330
}
308331

309-
var population = GetDictionaryPopulation(context.MappingData);
310-
var assignment = assignmentFactory?.Invoke(sourceDictionaryMember, context.MappingData);
311-
312-
yield return assignment;
313-
yield return population;
332+
return null;
314333
}
315334

316335
private static bool SourceMemberIsDictionary(
@@ -328,9 +347,9 @@ private static bool UseDictionaryCloneConstructor(
328347
{
329348
cloneConstructor = null;
330349

331-
return mapperData.TargetMember.ElementType.IsSimple() &&
332-
(sourceDictionaryMember.Type == mapperData.TargetType) &&
333-
((cloneConstructor = GetDictionaryCloneConstructor(mapperData)) != null);
350+
return (sourceDictionaryMember.Type == mapperData.TargetType) &&
351+
mapperData.TargetMember.ElementType.IsSimple() &&
352+
((cloneConstructor = GetDictionaryCloneConstructor(mapperData)) != null);
334353
}
335354

336355
private static ConstructorInfo GetDictionaryCloneConstructor(ITypePair mapperData)

AgileMapper/ObjectPopulation/MappingDataCreationFactory.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ private static bool UseAsConversion(ObjectMapperData childMapperData, out Expres
3535
return false;
3636
}
3737

38-
//[DebuggerStepThrough]
38+
[DebuggerStepThrough]
3939
public static Expression ForChild(
4040
MappingValues mappingValues,
4141
int dataSourceIndex,

AgileMapper/ObjectPopulation/ObjectMapperData.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,13 @@ private static bool TypeHasACompatibleChildMember(
239239

240240
checkedTypes.Add(parentType);
241241

242+
if (parentType.IsEnumerable())
243+
{
244+
parentType = parentType.GetEnumerableElementType();
245+
246+
return !parentType.IsSimple() && TypeHasACompatibleChildMember(targetType, parentType, checkedTypes);
247+
}
248+
242249
var childTargetMembers = GlobalContext.Instance.MemberCache.GetTargetMembers(parentType);
243250

244251
foreach (var childMember in childTargetMembers)

0 commit comments

Comments
 (0)