Skip to content

Commit 451c0cd

Browse files
committed
Support for custom target dictionary entries with names matching existing target members with custom key values
1 parent efe155a commit 451c0cd

File tree

10 files changed

+196
-66
lines changed

10 files changed

+196
-66
lines changed

AgileMapper.UnitTests/Dictionaries/Configuration/WhenConfiguringTargetDictionaryMapping.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,35 @@ public void ShouldApplyAConfiguredConditionalTargetEntryValue()
222222
}
223223
}
224224

225+
[Fact]
226+
public void ShouldAllowACustomTargetEntryKey()
227+
{
228+
using (var mapper = Mapper.CreateNew())
229+
{
230+
mapper.WhenMapping
231+
.From<MysteryCustomerViewModel>()
232+
.ToDictionaries
233+
.MapMember(mcvm => mcvm.Name)
234+
.ToFullKey("CustomerName")
235+
.And
236+
.If((mcvm, d) => mcvm.Discount > 0.5)
237+
.Map((mcvm, d) => mcvm.Name + " (Big discount!)")
238+
.To(d => d["Name"]);
239+
240+
var noDiscountSource = new MysteryCustomerViewModel { Name = "Schumer", Discount = 0.0 };
241+
var noDiscountResult = mapper.Map(noDiscountSource).ToANew<Dictionary<string, object>>();
242+
243+
noDiscountResult["CustomerName"].ShouldBe("Schumer");
244+
noDiscountResult.ContainsKey("Name").ShouldBeFalse();
245+
246+
var bigDiscountSource = new MysteryCustomerViewModel { Name = "Silverman", Discount = 0.6 };
247+
var bigDiscountResult = mapper.Map(bigDiscountSource).ToANew<Dictionary<string, object>>();
248+
249+
bigDiscountResult["CustomerName"].ShouldBe("Silverman");
250+
bigDiscountResult["Name"].ShouldBe("Silverman (Big discount!)");
251+
}
252+
}
253+
225254
[Fact]
226255
public void ShouldApplyACustomConfiguredMember()
227256
{

AgileMapper/Configuration/DictionarySettings.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,20 +36,31 @@ public void AddFullKey(CustomDictionaryKey configuredKey)
3636
}
3737

3838
public Expression GetFullKeyOrNull(IBasicMapperData mapperData)
39+
=> GetFullKeyValueOrNull(mapperData)?.ToConstantExpression();
40+
41+
public string GetFullKeyValueOrNull(IBasicMapperData mapperData)
3942
{
43+
if (mapperData.TargetMember.IsCustom)
44+
{
45+
return null;
46+
}
47+
4048
var matchingKey = FindKeyOrNull(
4149
_configuredFullKeys,
4250
mapperData.TargetMember.LeafMember,
4351
mapperData);
4452

45-
return matchingKey?.Key.ToConstantExpression();
53+
return matchingKey?.Key;
4654
}
4755

4856
public void AddMemberKey(CustomDictionaryKey customKey)
4957
{
5058
_configuredMemberKeys.Add(customKey);
5159
}
5260

61+
public string GetMemberKeyOrNull(IBasicMapperData mapperData)
62+
=> GetMemberKeyOrNull(mapperData.TargetMember.LeafMember, mapperData);
63+
5364
public string GetMemberKeyOrNull(Member member, IBasicMapperData mapperData)
5465
=> FindKeyOrNull(_configuredMemberKeys, member, mapperData)?.Key;
5566

