From 393ee67f9614527d23405ab1289e0f17b5582de3 Mon Sep 17 00:00:00 2001 From: Steve Wilkes Date: Wed, 8 May 2019 13:52:16 +0100 Subject: [PATCH 1/5] Adding intermediate-step ConstructionInfos to determine constructability without always creating the construction Expression --- .../WhenMappingToConstructors.cs | 27 ++ .../Members/MemberMapperDataExtensions.cs | 25 - .../Population/MemberPopulatorFactory.cs | 40 +- .../ComplexTypeConstructionFactory.cs | 439 ++++++++++-------- 4 files changed, 317 insertions(+), 214 deletions(-) diff --git a/AgileMapper.UnitTests/WhenMappingToConstructors.cs b/AgileMapper.UnitTests/WhenMappingToConstructors.cs index fefc53c98..18ade044b 100644 --- a/AgileMapper.UnitTests/WhenMappingToConstructors.cs +++ b/AgileMapper.UnitTests/WhenMappingToConstructors.cs @@ -98,6 +98,16 @@ public void ShouldIgnoreACopyConstructor() result.StringValue.ShouldBe("Copy!"); } + // See https://github.com/agileobjects/AgileMapper/issues/139 + [Fact] + public void ShouldPopulateMembersMatchingUnusedConstructorParameters() + { + var source = new { Value1 = 123 }; + var result = Mapper.Map(source).ToANew>(); + + result.Value1.ShouldBe(123); + } + #region Helper Classes // ReSharper disable ClassNeverInstantiated.Local @@ -124,6 +134,23 @@ public MultipleConstructors(T1 value1, T2 value2) public T2 Value2 { get; } } + private class MultipleUnusedConstructors + { + public MultipleUnusedConstructors() + { + } + + public MultipleUnusedConstructors(T1 value1, T2 value2) + { + Value1 = value1; + Value2 = value2; + } + + public T1 Value1 { get; set; } + + public T2 Value2 { get; set; } + } + private class CopyConstructor { public CopyConstructor() diff --git a/AgileMapper/Members/MemberMapperDataExtensions.cs b/AgileMapper/Members/MemberMapperDataExtensions.cs index 6751fdb26..69b228009 100644 --- a/AgileMapper/Members/MemberMapperDataExtensions.cs +++ b/AgileMapper/Members/MemberMapperDataExtensions.cs @@ -187,31 +187,6 @@ public static void RegisterTargetMemberDataSourcesIfRequired( mapperData.Parent.DataSourcesByTargetMember.Add(mapperData.TargetMember, dataSources); } - public static bool TargetMemberIsUnmappable(this IMemberMapperData mapperData, out string reason) - { - if (!mapperData.RuleSet.Settings.AllowSetMethods && - (mapperData.TargetMember.LeafMember.MemberType == MemberType.SetMethod)) - { - reason = "Set methods are unsupported by rule set '" + mapperData.RuleSet.Name + "'"; - return true; - } - - if (mapperData.TargetMember.LeafMember.HasMatchingCtorParameter && - ((mapperData.Parent?.IsRoot != true) || - !mapperData.RuleSet.Settings.RootHasPopulatedTarget)) - { - reason = "Expected to be populated by constructor parameter"; - return true; - } - - return TargetMemberIsUnmappable( - mapperData, - mapperData.TargetMember, - md => md.MapperContext.UserConfigurations.QueryDataSourceFactories(md), - mapperData.MapperContext.UserConfigurations, - out reason); - } - public static bool TargetMemberIsUnmappable( this TTMapperData mapperData, QualifiedMember targetMember, diff --git a/AgileMapper/Members/Population/MemberPopulatorFactory.cs b/AgileMapper/Members/Population/MemberPopulatorFactory.cs index 9d2844a6a..416fcfeab 100644 --- a/AgileMapper/Members/Population/MemberPopulatorFactory.cs +++ b/AgileMapper/Members/Population/MemberPopulatorFactory.cs @@ -52,7 +52,7 @@ private static IMemberPopulator Create(QualifiedMember targetMember, IObjectMapp { var childMapperData = new ChildMemberMapperData(targetMember, mappingData.MapperData); - if (childMapperData.TargetMemberIsUnmappable(out var reason)) + if (TargetMemberIsUnmappable(childMapperData, mappingData, out var reason)) { return MemberPopulator.Unmappable(childMapperData, reason); } @@ -76,6 +76,44 @@ private static IMemberPopulator Create(QualifiedMember targetMember, IObjectMapp return MemberPopulator.WithRegistration(childMappingData, dataSources, populateCondition); } + private static bool TargetMemberIsUnmappable( + IMemberMapperData mapperData, + IObjectMappingData mappingData, + out string reason) + { + if (!mapperData.RuleSet.Settings.AllowSetMethods && + (mapperData.TargetMember.LeafMember.MemberType == MemberType.SetMethod)) + { + reason = "Set methods are unsupported by rule set '" + mapperData.RuleSet.Name + "'"; + return true; + } + + if (TargetMemberWillBePopulatedByCtor(mapperData, mappingData)) + { + reason = "Expected to be populated by constructor parameter"; + return true; + } + + return mapperData.TargetMemberIsUnmappable( + mapperData.TargetMember, + md => md.MapperContext.UserConfigurations.QueryDataSourceFactories(md), + mapperData.MapperContext.UserConfigurations, + out reason); + } + + private static bool TargetMemberWillBePopulatedByCtor(IMemberMapperData mapperData, IObjectMappingData mappingData) + { + if (!mapperData.TargetMember.LeafMember.HasMatchingCtorParameter || + (mapperData.RuleSet.Settings.RootHasPopulatedTarget && (mapperData.Parent?.IsRoot == true))) + { + return false; + } + + + + return true; + } + private static bool TargetMemberIsUnconditionallyIgnored( IMemberMapperData mapperData, out ConfiguredIgnoredMember configuredIgnore, diff --git a/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeConstructionFactory.cs b/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeConstructionFactory.cs index 0844e5cc8..5619beda2 100644 --- a/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeConstructionFactory.cs +++ b/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeConstructionFactory.cs @@ -22,74 +22,51 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.ComplexTypes internal class ComplexTypeConstructionFactory { - private readonly ICache _constructorsCache; + private readonly ICache> _constructionInfosCache; + private readonly ICache _constructionsCache; public ComplexTypeConstructionFactory(CacheSet mapperScopedCacheSet) { - _constructorsCache = mapperScopedCacheSet.CreateScoped(); + _constructionInfosCache = mapperScopedCacheSet.CreateScoped>(); + _constructionsCache = mapperScopedCacheSet.CreateScoped(); } - public Expression GetNewObjectCreation(IObjectMappingData mappingData) + public IList GetNewObjectCreationInfos(IObjectMappingData mappingData) + => (IList)GetNewObjectCreationInfos(mappingData, out _); + + private IList GetNewObjectCreationInfos( + IObjectMappingData mappingData, + out ConstructionKey constructionKey) { - var objectCreation = _constructorsCache.GetOrAdd(new ConstructionKey(mappingData), key => + return _constructionInfosCache.GetOrAdd(constructionKey = new ConstructionKey(mappingData), key => { - var constructions = new List(); + IList constructionInfos = new List(); - AddConfiguredConstructions( - constructions, + AddConfiguredConstructionInfos( + constructionInfos, key, out var otherConstructionRequired); if (otherConstructionRequired && !key.MappingData.MapperData.TargetType.IsAbstract()) { - AddAutoConstructions(constructions, key); - } - - if (constructions.None()) - { - key.MappingData = null; - return null; + AddAutoConstructionInfos(constructionInfos, key); } - var construction = Construction.For(constructions, key); - - key.AddSourceMemberTypeTesterIfRequired(); key.MappingData = null; - return construction; + return constructionInfos.None() + ? Enumerable.EmptyArray + : constructionInfos; }); - - if (objectCreation == null) - { - return null; - } - - mappingData.MapperData.Context.UsesMappingDataObjectAsParameter = objectCreation.UsesMappingDataObjectParameter; - - var creationExpression = objectCreation.GetConstruction(mappingData); - - return creationExpression; - } - - public Expression GetFactoryMethodObjectCreationOrNull(IObjectMappingData mappingData) - { - var key = new ConstructionKey(mappingData); - var factoryData = GetGreediestAvailableFactories(key); - - return factoryData.Any() - ? factoryData.First().Construction.With(key).GetConstruction(mappingData) - : null; } - private static void AddConfiguredConstructions( - ICollection constructions, + private static void AddConfiguredConstructionInfos( + ICollection constructionInfos, ConstructionKey key, out bool otherConstructionRequired) { var mapperData = key.MappingData.MapperData; - otherConstructionRequired = true; - var configuredFactories = mapperData .MapperContext .UserConfigurations @@ -97,52 +74,58 @@ private static void AddConfiguredConstructions( foreach (var configuredFactory in configuredFactories) { - var configuredConstruction = new Construction(configuredFactory, mapperData); + var configuredConstructionInfo = new ConfiguredFactoryInfo(configuredFactory, mapperData); - constructions.Add(configuredConstruction); + constructionInfos.Add(configuredConstructionInfo); - if (configuredConstruction.IsUnconditional) + if (configuredConstructionInfo.IsUnconditional) { otherConstructionRequired = false; return; } } + + otherConstructionRequired = true; } - private static void AddAutoConstructions(IList constructions, ConstructionKey key) + private static void AddAutoConstructionInfos(IList constructionInfos, ConstructionKey key) { var mapperData = key.MappingData.MapperData; - var greediestAvailableFactories = GetGreediestAvailableFactories(key); - var greediestUnconditionalFactory = greediestAvailableFactories.LastOrDefault(f => f.IsUnconditional); + var greediestAvailableFactoryInfos = GetGreediestAvailableFactoryInfos(key); + var greediestUnconditionalFactoryInfo = greediestAvailableFactoryInfos.LastOrDefault(f => f.IsUnconditional); var constructors = mapperData.TargetInstance.Type .GetPublicInstanceConstructors() .ToArray(); - var greediestAvailableNewings = constructors.Any() - ? GetGreediestAvailableNewings(constructors, key, greediestUnconditionalFactory) - : Enumerable>.EmptyArray; - int i; - for (i = 0; i < greediestAvailableFactories.Length; i++) + for (i = 0; i < greediestAvailableFactoryInfos.Length;) { - greediestAvailableFactories[i].AddTo(constructions, key); + greediestAvailableFactoryInfos[i++].AddTo(constructionInfos, key); } - for (i = 0; i < greediestAvailableNewings.Length; i++) + if (constructors.Any()) { - greediestAvailableNewings[i].AddTo(constructions, key); + var greediestAvailableNewingInfos = GetGreediestAvailableNewingInfos( + constructors, + key, + greediestUnconditionalFactoryInfo); + + for (i = 0; i < greediestAvailableNewingInfos.Length;) + { + greediestAvailableNewingInfos[i++].AddTo(constructionInfos, key); + } } - if (constructions.None() && mapperData.TargetMemberIsUserStruct()) + if (constructionInfos.None() && mapperData.TargetMemberIsUserStruct()) { - constructions.Add(Construction.NewStruct(mapperData.TargetInstance.Type)); + constructionInfos.Add(new StructInfo(mapperData.TargetInstance.Type)); } } - private static ConstructionData[] GetGreediestAvailableFactories(ConstructionKey key) + private static ConstructionDataInfo[] GetGreediestAvailableFactoryInfos(ConstructionKey key) { var mapperData = key.MappingData.MapperData; @@ -150,9 +133,9 @@ private static ConstructionData[] GetGreediestAvailableFactories(Con .GetPublicStaticMethods() .Filter(m => IsFactoryMethod(m, mapperData.TargetInstance.Type)); - return CreateConstructionData( + return CreateConstructionInfo( candidateFactoryMethods, - fm => new ConstructionData(fm, Expression.Call, key, priority: 1)); + fm => new ConstructionDataInfo(fm, Expression.Call, key, priority: 1)); } private static bool IsFactoryMethod(MethodInfo method, Type targetType) @@ -161,20 +144,20 @@ private static bool IsFactoryMethod(MethodInfo method, Type targetType) (method.Name.StartsWith("Create", Ordinal) || method.Name.StartsWith("Get", Ordinal)); } - private static ConstructionData[] GetGreediestAvailableNewings( + private static ConstructionDataInfo[] GetGreediestAvailableNewingInfos( IEnumerable constructors, ConstructionKey key, - ConstructionData greediestUnconditionalFactory) + IBasicConstructionInfo greediestUnconditionalFactoryInfo) { var candidateConstructors = constructors - .Filter(ctor => IsCandidateCtor(ctor, greediestUnconditionalFactory)); + .Filter(ctor => IsCandidateCtor(ctor, greediestUnconditionalFactoryInfo)); - return CreateConstructionData( + return CreateConstructionInfo( candidateConstructors, - ctor => new ConstructionData(ctor, Expression.New, key, priority: 0)); + ctor => new ConstructionDataInfo(ctor, Expression.New, key, priority: 0)); } - private static bool IsCandidateCtor(MethodBase ctor, ConstructionData candidateFactoryMethod) + private static bool IsCandidateCtor(MethodBase ctor, IBasicConstructionInfo candidateFactoryMethod) { var ctorCarameters = ctor.GetParameters(); @@ -190,9 +173,9 @@ private static bool IsNotCopyConstructor(Type type, IList ctorPar return ctorParameters.None(p => p.ParameterType == type); } - private static ConstructionData[] CreateConstructionData( + private static ConstructionDataInfo[] CreateConstructionInfo( IEnumerable invokables, - Func> dataFactory) + Func> dataFactory) where T : MethodBase { return invokables @@ -203,7 +186,47 @@ private static ConstructionData[] CreateConstructionData( .ToArray(); } - public void Reset() => _constructorsCache.Empty(); + public Expression GetNewObjectCreation(IObjectMappingData mappingData) + { + var cachedInfos = GetNewObjectCreationInfos(mappingData, out var constructionKey); + + if (cachedInfos.None()) + { + return null; + } + + constructionKey.MappingData = mappingData; + constructionKey.Infos = cachedInfos; + + var cachedConstruction = _constructionsCache.GetOrAdd(constructionKey, key => + { + var constructions = key.Infos.ProjectToArray(info => info.ToConstruction()); + var construction = Construction.For(constructions, key); + + key.AddSourceMemberTypeTesterIfRequired(); + key.MappingData = null; + + return construction; + }); + + mappingData.MapperData.Context.UsesMappingDataObjectAsParameter = cachedConstruction.UsesMappingDataObjectParameter; + + var constructionExpression = cachedConstruction.GetConstruction(mappingData); + + return constructionExpression; + } + + public Expression GetFactoryMethodObjectCreationOrNull(IObjectMappingData mappingData) + { + var key = new ConstructionKey(mappingData); + var factoryData = GetGreediestAvailableFactoryInfos(key); + + return factoryData.Any() + ? factoryData.First().ToConstruction().With(key).GetConstruction(mappingData) + : null; + } + + public void Reset() => _constructionsCache.Empty(); #region Helper Classes @@ -221,6 +244,8 @@ public ConstructionKey(IObjectMappingData mappingData) _targetMember = mappingData.MapperData.TargetMember; } + public IList Infos { get; set; } + public override bool Equals(object obj) { var otherKey = (ConstructionKey)obj; @@ -240,21 +265,98 @@ public override bool Equals(object obj) public override int GetHashCode() => 0; } - private class ConstructionData : IConstructionInfo + public interface IBasicConstructionInfo + { + bool IsConfigured { get; } + + bool IsUnconditional { get; } + + int ParameterCount { get; } + + int Priority { get; } + } + + private interface IConstructionInfo : IBasicConstructionInfo, IComparable + { + Construction ToConstruction(); + } + + private abstract class ConstructionInfoBase : IConstructionInfo + { + public bool IsConfigured { get; protected set; } + + public bool IsUnconditional { get; protected set; } + + public int ParameterCount { get; protected set; } + + public int Priority { get; protected set; } + + public abstract Construction ToConstruction(); + + public int CompareTo(IConstructionInfo other) + { + // ReSharper disable once ImpureMethodCallOnReadonlyValueField + var isConfiguredComparison = other.IsConfigured.CompareTo(IsConfigured); + + if (isConfiguredComparison != 0) + { + return isConfiguredComparison; + } + + var conditionalComparison = IsUnconditional.CompareTo(other.IsUnconditional); + + if (conditionalComparison != 0) + { + return conditionalComparison; + } + + var paramCountComparison = ParameterCount.CompareTo(other.ParameterCount); + + if (paramCountComparison != 0) + { + return paramCountComparison; + } + + var priorityComparison = other.Priority.CompareTo(Priority); + + return priorityComparison; + } + } + + private sealed class ConfiguredFactoryInfo : ConstructionInfoBase + { + private readonly ConfiguredObjectFactory _configuredFactory; + private readonly IMemberMapperData _mapperData; + + public ConfiguredFactoryInfo(ConfiguredObjectFactory configuredFactory, IMemberMapperData mapperData) + { + _configuredFactory = configuredFactory; + _mapperData = mapperData; + IsConfigured = true; + IsUnconditional = !_configuredFactory.HasConfiguredCondition; + } + + public override Construction ToConstruction() => new Construction(_configuredFactory, _mapperData); + } + + private sealed class ConstructionDataInfo : ConstructionInfoBase where TInvokable : MethodBase { - private readonly Tuple[] _argumentDataSources; + private readonly TInvokable _invokable; + private readonly Func, Expression> _constructionFactory; - public ConstructionData( + public ConstructionDataInfo( TInvokable invokable, Func, Expression> constructionFactory, ConstructionKey key, int priority) { - var argumentDataSources = GetArgumentDataSources(invokable, key); + _invokable = invokable; + _constructionFactory = constructionFactory; - CanBeInvoked = argumentDataSources.All(ds => ds.Item2.HasValue); - ParameterCount = argumentDataSources.Length; + ArgumentDataSources = GetArgumentDataSources(invokable, key); + CanBeInvoked = ArgumentDataSources.All(ds => ds.HasValue); + ParameterCount = ArgumentDataSources.Length; Priority = priority; if (!CanBeInvoked) @@ -262,27 +364,84 @@ public ConstructionData( return; } + IsUnconditional = !ArgumentDataSources.Any(ds => ds.MapperData.TargetMember.IsComplex && ds.IsConditional); + } + + private static DataSourceSet[] GetArgumentDataSources(TInvokable invokable, ConstructionKey key) + { + return invokable + .GetParameters() + .ProjectToArray(p => + { + var parameterMapperData = new ChildMemberMapperData( + key.MappingData.MapperData.TargetMember.Append(Member.ConstructorParameter(p)), + key.MappingData.MapperData); + + var memberMappingData = key.MappingData.GetChildMappingData(parameterMapperData); + var dataSources = DataSourceFinder.FindFor(memberMappingData); + + return dataSources; + }); + } + + public DataSourceSet[] ArgumentDataSources { get; } + + public bool CanBeInvoked { get; } + + public void AddTo(IList constructionInfos, ConstructionKey key) + { + if (ParameterCount > 0) + { + var dataSources = key.MappingData.MapperData.DataSourcesByTargetMember; + + foreach (var dataSourceSet in ArgumentDataSources.Filter(ds => !dataSources.ContainsKey(ds.MapperData.TargetMember))) + { + dataSources.Add(dataSourceSet.MapperData.TargetMember, dataSourceSet); + } + } + + constructionInfos.AddSorted(this); + } + + public Expression GetConstructionExpression(IList argumentValues) + => _constructionFactory.Invoke(_invokable, argumentValues); + + public override Construction ToConstruction() => new ConstructionData(this).Construction; + } + + private sealed class StructInfo : ConstructionInfoBase + { + private readonly Type _targetType; + + public StructInfo(Type targetType) + { + _targetType = targetType; IsUnconditional = true; + } + + public override Construction ToConstruction() => new Construction(Expression.New(_targetType)); + } + private sealed class ConstructionData + where TInvokable : MethodBase + { + public ConstructionData(ConstructionDataInfo info) + { Expression constructionExpression; - if (argumentDataSources.None()) + if (info.ArgumentDataSources.None()) { - constructionExpression = constructionFactory.Invoke(invokable, Enumerable.EmptyArray); - Construction = new Construction(this, constructionExpression); + constructionExpression = info.GetConstructionExpression(Enumerable.EmptyArray); + Construction = new Construction(constructionExpression); return; } - _argumentDataSources = argumentDataSources; - var variables = default(List); - var argumentValues = new List(ParameterCount); + var argumentValues = new List(info.ParameterCount); var condition = default(Expression); - foreach (var argumentDataSource in argumentDataSources) + foreach (var dataSources in info.ArgumentDataSources) { - var dataSources = argumentDataSource.Item2; - if (dataSources.Variables.Any()) { if (variables == null) @@ -297,13 +456,11 @@ public ConstructionData( argumentValues.Add(dataSources.ValueExpression); - if (!argumentDataSource.Item1.IsComplex || !dataSources.IsConditional) + if (info.IsUnconditional) { continue; } - IsUnconditional = false; - var dataSourceCondition = BuildConditions(dataSources); if (condition == null) @@ -315,28 +472,11 @@ public ConstructionData( condition = Expression.AndAlso(condition, dataSourceCondition); } - constructionExpression = constructionFactory.Invoke(invokable, argumentValues); + constructionExpression = info.GetConstructionExpression(argumentValues); Construction = variables.NoneOrNull() - ? new Construction(this, constructionExpression, condition) - : new Construction(this, Expression.Block(variables, constructionExpression), condition); - } - - private static Tuple[] GetArgumentDataSources(TInvokable invokable, ConstructionKey key) - { - return invokable - .GetParameters() - .ProjectToArray(p => - { - var parameterMapperData = new ChildMemberMapperData( - key.MappingData.MapperData.TargetMember.Append(Member.ConstructorParameter(p)), - key.MappingData.MapperData); - - var memberMappingData = key.MappingData.GetChildMappingData(parameterMapperData); - var dataSources = DataSourceFinder.FindFor(memberMappingData); - - return Tuple.Create(memberMappingData.MapperData.TargetMember, dataSources); - }); + ? new Construction(constructionExpression, condition) + : new Construction(Expression.Block(variables, constructionExpression), condition); } private static Expression BuildConditions(DataSourceSet dataSources) @@ -357,57 +497,18 @@ private static Expression BuildConditions(DataSourceSet dataSources) return conditions; } - public bool CanBeInvoked { get; } - - public bool IsUnconditional { get; } - - public int ParameterCount { get; } - - public int Priority { get; } - public Construction Construction { get; } - - public void AddTo(IList constructions, ConstructionKey key) - { - if (ParameterCount > 0) - { - var dataSources = key.MappingData.MapperData.DataSourcesByTargetMember; - - foreach (var memberAndDataSourceSet in _argumentDataSources.Filter(ads => !dataSources.ContainsKey(ads.Item1))) - { - dataSources.Add(memberAndDataSourceSet.Item1, memberAndDataSourceSet.Item2); - } - } - - constructions.AddSorted(Construction); - } - } - - private interface IConstructionInfo - { - int ParameterCount { get; } - - int Priority { get; } } - private class Construction : IConditionallyChainable, IComparable + private class Construction : IConditionallyChainable { private readonly Expression _construction; - private readonly bool _isConfigured; - private readonly IConstructionInfo _info; private ParameterExpression _mappingDataObject; public Construction(ConfiguredObjectFactory configuredFactory, IMemberMapperData mapperData) : this(configuredFactory.Create(mapperData), configuredFactory.GetConditionOrNull(mapperData)) { UsesMappingDataObjectParameter = configuredFactory.UsesMappingDataObjectParameter; - _isConfigured = true; - } - - public Construction(IConstructionInfo info, Expression construction, Expression condition = null) - : this(construction, condition) - { - _info = info; } private Construction(IList constructions) @@ -416,7 +517,7 @@ private Construction(IList constructions) UsesMappingDataObjectParameter = constructions.Any(c => c.UsesMappingDataObjectParameter); } - private Construction(Expression construction, Expression condition = null) + public Construction(Expression construction, Expression condition = null) { _construction = construction; Condition = condition; @@ -424,13 +525,6 @@ private Construction(Expression construction, Expression condition = null) #region Factory Methods - public static Construction NewStruct(Type type) - { - var parameterlessNew = Expression.New(type); - - return new Construction(parameterlessNew); - } - public static Construction For(IList constructions, ConstructionKey key) { var construction = constructions.HasOne() @@ -450,8 +544,6 @@ public Construction With(ConstructionKey key) public Expression PreCondition => null; - public bool IsUnconditional => Condition == null; - public Expression Condition { get; } Expression IConditionallyChainable.Value => _construction; @@ -460,35 +552,6 @@ public Construction With(ConstructionKey key) public Expression GetConstruction(IObjectMappingData mappingData) => _construction.Replace(_mappingDataObject, mappingData.MapperData.MappingDataObject); - - public int CompareTo(Construction other) - { - // ReSharper disable once ImpureMethodCallOnReadonlyValueField - var isConfiguredComparison = other._isConfigured.CompareTo(_isConfigured); - - if (isConfiguredComparison != 0) - { - return isConfiguredComparison; - } - - var conditionalComparison = IsUnconditional.CompareTo(other.IsUnconditional); - - if (conditionalComparison != 0) - { - return conditionalComparison; - } - - var paramCountComparison = _info.ParameterCount.CompareTo(other._info.ParameterCount); - - if (paramCountComparison != 0) - { - return paramCountComparison; - } - - var priorityComparison = other._info.Priority.CompareTo(_info.Priority); - - return priorityComparison; - } } #endregion From f79167daebddc30cc00b7909273e011205c3c02f Mon Sep 17 00:00:00 2001 From: Steve Wilkes Date: Thu, 9 May 2019 07:40:32 +0100 Subject: [PATCH 2/5] Checking object constructability using ConstructionInfos --- AgileMapper/MappingDataExtensions.cs | 24 +++++++------------ .../ComplexTypeConstructionFactory.cs | 9 +++++-- .../PopulationExpressionFactoryBase.cs | 6 ++++- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/AgileMapper/MappingDataExtensions.cs b/AgileMapper/MappingDataExtensions.cs index 4b369be75..0e9f4d65a 100644 --- a/AgileMapper/MappingDataExtensions.cs +++ b/AgileMapper/MappingDataExtensions.cs @@ -1,10 +1,6 @@ namespace AgileObjects.AgileMapper { -#if NET35 - using Microsoft.Scripting.Ast; -#else - using System.Linq.Expressions; -#endif + using System.Linq; using DataSources; using Extensions.Internal; using Members; @@ -16,7 +12,14 @@ public static bool IsStandalone(this IObjectMappingData mappingData) => mappingData.IsRoot || mappingData.MappingTypes.RuntimeTypesNeeded; public static bool IsTargetConstructable(this IObjectMappingData mappingData) - => mappingData.GetTargetObjectCreation() != null; + { + return mappingData + .MapperData + .MapperContext + .ConstructionFactory + .GetNewObjectCreationInfos(mappingData) + .Any(); + } public static bool IsConstructableFromToTargetDataSource(this IObjectMappingData mappingData) => mappingData.GetToTargetDataSourceOrNullForTargetType() != null; @@ -48,15 +51,6 @@ public static IConfiguredDataSource GetToTargetDataSourceOrNullForTargetType(thi return null; } - public static Expression GetTargetObjectCreation(this IObjectMappingData mappingData) - { - return mappingData - .MapperData - .MapperContext - .ConstructionFactory - .GetNewObjectCreation(mappingData); - } - public static bool HasSameTypedConfiguredDataSource(this IObjectMappingData mappingData) { return diff --git a/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeConstructionFactory.cs b/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeConstructionFactory.cs index 5619beda2..c51ae7705 100644 --- a/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeConstructionFactory.cs +++ b/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeConstructionFactory.cs @@ -31,8 +31,12 @@ public ComplexTypeConstructionFactory(CacheSet mapperScopedCacheSet) _constructionsCache = mapperScopedCacheSet.CreateScoped(); } - public IList GetNewObjectCreationInfos(IObjectMappingData mappingData) - => (IList)GetNewObjectCreationInfos(mappingData, out _); + public IEnumerable GetNewObjectCreationInfos(IObjectMappingData mappingData) +#if NET35 + => GetNewObjectCreationInfos(mappingData, out _).Cast(); +#else + => GetNewObjectCreationInfos(mappingData, out _); +#endif private IList GetNewObjectCreationInfos( IObjectMappingData mappingData, @@ -52,6 +56,7 @@ private IList GetNewObjectCreationInfos( AddAutoConstructionInfos(constructionInfos, key); } + key.AddSourceMemberTypeTesterIfRequired(); key.MappingData = null; return constructionInfos.None() diff --git a/AgileMapper/ObjectPopulation/ComplexTypes/PopulationExpressionFactoryBase.cs b/AgileMapper/ObjectPopulation/ComplexTypes/PopulationExpressionFactoryBase.cs index 8b2ffa94a..5db9a1ad3 100644 --- a/AgileMapper/ObjectPopulation/ComplexTypes/PopulationExpressionFactoryBase.cs +++ b/AgileMapper/ObjectPopulation/ComplexTypes/PopulationExpressionFactoryBase.cs @@ -104,7 +104,11 @@ protected virtual Expression GetNewObjectCreation( IObjectMappingData mappingData, IList memberPopulations) { - return mappingData.GetTargetObjectCreation(); + return mappingData + .MapperData + .MapperContext + .ConstructionFactory + .GetNewObjectCreation(mappingData); } #region Object Registration From ff0b408adf97c502301e35cacb258c45b27314b7 Mon Sep 17 00:00:00 2001 From: Steve Wilkes Date: Thu, 9 May 2019 08:42:58 +0100 Subject: [PATCH 3/5] Tidying --- AgileMapper/MappingDataExtensions.cs | 2 +- .../ComplexTypeConstructionFactory.cs | 23 +++++-------------- .../ComplexTypes/IBasicConstructionInfo.cs | 13 +++++++++++ .../MemberInitPopulationExpressionFactory.cs | 8 +++---- .../PopulationExpressionFactoryBase.cs | 14 +++++------ 5 files changed, 31 insertions(+), 29 deletions(-) create mode 100644 AgileMapper/ObjectPopulation/ComplexTypes/IBasicConstructionInfo.cs diff --git a/AgileMapper/MappingDataExtensions.cs b/AgileMapper/MappingDataExtensions.cs index 0e9f4d65a..0cbfbc449 100644 --- a/AgileMapper/MappingDataExtensions.cs +++ b/AgileMapper/MappingDataExtensions.cs @@ -17,7 +17,7 @@ public static bool IsTargetConstructable(this IObjectMappingData mappingData) .MapperData .MapperContext .ConstructionFactory - .GetNewObjectCreationInfos(mappingData) + .GetTargetObjectCreationInfos(mappingData) .Any(); } diff --git a/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeConstructionFactory.cs b/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeConstructionFactory.cs index c51ae7705..a22cf3767 100644 --- a/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeConstructionFactory.cs +++ b/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeConstructionFactory.cs @@ -31,14 +31,14 @@ public ComplexTypeConstructionFactory(CacheSet mapperScopedCacheSet) _constructionsCache = mapperScopedCacheSet.CreateScoped(); } - public IEnumerable GetNewObjectCreationInfos(IObjectMappingData mappingData) + public IEnumerable GetTargetObjectCreationInfos(IObjectMappingData mappingData) #if NET35 - => GetNewObjectCreationInfos(mappingData, out _).Cast(); + => GetTargetObjectCreationInfos(mappingData, out _).Cast(); #else - => GetNewObjectCreationInfos(mappingData, out _); + => GetTargetObjectCreationInfos(mappingData, out _); #endif - private IList GetNewObjectCreationInfos( + private IList GetTargetObjectCreationInfos( IObjectMappingData mappingData, out ConstructionKey constructionKey) { @@ -191,9 +191,9 @@ private static ConstructionDataInfo[] CreateConstructionInfo( .ToArray(); } - public Expression GetNewObjectCreation(IObjectMappingData mappingData) + public Expression GetTargetObjectCreation(IObjectMappingData mappingData) { - var cachedInfos = GetNewObjectCreationInfos(mappingData, out var constructionKey); + var cachedInfos = GetTargetObjectCreationInfos(mappingData, out var constructionKey); if (cachedInfos.None()) { @@ -270,17 +270,6 @@ public override bool Equals(object obj) public override int GetHashCode() => 0; } - public interface IBasicConstructionInfo - { - bool IsConfigured { get; } - - bool IsUnconditional { get; } - - int ParameterCount { get; } - - int Priority { get; } - } - private interface IConstructionInfo : IBasicConstructionInfo, IComparable { Construction ToConstruction(); diff --git a/AgileMapper/ObjectPopulation/ComplexTypes/IBasicConstructionInfo.cs b/AgileMapper/ObjectPopulation/ComplexTypes/IBasicConstructionInfo.cs new file mode 100644 index 000000000..acf090fc4 --- /dev/null +++ b/AgileMapper/ObjectPopulation/ComplexTypes/IBasicConstructionInfo.cs @@ -0,0 +1,13 @@ +namespace AgileObjects.AgileMapper.ObjectPopulation.ComplexTypes +{ + internal interface IBasicConstructionInfo + { + bool IsConfigured { get; } + + bool IsUnconditional { get; } + + int ParameterCount { get; } + + int Priority { get; } + } +} \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/ComplexTypes/MemberInitPopulationExpressionFactory.cs b/AgileMapper/ObjectPopulation/ComplexTypes/MemberInitPopulationExpressionFactory.cs index 0784408f6..14c5010a2 100644 --- a/AgileMapper/ObjectPopulation/ComplexTypes/MemberInitPopulationExpressionFactory.cs +++ b/AgileMapper/ObjectPopulation/ComplexTypes/MemberInitPopulationExpressionFactory.cs @@ -1,13 +1,13 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.ComplexTypes { using System.Collections.Generic; - using Extensions.Internal; - using Members.Population; #if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using Extensions.Internal; + using Members.Population; internal class MemberInitPopulationExpressionFactory : PopulationExpressionFactoryBase { @@ -18,9 +18,9 @@ protected override IEnumerable GetPopulationExpressionsFor( yield return memberPopulator.GetPopulation(); } - protected override Expression GetNewObjectCreation(IObjectMappingData mappingData, IList memberPopulations) + protected override Expression GetTargetObjectCreation(IObjectMappingData mappingData, IList memberPopulations) { - var objectCreation = base.GetNewObjectCreation(mappingData, memberPopulations); + var objectCreation = base.GetTargetObjectCreation(mappingData, memberPopulations); if (objectCreation == null) { diff --git a/AgileMapper/ObjectPopulation/ComplexTypes/PopulationExpressionFactoryBase.cs b/AgileMapper/ObjectPopulation/ComplexTypes/PopulationExpressionFactoryBase.cs index 5db9a1ad3..133cfa07c 100644 --- a/AgileMapper/ObjectPopulation/ComplexTypes/PopulationExpressionFactoryBase.cs +++ b/AgileMapper/ObjectPopulation/ComplexTypes/PopulationExpressionFactoryBase.cs @@ -2,15 +2,15 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.ComplexTypes { using System.Collections.Generic; using System.Linq; - using Extensions.Internal; - using Members; - using Members.Population; - using NetStandardPolyfills; #if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using Extensions.Internal; + using Members; + using Members.Population; + using NetStandardPolyfills; using static CallbackPosition; internal abstract class PopulationExpressionFactoryBase @@ -92,7 +92,7 @@ private Expression GetLocalVariableInstantiation( IObjectMappingData mappingData) { var localVariableValue = TargetObjectResolutionFactory.GetObjectResolution( - GetNewObjectCreation, + GetTargetObjectCreation, mappingData, memberPopulations, assignCreatedObject); @@ -100,7 +100,7 @@ private Expression GetLocalVariableInstantiation( return mappingData.MapperData.LocalVariable.AssignTo(localVariableValue); } - protected virtual Expression GetNewObjectCreation( + protected virtual Expression GetTargetObjectCreation( IObjectMappingData mappingData, IList memberPopulations) { @@ -108,7 +108,7 @@ protected virtual Expression GetNewObjectCreation( .MapperData .MapperContext .ConstructionFactory - .GetNewObjectCreation(mappingData); + .GetTargetObjectCreation(mappingData); } #region Object Registration From e39b7b754e2f464bfb122e7cf5195c47bda9b7c9 Mon Sep 17 00:00:00 2001 From: Steve Wilkes Date: Thu, 9 May 2019 10:48:38 +0100 Subject: [PATCH 4/5] - Populating members with a matching constructor parameter that might not be used - Respecting single-data-source population conditions for readonly member populations --- AgileMapper/DataSources/DataSourceSet.cs | 17 +++-- AgileMapper/MappingDataExtensions.cs | 9 ++- .../Population/MemberPopulationFactoryBase.cs | 2 +- .../Population/MemberPopulatorFactory.cs | 19 +++--- .../ComplexTypeConstructionFactory.cs | 67 ++++++++++++------- .../ComplexTypes/IBasicConstructionInfo.cs | 10 ++- AgileMapper/Validation/MappingValidator.cs | 4 +- 7 files changed, 81 insertions(+), 47 deletions(-) diff --git a/AgileMapper/DataSources/DataSourceSet.cs b/AgileMapper/DataSources/DataSourceSet.cs index bb226972a..b66f1a425 100644 --- a/AgileMapper/DataSources/DataSourceSet.cs +++ b/AgileMapper/DataSources/DataSourceSet.cs @@ -79,14 +79,21 @@ private Expression BuildValueExpression() for (var i = _dataSources.Count - 1; i >= 0;) { + var isFirstDataSource = value == default(Expression); var dataSource = _dataSources[i--]; - value = dataSource.AddPreConditionIfNecessary(value == default(Expression) - ? dataSource.Value - : Expression.Condition( + var dataSourceValue = dataSource.IsConditional + ? Expression.Condition( dataSource.Condition, - dataSource.Value.GetConversionTo(value.Type), - value)); + isFirstDataSource + ? dataSource.Value + : dataSource.Value.GetConversionTo(value.Type), + isFirstDataSource + ? dataSource.Value.Type.ToDefaultExpression() + : value) + : dataSource.Value; + + value = dataSource.AddPreConditionIfNecessary(dataSourceValue); } return value; diff --git a/AgileMapper/MappingDataExtensions.cs b/AgileMapper/MappingDataExtensions.cs index 0cbfbc449..28b01cdde 100644 --- a/AgileMapper/MappingDataExtensions.cs +++ b/AgileMapper/MappingDataExtensions.cs @@ -1,10 +1,11 @@ namespace AgileObjects.AgileMapper { - using System.Linq; + using System.Collections.Generic; using DataSources; using Extensions.Internal; using Members; using ObjectPopulation; + using ObjectPopulation.ComplexTypes; internal static class MappingDataExtensions { @@ -12,13 +13,15 @@ public static bool IsStandalone(this IObjectMappingData mappingData) => mappingData.IsRoot || mappingData.MappingTypes.RuntimeTypesNeeded; public static bool IsTargetConstructable(this IObjectMappingData mappingData) + => GetTargetObjectCreationInfos(mappingData).Any(); + + public static IList GetTargetObjectCreationInfos(this IObjectMappingData mappingData) { return mappingData .MapperData .MapperContext .ConstructionFactory - .GetTargetObjectCreationInfos(mappingData) - .Any(); + .GetTargetObjectCreationInfos(mappingData); } public static bool IsConstructableFromToTargetDataSource(this IObjectMappingData mappingData) diff --git a/AgileMapper/Members/Population/MemberPopulationFactoryBase.cs b/AgileMapper/Members/Population/MemberPopulationFactoryBase.cs index 1e4b35824..ec8b55d77 100644 --- a/AgileMapper/Members/Population/MemberPopulationFactoryBase.cs +++ b/AgileMapper/Members/Population/MemberPopulationFactoryBase.cs @@ -1,12 +1,12 @@ namespace AgileObjects.AgileMapper.Members.Population { using System.Collections.Generic; - using Extensions.Internal; #if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using Extensions.Internal; internal abstract class MemberPopulationFactoryBase : IMemberPopulationFactory { diff --git a/AgileMapper/Members/Population/MemberPopulatorFactory.cs b/AgileMapper/Members/Population/MemberPopulatorFactory.cs index 416fcfeab..731696cd4 100644 --- a/AgileMapper/Members/Population/MemberPopulatorFactory.cs +++ b/AgileMapper/Members/Population/MemberPopulatorFactory.cs @@ -1,18 +1,18 @@ -namespace AgileObjects.AgileMapper.Members.Population + namespace AgileObjects.AgileMapper.Members.Population { using System; using System.Collections.Generic; +#if NET35 + using Microsoft.Scripting.Ast; +#else + using System.Linq.Expressions; +#endif using Configuration; using DataSources.Finders; using Extensions; using Extensions.Internal; using Members; using ObjectPopulation; -#if NET35 - using Microsoft.Scripting.Ast; -#else - using System.Linq.Expressions; -#endif internal class MemberPopulatorFactory { @@ -42,7 +42,7 @@ public IEnumerable Create(IObjectMappingData mappingData) { return memberPopulation; } - + return null; }) .WhereNotNull(); @@ -109,9 +109,10 @@ private static bool TargetMemberWillBePopulatedByCtor(IMemberMapperData mapperDa return false; } + var creationInfos = mappingData.GetTargetObjectCreationInfos(); - - return true; + return creationInfos.Any() && + creationInfos.All(ci => ci.IsUnconditional && ci.HasCtorParameterFor(mapperData.TargetMember.LeafMember)); } private static bool TargetMemberIsUnconditionallyIgnored( diff --git a/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeConstructionFactory.cs b/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeConstructionFactory.cs index a22cf3767..77c60c13f 100644 --- a/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeConstructionFactory.cs +++ b/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeConstructionFactory.cs @@ -31,15 +31,11 @@ public ComplexTypeConstructionFactory(CacheSet mapperScopedCacheSet) _constructionsCache = mapperScopedCacheSet.CreateScoped(); } - public IEnumerable GetTargetObjectCreationInfos(IObjectMappingData mappingData) -#if NET35 - => GetTargetObjectCreationInfos(mappingData, out _).Cast(); -#else - => GetTargetObjectCreationInfos(mappingData, out _); -#endif + public IList GetTargetObjectCreationInfos(IObjectMappingData mappingData) + => GetTargetObjectCreationInfos(mappingData, out _).ProjectToArray(c => (IBasicConstructionInfo)c); private IList GetTargetObjectCreationInfos( - IObjectMappingData mappingData, + IObjectMappingData mappingData, out ConstructionKey constructionKey) { return _constructionInfosCache.GetOrAdd(constructionKey = new ConstructionKey(mappingData), key => @@ -138,9 +134,7 @@ private static ConstructionDataInfo[] GetGreediestAvailableFactoryIn .GetPublicStaticMethods() .Filter(m => IsFactoryMethod(m, mapperData.TargetInstance.Type)); - return CreateConstructionInfo( - candidateFactoryMethods, - fm => new ConstructionDataInfo(fm, Expression.Call, key, priority: 1)); + return CreateConstructionInfo(candidateFactoryMethods, fm => new FactoryMethodInfo(fm, key)); } private static bool IsFactoryMethod(MethodInfo method, Type targetType) @@ -157,9 +151,7 @@ private static ConstructionDataInfo[] GetGreediestAvailableNewi var candidateConstructors = constructors .Filter(ctor => IsCandidateCtor(ctor, greediestUnconditionalFactoryInfo)); - return CreateConstructionInfo( - candidateConstructors, - ctor => new ConstructionDataInfo(ctor, Expression.New, key, priority: 0)); + return CreateConstructionInfo(candidateConstructors, ctor => new ObjectNewingInfo(ctor, key)); } private static bool IsCandidateCtor(MethodBase ctor, IBasicConstructionInfo candidateFactoryMethod) @@ -285,6 +277,8 @@ private abstract class ConstructionInfoBase : IConstructionInfo public int Priority { get; protected set; } + public virtual bool HasCtorParameterFor(Member targetMember) => false; + public abstract Construction ToConstruction(); public int CompareTo(IConstructionInfo other) @@ -333,21 +327,14 @@ public ConfiguredFactoryInfo(ConfiguredObjectFactory configuredFactory, IMemberM public override Construction ToConstruction() => new Construction(_configuredFactory, _mapperData); } - private sealed class ConstructionDataInfo : ConstructionInfoBase + private abstract class ConstructionDataInfo : ConstructionInfoBase where TInvokable : MethodBase { - private readonly TInvokable _invokable; - private readonly Func, Expression> _constructionFactory; - - public ConstructionDataInfo( + protected ConstructionDataInfo( TInvokable invokable, - Func, Expression> constructionFactory, ConstructionKey key, int priority) { - _invokable = invokable; - _constructionFactory = constructionFactory; - ArgumentDataSources = GetArgumentDataSources(invokable, key); CanBeInvoked = ArgumentDataSources.All(ds => ds.HasValue); ParameterCount = ArgumentDataSources.Length; @@ -397,12 +384,44 @@ public void AddTo(IList constructionInfos, ConstructionKey ke constructionInfos.AddSorted(this); } - public Expression GetConstructionExpression(IList argumentValues) - => _constructionFactory.Invoke(_invokable, argumentValues); + public abstract Expression GetConstructionExpression(IList argumentValues); public override Construction ToConstruction() => new ConstructionData(this).Construction; } + private sealed class ObjectNewingInfo : ConstructionDataInfo + { + private readonly ConstructorInfo _ctor; + private readonly string[] _parameterNames; + + public ObjectNewingInfo(ConstructorInfo ctor, ConstructionKey key) + : base(ctor, key, priority: 0) + { + _ctor = ctor; + _parameterNames = _ctor.GetParameters().ProjectToArray(p => p.Name); + } + + public override bool HasCtorParameterFor(Member targetMember) + => _parameterNames.Contains(targetMember.Name, StringComparer.OrdinalIgnoreCase); + + public override Expression GetConstructionExpression(IList argumentValues) + => Expression.New(_ctor, argumentValues); + } + + private sealed class FactoryMethodInfo : ConstructionDataInfo + { + private readonly MethodInfo _factoryMethod; + + public FactoryMethodInfo(MethodInfo factoryMethod, ConstructionKey key) + : base(factoryMethod, key, priority: 1) + { + _factoryMethod = factoryMethod; + } + + public override Expression GetConstructionExpression(IList argumentValues) + => Expression.Call(_factoryMethod, argumentValues); + } + private sealed class StructInfo : ConstructionInfoBase { private readonly Type _targetType; diff --git a/AgileMapper/ObjectPopulation/ComplexTypes/IBasicConstructionInfo.cs b/AgileMapper/ObjectPopulation/ComplexTypes/IBasicConstructionInfo.cs index acf090fc4..1af3d52c5 100644 --- a/AgileMapper/ObjectPopulation/ComplexTypes/IBasicConstructionInfo.cs +++ b/AgileMapper/ObjectPopulation/ComplexTypes/IBasicConstructionInfo.cs @@ -1,13 +1,17 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.ComplexTypes { + using Members; + internal interface IBasicConstructionInfo { bool IsConfigured { get; } - + bool IsUnconditional { get; } - + int ParameterCount { get; } - + int Priority { get; } + + bool HasCtorParameterFor(Member targetMember); } } \ No newline at end of file diff --git a/AgileMapper/Validation/MappingValidator.cs b/AgileMapper/Validation/MappingValidator.cs index 2929d09e3..754be1639 100644 --- a/AgileMapper/Validation/MappingValidator.cs +++ b/AgileMapper/Validation/MappingValidator.cs @@ -82,7 +82,7 @@ private static ICollection GetIncompleteMappingPlanData( IsUnmappable = !md.IsRoot && md.TargetMember.IsComplex && - md.DataSourcesByTargetMember.None(), + md.DataSourcesByTargetMember.None(ds => ds.Value.HasValue), UnmappedMembers = md .DataSourcesByTargetMember .Filter(pair => !pair.Value.HasValue) @@ -98,7 +98,7 @@ private static ICollection GetIncompleteMappingPlanData( UnmappableTargetTypes = g .Filter(d => d.IsUnmappable) .Project(d => d.MapperData) - .ToList(), + .ToArray(), UnmappedMembers = g .SelectMany(d => d.UnmappedMembers) .ToDictionary(kvp => kvp.Key, kvp => kvp.Value), From e1762ee1c679cb8842ee71ef343b0b95a722e8e3 Mon Sep 17 00:00:00 2001 From: Steve Wilkes Date: Thu, 9 May 2019 18:41:35 +0100 Subject: [PATCH 5/5] Fixing mapping validation detection of unconstructable members --- .../WhenValidatingMappings.cs | 410 +++++++++++------- .../CustomDataSourceTargetMemberSpecifier.cs | 4 +- .../Dictionaries/DictionaryTargetMember.cs | 2 +- AgileMapper/Members/MemberExtensions.cs | 7 + .../Members/MemberMapperDataExtensions.cs | 2 +- AgileMapper/Members/QualifiedMember.cs | 5 +- AgileMapper/ObjectPopulation/ObjectMapper.cs | 8 +- .../ObjectPopulation/ObjectMapperData.cs | 2 +- AgileMapper/Validation/MappingValidator.cs | 44 +- 9 files changed, 312 insertions(+), 172 deletions(-) diff --git a/AgileMapper.UnitTests/WhenValidatingMappings.cs b/AgileMapper.UnitTests/WhenValidatingMappings.cs index d98ac94b6..325e0a420 100644 --- a/AgileMapper.UnitTests/WhenValidatingMappings.cs +++ b/AgileMapper.UnitTests/WhenValidatingMappings.cs @@ -17,255 +17,369 @@ public class WhenValidatingMappings [Fact] public void ShouldSupportCachedMapperValidation() { - using (var mapper = Mapper.CreateNew()) + Should.NotThrow(() => { - mapper.GetPlanFor>().ToANew>(); + using (var mapper = Mapper.CreateNew()) + { + mapper.GetPlanFor>().ToANew>(); - Should.NotThrow(() => mapper.ThrowNowIfAnyMappingPlanIsIncomplete()); - } + mapper.ThrowNowIfAnyMappingPlanIsIncomplete(); + } + }); } [Fact] public void ShouldErrorIfCachedMappingMembersHaveNoDataSources() { - using (var mapper = Mapper.CreateNew()) + var validationEx = Should.Throw(() => { - mapper.GetPlanFor(new { Thingy = default(string) }).ToANew>(); - - var validationEx = Should.Throw(() => - mapper.ThrowNowIfAnyMappingPlanIsIncomplete()); - - validationEx.Message.ShouldContain("AnonymousType -> PublicProperty"); - validationEx.Message.ShouldContain("Rule set: CreateNew"); - validationEx.Message.ShouldContain("Unmapped target members"); - validationEx.Message.ShouldContain("PublicProperty.Value"); - } + using (var mapper = Mapper.CreateNew()) + { + mapper.GetPlanFor(new { Thingy = default(string) }).ToANew>(); + + mapper.ThrowNowIfAnyMappingPlanIsIncomplete(); + } + }); + + validationEx.Message.ShouldContain("AnonymousType -> PublicProperty"); + validationEx.Message.ShouldContain("Rule set: CreateNew"); + validationEx.Message.ShouldContain("Unmapped target members"); + validationEx.Message.ShouldContain("PublicProperty.Value"); } [Fact] public void ShouldErrorIfCachedNestedMappingMembersHaveNoDataSources() { - using (var mapper = Mapper.CreateNew()) + var validationEx = Should.Throw(() => { - mapper - .GetPlanFor(new + using (var mapper = Mapper.CreateNew()) + { + var exampleSource = new { Id = default(string), Title = default(int), Name = default(string) - }) - .Over(); - - var validationEx = Should.Throw(() => - mapper.ThrowNowIfAnyMappingPlanIsIncomplete()); - - validationEx.Message.ShouldContain(" -> Person"); - validationEx.Message.ShouldNotContain(" -> Person.Address"); - validationEx.Message.ShouldContain("Rule set: Overwrite"); - validationEx.Message.ShouldContain("Unmapped target members"); - validationEx.Message.ShouldContain("Person.Address"); - validationEx.Message.ShouldContain("Person.Address.Line1"); - validationEx.Message.ShouldContain("Person.Address.Line2"); - } + }; + + mapper.GetPlanFor(exampleSource).Over(); + + mapper.ThrowNowIfAnyMappingPlanIsIncomplete(); + } + }); + + validationEx.Message.ShouldContain(" -> Person"); + validationEx.Message.ShouldNotContain(" -> Person.Address"); + validationEx.Message.ShouldContain("Rule set: Overwrite"); + validationEx.Message.ShouldContain("Unmapped target members"); + validationEx.Message.ShouldContain("Person.Address"); + validationEx.Message.ShouldContain("Person.Address.Line1"); + validationEx.Message.ShouldContain("Person.Address.Line2"); } [Fact] public void ShouldNotErrorIfCachedMappingMemberIsIgnored() { - using (var mapper = Mapper.CreateNew()) + Should.NotThrow(() => { - mapper.WhenMapping - .From>().To>() - .Ignore(pf => pf.Value); - - string plan = mapper.GetPlanFor>().OnTo>(); - - Should.NotThrow(() => mapper.ThrowNowIfAnyMappingPlanIsIncomplete()); - } + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From>().To>() + .Ignore(pf => pf.Value); + + // ReSharper disable once UnusedVariable + string plan = mapper.GetPlanFor>().OnTo>(); + + mapper.ThrowNowIfAnyMappingPlanIsIncomplete(); + } + }); } [Fact] public void ShouldNotErrorIfUnmappedCachedMappingMemberIsIgnored() { - using (var mapper = Mapper.CreateNew()) + Should.NotThrow(() => { - mapper.WhenMapping - .From(new { LaLaLa = default(int) }).To>() - .Ignore(pf => pf.Value); + var exampleSource = new { LaLaLa = default(int) }; - mapper.GetPlanFor(new { LaLaLa = default(int) }).OnTo>(); + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From(exampleSource).To>() + .Ignore(pf => pf.Value); - Should.NotThrow(() => mapper.ThrowNowIfAnyMappingPlanIsIncomplete()); - } + mapper.GetPlanFor(exampleSource).OnTo>(); + + mapper.ThrowNowIfAnyMappingPlanIsIncomplete(); + } + }); } [Fact] public void ShouldErrorIfComplexTypeMemberIsUnconstructable() { - using (var mapper = Mapper.CreateNew()) + var validationEx = Should.Throw(() => { - mapper - .GetPlanFor>>() - .ToANew>>(); - - var validationEx = Should.Throw(() => - mapper.ThrowNowIfAnyMappingPlanIsIncomplete()); - - validationEx.Message.ShouldContain("PublicField> -> PublicProperty>"); - validationEx.Message.ShouldContain("Unconstructable target Types"); - validationEx.Message.ShouldContain("PublicField -> PublicTwoParamCtor"); - validationEx.Message.ShouldContain("Unmapped target members"); - validationEx.Message.ShouldContain("PublicProperty>.Value"); - } + using (var mapper = Mapper.CreateNew()) + { + var exampleSource = new { Value = new { Value2 = default(int) } }; + + mapper + .GetPlanFor(exampleSource) + .ToANew>>(); + + mapper.ThrowNowIfAnyMappingPlanIsIncomplete(); + } + }); + + validationEx.Message.ShouldContain("AnonymousType> -> PublicProperty>"); + validationEx.Message.ShouldContain("Unconstructable target Types"); + validationEx.Message.ShouldContain("AnonymousType -> PublicTwoParamCtor"); + } + + [Fact] + public void ShouldRecogniseConstructableComplexTypeMembersWithNoMappableMembers() + { + var validationEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper + .GetPlanFor>>() + .ToANew>>(); + + mapper.ThrowNowIfAnyMappingPlanIsIncomplete(); + } + }); + + validationEx.Message.ShouldContain("PublicProperty> -> PublicField>"); + validationEx.Message.ShouldNotContain("Unconstructable target Types"); + validationEx.Message.ShouldNotContain("PublicTwoFieldsStruct -> PublicField"); + validationEx.Message.ShouldContain("Unmapped target members"); + validationEx.Message.ShouldContain("- PublicField>.Value.Value"); } [Fact] public void ShouldNotErrorIfConstructableComplexTypeMemberHasNoMatchingSource() { - using (var mapper = Mapper.CreateNew()) + Should.NotThrow(() => { - mapper.WhenMapping - .From() - .To() - .Ignore(c => c.Title, c => c.Address.Line2); + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From() + .To() + .Ignore(c => c.Title, c => c.Address.Line2); + + mapper.GetPlanFor().ToANew(); + + mapper.ThrowNowIfAnyMappingPlanIsIncomplete(); + } + }); + } - mapper.GetPlanFor().ToANew(); + [Fact] + public void ShouldNotErrorIfUnconstructableComplexTypeMemberHasFactoryMethod() + { + Should.NotThrow(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.GetPlanFor>().ToANew(); - Should.NotThrow(() => mapper.ThrowNowIfAnyMappingPlanIsIncomplete()); - } + mapper.ThrowNowIfAnyMappingPlanIsIncomplete(); + } + }); } [Fact] - public void ShouldErrorIfEnumerableMemberHasNonEnumerableSource() + public void ShouldNotErrorIfUnconstructableComplexTypeMemberHasConfiguredFactoryMethod() { - using (var mapper = Mapper.CreateNew()) + Should.NotThrow(() => { - mapper - .GetPlanFor>() - .ToANew>>(); - - var validationEx = Should.Throw(() => - mapper.ThrowNowIfAnyMappingPlanIsIncomplete()); + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .From
() + .ToANew>() + .CreateInstancesUsing(ctx => new PublicCtor(ctx.Source.Line1)); + + mapper + .GetPlanFor>() + .ToANew>>(); + + mapper.ThrowNowIfAnyMappingPlanIsIncomplete(); + } + }); + } - validationEx.Message.ShouldContain("PublicField
-> PublicProperty>"); - validationEx.Message.ShouldContain("Unmapped target members"); - validationEx.Message.ShouldContain("PublicProperty>.Value"); - } + [Fact] + public void ShouldErrorIfEnumerableMemberHasNonEnumerableSource() + { + var validationEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper + .GetPlanFor>() + .ToANew>>(); + + mapper.ThrowNowIfAnyMappingPlanIsIncomplete(); + } + }); + + validationEx.Message.ShouldContain("PublicField
-> PublicProperty>"); + validationEx.Message.ShouldContain("Unmapped target members"); + validationEx.Message.ShouldContain("PublicProperty>.Value"); } [Fact] public void ShouldErrorIfEnumerableMemberHasUnconstructableElements() { - using (var mapper = Mapper.CreateNew()) + var validationEx = Should.Throw(() => { - mapper - .GetPlanFor() - .ToANew[]>(); - - var validationEx = Should.Throw(() => - mapper.ThrowNowIfAnyMappingPlanIsIncomplete()); - - validationEx.Message.ShouldContain("Address[] -> PublicCtor[]"); - validationEx.Message.ShouldContain("Unconstructable target Types"); - validationEx.Message.ShouldContain("Address -> PublicCtor"); - } + using (var mapper = Mapper.CreateNew()) + { + mapper + .GetPlanFor() + .ToANew[]>(); + + mapper.ThrowNowIfAnyMappingPlanIsIncomplete(); + } + }); + + validationEx.Message.ShouldContain("Address[] -> PublicCtor[]"); + validationEx.Message.ShouldContain("Unconstructable target Types"); + validationEx.Message.ShouldContain("Address -> PublicCtor"); } [Fact] public void ShouldShowMultipleIncompleteCachedMappingPlans() { - using (var mapper = Mapper.CreateNew()) + var validationEx = Should.Throw(() => { - mapper.GetPlansFor().To(); - mapper.GetPlansFor().To(); - - var validationEx = Should.Throw(() => - mapper.ThrowNowIfAnyMappingPlanIsIncomplete()); - - validationEx.Message.ShouldContain("Person -> ProductDto"); - validationEx.Message.ShouldNotContain("ProductDto.ProductId"); // <- Because PVM has a PersonId - validationEx.Message.ShouldContain("ProductDto.Price"); - - validationEx.Message.ShouldContain("Product -> PersonViewModel"); - validationEx.Message.ShouldContain("PersonViewModel.Name"); - validationEx.Message.ShouldContain("PersonViewModel.AddressLine1"); - } + using (var mapper = Mapper.CreateNew()) + { + mapper.GetPlansFor().To(); + mapper.GetPlansFor().To(); + + mapper.ThrowNowIfAnyMappingPlanIsIncomplete(); + } + }); + + validationEx.Message.ShouldContain("Person -> ProductDto"); + validationEx.Message.ShouldNotContain("ProductDto.ProductId"); // <- Because PVM has a PersonId + validationEx.Message.ShouldContain("ProductDto.Price"); + + validationEx.Message.ShouldContain("Product -> PersonViewModel"); + validationEx.Message.ShouldContain("PersonViewModel.Name"); + validationEx.Message.ShouldContain("PersonViewModel.AddressLine1"); } [Fact] public void ShouldValidateMappingPlanMemberMappingByDefault() { - using (var mapper = Mapper.CreateNew()) + var validationEx = Should.Throw(() => { - mapper.WhenMapping.ThrowIfAnyMappingPlanIsIncomplete(); + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping.ThrowIfAnyMappingPlanIsIncomplete(); - var validationEx = Should.Throw(() => - mapper.GetPlanFor(new { Head = "Spinning" }).ToANew>()); + mapper.GetPlanFor(new { Head = "Spinning" }).ToANew>(); + } + }); - validationEx.Message.ShouldContain("AnonymousType -> PublicField"); - validationEx.Message.ShouldContain("Unmapped target members"); - validationEx.Message.ShouldContain("PublicField.Value"); - } + validationEx.Message.ShouldContain("AnonymousType -> PublicField"); + validationEx.Message.ShouldContain("Unmapped target members"); + validationEx.Message.ShouldContain("PublicField.Value"); } [Fact] public void ShouldNotErrorIfUnmappedMemberHasConfiguredDataSourceWhenValidatingMappingPlansByDefault() { - using (var mapper = Mapper.CreateNew()) + Should.NotThrow(() => { - mapper.WhenMapping - .ThrowIfAnyMappingPlanIsIncomplete() - .AndWhenMapping - .From().To>() - .Map((p, pp) => p.ProductId) - .To(p => p.Value); - - Should.NotThrow(() => - mapper.GetPlansFor().To>()); - } + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .ThrowIfAnyMappingPlanIsIncomplete() + .AndWhenMapping + .From().To>() + .Map((p, pp) => p.ProductId) + .To(p => p.Value); + + mapper.GetPlansFor().To>(); + } + }); } [Fact] public void ShouldValidateMappingPlanEnumMatchingByDefault() { - using (var mapper = Mapper.CreateNew()) + var validationEx = Should.Throw(() => { - mapper.WhenMapping.ThrowIfAnyMappingPlanIsIncomplete(); + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping.ThrowIfAnyMappingPlanIsIncomplete(); - var validationEx = Should.Throw(() => - mapper.Map(new PublicField()).ToANew>()); + mapper.Map(new PublicField()).ToANew>(); + } + }); - validationEx.Message.ShouldContain("PublicField -> PublicField"); - validationEx.Message.ShouldContain("Unpaired enum values"); - validationEx.Message.ShouldContain("PaymentTypeUk.Cheque matches no PaymentTypeUs"); - } + validationEx.Message.ShouldContain("PublicField -> PublicField"); + validationEx.Message.ShouldContain("Unpaired enum values"); + validationEx.Message.ShouldContain("PaymentTypeUk.Cheque matches no PaymentTypeUs"); } [Fact] public void ShouldNotErrorIfEnumMismatchesAreAllTargetToSource() { - using (var mapper = Mapper.CreateNew()) + Should.NotThrow(() => { - mapper.WhenMapping.ThrowIfAnyMappingPlanIsIncomplete(); + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping.ThrowIfAnyMappingPlanIsIncomplete(); - Should.NotThrow(() => - mapper.GetPlanFor>().ToANew>()); - } + mapper.GetPlanFor>().ToANew>(); + } + }); } [Fact] public void ShouldNotErrorIfEnumValuesArePairedWhenValidatingMappingPlansByDefault() { - using (var mapper = Mapper.CreateNew()) + Should.NotThrow(() => { - mapper.WhenMapping - .ThrowIfAnyMappingPlanIsIncomplete() - .AndWhenMapping - .PairEnum(PaymentTypeUk.Cheque).With(PaymentTypeUs.Check); + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .ThrowIfAnyMappingPlanIsIncomplete() + .AndWhenMapping + .PairEnum(PaymentTypeUk.Cheque).With(PaymentTypeUs.Check); + + mapper.Map(new PublicField()).ToANew>(); + } + }); + } - Should.NotThrow(() => - mapper.Map(new PublicField()).ToANew>()); + #region Helper Classes + + private class UnconstructableFactoryMethod + { + private UnconstructableFactoryMethod(string valueString) + { + Value = valueString; } + + // ReSharper disable once UnusedMember.Local + public static UnconstructableFactoryMethod Create(string value) + => new UnconstructableFactoryMethod(value); + + // ReSharper disable once MemberCanBePrivate.Local + // ReSharper disable once UnusedAutoPropertyAccessor.Local + public string Value { get; } } + + #endregion } } diff --git a/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs b/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs index 5b0ea7525..aba1e77f6 100644 --- a/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs +++ b/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs @@ -130,9 +130,7 @@ private void ThrowIfRedundantSourceMember(ConfiguredLambdaInfo valueLambdaInfo, return; } - var targetMemberType = (targetMember.LeafMember.MemberType == MemberType.ConstructorParameter) - ? "constructor parameter" - : "member"; + var targetMemberType = targetMember.IsConstructorParameter() ? "constructor parameter" : "member"; throw new MappingConfigurationException(string.Format( CultureInfo.InvariantCulture, diff --git a/AgileMapper/Members/Dictionaries/DictionaryTargetMember.cs b/AgileMapper/Members/Dictionaries/DictionaryTargetMember.cs index 546aeebad..2c9a1eac4 100644 --- a/AgileMapper/Members/Dictionaries/DictionaryTargetMember.cs +++ b/AgileMapper/Members/Dictionaries/DictionaryTargetMember.cs @@ -362,7 +362,7 @@ private bool CreateNonDictionaryChildMembers(Type sourceType) // entry and we're mapping from a source of type object, we switch from // mapping to flattened entries to mapping entire objects: return HasObjectEntries && - LeafMember.IsEnumerableElement() && + this.IsEnumerableElement() && (MemberChain[Depth - 2] == _rootDictionaryMember.LeafMember) && (sourceType == typeof(object)); } diff --git a/AgileMapper/Members/MemberExtensions.cs b/AgileMapper/Members/MemberExtensions.cs index a2ef0e3a8..11ca1d2f7 100644 --- a/AgileMapper/Members/MemberExtensions.cs +++ b/AgileMapper/Members/MemberExtensions.cs @@ -148,9 +148,16 @@ public static Expression GetQualifiedAccess(this IEnumerable memberChain (accessSoFar, member) => member.GetAccess(accessSoFar)); } + [DebuggerStepThrough] + public static bool IsEnumerableElement(this QualifiedMember member) => member.LeafMember.IsEnumerableElement(); + [DebuggerStepThrough] public static bool IsEnumerableElement(this Member member) => member.MemberType == MemberType.EnumerableElement; + [DebuggerStepThrough] + public static bool IsConstructorParameter(this QualifiedMember member) + => member.LeafMember.MemberType == MemberType.ConstructorParameter; + public static IList ExtendWith( this ICollection parentJoinedNames, string[] memberMatchingNames, diff --git a/AgileMapper/Members/MemberMapperDataExtensions.cs b/AgileMapper/Members/MemberMapperDataExtensions.cs index 69b228009..3f5029e19 100644 --- a/AgileMapper/Members/MemberMapperDataExtensions.cs +++ b/AgileMapper/Members/MemberMapperDataExtensions.cs @@ -225,7 +225,7 @@ public static bool TargetMemberIsUnmappable( [DebuggerStepThrough] public static bool TargetMemberIsEnumerableElement(this IBasicMapperData mapperData) - => mapperData.TargetMember.LeafMember.IsEnumerableElement(); + => mapperData.TargetMember.IsEnumerableElement(); [DebuggerStepThrough] public static bool TargetMemberHasInitAccessibleValue(this IMemberMapperData mapperData) diff --git a/AgileMapper/Members/QualifiedMember.cs b/AgileMapper/Members/QualifiedMember.cs index d819b891a..b8a94d423 100644 --- a/AgileMapper/Members/QualifiedMember.cs +++ b/AgileMapper/Members/QualifiedMember.cs @@ -83,10 +83,7 @@ private QualifiedMember(Member leafMember, MapperContext mapperContext) _childMemberCache = mapperContext.Cache.CreateNew(default(HashCodeComparer)); } - RegistrationName = (LeafMember.MemberType != MemberType.ConstructorParameter) - ? Name - : "ctor:" + Name; - + RegistrationName = this.IsConstructorParameter() ? "ctor:" + Name : Name; IsReadOnly = IsReadable && !leafMember.IsWriteable; } diff --git a/AgileMapper/ObjectPopulation/ObjectMapper.cs b/AgileMapper/ObjectPopulation/ObjectMapper.cs index 2984e72f1..dcbff9ad3 100644 --- a/AgileMapper/ObjectPopulation/ObjectMapper.cs +++ b/AgileMapper/ObjectPopulation/ObjectMapper.cs @@ -3,15 +3,15 @@ namespace AgileObjects.AgileMapper.ObjectPopulation using System; using System.Collections.Generic; using System.Linq; - using Caching; - using MapperKeys; - using NetStandardPolyfills; - using RepeatedMappings; #if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using Caching; + using MapperKeys; + using NetStandardPolyfills; + using RepeatedMappings; internal class ObjectMapper : IObjectMapper { diff --git a/AgileMapper/ObjectPopulation/ObjectMapperData.cs b/AgileMapper/ObjectPopulation/ObjectMapperData.cs index ead7fd1e6..a7f3611f0 100644 --- a/AgileMapper/ObjectPopulation/ObjectMapperData.cs +++ b/AgileMapper/ObjectPopulation/ObjectMapperData.cs @@ -668,7 +668,7 @@ public MethodCallExpression GetMapRepeatedCall( MethodInfo mapRepeatedMethod; Expression[] arguments; - if (targetMember.LeafMember.IsEnumerableElement()) + if (targetMember.IsEnumerableElement()) { mapRepeatedMethod = _mapRepeatedElementMethod; diff --git a/AgileMapper/Validation/MappingValidator.cs b/AgileMapper/Validation/MappingValidator.cs index 754be1639..26a8ded5c 100644 --- a/AgileMapper/Validation/MappingValidator.cs +++ b/AgileMapper/Validation/MappingValidator.cs @@ -8,6 +8,7 @@ using Extensions; using Extensions.Internal; using Members; + using NetStandardPolyfills; using ObjectPopulation; using ReadableExpressions.Extensions; @@ -64,7 +65,7 @@ private static void VerifyMappingPlanIsComplete(IEnumerable ma failureMessage .Append(" Rule set: ").AppendLine(rootData.RuleSet.Name).AppendLine(); - AddUnmappableTargetTypesInfo(mappingData.UnmappableTargetTypes, failureMessage); + AddUnconstructableTargetTypesInfo(mappingData.UnconstructableTargetTypes, failureMessage); AddUnmappedTargetMembersInfo(mappingData.UnmappedMembers, failureMessage, rootData); AddUnpairedEnumsInfo(mappingData.UnpairedEnums, failureMessage); } @@ -79,10 +80,7 @@ private static ICollection GetIncompleteMappingPlanData( .Project(md => new { MapperData = md, - IsUnmappable = - !md.IsRoot && - md.TargetMember.IsComplex && - md.DataSourcesByTargetMember.None(ds => ds.Value.HasValue), + IsUnconstructable = TargetIsUnconstructable(md), UnmappedMembers = md .DataSourcesByTargetMember .Filter(pair => !pair.Value.HasValue) @@ -90,13 +88,13 @@ private static ICollection GetIncompleteMappingPlanData( .ToArray(), UnpairedEnums = EnumMappingMismatchFinder.FindMismatches(md) }) - .Filter(d => d.IsUnmappable || d.UnmappedMembers.Any() || d.UnpairedEnums.Any()) + .Filter(d => d.IsUnconstructable || d.UnmappedMembers.Any() || d.UnpairedEnums.Any()) .GroupBy(d => d.MapperData.GetRootMapperData()) .Project(g => new IncompleteMappingData { RootMapperData = g.Key, - UnmappableTargetTypes = g - .Filter(d => d.IsUnmappable) + UnconstructableTargetTypes = g + .Filter(d => d.IsUnconstructable) .Project(d => d.MapperData) .ToArray(), UnmappedMembers = g @@ -109,6 +107,32 @@ private static ICollection GetIncompleteMappingPlanData( .ToArray(); } + private static bool TargetIsUnconstructable(ObjectMapperData mapperData) + { + if (!mapperData.TargetMember.IsComplex || + mapperData.TargetIsDefinitelyPopulated()) + { + return false; + } + + if (mapperData.TargetType.GetPublicInstanceConstructor(Constants.EmptyTypeArray) != null) + { + return false; + } + + if (mapperData.DataSourcesByTargetMember.Any(ds => ds.Key.IsConstructorParameter() && ds.Value.HasValue)) + { + return false; + } + + var configuredFactories = mapperData + .MapperContext + .UserConfigurations + .GetObjectFactories(mapperData); + + return configuredFactories.None(); + } + private static void AddMappingTypeHeaderIfRequired( StringBuilder failureMessage, IMemberMapperData rootData, @@ -135,7 +159,7 @@ private static void AddMappingTypeHeaderIfRequired( previousRootMapperData = rootData; } - private static void AddUnmappableTargetTypesInfo( + private static void AddUnconstructableTargetTypesInfo( ICollection unmappableTargetTypeData, StringBuilder failureMessage) { @@ -218,7 +242,7 @@ private class IncompleteMappingData { public IMemberMapperData RootMapperData { get; set; } - public ICollection UnmappableTargetTypes { get; set; } + public ICollection UnconstructableTargetTypes { get; set; } public Dictionary UnmappedMembers { get; set; }