Skip to content

Commit 610f6b5

Browse files
authored
Factory method support (#98)
* Simplifying MappingFactory.UseLocalSourceValueVariable * Auto-discovering factory methods * Extending factory method support test coverage * Deduplicating factory method - constructor code / Fixing unconstructable type tests * Erroring if factory configured which would be used anyway * Minimising work done to check if a configured factory method is redundant
1 parent a4d22fd commit 610f6b5

15 files changed

+401
-118
lines changed

AgileMapper.UnitTests/AgileMapper.UnitTests.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,13 +215,13 @@
215215
<Compile Include="TestClasses\PaymentTypeUk.cs" />
216216
<Compile Include="TestClasses\PaymentTypeUs.cs" />
217217
<Compile Include="TestClasses\PublicCtorStruct.cs" />
218-
<Compile Include="TestClasses\PublicFactoryMethod.cs" />
219218
<Compile Include="TestClasses\PublicImplementation.cs" />
220219
<Compile Include="TestClasses\PublicSealed.cs" />
221220
<Compile Include="TestClasses\PublicPropertyStruct.cs" />
222221
<Compile Include="TestClasses\PublicTwoFields.cs" />
223222
<Compile Include="TestClasses\PublicTwoFieldsStruct.cs" />
224223
<Compile Include="TestClasses\PublicTwoParamCtor.cs" />
224+
<Compile Include="TestClasses\PublicUnconstructable.cs" />
225225
<Compile Include="TestClasses\SaveOrderItemRequest.cs" />
226226
<Compile Include="TestClasses\SaveOrderRequest.cs" />
227227
<Compile Include="TestClasses\Status.cs" />
@@ -234,6 +234,7 @@
234234
<Compile Include="WhenMappingEntities.cs" />
235235
<Compile Include="WhenMappingToMetaMembers.cs" />
236236
<Compile Include="WhenUnflatteningFromQueryStrings.cs" />
237+
<Compile Include="WhenUsingFactoryMethods.cs" />
237238
<Compile Include="WhenValidatingMappings.cs" />
238239
<Compile Include="WhenAnalysingCollections.cs" />
239240
<Compile Include="MapperCloning\WhenCloningDataSources.cs" />

AgileMapper.UnitTests/Configuration/WhenConfiguringObjectCreation.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -470,11 +470,11 @@ public void ShouldErrorIfConflictingFactoryConfigured()
470470
{
471471
mapper.WhenMapping
472472
.InstancesOf<Address>()
473-
.CreateUsing(ctx => new Address());
473+
.CreateUsing(ctx => new Address { Line1 = "Hello!" });
474474

475475
mapper.WhenMapping
476476
.InstancesOf<Address>()
477-
.CreateUsing(ctx => new Address());
477+
.CreateUsing(ctx => new Address { Line1 = "Hello!" });
478478
}
479479
});
480480

AgileMapper.UnitTests/TestClasses/PublicFactoryMethod.cs

Lines changed: 0 additions & 15 deletions
This file was deleted.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace AgileObjects.AgileMapper.UnitTests.TestClasses
2+
{
3+
internal class PublicUnconstructable<T>
4+
{
5+
protected PublicUnconstructable(T value)
6+
{
7+
Value = value;
8+
}
9+
10+
internal static PublicUnconstructable<T> MakeOne(T unconstructableValue)
11+
=> new PublicUnconstructable<T>(unconstructableValue);
12+
13+
public T Value { get; }
14+
}
15+
}

AgileMapper.UnitTests/WhenMappingToNewComplexTypeMembers.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -256,14 +256,14 @@ public void ShouldMapToANonNullUnconstructableNestedMember()
256256
var existingValue = new PublicField<string>();
257257