AgileMapper/DataSources/DataSourceFinder.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,11 @@ private static IEnumerable<IDataSource> GetSourceMemberDataSources(
107107
int dataSourceIndex,
108108
IChildMemberMappingData mappingData)
109109
{
110+
if (mappingData.MapperData.TargetMember.IsCustom)
111+
{
112+
yield break;
113+
}
114+
110115
var bestMatchingSourceMember = SourceMemberMatcher.GetMatchFor(mappingData);
111116
var matchingSourceMemberDataSource = GetSourceMemberDataSourceOrNull(bestMatchingSourceMember, mappingData);
112117

AgileMapper/Extensions/EnumerableExtensions.cs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,14 +141,19 @@ public static T[] ToArray<T>(this IList<T> items)
141141
{
142142
var array = new T[items.Count];
143143

144-
for (var i = 0; i < array.Length; i++)
145-
{
146-
array[i] = items[i];
147-
}
144+
CopyItemsTo(array, items);
148145

149146
return array;
150147
}
151148

149+
private static void CopyItemsTo<T>(IList<T> array, IList<T> items, int startIndex = 0)
150+
{
151+
for (var i = 0; i < items.Count; i++)
152+
{
153+
array[i + startIndex] = items[i];
154+
}
155+
}
156+
152157
public static T[] ToArray<T>(this ICollection<T> items)
153158
{
154159
var array = new T[items.Count];
@@ -198,6 +203,17 @@ public static T[] Append<T>(this T[] array, T extraItem)
198203
}
199204
}
200205

206+
// TODO: Replace uses of Linq Concat
207+
public static T[] Append<T>(this IList<T> array, IList<T> extraItems)
208+
{
209+
var combinedArray = new T[array.Count + extraItems.Count];
210+
211+
CopyItemsTo(combinedArray, array);
212+
CopyItemsTo(combinedArray, extraItems, array.Count);
213+
214+
return combinedArray;
215+
}
216+
201217
public static IEnumerable<T> Exclude<T>(this IEnumerable<T> items, IEnumerable<T> excludedItems)
202218
=> (excludedItems != null) ? items.StreamExclude(excludedItems) : items;
203219

AgileMapper/Members/DictionaryTargetMember.cs

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ private DictionaryTargetMember(
3939
_createDictionaryChildMembers = HasObjectEntries || HasSimpleEntries;
4040
}
4141

42+
public override string RegistrationName => GetKeyNameOrNull() ?? base.RegistrationName;
43+
4244
public Type KeyType { get; }
4345

4446
public Type ValueType { get; }
@@ -120,6 +122,18 @@ protected override QualifiedMember CreateRuntimeTypedMember(Type runtimeType)
120122
};
121123
}
122124

125+
public override bool Matches(IQualifiedMember otherMember)
126+
{
127+
var matches = base.Matches(otherMember);
128+
129+
if (_key == null)
130+
{
131+
return matches;
132+
}
133+
134+
return GetKeyNameOrNull() == otherMember.Name;
135+
}
136+
123137
public override Expression GetAccess(Expression instance, IMemberMapperData mapperData)
124138
{
125139
if (this == _rootDictionaryMember)
@@ -132,7 +146,7 @@ public override Expression GetAccess(Expression instance, IMemberMapperData mapp
132146
return Type.ToDefaultExpression();
133147
}
134148

135-
return GetIndexAccess(mapperData);
149+
return GetKeyedAccess(mapperData);
136150
}
137151

138152
private bool ReturnNullAccess()
@@ -150,13 +164,13 @@ private bool ReturnNullAccess()
150164
return true;
151165
}
152166

153-
private Expression GetIndexAccess(IMemberMapperData mapperData)
167+
private Expression GetKeyedAccess(IMemberMapperData mapperData)
154168
{
155-
var index = GetKey(mapperData);
169+
var key = GetKey(mapperData);
156170
var dictionaryAccess = GetDictionaryAccess(mapperData);
157-
var indexAccess = dictionaryAccess.GetIndexAccess(index);
171+
var keyedAccess = dictionaryAccess.GetIndexAccess(key);
158172

159-
return indexAccess;
173+
return keyedAccess;
160174
}
161175

