From 7974734b58913f7c74c0e8497f57cfc5117e967d Mon Sep 17 00:00:00 2001 From: Steve Wilkes Date: Tue, 23 Apr 2019 12:39:30 +0100 Subject: [PATCH 1/2] Erroring if redundant matching source member is configured --- .../WhenConfiguringDataSourcesIncorrectly.cs | 27 ++++- ...onfiguringReverseDataSourcesIncorrectly.cs | 4 +- .../WhenUsingPartialTrust.cs | 8 +- .../CustomDataSourceTargetMemberSpecifier.cs | 109 +++++++++++++----- AgileMapper/Members/MemberExtensions.cs | 14 +++ 5 files changed, 125 insertions(+), 37 deletions(-) diff --git a/AgileMapper.UnitTests/Configuration/WhenConfiguringDataSourcesIncorrectly.cs b/AgileMapper.UnitTests/Configuration/WhenConfiguringDataSourcesIncorrectly.cs index 4cb2ce33a..4f1e44da2 100644 --- a/AgileMapper.UnitTests/Configuration/WhenConfiguringDataSourcesIncorrectly.cs +++ b/AgileMapper.UnitTests/Configuration/WhenConfiguringDataSourcesIncorrectly.cs @@ -112,7 +112,26 @@ public void ShouldErrorIfDuplicateDataSourceIsConfigured() [Fact] public void ShouldErrorIfRedundantDataSourceIsConfigured() { - Should.Throw(() => + var configEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .To>() + .Map(pp => pp.Value, pf => pf.Value); + } + }); + + configEx.Message.ShouldContain("PublicProperty.Value"); + configEx.Message.ShouldContain("PublicField.Value"); + configEx.Message.ShouldContain("does not need to be configured"); + } + + [Fact] + public void ShouldErrorIfRedundantDerivedTypeDataSourceIsConfigured() + { + var configEx = Should.Throw(() => { using (var mapper = Mapper.CreateNew()) { @@ -129,6 +148,8 @@ public void ShouldErrorIfRedundantDataSourceIsConfigured() .To(x => x.Value); } }); + + configEx.Message.ShouldContain("already has configured data source"); } [Fact] @@ -308,9 +329,9 @@ public void ShouldErrorIfUnconvertibleEnumerableElementTypeConfigured() using (var mapper = Mapper.CreateNew()) { mapper.WhenMapping - .From[]>>() + .From[], int[]>>() .To>() - .Map(s => s.Value, t => t.Value); + .Map(s => s.Value1, t => t.Value); } }); diff --git a/AgileMapper.UnitTests/Configuration/WhenConfiguringReverseDataSourcesIncorrectly.cs b/AgileMapper.UnitTests/Configuration/WhenConfiguringReverseDataSourcesIncorrectly.cs index af52e5c74..b45c36b90 100644 --- a/AgileMapper.UnitTests/Configuration/WhenConfiguringReverseDataSourcesIncorrectly.cs +++ b/AgileMapper.UnitTests/Configuration/WhenConfiguringReverseDataSourcesIncorrectly.cs @@ -213,9 +213,9 @@ public void ShouldErrorOnMemberScopeOptInOfConfiguredSourceMemberDataSourceForWr using (var mapper = Mapper.CreateNew()) { mapper.WhenMapping - .From>() + .From>() .To>() - .Map((pp, pwop) => pp.Value).To(pwop => pwop.Value) + .Map((pp, pwop) => pp.Value1).To(pwop => pwop.Value) .AndViceVersa(); } }); diff --git a/AgileMapper.UnitTests/WhenUsingPartialTrust.cs b/AgileMapper.UnitTests/WhenUsingPartialTrust.cs index 3caa285d7..cd0714a43 100644 --- a/AgileMapper.UnitTests/WhenUsingPartialTrust.cs +++ b/AgileMapper.UnitTests/WhenUsingPartialTrust.cs @@ -206,13 +206,13 @@ public MappingException TestMappingException() using (var mapper = Mapper.CreateNew()) { mapper.WhenMapping - .From>() + .From>() .To>() - .If((s, t) => int.Parse(s.Value) > 0) - .Map(ctx => ctx.Source.Value) + .If((s, t) => int.Parse(s.Value1) > 0) + .Map(ctx => ctx.Source.Value1) .To(x => x.Value); - var source = new PublicProperty { Value = "CantParseThis" }; + var source = new PublicTwoFields { Value1 = "CantParseThis" }; mapper.Map(source).ToANew>(); } diff --git a/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs b/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs index bee6e5e20..5b2180159 100644 --- a/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs +++ b/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs @@ -29,7 +29,7 @@ internal class CustomDataSourceTargetMemberSpecifier : private readonly MappingConfigInfo _configInfo; private readonly LambdaExpression _customValueLambda; private readonly bool _valueCouldBeSourceMember; - private readonly ConfiguredLambdaInfo _customValueLambdaInfo; + private ConfiguredLambdaInfo _customValueLambdaInfo; public CustomDataSourceTargetMemberSpecifier( MappingConfigInfo configInfo, @@ -49,11 +49,14 @@ public CustomDataSourceTargetMemberSpecifier( _customValueLambdaInfo = customValueLambda; } + private MapperContext MapperContext => _configInfo.MapperContext; + public ICustomDataSourceMappingConfigContinuation To( Expression> targetMember) { ThrowIfTargetParameterSpecified(targetMember); ThrowIfSimpleSourceForNonSimpleTargetMember(typeof(TTargetValue)); + ThrowIfRedundantSourceMemberSpecified(targetMember); return RegisterDataSource(() => CreateFromLambda(targetMember)); } @@ -94,9 +97,56 @@ private void ThrowIfSimpleSourceForNonSimpleTargetMember(Type targetMemberType) } } + private void ThrowIfRedundantSourceMemberSpecified(LambdaExpression targetMemberLambda) + { + if (!_valueCouldBeSourceMember) + { + return; + } + + var targetMember = targetMemberLambda.ToTargetMember(MapperContext, nt => { }); + + if (targetMember == null) + { + return; + } + + var valueLambdaInfo = GetValueLambdaInfo(); + + if (!valueLambdaInfo.IsSourceMember(out var sourceMemberLambda)) + { + return; + } + + var mappingData = _configInfo.ToMappingData(); + + var targetMemberMapperData = new ChildMemberMapperData(targetMember, mappingData.MapperData); + var targetMemberMappingData = mappingData.GetChildMappingData(targetMemberMapperData); + var bestMatchingSourceMember = SourceMemberMatcher.GetMatchFor(targetMemberMappingData, out _); + + if (bestMatchingSourceMember == null) + { + return; + } + + var sourceMember = sourceMemberLambda.ToSourceMember(MapperContext); + + if (!bestMatchingSourceMember.Matches(sourceMember)) + { + return; + } + + throw new MappingConfigurationException(string.Format( + CultureInfo.InvariantCulture, + "Source member {0} will automatically be mapped to target member {1}, " + + "and does not need to be configured", + GetSourceMemberDescription(sourceMember), + targetMember.GetFriendlyTargetPath(_configInfo))); + } + private ConfiguredDataSourceFactory CreateFromLambda(LambdaExpression targetMemberLambda) { - var valueLambdaInfo = GetValueLambdaInfo(typeof(TTargetValue)); + var valueLambdaInfo = GetValueLambdaInfo(); if (IsDictionaryEntry(targetMemberLambda, out var dictionaryEntryMember)) { @@ -106,7 +156,7 @@ private ConfiguredDataSourceFactory CreateFromLambda(LambdaExpress return CreateDataSourceFactory(valueLambdaInfo, targetMemberLambda); } - private ConfiguredLambdaInfo GetValueLambdaInfo(Type targetValueType) + private ConfiguredLambdaInfo GetValueLambdaInfo() { if (_customValueLambdaInfo != null) { @@ -121,25 +171,24 @@ private ConfiguredLambdaInfo GetValueLambdaInfo(Type targetValueType) const ExpressionType CONSTANT = ExpressionType.Constant; #endif if ((customValueLambda.Body.NodeType != CONSTANT) || - (targetValueType == typeof(object)) || - customValueLambda.ReturnType.IsAssignableTo(targetValueType)) + (typeof(TTargetValue) == typeof(object)) || + customValueLambda.ReturnType.IsAssignableTo(typeof(TTargetValue))) { - return ConfiguredLambdaInfo.For(customValueLambda); + return _customValueLambdaInfo = ConfiguredLambdaInfo.For(customValueLambda); } - var convertedConstantValue = _configInfo - .MapperContext + var convertedConstantValue = MapperContext .ValueConverters - .GetConversion(customValueLambda.Body, targetValueType); + .GetConversion(customValueLambda.Body, typeof(TTargetValue)); - var funcType = GetFuncType(targetValueType); + var funcType = GetFuncType(typeof(TTargetValue)); var valueLambda = Lambda(funcType, convertedConstantValue); var valueFunc = valueLambda.Compile(); - var value = valueFunc.DynamicInvoke().ToConstantExpression(targetValueType); + var value = valueFunc.DynamicInvoke().ToConstantExpression(typeof(TTargetValue)); var constantValueLambda = Lambda(funcType, value); var valueLambdaInfo = ConfiguredLambdaInfo.For(constantValueLambda); - return valueLambdaInfo; + return _customValueLambdaInfo = valueLambdaInfo; } private bool IsDictionaryEntry(LambdaExpression targetMemberLambda, out DictionaryTargetMember entryMember) @@ -180,8 +229,8 @@ private bool IsDictionaryEntry(LambdaExpression targetMemberLambda, out Dictiona private QualifiedMember CreateRootTargetQualifiedMember() { return (_configInfo.TargetType == typeof(ExpandoObject)) - ? _configInfo.MapperContext.QualifiedMemberFactory.RootTarget() - : _configInfo.MapperContext.QualifiedMemberFactory.RootTarget(); + ? MapperContext.QualifiedMemberFactory.RootTarget() + : MapperContext.QualifiedMemberFactory.RootTarget(); } private ConfiguredDataSourceFactory CreateDataSourceFactory( @@ -282,7 +331,7 @@ private static Exception AmbiguousParameterException(string parameterMatchInfo) private ConfiguredDataSourceFactory CreateForCtorParam(ParameterInfo parameter) { - var valueLambda = GetValueLambdaInfo(typeof(TParam)); + var valueLambda = GetValueLambdaInfo(); var constructorParameter = CreateRootTargetQualifiedMember().Append(Member.ConstructorParameter(parameter)); return new ConfiguredDataSourceFactory(_configInfo, valueLambda, constructorParameter); @@ -297,7 +346,7 @@ public IMappingConfigContinuation ToTarget() return RegisterDataSource(() => new ConfiguredDataSourceFactory( _configInfo, - GetValueLambdaInfo(typeof(TTarget)), + GetValueLambdaInfo(), CreateRootTargetQualifiedMember())); } @@ -331,13 +380,12 @@ private void ThrowIfSimpleSource(Type targetMemberType) return; } - var sourceValue = GetSourceValue(customValue); + var sourceValue = GetSourceValueDescription(customValue); throw new MappingConfigurationException(string.Format( CultureInfo.InvariantCulture, - "{0}'{1}' cannot be mapped to target type '{2}'", + "{0} cannot be mapped to target type '{1}'", sourceValue, - customValue.Type.GetFriendlyName(), targetMemberType.GetFriendlyName())); } @@ -364,30 +412,35 @@ private void ThrowIfEnumerableSourceAndTargetMismatch(Type targetMemberType) targetEnumerableState = "non-enumerable"; } - var sourceValue = GetSourceValue(customValue); + var sourceValue = GetSourceValueDescription(customValue); throw new MappingConfigurationException(string.Format( CultureInfo.InvariantCulture, - "{0} {1}'{2}' cannot be mapped to {3} target type '{4}'", + "{0} {1} cannot be mapped to {2} target type '{3}'", sourceEnumerableState, sourceValue, - customValue.Type.GetFriendlyName(), targetEnumerableState, targetMemberType.GetFriendlyName())); } - private string GetSourceValue(Expression customValue) + private string GetSourceValueDescription(Expression customValue) { if (customValue.NodeType != ExpressionType.MemberAccess) { - return "Source type "; + return $"Source type '{customValue.Type.GetFriendlyName()}'"; } - var rootSourceMember = _configInfo.MapperContext.QualifiedMemberFactory.RootSource(); - var sourceMember = customValue.ToSourceMember(_configInfo.MapperContext); - var sourceValue = sourceMember.GetFriendlyMemberPath(rootSourceMember) + " of type "; + var sourceMember = customValue.ToSourceMember(MapperContext); + + return GetSourceMemberDescription(sourceMember); + } + + private string GetSourceMemberDescription(IQualifiedMember sourceMember) + { + var rootSourceMember = MapperContext.QualifiedMemberFactory.RootSource(); + var sourceMemberPath = sourceMember.GetFriendlyMemberPath(rootSourceMember); - return sourceValue; + return $"{sourceMemberPath} of type '{sourceMember.Type.GetFriendlyName()}'"; } private MappingConfigContinuation RegisterDataSource( diff --git a/AgileMapper/Members/MemberExtensions.cs b/AgileMapper/Members/MemberExtensions.cs index 67ea4d890..a2ef0e3a8 100644 --- a/AgileMapper/Members/MemberExtensions.cs +++ b/AgileMapper/Members/MemberExtensions.cs @@ -321,6 +321,14 @@ public static QualifiedMember ToTargetMemberOrNull( #if NET35 public static QualifiedMember ToTargetMember(this LinqExp.LambdaExpression memberAccess, MapperContext mapperContext) => memberAccess.ToDlrExpression().ToTargetMember(mapperContext); + + public static QualifiedMember ToTargetMember( + this LinqExp.LambdaExpression memberAccess, + MapperContext mapperContext, + Action nonMemberAction) + { + return memberAccess.ToDlrExpression().ToTargetMember(mapperContext, nonMemberAction); + } #endif public static QualifiedMember ToTargetMember( this LambdaExpression memberAccess, @@ -443,6 +451,12 @@ private static void AdjustMemberAccessesIfRootedInMappingData(IList } var mappingDataRoot = memberAccesses[0]; + + if (mappingDataRoot.NodeType != ExpressionType.MemberAccess) + { + return; + } + expression = Parameters.Create(mappingDataRoot.Type); memberAccesses.RemoveAt(0); From e627863bb3f0ea0c53f4c342bea81e10e53f4ba0 Mon Sep 17 00:00:00 2001 From: Steve Wilkes Date: Wed, 24 Apr 2019 11:41:03 +0100 Subject: [PATCH 2/2] Improving configured data source validation Support for same-typed configured data source for otherwise-unconstructable target members --- .../WhenConfiguringDataSourcesIncorrectly.cs | 78 +++++++++++ .../WhenConfiguringObjectCreation.cs | 19 +++ .../CustomDataSourceTargetMemberSpecifier.cs | 127 +++++++++++------- .../Configuration/MappingConfigInfo.cs | 4 +- AgileMapper/MappingDataExtensions.cs | 8 ++ .../TargetObjectResolutionFactory.cs | 5 +- .../ObjectPopulation/MappingFactory.cs | 6 + 7 files changed, 198 insertions(+), 49 deletions(-) diff --git a/AgileMapper.UnitTests/Configuration/WhenConfiguringDataSourcesIncorrectly.cs b/AgileMapper.UnitTests/Configuration/WhenConfiguringDataSourcesIncorrectly.cs index 4f1e44da2..ac94ae97c 100644 --- a/AgileMapper.UnitTests/Configuration/WhenConfiguringDataSourcesIncorrectly.cs +++ b/AgileMapper.UnitTests/Configuration/WhenConfiguringDataSourcesIncorrectly.cs @@ -128,6 +128,28 @@ public void ShouldErrorIfRedundantDataSourceIsConfigured() configEx.Message.ShouldContain("does not need to be configured"); } + [Fact] + public void ShouldErrorIfRedundantConstructorParameterDataSourceIsConfigured() + { + var configEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .To>() + .Map(ctx => ctx.Source.Value) + .ToCtor(); + } + }); + + configEx.Message.ShouldContain("PublicProperty.Value"); + configEx.Message.ShouldContain("will automatically be mapped"); + configEx.Message.ShouldContain("target constructor parameter"); + configEx.Message.ShouldContain("PublicCtor.value"); + configEx.Message.ShouldContain("does not need to be configured"); + } + [Fact] public void ShouldErrorIfRedundantDerivedTypeDataSourceIsConfigured() { @@ -283,6 +305,24 @@ public void ShouldErrorIfUnconvertibleConstructorValueConstantSpecified() configurationException.Message.ShouldContain("Unable to convert"); } + [Fact] + public void ShouldErrorIfUnconvertibleConstructorSourceValueSpecified() + { + var configurationException = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .To>() + .Map(ctx => ctx.Source.Value) + .ToCtor(); + } + }); + + configurationException.Message.ShouldContain("Unable to convert"); + } + [Fact] public void ShouldErrorIfSimpleTypeConfiguredForComplexTarget() { @@ -302,6 +342,25 @@ public void ShouldErrorIfSimpleTypeConfiguredForComplexTarget() "Person.Id of type 'Guid' cannot be mapped to target type 'Address'"); } + [Fact] + public void ShouldErrorIfSimpleTypeConfiguredForComplexConstructorParameter() + { + var configurationException = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .To>() + .Map(ctx => ctx.Source.Value) + .ToCtor
(); + } + }); + + configurationException.Message.ShouldContain( + "PublicField.Value of type 'int' cannot be mapped to target type 'Address'"); + } + [Fact] public void ShouldErrorIfSimpleTypeConfiguredForEnumerableTarget() { @@ -321,6 +380,25 @@ public void ShouldErrorIfSimpleTypeConfiguredForEnumerableTarget() "PublicField.Value of type 'int' cannot be mapped to target type 'int[]'"); } + [Fact] + public void ShouldErrorIfSimpleTypeConfiguredForEnumerableConstructorParameter() + { + var configurationException = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .To>() + .Map(ctx => ctx.Source.Value) + .ToCtor("value"); + } + }); + + configurationException.Message.ShouldContain( + "PublicField.Value of type 'string' cannot be mapped to target type 'int[]'"); + } + [Fact] public void ShouldErrorIfUnconvertibleEnumerableElementTypeConfigured() { diff --git a/AgileMapper.UnitTests/Configuration/WhenConfiguringObjectCreation.cs b/AgileMapper.UnitTests/Configuration/WhenConfiguringObjectCreation.cs index 8b88d5ee6..d6694e096 100644 --- a/AgileMapper.UnitTests/Configuration/WhenConfiguringObjectCreation.cs +++ b/AgileMapper.UnitTests/Configuration/WhenConfiguringObjectCreation.cs @@ -36,6 +36,25 @@ public void ShouldUseAConfiguredFactoryForAGivenType() } } + [Fact] + public void ShouldUseAConfiguredFactoryWithASimpleSourceType() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>() + .ToANew>>() + .Map(ctx => new PublicCtor(ctx.Source.Value)) + .To(t => t.Value); + + var source = new PublicField { Value = "Hello!" }; + var result = mapper.Map(source).ToANew>>(); + + result.Value.ShouldNotBeNull(); + result.Value.Value.ShouldBe("Hello!"); + } + } + [Fact] public void ShouldUseAConfiguredFactoryWithAComplexTypeMemberBinding() { diff --git a/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs b/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs index 5b2180159..5b0ea7525 100644 --- a/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs +++ b/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs @@ -15,6 +15,7 @@ using NetStandardPolyfills; using Projection; using ReadableExpressions.Extensions; + using TypeConversion; #if NET35 using Dlr = Microsoft.Scripting.Ast; using static Microsoft.Scripting.Ast.Expression; @@ -55,8 +56,7 @@ public ICustomDataSourceMappingConfigContinuation To> targetMember) { ThrowIfTargetParameterSpecified(targetMember); - ThrowIfSimpleSourceForNonSimpleTargetMember(typeof(TTargetValue)); - ThrowIfRedundantSourceMemberSpecified(targetMember); + ThrowIfRedundantSourceMember(targetMember); return RegisterDataSource(() => CreateFromLambda(targetMember)); } @@ -65,7 +65,6 @@ IProjectionConfigContinuation ICustomProjectionDataSourceTarge Expression> resultMember) { ThrowIfTargetParameterSpecified(resultMember); - ThrowIfSimpleSourceForNonSimpleTargetMember(typeof(TResultValue)); return RegisterDataSource(() => CreateFromLambda(resultMember)); } @@ -73,8 +72,6 @@ IProjectionConfigContinuation ICustomProjectionDataSourceTarge public IMappingConfigContinuation To( Expression>> targetSetMethod) { - ThrowIfSimpleSourceForNonSimpleTargetMember(typeof(TTargetValue)); - return RegisterDataSource(() => CreateFromLambda(targetSetMethod)); } @@ -89,15 +86,7 @@ private static void ThrowIfTargetParameterSpecified(LambdaExpression targetMembe } } - private void ThrowIfSimpleSourceForNonSimpleTargetMember(Type targetMemberType) - { - if ((targetMemberType != typeof(object)) && !targetMemberType.IsSimple()) - { - ThrowIfSimpleSource(targetMemberType); - } - } - - private void ThrowIfRedundantSourceMemberSpecified(LambdaExpression targetMemberLambda) + private void ThrowIfRedundantSourceMember(LambdaExpression targetMemberLambda) { if (!_valueCouldBeSourceMember) { @@ -113,6 +102,11 @@ private void ThrowIfRedundantSourceMemberSpecified(LambdaExpressio var valueLambdaInfo = GetValueLambdaInfo(); + ThrowIfRedundantSourceMember(valueLambdaInfo, targetMember); + } + + private void ThrowIfRedundantSourceMember(ConfiguredLambdaInfo valueLambdaInfo, QualifiedMember targetMember) + { if (!valueLambdaInfo.IsSourceMember(out var sourceMemberLambda)) { return; @@ -136,11 +130,16 @@ private void ThrowIfRedundantSourceMemberSpecified(LambdaExpressio return; } + var targetMemberType = (targetMember.LeafMember.MemberType == MemberType.ConstructorParameter) + ? "constructor parameter" + : "member"; + throw new MappingConfigurationException(string.Format( CultureInfo.InvariantCulture, - "Source member {0} will automatically be mapped to target member {1}, " + + "Source member {0} will automatically be mapped to target {1} {2}, " + "and does not need to be configured", GetSourceMemberDescription(sourceMember), + targetMemberType, targetMember.GetFriendlyTargetPath(_configInfo))); } @@ -156,7 +155,9 @@ private ConfiguredDataSourceFactory CreateFromLambda(LambdaExpress return CreateDataSourceFactory(valueLambdaInfo, targetMemberLambda); } - private ConfiguredLambdaInfo GetValueLambdaInfo() + private ConfiguredLambdaInfo GetValueLambdaInfo() => GetValueLambdaInfo(typeof(TTargetValue)); + + private ConfiguredLambdaInfo GetValueLambdaInfo(Type targetValueType) { if (_customValueLambdaInfo != null) { @@ -171,20 +172,20 @@ private ConfiguredLambdaInfo GetValueLambdaInfo() const ExpressionType CONSTANT = ExpressionType.Constant; #endif if ((customValueLambda.Body.NodeType != CONSTANT) || - (typeof(TTargetValue) == typeof(object)) || - customValueLambda.ReturnType.IsAssignableTo(typeof(TTargetValue))) + (targetValueType == typeof(object)) || + customValueLambda.ReturnType.IsAssignableTo(targetValueType)) { return _customValueLambdaInfo = ConfiguredLambdaInfo.For(customValueLambda); } var convertedConstantValue = MapperContext .ValueConverters - .GetConversion(customValueLambda.Body, typeof(TTargetValue)); + .GetConversion(customValueLambda.Body, targetValueType); - var funcType = GetFuncType(typeof(TTargetValue)); + var funcType = GetFuncType(targetValueType); var valueLambda = Lambda(funcType, convertedConstantValue); var valueFunc = valueLambda.Compile(); - var value = valueFunc.DynamicInvoke().ToConstantExpression(typeof(TTargetValue)); + var value = valueFunc.DynamicInvoke().ToConstantExpression(targetValueType); var constantValueLambda = Lambda(funcType, value); var valueLambdaInfo = ConfiguredLambdaInfo.For(constantValueLambda); @@ -255,21 +256,25 @@ IProjectionConfigContinuation ICustomProjectionDataSourceTarge => RegisterDataSource(CreateForCtorParam); public IMappingConfigContinuation ToCtor(string parameterName) - => RegisterDataSource(() => CreateForCtorParam(parameterName)); + => RegisterNamedContructorParameterDataSource(parameterName); IProjectionConfigContinuation ICustomProjectionDataSourceTargetMemberSpecifier.ToCtor( string parameterName) { - return RegisterDataSource(() => CreateForCtorParam(parameterName)); + return RegisterNamedContructorParameterDataSource(parameterName); } #region Ctor Helpers private ConfiguredDataSourceFactory CreateForCtorParam() - => CreateForCtorParam(GetUniqueConstructorParameterOrThrow()); + => CreateForCtorParam(GetUniqueConstructorParameterOrThrow()); - private ConfiguredDataSourceFactory CreateForCtorParam(string name) - => CreateForCtorParam(GetUniqueConstructorParameterOrThrow(name)); + private MappingConfigContinuation RegisterNamedContructorParameterDataSource(string name) + { + var parameter = GetUniqueConstructorParameterOrThrow(name); + + return RegisterDataSource(parameter.ParameterType, () => CreateForCtorParam(parameter)); + } private static ParameterInfo GetUniqueConstructorParameterOrThrow(string name = null) { @@ -309,7 +314,7 @@ private static ParameterInfo GetUniqueConstructorParameterOrThrow(string } private static string GetParameterMatchInfo(string name, bool matchParameterType) - => matchParameterType ? "of type " + typeof(TParam).GetFriendlyName() : "named '" + name + "'"; + => matchParameterType ? GetTypeDescription(typeof(TParam)) : $"named '{name}'"; private static Exception MissingParameterException(string parameterMatchInfo) { @@ -329,11 +334,13 @@ private static Exception AmbiguousParameterException(string parameterMatchInfo) typeof(TTarget).GetFriendlyName())); } - private ConfiguredDataSourceFactory CreateForCtorParam(ParameterInfo parameter) + private ConfiguredDataSourceFactory CreateForCtorParam(ParameterInfo parameter) { - var valueLambda = GetValueLambdaInfo(); + var valueLambda = GetValueLambdaInfo(parameter.ParameterType); var constructorParameter = CreateRootTargetQualifiedMember().Append(Member.ConstructorParameter(parameter)); + ThrowIfRedundantSourceMember(valueLambda, constructorParameter); + return new ConfiguredDataSourceFactory(_configInfo, valueLambda, constructorParameter); } @@ -341,9 +348,6 @@ private ConfiguredDataSourceFactory CreateForCtorParam(ParameterInfo par public IMappingConfigContinuation ToTarget() { - ThrowIfSimpleSourceForNonSimpleTargetMember(typeof(TTarget)); - ThrowIfEnumerableSourceAndTargetMismatch(typeof(TTarget)); - return RegisterDataSource(() => new ConfiguredDataSourceFactory( _configInfo, GetValueLambdaInfo(), @@ -371,16 +375,44 @@ private void SetDerivedToTargetSource(MappingConfigInfo derivedT .ToTarget(); } - private void ThrowIfSimpleSource(Type targetMemberType) + private static string GetTypeDescription(Type type) => $"of type '{type.GetFriendlyName()}'"; + + private MappingConfigContinuation RegisterDataSource( + Func dataSourceFactoryFactory) + { + return RegisterDataSource(typeof(TTargetValue), dataSourceFactoryFactory); + } + + private MappingConfigContinuation RegisterDataSource( + Type targetMemberType, + Func dataSourceFactoryFactory) { - var customValue = _customValueLambda.Body; + ThrowIfInvalid(targetMemberType); + + MapperContext.UserConfigurations.Add(dataSourceFactoryFactory.Invoke()); - if (!customValue.Type.IsSimple()) + return new MappingConfigContinuation(_configInfo); + } + + private void ThrowIfInvalid(Type targetMemberType) + { + ThrowIfSimpleSourceForNonSimpleTargetMember(targetMemberType); + ThrowIfEnumerableSourceAndTargetMismatch(targetMemberType); + + _configInfo.ThrowIfSourceTypeUnconvertible(targetMemberType); + } + + private void ThrowIfSimpleSourceForNonSimpleTargetMember(Type targetMemberType) + { + if ((targetMemberType == typeof(object)) || + targetMemberType.IsSimple() || + !_customValueLambda.Body.Type.IsSimple() || + ConversionOperatorExists(targetMemberType)) { return; } - var sourceValue = GetSourceValueDescription(customValue); + var sourceValue = GetSourceValueDescription(_customValueLambda.Body); throw new MappingConfigurationException(string.Format( CultureInfo.InvariantCulture, @@ -389,8 +421,20 @@ private void ThrowIfSimpleSource(Type targetMemberType) targetMemberType.GetFriendlyName())); } + private bool ConversionOperatorExists(Type targetMemberType) + { + return default(OperatorConverter).CanConvert( + _customValueLambda.Body.Type.GetNonNullableType(), + targetMemberType.GetNonNullableType()); + } + private void ThrowIfEnumerableSourceAndTargetMismatch(Type targetMemberType) { + if (_customValueLambda == null) + { + return; + } + var customValue = _customValueLambda.Body; if ((targetMemberType.IsDictionary() || customValue.Type.IsDictionary()) || @@ -440,16 +484,7 @@ private string GetSourceMemberDescription(IQualifiedMember sourceMember) var rootSourceMember = MapperContext.QualifiedMemberFactory.RootSource(); var sourceMemberPath = sourceMember.GetFriendlyMemberPath(rootSourceMember); - return $"{sourceMemberPath} of type '{sourceMember.Type.GetFriendlyName()}'"; - } - - private MappingConfigContinuation RegisterDataSource( - Func factoryFactory) - { - _configInfo.ThrowIfSourceTypeUnconvertible(); - _configInfo.MapperContext.UserConfigurations.Add(factoryFactory.Invoke()); - - return new MappingConfigContinuation(_configInfo); + return sourceMemberPath + " " + GetTypeDescription(sourceMember.Type); } private struct AnyParameterType { } diff --git a/AgileMapper/Configuration/MappingConfigInfo.cs b/AgileMapper/Configuration/MappingConfigInfo.cs index 8af37e956..b9c853b8e 100644 --- a/AgileMapper/Configuration/MappingConfigInfo.cs +++ b/AgileMapper/Configuration/MappingConfigInfo.cs @@ -99,8 +99,8 @@ public MappingConfigInfo ForSourceValueType(Type sourceValueType) return this; } - public void ThrowIfSourceTypeUnconvertible() - => MapperContext.ValueConverters.ThrowIfUnconvertible(SourceValueType, typeof(TTargetValue)); + public void ThrowIfSourceTypeUnconvertible(Type targetValueType) + => MapperContext.ValueConverters.ThrowIfUnconvertible(SourceValueType, targetValueType); #region Conditions diff --git a/AgileMapper/MappingDataExtensions.cs b/AgileMapper/MappingDataExtensions.cs index eea14457d..fac6dc77e 100644 --- a/AgileMapper/MappingDataExtensions.cs +++ b/AgileMapper/MappingDataExtensions.cs @@ -7,6 +7,7 @@ #endif using DataSources; using Extensions.Internal; + using Members; using ObjectPopulation; internal static class MappingDataExtensions @@ -54,5 +55,12 @@ public static Expression GetTargetObjectCreation(this IObjectMappingData mapping .ConstructionFactory .GetNewObjectCreation(mappingData); } + + public static bool HasSameTypedConfiguredDataSource(this IObjectMappingData mappingData) + { + return + (mappingData.MapperData.SourceType == mappingData.MapperData.TargetType) && + (mappingData.MapperData.SourceMember is ConfiguredSourceMember); + } } } diff --git a/AgileMapper/ObjectPopulation/ComplexTypes/TargetObjectResolutionFactory.cs b/AgileMapper/ObjectPopulation/ComplexTypes/TargetObjectResolutionFactory.cs index ee6425386..e15376517 100644 --- a/AgileMapper/ObjectPopulation/ComplexTypes/TargetObjectResolutionFactory.cs +++ b/AgileMapper/ObjectPopulation/ComplexTypes/TargetObjectResolutionFactory.cs @@ -43,7 +43,10 @@ public static Expression GetObjectResolution( if (objectValue == null) { - mapperData.TargetMember.IsReadOnly = true; + if (!mappingData.HasSameTypedConfiguredDataSource()) + { + mapperData.TargetMember.IsReadOnly = true; + } // Use the existing target object if it might have a value and // the mapper can't create an instance: diff --git a/AgileMapper/ObjectPopulation/MappingFactory.cs b/AgileMapper/ObjectPopulation/MappingFactory.cs index 2ccd93b48..8d34258f7 100644 --- a/AgileMapper/ObjectPopulation/MappingFactory.cs +++ b/AgileMapper/ObjectPopulation/MappingFactory.cs @@ -177,6 +177,12 @@ public static Expression GetInlineMappingBlock( if (mapper == null) { + if (mappingData.HasSameTypedConfiguredDataSource()) + { + // Configured data source for an otherwise-unconstructable complex type: + return mappingValues.SourceValue; + } + return Constants.EmptyExpression; }