258258
mapper.WhenMapping
259-
.ToANew<PublicField<PublicFactoryMethod<PublicField<string>>>>()
260-
.CreateInstancesUsing(data => new PublicField<PublicFactoryMethod<PublicField<string>>>
259+
.ToANew<PublicField<PublicUnconstructable<PublicField<string>>>>()
260+
.CreateInstancesUsing(data => new PublicField<PublicUnconstructable<PublicField<string>>>
261261
{
262-
Value = PublicFactoryMethod<PublicField<string>>.Create(existingValue)
262+
Value = PublicUnconstructable<PublicField<string>>.MakeOne(existingValue)
263263
});
264264

265265
var source = new { Value = new { Value = new { Value = "Hello!" } } };
266-
var result = mapper.Map(source).ToANew<PublicField<PublicFactoryMethod<PublicField<string>>>>();
266+
var result = mapper.Map(source).ToANew<PublicField<PublicUnconstructable<PublicField<string>>>>();
267267

268268
result.Value.ShouldNotBeNull();
269269
result.Value.Value.ShouldNotBeNull();
@@ -276,7 +276,7 @@ public void ShouldMapToANonNullUnconstructableNestedMember()
276276
public void ShouldHandleANullUnconstructableRootTarget()
277277
{
278278
var source = new { Value = new { Value = "Goodbye!" } };
279-
var result = Mapper.Map(source).ToANew<PublicFactoryMethod<PublicField<string>>>();
279+
var result = Mapper.Map(source).ToANew<PublicUnconstructable<PublicField<string>>>();
280280

281281
result.ShouldBeNull();
282282
}
@@ -285,7 +285,7 @@ public void ShouldHandleANullUnconstructableRootTarget()
285285
public void ShouldHandleANullUnconstructableNestedMember()
286286
{
287287
var source = new { Value = new { Value = new { Value = "Goodbye!" } } };
288-
var result = Mapper.Map(source).ToANew<PublicField<PublicFactoryMethod<PublicField<string>>>>();
288+
var result = Mapper.Map(source).ToANew<PublicField<PublicUnconstructable<PublicField<string>>>>();
289289

290290
result.Value.ShouldBeNull();
291291
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
namespace AgileObjects.AgileMapper.UnitTests
2+
{
3+
using AgileMapper.Configuration;
4+
using Common;
5+
using TestClasses;
6+
#if !NET35
7+
using Xunit;
8+
#else
9+
using Fact = NUnit.Framework.TestAttribute;
10+
11+
[NUnit.Framework.TestFixture]
12+
#endif
13+
public class WhenUsingFactoryMethods
14+
{
15+
[Fact]
16+
public void ShouldUseAParameterlessGetObjectFactoryMethod()
17+
{
18+
var source = new ParameterlessGetMethod();
19+
20+
var result = Mapper.Map(source).ToANew<ParameterlessGetMethod>();
21+
22+
result.ShouldNotBeNull();
23+
result.Value.ShouldBe("123");
24+
}
25+
26+
[Fact]
27+
public void ShouldUseASingleParameterCreateObjectFactoryMethod()
28+
{
29+
var source = new PublicField<int> { Value = 456 };
30+
31+
var result = Mapper.Map(source).ToANew<SingleParameterCreateMethod>();
32+
33+
result.ShouldNotBeNull();
34+
result.Value.ShouldBe("456");
35+
}
36+
37+
[Fact]
38+
public void ShouldUseGreediestFactoryMethod()
39+
{
40+
var source = new PublicTwoFieldsStruct<int, int> { Value1 = 111, Value2 = 222 };
41+
42+
var result = Mapper.Map(source).ToANew<MultiParameterCreateMethod>();
43+
44+
result.ShouldNotBeNull();
45+
result.Value1.ShouldBe("111");
46+
result.Value2.ShouldBe("222");
47+
}
48+
49+
[Fact]
50+
public void ShouldUseFactoryMethodWithAvailableDataSources()
51+
{
52+
var source = new PublicProperty<long> { Value = 999L };
53+
54+
var result = Mapper.Map(source).ToANew<MultiParameterCreateMethod>();
55+
56+
result.ShouldNotBeNull();
57+
result.Value1.ShouldBe("999");
58+
result.Value2.ShouldBe("999");
59+
}
60+
61+
[Fact]
62+
public void ShouldUseCtorIfMoreAvailableDataSources()
63+
{
64+
var source = new PublicTwoFields<long, long> { Value1 = 123L, Value2 = 987L };
65+
66+
var result = Mapper.Map(source).ToANew<SingleParameterGetMethodMultiParameterCtor>();
67+
68+
result.ShouldNotBeNull();
69+
result.Value1.ShouldBe("123");
70+
result.Value2.ShouldBe("987");
71+
}
72+
73+
[Fact]
74+
public void ShouldErrorIfRedundantFactoryMethodConfigured()
75+
{
76+
var configEx = Should.Throw<MappingConfigurationException>(() =>
77+
{
78+
using (var mapper = Mapper.CreateNew())
79+
{
80+
mapper.WhenMapping
81+
.From<PublicTwoFieldsStruct<string, string>>()
82+
.ToANew<MultiParameterCreateMethod>()
83+
.CreateInstancesUsing(ctx => MultiParameterCreateMethod.CreateObject(
84+
ctx.Source.Value1,
85+
ctx.Source.Value2));
86+
}
87+
});
88+
89+
configEx.Message.ShouldContain(
90+
"MultiParameterCreateMethod.CreateObject(ctx.Source.Value1, ctx.Source.Value2)");
91+
92+
configEx.Message.ShouldContain("does not need to be configured");
93+
}
94+
95+
#region Helper Classes
96+
97+
// ReSharper disable UnusedMember.Global
98+
// ReSharper disable UnusedMember.Local
99+
// ReSharper disable MemberCanBePrivate.Local
100+
private class ParameterlessGetMethod
101+
{
102+
public static ParameterlessGetMethod GetObject()
103+
=> new ParameterlessGetMethod { Value = "123" };
104+
105+
public string Value { get; private set; }
106+
}
107+
108+
private class SingleParameterCreateMethod
109+
{
110+
public static SingleParameterCreateMethod CreateObject(string value)
111+
=> new SingleParameterCreateMethod { Value = value };
112+
113+
public string Value { get; private set; }
114+
}
115+
116+
private class MultiParameterCreateMethod
117+
{
118+
public static MultiParameterCreateMethod CreateObject(string value)
119+
=> CreateObject(value, value);
120+
121+
public static MultiParameterCreateMethod CreateObject(string value1, string value2)
122+
=> new MultiParameterCreateMethod { Value1 = value1, Value2 = value2 };
123+
124+
public string Value1 { get; private set; }
125+
126+
public string Value2 { get; private set; }
127+
}
128+
129+
private class SingleParameterGetMethodMultiParameterCtor
130+
{
131+
public SingleParameterGetMethodMultiParameterCtor()
132+
{
133+
}
134+
135+
public SingleParameterGetMethodMultiParameterCtor(string value1, string value2)
136+
{
137+
Value1 = value1;
138+
Value2 = value2;
139+
}
140+
141+
public static SingleParameterGetMethodMultiParameterCtor GetObject(string value)
142+
=> new SingleParameterGetMethodMultiParameterCtor(value, value);
143+
144+
public string Value1 { get; private set; }
145+
146+
public string Value2 { get; private set; }
147+
}
148+
149+
// ReSharper restore MemberCanBePrivate.Local
150+
// ReSharper restore UnusedMember.Local
151+
// ReSharper restore UnusedMember.Global
152+
153+
#endregion
154+
}
155+
}

AgileMapper/Api/Configuration/FactorySpecifier.cs

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@ namespace AgileObjects.AgileMapper.Api.Configuration
44
using System.Globalization;
55
using System.Linq.Expressions;
66
using AgileMapper.Configuration;
7-
#if NET35
87
using Extensions.Internal;
9-
#endif
108
using Members;
119
using ObjectPopulation;
1210
using Projection;
11+
using ReadableExpressions;
1312
using ReadableExpressions.Extensions;
13+
#if NET35
14+
using LambdaExpr = Microsoft.Scripting.Ast.LambdaExpression;
15+
#else
16+
using LambdaExpr = System.Linq.Expressions.LambdaExpression;
17+
#endif
1418

1519
internal class FactorySpecifier<TSource, TTarget, TObject> :
1620
IMappingFactorySpecifier<TSource, TTarget, TObject>,
@@ -26,21 +30,21 @@ public FactorySpecifier(MappingConfigInfo configInfo)
2630
public IMappingConfigContinuation<TSource, TTarget> Using(
2731
Expression<Func<IMappingData<TSource, TTarget>, TObject>> factory)
2832
#if NET35
29-
=> RegisterObjectFactory(factory.ToDlrExpression(), ConfiguredObjectFactory.For);
33+
=> RegisterObjectFactory(factory.ToDlrExpression());
3034
#else
31-
=> RegisterObjectFactory(factory, ConfiguredObjectFactory.For);
35+
=> RegisterObjectFactory(factory);
3236
#endif
3337
public IProjectionConfigContinuation<TSource, TTarget> Using(Expression<Func<TSource, TObject>> factory)
3438
#if NET35
35-
=> RegisterObjectFactory(factory.ToDlrExpression(), ConfiguredObjectFactory.For);
39+
=> RegisterObjectFactory(factory.ToDlrExpression());
3640
#else
37-
=> RegisterObjectFactory(factory, ConfiguredObjectFactory.For);
41+
=> RegisterObjectFactory(factory);
3842
#endif
3943
public IMappingConfigContinuation<TSource, TTarget> Using(LambdaExpression factory)
4044
#if NET35
41-
=> RegisterObjectFactory(factory.ToDlrExpression(), ConfiguredObjectFactory.For);
45+
=> RegisterObjectFactory(factory.ToDlrExpression());
4246
#else
43-
=> RegisterObjectFactory(factory, ConfiguredObjectFactory.For);
47+
=> RegisterObjectFactory(factory);
4448
#endif
4549
public IMappingConfigContinuation<TSource, TTarget> Using<TFactory>(TFactory factory)
4650
where TFactory : class
@@ -73,6 +77,47 @@ public IMappingConfigContinuation<TSource, TTarget> Using<TFactory>(TFactory fac
7377
string.Join(", ", validSignatures)));
7478
}
7579