162176
private Expression GetKey(IMemberMapperData mapperData)
@@ -204,18 +218,23 @@ private Expression GetTryGetValueCall(IMemberMapperData mapperData, out Paramete
204218
{
205219
var dictionaryAccess = GetDictionaryAccess(mapperData);
206220
var tryGetValueMethod = dictionaryAccess.Type.GetMethod("TryGetValue");
207-
var index = GetKey(mapperData);
221+
var key = GetKey(mapperData);
208222
valueVariable = Expression.Variable(ValueType, "existingValue");
209223

210224
var tryGetValueCall = Expression.Call(
211225
dictionaryAccess,
212226
tryGetValueMethod,
213-
index,
227+
key,
214228
valueVariable);
215229

216230
return tryGetValueCall;
217231
}
218232

233+
public void SetCustomKey(string key)
234+
{
235+
_key = key.ToConstantExpression();
236+
}
237+
219238
public override Expression GetPopulation(Expression value, IMemberMapperData mapperData)
220239
{
221240
if (mapperData.TargetMember.IsRecursion)
@@ -233,11 +252,11 @@ public override Expression GetPopulation(Expression value, IMemberMapperData map
233252
return flattening;
234253
}
235254

236-
var indexAccess = GetAccess(mapperData.InstanceVariable, mapperData);
255+
var keyedAccess = GetAccess(mapperData.InstanceVariable, mapperData);
237256
var convertedValue = mapperData.GetValueConversion(value, ValueType);
238-
var indexAssignment = indexAccess.AssignTo(convertedValue);
257+
var keyedAssignment = keyedAccess.AssignTo(convertedValue);
239258

240-
return indexAssignment;
259+
return keyedAssignment;
241260
}
242261

243262
private bool ValueIsFlattening(Expression value, out BlockExpression flattening)
@@ -338,9 +357,24 @@ public override string ToString()
338357
return base.ToString();
339358
}
340359

341-
var path = GetPath().Substring("Target.".Length);
360+
var path = GetKeyNameOrNull() ?? GetPath().Substring("Target.".Length);
361+
362+
return $"[\"{path}\"]: {Type.GetFriendlyName()}";
363+
}
364+
365+
private string GetKeyNameOrNull()
366+
{
367+
if (_key == null)
368+
{
369+
return null;
370+
}
371+
372+
if (_key.NodeType == ExpressionType.Constant)
373+
{
374+
return (string)((ConstantExpression)_key).Value;
375+
}
342376

343-
return "[\"" + path + "\"]: " + Type.GetFriendlyName();
377+
return _key.ToString();
344378
}
345379

346380
#region Helper Classes

AgileMapper/Members/QualifiedMember.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ public static QualifiedMember From(Member[] memberChain, MapperContext mapperCon
150150

151151
public string Name => LeafMember.Name;
152152

153-
public string RegistrationName { get; }
153+
public virtual string RegistrationName { get; }
154154

155155
public ICollection<string> JoinedNames { get; }
156156

@@ -205,6 +205,8 @@ public bool IsRecursionRoot()
205205
return true;
206206
}
207207

208+
public bool IsCustom { get; set; }
209+
208210
public virtual bool GuardObjectValuePopulations => false;
209211

210212
IQualifiedMember IQualifiedMember.GetElementMember() => this.GetElementMember();
@@ -273,7 +275,7 @@ protected virtual QualifiedMember CreateRuntimeTypedMember(Type runtimeType)
273275

274276
public bool CouldMatch(QualifiedMember otherMember) => JoinedNames.CouldMatch(otherMember.JoinedNames);
275277

