Skip to content

Commit 242c1f6

Browse files
Merge pull request #1 from cbcrc/map-nested-properties
Added MapLinkedSource(x => x.PropertyName) to MappingExpressionExtensions
2 parents 10e63c7 + 0087afb commit 242c1f6

File tree

3 files changed

+210
-89
lines changed

3 files changed

+210
-89
lines changed
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
#region copyright
2+
// Copyright (c) CBC/Radio-Canada. All rights reserved.
3+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
4+
#endregion
5+
6+
using AutoMapper;
7+
using Xunit;
8+
9+
namespace LinkIt.AutoMapperExtensions.Tests
10+
{
11+
public class AutoMapReference_NestedPropertyMappingTests
12+
{
13+
[Fact]
14+
public void MapModel_WithNestedPropertiesMappingForModelProperty_ShouldMap()
15+
{
16+
var config = new MapperConfiguration(cfg =>
17+
{
18+
cfg
19+
.CreateMap<MyNestedLinkedSource, MyNestedDto>()
20+
.MapLinkedSource(x => x.Model.Property);
21+
});
22+
config.AssertConfigurationIsValid();
23+
var mapper = config.CreateMapper();
24+
25+
var source = new MyNestedLinkedSource
26+
{
27+
Model = CreateMyNestedModel(1),
28+
Point = new MyPoint { X = 1, Y = 2 }
29+
};
30+
31+
var actual = mapper.Map<MyNestedDto>(source);
32+
33+
Assert.Equal(100, actual.Number);
34+
Assert.Equal($"Title {actual.Number}", actual.Title);
35+
Assert.Equal($"Text {actual.Number}", actual.Text);
36+
Assert.Equal(1, actual.Point.X);
37+
Assert.Equal(2, actual.Point.Y);
38+
}
39+
40+
private static MyNestedModel CreateMyNestedModel(int id)
41+
{
42+
var propId = id * 100;
43+
return new MyNestedModel
44+
{
45+
Id = id,
46+
Title = $"Title {id}",
47+
Property = new MyNestedProperty
48+
{
49+
Number = propId,
50+
Title = $"Title {propId}",
51+
Text = $"Text {propId}"
52+
}
53+
};
54+
}
55+
56+
public class MyNestedLinkedSource
57+
{
58+
public MyNestedModel Model { get; set; }
59+
public MyPoint Point { get; set; }
60+
}
61+
62+
public class MyPoint {
63+
public float X { get; set; }
64+
public float Y { get; set; }
65+
}
66+
67+
public class MyNestedModel
68+
{
69+
public int Id { get; set; }
70+
public string Title { get; set; }
71+
public MyNestedProperty Property { get; set; }
72+
}
73+
74+
public class MyNestedProperty
75+
{
76+
public int Number { get; set; }
77+
public string Title { get; set; }
78+
public string Text { get; set; }
79+
}
80+
81+
public class MyNestedDto
82+
{
83+
public string Title { get; set; }
84+
public int Number { get; set; }
85+
public string Text { get; set; }
86+
public MyPointDto Point { get; set; }
87+
}
88+
89+
public class MyPointDto {
90+
public float X { get; set; }
91+
public float Y { get; set; }
92+
}
93+
}
94+
}

LinkIt.AutoMapperExtensions/LinkSourceMapper.cs

Lines changed: 106 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -13,55 +13,21 @@
1313
using AutoMapper;
1414