80+
private MappingConfigContinuation<TSource, TTarget> RegisterObjectFactory(LambdaExpr factoryLambda)
81+
{
82+
ThrowIfRedundantFactoryConfiguration(factoryLambda);
83+
84+
return RegisterObjectFactory(factoryLambda, ConfiguredObjectFactory.For);
85+
}
86+
87+
private void ThrowIfRedundantFactoryConfiguration(LambdaExpr factoryLambda)
88+
{
89+
var ruleSet = _configInfo.IsForAllRuleSets
90+
? _configInfo.MapperContext.RuleSets.CreateNew
91+
: _configInfo.RuleSet;
92+
93+
var mappingContext = new SimpleMappingContext(
94+
ruleSet,
95+
_configInfo.MapperContext);
96+
97+
var mappingData = ObjectMappingDataFactory
98+
.ForRootFixedTypes<TSource, TObject>(mappingContext, createMapper: false);
99+
100+
var factoryMethodObjectCreation = _configInfo
101+
.MapperContext
102+
.ConstructionFactory
103+
.GetFactoryMethodObjectCreationOrNull(mappingData);
104+
105+
if (factoryMethodObjectCreation == null)
106+
{
107+
return;
108+
}
109+
110+
var factory = factoryLambda
111+
.ReplaceParameterWith(mappingData.MapperData.MappingDataObject);
112+
113+
if (ExpressionEvaluation.AreEquivalent(factory, factoryMethodObjectCreation))
114+
{
115+
throw new MappingConfigurationException(
116+
$"{factoryLambda.Body.ToReadableString()} will automatically be used to create " +
117+
$"{typeof(TObject).GetFriendlyName()} instances, and does not need to be configured.");
118+
}
119+
}
120+
76121
private MappingConfigContinuation<TSource, TTarget> RegisterObjectFactory<TFactory>(
77122
TFactory factory,
78123
Func<MappingConfigInfo, Type, TFactory, ConfiguredObjectFactory> objectFactoryFactory)

AgileMapper/Api/PlanTargetSelector.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
using AgileMapper.Configuration.Inline;
88
using Configuration;
99
using Extensions;
10-
using ObjectPopulation;
1110
using Extensions.Internal;
11+
using ObjectPopulation;
1212
using Plans;
1313
using Queryables.Api;
1414

AgileMapper/Configuration/MappingConfigInfo.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ public MappingConfigInfo ForRuleSet(MappingRuleSet ruleSet)
8585
return this;
8686
}
8787

88+
public bool IsForAllRuleSets => IsFor(_allRuleSets);
89+
8890
public bool IsFor(MappingRuleSet mappingRuleSet)
8991
=> (RuleSet == _allRuleSets) || (mappingRuleSet == _allRuleSets) || (mappingRuleSet == RuleSet);
9092

0 commit comments

Comments
 (0)