276-
public bool Matches(IQualifiedMember otherMember)
278+
public virtual bool Matches(IQualifiedMember otherMember)
277279
{
278280
if (otherMember == this)
279281
{

AgileMapper/Members/SourceMemberMatcher.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ private static bool MembersHaveCompatibleTypes(Member sourceMember, IChildMember
130130

131131
private static bool IsMatchingMember(IQualifiedMember sourceMember, IMemberMapperData mapperData)
132132
{
133-
return sourceMember.Matches(mapperData.TargetMember) && TypesAreCompatible(sourceMember.Type, mapperData);
133+
return mapperData.TargetMember.Matches(sourceMember) && TypesAreCompatible(sourceMember.Type, mapperData);
134134
}
135135

136136
private static bool TypesAreCompatible(Type sourceType, IMemberMapperData mapperData)

AgileMapper/ObjectPopulation/DictionaryMappingExpressionFactory.cs

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ public DictionaryMappingExpressionFactory()
2222
_memberPopulationFactory = new MemberPopulationFactory(GetAllTargetMembers);
2323
}
2424

25-
private static IEnumerable<QualifiedMember> GetAllTargetMembers(IMemberMapperData mapperData)
25+
private static IEnumerable<QualifiedMember> GetAllTargetMembers(ObjectMapperData mapperData)
2626
{
27-
var allTargetMembers = EnumerateTargetMembers(mapperData).ToArray();
27+
var targetMembersFromSource = EnumerateTargetMembers(mapperData).ToArray();
2828

2929
var configuredDataSourceFactories = mapperData.MapperContext
3030
.UserConfigurations
@@ -35,23 +35,18 @@ private static IEnumerable<QualifiedMember> GetAllTargetMembers(IMemberMapperDat
3535

3636
if (configuredDataSourceFactories.None())
3737
{
38-
return allTargetMembers;
38+
return targetMembersFromSource;
3939
}
4040

41-
var configuredCustomTargetMembers = configuredDataSourceFactories
42-
.Where(dsf => allTargetMembers.None(dsf.Matches))
43-
.GroupBy(dsf => dsf.TargetDictionaryEntryMember.Name)
44-
.Select(group => group.First().TargetDictionaryEntryMember)
45-
.ToArray();
41+
var configuredCustomTargetMembers =
42+
GetConfiguredTargetMembers(configuredDataSourceFactories, targetMembersFromSource);
4643

47-
allTargetMembers = allTargetMembers
48-
.Concat(configuredCustomTargetMembers)
49-
.ToArray();
44+
var allTargetMembers = targetMembersFromSource.Append(configuredCustomTargetMembers);
5045

5146
return allTargetMembers;
5247
}
5348

54-
private static IEnumerable<DictionaryTargetMember> EnumerateTargetMembers(IBasicMapperData mapperData)
49+
private static IEnumerable<DictionaryTargetMember> EnumerateTargetMembers(ObjectMapperData mapperData)
5550
{
5651
var targetDictionaryMember = (DictionaryTargetMember)mapperData.TargetMember;
5752
var sourceMembers = GlobalContext.Instance.MemberFinder.GetSourceMembers(mapperData.SourceType);
@@ -60,6 +55,14 @@ private static IEnumerable<DictionaryTargetMember> EnumerateTargetMembers(IBasic
6055
{
6156
var entryTargetMember = targetDictionaryMember.Append(sourceMember.DeclaringType, sourceMember.Name);
6257

58+
var entryMapperData = new ChildMemberMapperData(entryTargetMember, mapperData);
59+
var configuredKey = GetCustomKeyOrNull(entryMapperData);
60+
61+
if (configuredKey != null)
62+
{
63+
entryTargetMember.SetCustomKey(configuredKey);
64+
}
65+
6366
if (!sourceMember.IsSimple)
6467
{
6568
entryTargetMember = entryTargetMember.WithTypeOf(sourceMember);
@@ -69,6 +72,33 @@ private static IEnumerable<DictionaryTargetMember> EnumerateTargetMembers(IBasic
6972
}
7073
}
7174

75+
private static string GetCustomKeyOrNull(IMemberMapperData entryMapperData)
76+
{
77+
var dictionaries = entryMapperData.MapperContext.UserConfigurations.Dictionaries;
78+
var configuredFullKey = dictionaries.GetFullKeyValueOrNull(entryMapperData);
79+
80+
return configuredFullKey ?? dictionaries.GetMemberKeyOrNull(entryMapperData);
81+
}
82+
83+
private static DictionaryTargetMember[] GetConfiguredTargetMembers(
84+
IEnumerable<ConfiguredDictionaryDataSourceFactory> configuredDataSourceFactories,
85+
IList<DictionaryTargetMember> targetMembersFromSource)
86+
{
87+
return configuredDataSourceFactories
88+
.GroupBy(dsf => dsf.TargetDictionaryEntryMember.Name)
89+
.Select(group =>
90+
{
91+
var factory = group.First();
92+
var targetMember = factory.TargetDictionaryEntryMember;
93+
94+
targetMember.IsCustom = targetMembersFromSource.None(factory.Matches);
95+
96+
return targetMember.IsCustom ? targetMember : null;
97+
})
98+
.WhereNotNull()
99+
.ToArray();
100+
}
101+
72102
public override bool IsFor(IObjectMappingData mappingData)
73103
{
74104
if (mappingData.MapperData.TargetMember.IsDictionary)

0 commit comments

Comments
 (0)