1515
namespace LinkIt.AutoMapperExtensions
16-
{public class LinkSourceMapper<TLinkedSource, TDestination>
16+
{
17+
public class LinkSourceMapper<TLinkedSource, TDestination>
1718
{
1819
private const string ModelPropertyName = "Model";
1920
private const string ContextualizationPropertyName = "Contextualization";
20-
private List<PropertyInfo> _contextualizationProperties;
21-
private List<PropertyInfo> _destinationProperties;
2221

23-
private List<PropertyInfo> _modelProperties;
24-
private readonly PropertyNameComparer _propertyNameComparer;
25-
private List<PropertyInfo> _referenceProperties;
22+
private static readonly Type LinkedSourceType = typeof(TLinkedSource);
2623

27-
public IMappingExpression<TLinkedSource, TDestination> MapLinkedSource(IMappingExpression<TLinkedSource, TDestination> expression)
28-
{
29-
MapModelProperties(expression);
30-
MapContextualizedModelProperties(expression);
31-
MapPropertiesAddedInContextualization(expression);
32-
33-
return expression;
34-
}
35-
36-
private void MapModelProperties(IMappingExpression<TLinkedSource, TDestination> expression)
37-
{
38-
var modelPropertiesToMap = _modelProperties
39-
.Intersect(_destinationProperties, _propertyNameComparer)
40-
.Except(_referenceProperties, _propertyNameComparer)
41-
.Except(_contextualizationProperties, _propertyNameComparer);
42-
43-
MapNestedProperties(ModelPropertyName, modelPropertiesToMap, expression);
44-
}
45-
46-
private void MapContextualizedModelProperties(IMappingExpression<TLinkedSource, TDestination> expression)
47-
{
48-
var contextualizedModelPropertiesToMap = _modelProperties
49-
.Intersect(_destinationProperties, _propertyNameComparer)
50-
.Intersect(_contextualizationProperties, _propertyNameComparer)
51-
.Except(_referenceProperties, _propertyNameComparer);
52-
53-
MapContextualizedProperties(contextualizedModelPropertiesToMap, expression);
54-
}
55-
56-
private void MapPropertiesAddedInContextualization(IMappingExpression<TLinkedSource, TDestination> expression)
57-
{
58-
var propertiesAddedInContextualization = _contextualizationProperties
59-
.Intersect(_destinationProperties, _propertyNameComparer)
60-
.Except(_referenceProperties, _propertyNameComparer)
61-
.Except(_modelProperties, _propertyNameComparer);
24+
private readonly PropertyNameComparer _propertyNameComparer = new PropertyNameComparer();
6225

63-
MapNestedProperties(ContextualizationPropertyName, propertiesAddedInContextualization, expression);
64-
}
26+
private readonly string _sourcePropertyPath = ModelPropertyName;
27+
private readonly List<PropertyInfo> _sourceProperties;
28+
private readonly List<PropertyInfo> _destinationProperties;
29+
private readonly List<PropertyInfo> _referenceProperties;
30+
private readonly List<PropertyInfo> _contextualizationProperties;
6531

6632
#region Construction
6733

@@ -70,85 +36,136 @@ public LinkSourceMapper()
7036
EnsureHasModelProperty();
7137
EnsureHasModelPropertyWhichIsAClass();
7238

73-
_propertyNameComparer = new PropertyNameComparer();
74-
InitModelProperties();
75-
InitDestinationProperties();
76-
InitReferenceProperties();
77-
InitContextualizationProperties();
39+
_sourceProperties = GetModelProperties();
40+
_destinationProperties = GetDestinationProperties();
41+
_referenceProperties = GetReferenceProperties();
42+
_contextualizationProperties = GetContextualizationProperties();
7843
}
7944

80-
private void InitModelProperties()
45+
public LinkSourceMapper(Expression<Func<TLinkedSource, object>> sourceProperty)
8146
{
82-
var linkedSourceType = typeof(TLinkedSource);
83-
var modelType = linkedSourceType.GetProperty(ModelPropertyName).PropertyType;
84-
_modelProperties = modelType.GetProperties().ToList();
47+
if (!(sourceProperty.Body is MemberExpression me))
48+
{
49+
throw new ArgumentException("Expression must be of type System.Linq.Expressions.MemberExpression", "propertyExpression");
50+
}
51+
52+
_sourcePropertyPath = GetPropertiesPrefix(me);
53+
_sourceProperties = me.Type.GetProperties().ToList();
54+
_destinationProperties = GetDestinationProperties();
55+
_referenceProperties = GetReferenceProperties();
56+
_contextualizationProperties = GetContextualizationProperties();
8557
}
8658

87-
private void InitDestinationProperties()
59+
private static List<PropertyInfo> GetModelProperties()
8860
{
89-
var destinationType = typeof(TDestination);
90-
_destinationProperties = destinationType.GetProperties().ToList();
61+
var modelType = LinkedSourceType.GetProperty(ModelPropertyName).PropertyType;
62+
return modelType.GetProperties().ToList();
9163
}
9264

93-
private void InitReferenceProperties()
65+
private static List<PropertyInfo> GetDestinationProperties()
9466
{
95-
var linkedSourceType = typeof(TLinkedSource);
67+
return typeof(TDestination).GetProperties().ToList();
68+
}
9669

97-
_referenceProperties = linkedSourceType.GetProperties()
70+
private static List<PropertyInfo> GetReferenceProperties()
71+
{
72+
return LinkedSourceType.GetProperties()
9873
.Where(property => property.Name != ModelPropertyName)
9974
.Where(property => property.Name != ContextualizationPropertyName)
10075
.ToList();
10176
}
10277

103-
private void InitContextualizationProperties()
78+
private static List<PropertyInfo> GetContextualizationProperties()
10479
{
105-
var linkedSourceType = typeof(TLinkedSource);
106-
var modelContextualization = linkedSourceType.GetProperty(ContextualizationPropertyName);
80+
var modelContextualization = LinkedSourceType.GetProperty(ContextualizationPropertyName);
10781
if (modelContextualization == null)
10882
{
109-
_contextualizationProperties = new List<PropertyInfo>();
110-
}
111-
else
112-
{
113-
var modelContextualizationType = modelContextualization.PropertyType;
114-
_contextualizationProperties = modelContextualizationType.GetProperties()
115-
// By convention, we don't override the Id using the contextualization
116-
.Where(property => !property.Name.Equals("Id", StringComparison.OrdinalIgnoreCase))
117-
.ToList();
83+
return new List<PropertyInfo>();
11884
}
85+
86+
var modelContextualizationType = modelContextualization.PropertyType;
87+
return modelContextualizationType.GetProperties()
88+
// By convention, we don't override the Id using the contextualization
89+
.Where(property => !property.Name.Equals("Id", StringComparison.OrdinalIgnoreCase))
90+
.ToList();
11991
}
12092

12193
private static void EnsureHasModelProperty()
12294
{
123-
var linkedSourceType = typeof(TLinkedSource);
124-
var linkedSourceTypeFullName = linkedSourceType.FullName;
125-
if (linkedSourceType.GetProperty(ModelPropertyName) == null)
95+
if (LinkedSourceType.GetProperty(ModelPropertyName) == null)
12696
throw new ArgumentException(
127-
string.Format(
128-
"{0} must have a property named Model, otherwise it cannot be used as a linked source.",
129-
linkedSourceTypeFullName
130-
),
131-
"TLinkedSource"
97+
$"{LinkedSourceType.FullName} must have a property named Model, otherwise it cannot be used as a linked source.",
98+
nameof(TLinkedSource)
13299
);
133100
}
134101

135102
private static void EnsureHasModelPropertyWhichIsAClass()
136103
{
137-
var linkedSourceType = typeof(TLinkedSource);
138-
var linkedSourceTypeFullName = linkedSourceType.FullName;
139-
var modelType = linkedSourceType.GetProperty(ModelPropertyName).PropertyType;
104+
var linkedSourceTypeFullName = LinkedSourceType.FullName;
105+
var modelType = LinkedSourceType.GetProperty(ModelPropertyName).PropertyType;
140106
if (modelType.IsClass == false)
141107
throw new ArgumentException(
142-
string.Format(
143-
"{0} must have a property named Model which is a class, otherwise it cannot be used as a linked source.",
144-
linkedSourceTypeFullName
145-
),
146-
"TLinkedSource"
108+
$"{linkedSourceTypeFullName} must have a property named Model which is a class, otherwise it cannot be used as a linked source.",
109+
nameof(TLinkedSource)
147110
);
148111
}
149112

150113
#endregion
151114

115+
116+
public IMappingExpression<TLinkedSource, TDestination> MapLinkedSource(IMappingExpression<TLinkedSource, TDestination> expression)
117+
{
118+
MapModelProperties(expression);
119+
MapContextualizedModelProperties(expression);
120+
MapPropertiesAddedInContextualization(expression);
121+
122+
return expression;
123+
}
124+
125+
private static string GetPropertiesPrefix(MemberExpression me)
126+
{
127+
var propertiesPrefix = "";
128+
129+
while (me != null)
130+
{
131+
propertiesPrefix = string.IsNullOrEmpty(propertiesPrefix) ?
132+
me.Member.Name :
133+
$"{me.Member.Name}.{propertiesPrefix}";
134+
me = me.Expression as MemberExpression;
135+
}
136+
137+
return propertiesPrefix;
138+
}
139+
private void MapModelProperties(IMappingExpression<TLinkedSource, TDestination> expression)
140+
{
141+
var modelPropertiesToMap = _sourceProperties
142+
.Intersect(_destinationProperties, _propertyNameComparer)
143+
.Except(_referenceProperties, _propertyNameComparer)
144+
.Except(_contextualizationProperties, _propertyNameComparer);
145+
146+
MapNestedProperties(_sourcePropertyPath, modelPropertiesToMap, expression);
147+
}
148+
149+
private void MapContextualizedModelProperties(IMappingExpression<TLinkedSource, TDestination> expression)
150+
{
151+
var contextualizedModelPropertiesToMap = _sourceProperties
152+
.Intersect(_destinationProperties, _propertyNameComparer)
153+
.Intersect(_contextualizationProperties, _propertyNameComparer)
154+
.Except(_referenceProperties, _propertyNameComparer);
155+
156+
MapContextualizedProperties(contextualizedModelPropertiesToMap, expression);
157+
}
158+
159+
private void MapPropertiesAddedInContextualization(IMappingExpression<TLinkedSource, TDestination> expression)
160+
{
161+
var propertiesAddedInContextualization = _contextualizationProperties
162+
.Intersect(_destinationProperties, _propertyNameComparer)
163+
.Except(_referenceProperties, _propertyNameComparer)
164+
.Except(_sourceProperties, _propertyNameComparer);
165+
166+
MapNestedProperties(ContextualizationPropertyName, propertiesAddedInContextualization, expression);
167+
}
168+
152169
#region MapNestedProperties
153170

154171
private static void MapNestedProperties(
@@ -158,7 +175,7 @@ private static void MapNestedProperties(
158175
{
159176
foreach (var property in nestedProperties)
160177
{
161-
var sourcePropertyInDotNotation = string.Format("{0}.{1}", sourcePropertiesPrefix, property.Name);
178+
var sourcePropertyInDotNotation = $"{sourcePropertiesPrefix}.{property.Name}";
162179
var method = ThisType.GetMethod("MapProperty");
163180
var genericMethod = method.MakeGenericMethod(property.PropertyType);
164181

@@ -183,7 +200,7 @@ public static void MapProperty<TSourceProperty>(
183200

184201
private static Expression<Func<TLinkedSource, TProperty>> CreateMemberExpression<TProperty>(string propertyInDotNotation)
185202
{
186-
var root = Expression.Parameter(typeof(TLinkedSource), "root");
203+
var root = Expression.Parameter(LinkedSourceType, "root");
187204
var lambdaBody = GenerateGetProperty(root, propertyInDotNotation);
188205
return Expression.Lambda<Func<TLinkedSource, TProperty>>(lambdaBody, root);
189206
}
@@ -225,7 +242,7 @@ public static void MapContextualizedProperty<TSourceProperty>(
225242

226243
private static Func<TLinkedSource, TProperty> CreateContextualizationFunc<TProperty>(string overridingPropertyInDotNotation, string defaultPropertyInDotNotation)
227244
{
228-
var root = Expression.Parameter(typeof(TLinkedSource), "root");
245+
var root = Expression.Parameter(LinkedSourceType, "root");
229246

230247
var contextualizationProperty = GenerateGetProperty(root, ContextualizationPropertyName);
231248

LinkIt.AutoMapperExtensions/MappingExpressionExtensions.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
#endregion
55

66
using AutoMapper;
7+
using System;
8+
using System.Linq.Expressions;
79

810
namespace LinkIt.AutoMapperExtensions
911
{
@@ -13,6 +15,14 @@ public static IMappingExpression<TLinkedSource, TDestination> MapLinkedSource<TL
1315
{
1416
var mapper = new LinkSourceMapper<TLinkedSource, TDestination>();
1517
return mapper.MapLinkedSource(expression);
18+
}
19+
20+
public static IMappingExpression<TLinkedSource, TDestination> MapLinkedSource<TLinkedSource, TDestination>(
21+
this IMappingExpression<TLinkedSource, TDestination> expression,
22+
Expression<Func<TLinkedSource, object>> sourceProperty)
23+
{
24+
var mapper = new LinkSourceMapper<TLinkedSource, TDestination>(sourceProperty);
25+
return mapper.MapLinkedSource(expression);
1626
}
1727
}
1828
}

0 commit comments

Comments
 (0)