diff --git a/AgileMapper.UnitTests/Dictionaries/Configuration/WhenConfiguringSourceDictionaryMapping.cs b/AgileMapper.UnitTests/Dictionaries/Configuration/WhenConfiguringSourceDictionaryMapping.cs index 399b7f500..b7214e9f2 100644 --- a/AgileMapper.UnitTests/Dictionaries/Configuration/WhenConfiguringSourceDictionaryMapping.cs +++ b/AgileMapper.UnitTests/Dictionaries/Configuration/WhenConfiguringSourceDictionaryMapping.cs @@ -94,7 +94,7 @@ public void ShouldUseACustomMemberNameDictionaryKeyForANestedMember() } [Fact] - public void ShouldUseACustomMemberNameDictionaryKeyForANestedEnumerableMember() + public void ShouldUseACustomMemberNameDictionaryKeyForANestedArrayMember() { using (var mapper = Mapper.CreateNew()) { diff --git a/AgileMapper.UnitTests/WhenMappingCircularReferences.cs b/AgileMapper.UnitTests/WhenMappingCircularReferences.cs index 1776a0562..2bf841df6 100644 --- a/AgileMapper.UnitTests/WhenMappingCircularReferences.cs +++ b/AgileMapper.UnitTests/WhenMappingCircularReferences.cs @@ -2,6 +2,7 @@ { using System; using System.Collections.Generic; + using System.Collections.ObjectModel; using AgileMapper.Extensions; using Shouldly; using TestClasses; @@ -260,6 +261,49 @@ public void ShouldMapMultiplyRecursiveRelationships() } } + [Fact] + public void ShouldMapNestedMultiplyRecursiveRelationships() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .To>>() + .TrackMappedObjects(); + + var recursorOne = new MultipleRecursor { Name = "One" }; + var recursorTwo = new MultipleRecursor { Name = "Two" }; + + recursorOne.ChildRecursor = recursorTwo; + recursorTwo.ChildRecursor = recursorOne; + + var source = new PublicField + { + Value = new[] { recursorOne, recursorTwo } + }; + + var result = mapper.Map(source).ToANew>>(); + + result.ShouldNotBeNull(); + result.ShouldNotBeSameAs(source); + result.Value.Count.ShouldBe(2); + + var resultOne = result.Value.First(); + resultOne.ShouldNotBeSameAs(recursorOne); + resultOne.Name.ShouldBe("One"); + resultOne.ChildRecursor.ShouldNotBeNull(); + resultOne.ChildRecursor.ShouldNotBeSameAs(recursorTwo); + + var resultTwo = result.Value.Second(); + resultTwo.ShouldNotBeSameAs(recursorTwo); + resultTwo.Name.ShouldBe("Two"); + resultTwo.ChildRecursor.ShouldNotBeNull(); + resultTwo.ChildRecursor.ShouldNotBeSameAs(recursorOne); + + resultOne.ChildRecursor.ShouldBeSameAs(resultTwo); + resultTwo.ChildRecursor.ShouldBeSameAs(resultOne); + } + } + [Fact] public void ShouldGenerateAMappingPlanForLinkRelationships() { diff --git a/AgileMapper.UnitTests/WhenMappingOnToEnumerableMembers.cs b/AgileMapper.UnitTests/WhenMappingOnToEnumerableMembers.cs index 7f0479277..43dda71dd 100644 --- a/AgileMapper.UnitTests/WhenMappingOnToEnumerableMembers.cs +++ b/AgileMapper.UnitTests/WhenMappingOnToEnumerableMembers.cs @@ -2,6 +2,7 @@ { using System; using System.Collections.Generic; + using System.Collections.ObjectModel; using System.Linq; using Shouldly; using TestClasses; @@ -50,6 +51,26 @@ public void ShouldMergeANullableIntArray() result.Value.ShouldBe(1, 2, null, 3); } + [Fact] + public void ShouldMergeANullableGuidReadOnlyCollection() + { + var source = new PublicProperty> + { + Value = new List { Guid.NewGuid() } + }; + + var target = new PublicField> + { + Value = new ReadOnlyCollection(new[] { Guid.Empty, default(Guid?), Guid.NewGuid() }) + }; + + var originalCollection = target.Value; + var result = Mapper.Map(source).OnTo(target); + + result.Value.ShouldNotBeSameAs(originalCollection); + result.Value.ShouldBe(target.Value.First(), default(Guid?), target.Value.Third(), source.Value.First()); + } + [Fact] public void ShouldMergeAComplexTypeCollection() { @@ -98,6 +119,34 @@ public void ShouldMergeAnIdentifiableComplexTypeList() result.Value.ShouldBe(r => r.ProductId, "Magic", "Science"); } + [Fact] + public void ShouldMergeAnIdentifiableComplexTypeReadOnlyCollection() + { + var source = new PublicProperty + { + Value = new[] + { + new Product { ProductId = "Science", Price = 1000.00 } + } + }; + + var target = new PublicField> + { + Value = new ReadOnlyCollection(new List + { + new Product { ProductId = "Science" }, + new Product { ProductId = "Magic", Price = 1.00 } + }) + }; + + var existingProduct = target.Value.First(); + var result = Mapper.Map(source).OnTo(target); + + result.Value.First().ShouldBeSameAs(existingProduct); + result.Value.First().Price.ShouldBe(1000.00); + result.Value.ShouldBe(r => r.ProductId, "Science", "Magic"); + } + [Fact] public void ShouldHandleANullSourceMember() { diff --git a/AgileMapper.UnitTests/WhenMappingOnToEnumerables.cs b/AgileMapper.UnitTests/WhenMappingOnToEnumerables.cs index 24bfaa93c..76579c745 100644 --- a/AgileMapper.UnitTests/WhenMappingOnToEnumerables.cs +++ b/AgileMapper.UnitTests/WhenMappingOnToEnumerables.cs @@ -2,6 +2,7 @@ { using System; using System.Collections.Generic; + using System.Collections.ObjectModel; using System.Linq; using Shouldly; using TestClasses; @@ -17,8 +18,18 @@ public void ShouldMergeARootSimpleTypeArray() var result = Mapper.Map(source).OnTo(target); result.ShouldNotBeNull(); - result.ShouldNotBeSameAs(source); - result.SequenceEqual(target.Concat(source)).ShouldBeTrue(); + result.ShouldBe(1, 2, 3, 4, 5, 6); + } + + [Fact] + public void ShouldMergeARootSimpleTypeReadOnlyCollection() + { + var source = new[] { 2, 3, 4 }; + var target = new ReadOnlyCollection(new[] { "1", "2" }); + var result = Mapper.Map(source).OnTo(target); + + result.ShouldNotBeNull(); + result.ShouldBe("1", "2", "3", "4"); } [Fact] @@ -84,6 +95,24 @@ public void ShouldMergeARootComplexTypeList() result.ShouldBe(p => p.Name, "Kate", "Pete"); } + [Fact] + public void ShouldMergeARootComplexTypeReadOnlyCollection() + { + var source = new[] + { + new Product { ProductId = "Pete" } + }; + + var target = new ReadOnlyCollection(new[] + { + new Product { ProductId = "Kate" } + }); + + var result = Mapper.Map(source).OnTo(target); + + result.ShouldBe(p => p.ProductId, "Kate", "Pete"); + } + [Fact] public void ShouldUpdateAnExistingObject() { diff --git a/AgileMapper.UnitTests/WhenMappingOverEnumerableMembers.cs b/AgileMapper.UnitTests/WhenMappingOverEnumerableMembers.cs index 41b3d4b2d..31a68edb9 100644 --- a/AgileMapper.UnitTests/WhenMappingOverEnumerableMembers.cs +++ b/AgileMapper.UnitTests/WhenMappingOverEnumerableMembers.cs @@ -51,6 +51,24 @@ public void ShouldOverwriteAnArray() result.Value.ShouldBe(MinusOne, Zero); } + [Fact] + public void ShouldOverwriteAReadOnlyCollection() + { + var source = new PublicProperty> + { + Value = new[] { MinValue, MaxValue } + }; + + var target = new PublicField> + { + Value = new ReadOnlyCollection(new[] { MinusOne, Zero }) + }; + + var result = Mapper.Map(source).Over(target); + + result.Value.ShouldBe(MinValue, MaxValue); + } + [Fact] public void ShouldOverwriteAComplexTypeCollection() { @@ -138,5 +156,18 @@ public void ShouldOverwriteANonNullReadOnlyNestedCollection() result.Value.ShouldBeSameAs(strings); result.Value.ShouldBe("One!", "Two!", "Three"); } + + [Fact] + public void ShouldHandleAReadOnlyNestedReadOnlyCollection() + { + var source = new PublicField { Value = new[] { "One!", "Two!" } }; + var strings = new List { "A", "B", "C" }.AsReadOnly(); + var target = new PublicReadOnlyField>(strings); + var result = Mapper.Map(source).Over(target); + + result.Value.ShouldNotBeNull(); + result.Value.ShouldBeSameAs(strings); + result.Value.ShouldBe("A", "B", "C"); + } } } diff --git a/AgileMapper.UnitTests/WhenMappingOverEnumerables.cs b/AgileMapper.UnitTests/WhenMappingOverEnumerables.cs index 87a4c1499..8ac66bca1 100644 --- a/AgileMapper.UnitTests/WhenMappingOverEnumerables.cs +++ b/AgileMapper.UnitTests/WhenMappingOverEnumerables.cs @@ -22,6 +22,17 @@ public void ShouldOverwriteARootSimpleTypeArray() result.ShouldBe(5, 4, 3, 2); } + [Fact] + public void ShouldOverwriteARootSimpleTypeReadOnlyCollection() + { + var source = new[] { '2', '3' }; + var target = new ReadOnlyCollection(new List { '5', '4' }); + var result = Mapper.Map(source).Over(target); + + result.ShouldNotBeNull(); + result.ShouldBe('2', '3'); + } + [Fact] public void ShouldOverwriteARootSimpleTypeList() { @@ -91,7 +102,7 @@ public void ShouldOverwriteARootComplexTypeCollectionElementByRuntimeType() } [Fact] - public void ShouldOverwriteAnExistingObject() + public void ShouldOverwriteAnExistingObjectById() { var source = new[] { @@ -112,6 +123,28 @@ public void ShouldOverwriteAnExistingObject() result.ShouldBe(p => p.Name, "Lisa", "Bart"); } + [Fact] + public void ShouldOverwriteAnExistingObjectByIdInAReadOnlyCollection() + { + var source = new[] + { + new CustomerViewModel { Id = Guid.NewGuid(), Name = "Homer" } + }; + + var target = new ReadOnlyCollection(new List + { + new CustomerViewModel { Id = source.First().Id, Name = "Maggie" } + }); + + var originalObject = target.First(); + var result = Mapper.Map(source).Over(target); + + result.ShouldNotBeSameAs(target); + result.ShouldHaveSingleItem(); + result.First().ShouldBeSameAs(originalObject); + result.First().Name.ShouldBe("Homer"); + } + [Fact] public void ShouldOverwriteUsingAConfiguredDataSource() { diff --git a/AgileMapper.UnitTests/WhenMappingToNewEnumerableMembers.cs b/AgileMapper.UnitTests/WhenMappingToNewEnumerableMembers.cs index 7520e5d5b..d7c559680 100644 --- a/AgileMapper.UnitTests/WhenMappingToNewEnumerableMembers.cs +++ b/AgileMapper.UnitTests/WhenMappingToNewEnumerableMembers.cs @@ -58,6 +58,49 @@ public void ShouldCreateANewObjectCollection() result.Value.Third().ShouldBe(string.Empty); } + [Fact] + public void ShouldCreateANewObjectReadOnlyCollection() + { + var source = new PublicField + { + Value = new object[] { 9, new CustomerViewModel { Name = "Boycee" }, default(string) } + }; + var result = Mapper.Map(source).ToANew>>(); + + result.Value.ShouldNotBeNull(); + result.Value.First().ShouldBe(9); + result.Value.Second().ShouldBeOfType(); + ((CustomerViewModel)result.Value.Second()).Name.ShouldBe("Boycee"); + result.Value.Third().ShouldBeNull(); + } + + [Fact] + public void ShouldMapFromAReadOnlyCollection() + { + var source = new PublicField> + { + Value = new ReadOnlyCollection(new[] { "R", "A", "T", "M" }) + }; + var result = Mapper.Map(source).ToANew>(); + + result.Value.ShouldNotBeNull(); + result.Value.ShouldBe("R", "A", "T", "M"); + } + + [Fact] + public void ShouldMapFromAnEmptyReadOnlyCollection() + { + var source = new PublicField> + { + Value = new ReadOnlyCollection(Enumerable.EmptyArray) + }; + var result = Mapper.Map(source).ToANew>>(); + + result.Value.ShouldNotBeNull(); + result.Value.ShouldNotBeSameAs(source.Value); + result.Value.ShouldBeEmpty(); + } + [Fact] public void ShouldCreateANewComplexTypeEnumerable() { @@ -192,7 +235,7 @@ public void ShouldRetainAnExistingCollection() } [Fact] - public void ShouldApplyAConfiguredExpressionToAnEnumerable() + public void ShouldApplyAConfiguredExpressionToAnArray() { using (var mapper = Mapper.CreateNew()) { @@ -289,6 +332,42 @@ public void ShouldHandleANonNullReadOnlyNestedReadOnlyICollection() } } + [Fact] + public void ShouldHandleANonNullReadOnlyNestedArray() + { + using (var mapper = Mapper.CreateNew()) + { + var readOnlyStrings = new[] { "1", "2" }; + + mapper.CreateAReadOnlyFieldUsing(readOnlyStrings); + + var source = new PublicField { Value = new[] { "3" } }; + var result = mapper.Map(source).ToANew>(); + + result.Value.ShouldNotBeNull(); + result.Value.ShouldBeSameAs(readOnlyStrings); + result.Value.ShouldBe("1", "2"); + } + } + + [Fact] + public void ShouldHandleANonNullReadOnlyNestedReadOnlyCollection() + { + using (var mapper = Mapper.CreateNew()) + { + var readOnlyInts = new ReadOnlyCollection(new[] { 5, 5, 5 }); + + mapper.CreateAReadOnlyPropertyUsing(readOnlyInts); + + var source = new PublicField { Value = new[] { "3" } }; + var result = mapper.Map(source).ToANew>>(); + + result.Value.ShouldNotBeNull(); + result.Value.ShouldBeSameAs(readOnlyInts); + result.Value.ShouldBe(5, 5, 5); + } + } + [Fact] public void ShouldPopulateANonNullReadOnlyNestedEnumerable() { diff --git a/AgileMapper.UnitTests/WhenMappingToNewEnumerables.cs b/AgileMapper.UnitTests/WhenMappingToNewEnumerables.cs index cb7697c06..3acae3fe1 100644 --- a/AgileMapper.UnitTests/WhenMappingToNewEnumerables.cs +++ b/AgileMapper.UnitTests/WhenMappingToNewEnumerables.cs @@ -91,6 +91,26 @@ public void ShouldCreateAComplexTypeArrayUsingRuntimeTypedElements() } } + [Fact] + public void ShouldCreateAReadOnlyCollection() + { + var source = new[] { 1, 2, 3 }; + var result = Mapper.Map(source).ToANew>(); + + result.ShouldNotBeNull(); + result.ShouldBe(1, 2, 3); + } + + [Fact] + public void ShouldMapFromAReadOnlyCollection() + { + var source = new ReadOnlyCollection(new[] { 1, 2, 3L }); + var result = Mapper.Map(source).ToANew(); + + result.ShouldNotBeNull(); + result.ShouldBe(1, 2, 3); + } + [Fact] public void ShouldHandleANullComplexTypeElement() { diff --git a/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs b/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs index f6891afe1..fa8ea526c 100644 --- a/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs +++ b/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs @@ -83,6 +83,7 @@ private bool IsDictionaryEntry(LambdaExpression targetMemberLambda, out Dictiona (methodCall.Method.Name != "get_Item") || !methodCall.Method.DeclaringType.IsDictionary()) { + // TODO: Test coverage - specified, non-dictionary indexed target member entryMember = null; return false; } diff --git a/AgileMapper/Api/Configuration/Dictionaries/TargetDictionaryMappingConfigurator.cs b/AgileMapper/Api/Configuration/Dictionaries/TargetDictionaryMappingConfigurator.cs index 21444211d..5619cf6f6 100644 --- a/AgileMapper/Api/Configuration/Dictionaries/TargetDictionaryMappingConfigurator.cs +++ b/AgileMapper/Api/Configuration/Dictionaries/TargetDictionaryMappingConfigurator.cs @@ -58,6 +58,7 @@ private QualifiedMember GetSourceMemberOrThrow(LambdaExpression lambda) return sourceMember; } + // TODO: test coverage - invalid source member expression throw new MappingConfigurationException( $"Source member {lambda.Body.ToReadableString()} is not readable."); } diff --git a/AgileMapper/Extensions/EnumerableExtensions.cs b/AgileMapper/Extensions/EnumerableExtensions.cs index ecfa8e0b3..a2854db49 100644 --- a/AgileMapper/Extensions/EnumerableExtensions.cs +++ b/AgileMapper/Extensions/EnumerableExtensions.cs @@ -141,12 +141,12 @@ public static T[] ToArray(this IList items) { var array = new T[items.Count]; - CopyItemsTo(array, items); + array.CopyFrom(items); return array; } - private static void CopyItemsTo(IList array, IList items, int startIndex = 0) + public static void CopyFrom(this IList array, IList items, int startIndex = 0) { for (var i = 0; i < items.Count; i++) { @@ -208,8 +208,8 @@ public static T[] Append(this IList array, IList extraItems) { var combinedArray = new T[array.Count + extraItems.Count]; - CopyItemsTo(combinedArray, array); - CopyItemsTo(combinedArray, extraItems, array.Count); + combinedArray.CopyFrom(array); + combinedArray.CopyFrom(extraItems, array.Count); return combinedArray; } diff --git a/AgileMapper/Extensions/ExpressionExtensions.cs b/AgileMapper/Extensions/ExpressionExtensions.cs index 07aed1e1a..2e512e698 100644 --- a/AgileMapper/Extensions/ExpressionExtensions.cs +++ b/AgileMapper/Extensions/ExpressionExtensions.cs @@ -21,7 +21,7 @@ internal static partial class ExpressionExtensions private static readonly MethodInfo _linqToArrayMethod = typeof(Enumerable) .GetPublicStaticMethod("ToArray"); - private static readonly MethodInfo _toListMethod = typeof(Enumerable) + private static readonly MethodInfo _linqToListMethod = typeof(Enumerable) .GetPublicStaticMethod("ToList"); private static readonly MethodInfo _stringEqualsMethod = typeof(string) @@ -126,7 +126,7 @@ public static Expression GetIndexAccess(this Expression indexedExpression, Expre .GetPublicInstanceProperties() .First(p => p.GetIndexParameters().HasOne() && - (p.GetIndexParameters()[0].ParameterType == indexValue.Type)); + (p.GetIndexParameters()[0].ParameterType == indexValue.Type)); return Expression.MakeIndex(indexedExpression, indexer, new[] { indexValue }); } @@ -153,29 +153,72 @@ public static Expression WithToArrayCall(this Expression enumerable, Type elemen private static MethodInfo GetToArrayConversionMethod(Expression enumerable, Type elementType) { - var wrapperType = typeof(ReadOnlyCollectionWrapper<>).MakeGenericType(elementType); + var typeHelper = new EnumerableTypeHelper(enumerable.Type, elementType); - if (enumerable.Type == wrapperType) + if (TryGetWrapperMethod(typeHelper, "ToArray", out var method)) { - return wrapperType.GetMethod("ToArray"); + return method; } - var listType = typeof(IList<>).MakeGenericType(elementType); - - if (listType.IsAssignableFrom(enumerable.Type)) + if (typeHelper.HasListInterface) { return _listToArrayMethod; } - var collectionType = typeof(ICollection<>).MakeGenericType(elementType); + return GetNonListToArrayConversionMethod(typeHelper); + } + + private static bool TryGetWrapperMethod( + EnumerableTypeHelper typeHelper, + string methodName, + out MethodInfo method) + { + var wrapperType = typeHelper.WrapperType; + + if (typeHelper.EnumerableType != wrapperType) + { + method = null; + return false; + } + + method = wrapperType.GetMethod(methodName); + return true; + } - return collectionType.IsAssignableFrom(enumerable.Type) + private static MethodInfo GetNonListToArrayConversionMethod(EnumerableTypeHelper typeHelper) + { + return typeHelper.HasCollectionInterface ? _collectionToArrayMethod : _linqToArrayMethod; } + public static Expression WithToReadOnlyCollectionCall(this Expression enumerable, Type elementType) + { + var typeHelper = new EnumerableTypeHelper(enumerable.Type, elementType); + + if (TryGetWrapperMethod(typeHelper, "ToReadOnlyCollection", out var method)) + { + return GetToEnumerableCall(enumerable, method, elementType); + } + + if (typeHelper.IsList) + { + return Expression.Call(enumerable, typeHelper.ListType.GetMethod("AsReadOnly")); + } + + if (typeHelper.HasListInterface) + { + return GetReadOnlyCollectionCreation(typeHelper, enumerable); + } + + var nonListToArrayMethod = GetNonListToArrayConversionMethod(typeHelper); + var toArrayCall = GetToEnumerableCall(enumerable, nonListToArrayMethod, elementType); + + return GetReadOnlyCollectionCreation(typeHelper, toArrayCall); + } + public static Expression WithToListCall(this Expression enumerable, Type elementType) - => GetToEnumerableCall(enumerable, _toListMethod, elementType); + => GetToEnumerableCall(enumerable, _linqToListMethod, elementType); private static Expression GetToEnumerableCall(Expression enumerable, MethodInfo method, Type elementType) { @@ -200,7 +243,7 @@ public static Expression GetEmptyInstanceCreation(this Type enumerableType, Type if (enumerableType.IsArray) { - return Expression.Field(null, _typedEnumerable.MakeGenericType(elementType), "EmptyArray"); + return GetEmptyArray(elementType); } var typeHelper = new EnumerableTypeHelper(enumerableType, elementType); @@ -210,6 +253,11 @@ public static Expression GetEmptyInstanceCreation(this Type enumerableType, Type return Expression.Field(null, _typedEnumerable.MakeGenericType(elementType), "Empty"); } + if (typeHelper.IsReadOnlyCollection) + { + return GetReadOnlyCollectionCreation(typeHelper, GetEmptyArray(elementType)); + } + var fallbackType = typeHelper.IsCollection ? typeHelper.CollectionType : typeHelper.IsDictionary @@ -219,6 +267,17 @@ public static Expression GetEmptyInstanceCreation(this Type enumerableType, Type return Expression.New(fallbackType); } + private static Expression GetEmptyArray(Type elementType) + => Expression.Field(null, _typedEnumerable.MakeGenericType(elementType), "EmptyArray"); + + private static Expression GetReadOnlyCollectionCreation(EnumerableTypeHelper typeHelper, Expression list) + { + // ReSharper disable once AssignNullToNotNullAttribute + return Expression.New( + typeHelper.ReadOnlyCollectionType.GetConstructor(new[] { typeHelper.ListInterfaceType }), + list); + } + private static Type GetDictionaryType(Type dictionaryType) { return dictionaryType.IsInterface() diff --git a/AgileMapper/Extensions/StringExpressionExtensions.cs b/AgileMapper/Extensions/StringExpressionExtensions.cs index 36a67394c..450ff297b 100644 --- a/AgileMapper/Extensions/StringExpressionExtensions.cs +++ b/AgileMapper/Extensions/StringExpressionExtensions.cs @@ -35,7 +35,7 @@ public static Expression GetStringConcatCall(this IList expressions) return string.Empty.ToConstantExpression(); } - if (expressions.HasOne() && (expressions.First().NodeType == ExpressionType.Constant)) + if (expressions.HasOne()) { return expressions.First(); } @@ -57,11 +57,6 @@ public static Expression GetStringConcatCall(this IList expressions) private static void OptimiseForStringConcat(IList expressions) { - if (expressions.HasOne()) - { - return; - } - var currentNamePart = string.Empty; for (var i = expressions.Count - 1; i >= 0; --i) diff --git a/AgileMapper/Members/ConfiguredSourceMember.cs b/AgileMapper/Members/ConfiguredSourceMember.cs index a6ec3234f..15e132efb 100644 --- a/AgileMapper/Members/ConfiguredSourceMember.cs +++ b/AgileMapper/Members/ConfiguredSourceMember.cs @@ -90,11 +90,6 @@ public bool CouldMatch(QualifiedMember otherMember) public bool Matches(IQualifiedMember otherMember) { - if (otherMember == this) - { - return true; - } - if (otherMember is QualifiedMember otherQualifiedMember) { return _matchedTargetMemberJoinedNames.Match(otherQualifiedMember.JoinedNames); diff --git a/AgileMapper/Members/DictionarySourceMember.cs b/AgileMapper/Members/DictionarySourceMember.cs index 4d99cb1e2..978dafef6 100644 --- a/AgileMapper/Members/DictionarySourceMember.cs +++ b/AgileMapper/Members/DictionarySourceMember.cs @@ -96,18 +96,13 @@ public IQualifiedMember GetElementMember() public IQualifiedMember RelativeTo(IQualifiedMember otherMember) { - if (IsEntireDictionaryMatch) - { - return new DictionarySourceMember( - Type, - _wrappedSourceMember.RelativeTo(otherMember), - _matchedTargetMember, - IsEntireDictionaryMatch, - KeyType, - ValueType); - } - - return this; + return new DictionarySourceMember( + Type, + _wrappedSourceMember.RelativeTo(otherMember), + _matchedTargetMember, + IsEntireDictionaryMatch, + KeyType, + ValueType); } public IQualifiedMember WithType(Type runtimeType) => this; diff --git a/AgileMapper/Members/DictionaryTargetMember.cs b/AgileMapper/Members/DictionaryTargetMember.cs index 437dd0847..2e094be25 100644 --- a/AgileMapper/Members/DictionaryTargetMember.cs +++ b/AgileMapper/Members/DictionaryTargetMember.cs @@ -318,6 +318,7 @@ public DictionaryTargetMember WithTypeOf(Member sourceMember) { if (sourceMember.Type == Type) { + // TODO: Test coverage - source object with a non-simple member of the dictionary value type return this; } @@ -362,20 +363,7 @@ public override string ToString() return $"[\"{path}\"]: {Type.GetFriendlyName()}"; } - private string GetKeyNameOrNull() - { - if (_key == null) - { - return null; - } - - if (_key.NodeType == ExpressionType.Constant) - { - return (string)((ConstantExpression)_key).Value; - } - - return _key.ToString(); - } + private string GetKeyNameOrNull() => (string)((ConstantExpression)_key)?.Value; #region Helper Classes diff --git a/AgileMapper/Members/ExpressionInfoFinder.cs b/AgileMapper/Members/ExpressionInfoFinder.cs index 65f43151a..b439fc025 100644 --- a/AgileMapper/Members/ExpressionInfoFinder.cs +++ b/AgileMapper/Members/ExpressionInfoFinder.cs @@ -179,20 +179,15 @@ private bool AccessSubjectCouldBeNull(Expression expression) Expression subject; MemberExpression memberAccess; - switch (expression.NodeType) + if (expression.NodeType == ExpressionType.MemberAccess) { - case ExpressionType.Call: - memberAccess = null; - subject = ((MethodCallExpression)expression).Object; - break; - - case ExpressionType.MemberAccess: - memberAccess = (MemberExpression)expression; - subject = memberAccess.Expression; - break; - - default: - return false; + memberAccess = (MemberExpression)expression; + subject = memberAccess.Expression; + } + else + { + memberAccess = null; + subject = ((MethodCallExpression)expression).Object; } if (subject == null) diff --git a/AgileMapper/Members/Member.cs b/AgileMapper/Members/Member.cs index a4da154f4..361f85afa 100644 --- a/AgileMapper/Members/Member.cs +++ b/AgileMapper/Members/Member.cs @@ -198,10 +198,10 @@ public bool HasAttribute() { if (MemberInfo == null) { + // TODO: test coverage - DictionaryEntry? return false; } - return MemberInfo .GetCustomAttributes(typeof(TAttribute), inherit: true) #if NET_STANDARD @@ -227,11 +227,6 @@ public Expression GetAccess(Expression instance) public Member WithType(Type runtimeType) { - if (runtimeType == Type) - { - return this; - } - if (_accessFactory != null) { return new Member( diff --git a/AgileMapper/Members/MemberFinder.cs b/AgileMapper/Members/MemberFinder.cs index 13b87a183..fe42c55b0 100644 --- a/AgileMapper/Members/MemberFinder.cs +++ b/AgileMapper/Members/MemberFinder.cs @@ -2,6 +2,7 @@ { using System; using System.Collections.Generic; + using System.Collections.ObjectModel; using System.Linq; using System.Reflection; using Caching; @@ -82,15 +83,7 @@ private static IEnumerable GetFields(Type targetType, Func true; private static bool OnlyTargets(FieldInfo field) - { - if (field.IsInitOnly) - { - // Include readonly object fields (except arrays): - return !field.FieldType.IsArray && !field.FieldType.IsSimple(); - } - - return true; - } + => !field.IsInitOnly || IsUseableReadOnlyTarget(field.FieldType); #endregion @@ -115,13 +108,24 @@ private static bool OnlyTargets(PropertyInfo property) return false; } - if (property.IsWriteable()) + return property.IsWriteable() || IsUseableReadOnlyTarget(property.PropertyType); + } + + private static bool IsUseableReadOnlyTarget(Type memberType) + { + // Include readonly object type properties (except arrays): + if (memberType.IsArray || memberType.IsSimple()) { - return true; + return false; } - // Include readonly object type properties (except arrays): - return !property.PropertyType.IsArray && !property.PropertyType.IsSimple(); + if (memberType.IsGenericType() && + (memberType.GetGenericTypeDefinition() == typeof(ReadOnlyCollection<>))) + { + return false; + } + + return true; } #endregion diff --git a/AgileMapper/Members/MemberMapperDataExtensions.cs b/AgileMapper/Members/MemberMapperDataExtensions.cs index 07799d0d0..009472de8 100644 --- a/AgileMapper/Members/MemberMapperDataExtensions.cs +++ b/AgileMapper/Members/MemberMapperDataExtensions.cs @@ -317,11 +317,6 @@ public static Expression GetTypedContextAccess( public static Expression GetTargetMemberPopulation(this IMemberMapperData mapperData, Expression value) { - if (value.Type == typeof(void)) - { - return value; - } - return mapperData.TargetMember.GetPopulation(value, mapperData); } diff --git a/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeMappingExpressionFactory.cs b/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeMappingExpressionFactory.cs index 1bc0491ef..7853747ee 100644 --- a/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeMappingExpressionFactory.cs +++ b/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeMappingExpressionFactory.cs @@ -94,12 +94,7 @@ private static bool SourceObjectCouldBeNull(IMemberMapperData mapperData) private static Expression GetAlreadyMappedObjectShortCircuitOrNull(ObjectMapperData mapperData) { - if (mapperData.TargetTypeHasNotYetBeenMapped) - { - return null; - } - - if (mapperData.MapperContext.UserConfigurations.DisableObjectTracking(mapperData)) + if (!mapperData.MappedObjectCachingNeeded || mapperData.TargetTypeHasNotYetBeenMapped) { return null; } @@ -180,12 +175,7 @@ private static Expression GetCreationCallbackOrNull(CallbackPosition callbackPos private static Expression GetObjectRegistrationCallOrNull(ObjectMapperData mapperData) { - if (mapperData.TargetTypeWillNotBeMappedAgain) - { - return null; - } - - if (mapperData.MapperContext.UserConfigurations.DisableObjectTracking(mapperData)) + if (!mapperData.MappedObjectCachingNeeded || mapperData.TargetTypeWillNotBeMappedAgain) { return null; } diff --git a/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/DictionaryPopulationBuilder.cs b/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/DictionaryPopulationBuilder.cs index f7abd1127..19c29044d 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/DictionaryPopulationBuilder.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/DictionaryPopulationBuilder.cs @@ -27,16 +27,9 @@ public DictionaryPopulationBuilder(EnumerablePopulationBuilder wrappedBuilder) public static implicit operator BlockExpression(DictionaryPopulationBuilder builder) { - var mappingBlock = (BlockExpression)builder._wrappedBuilder; - if (builder._mappingExpressions.None()) { - return mappingBlock; - } - - if (mappingBlock != null) - { - builder._mappingExpressions.InsertRange(0, mappingBlock.Expressions); + return builder._wrappedBuilder; } return Expression.Block(builder._mappingExpressions); diff --git a/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationBuilder.cs b/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationBuilder.cs index 9b5ae6152..6792b371e 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationBuilder.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationBuilder.cs @@ -346,18 +346,11 @@ private Expression GetTargetVariableValue() } private Expression GetCopyIntoWrapperConstruction() - { - var constructor = TargetTypeHelper - .WrapperType - .GetConstructor(new[] { TargetTypeHelper.EnumerableInterfaceType, typeof(int) }); - - // ReSharper disable once AssignNullToNotNullAttribute - return Expression.New(constructor, MapperData.TargetObject, GetSourceCountAccess()); - } + => TargetTypeHelper.GetWrapperConstruction(MapperData.TargetObject, GetSourceCountAccess()); private Expression GetNonNullEnumerableTargetVariableValue() { - if (TargetTypeHelper.IsArray) + if (TargetTypeHelper.IsReadOnly) { return GetCopyIntoListConstruction(); } @@ -620,7 +613,7 @@ private Expression ConvertForReturnValue(Expression value) return value; } - return value.WithToArrayCall(Context.TargetElementType); + return TargetTypeHelper.GetEnumerableConversion(value); } private Expression GetTargetMethodCall(string methodName, Expression argument = null) @@ -705,9 +698,7 @@ public Expression GetResult() return _result; } - _result = _builder.TargetTypeHelper.IsArray - ? _result.WithToArrayCall(_builder.Context.TargetElementType) - : _result.WithToListCall(_builder.Context.TargetElementType); + _result = _builder.TargetTypeHelper.GetEnumerableConversion(_result); return _result; } diff --git a/AgileMapper/ObjectPopulation/Enumerables/EnumerableTypeHelper.cs b/AgileMapper/ObjectPopulation/Enumerables/EnumerableTypeHelper.cs index a0d68f3d7..63a3a2dc4 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/EnumerableTypeHelper.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/EnumerableTypeHelper.cs @@ -3,8 +3,8 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.Enumerables using System; using System.Collections.Generic; using System.Collections.ObjectModel; + using System.Linq.Expressions; using Extensions; - #if NET_STANDARD using System.Reflection; #endif @@ -12,10 +12,10 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.Enumerables internal class EnumerableTypeHelper { private bool? _isDictionary; - private Type _wrapperType; private Type _listType; private Type _listInterfaceType; private Type _collectionType; + private Type _readOnlyCollectionType; private Type _collectionInterfaceType; private Type _enumerableInterfaceType; @@ -36,29 +36,59 @@ public bool IsDictionary public bool IsCollection => CollectionType.IsAssignableFrom(EnumerableType); + public bool IsReadOnlyCollection => EnumerableType == ReadOnlyCollectionType; + public bool IsEnumerableInterface => EnumerableType == EnumerableInterfaceType; public bool HasCollectionInterface => CollectionInterfaceType.IsAssignableFrom(EnumerableType); - public bool IsDeclaredReadOnly => IsArray || IsEnumerableInterface; + public bool IsReadOnly => IsArray || IsReadOnlyCollection; + + public bool IsDeclaredReadOnly => IsReadOnly || IsEnumerableInterface; public Type EnumerableType { get; } public Type ElementType { get; } - public Type WrapperType => GetEnumerableType(ref _wrapperType, typeof(ReadOnlyCollectionWrapper<>)); - public Type ListType => GetEnumerableType(ref _listType, typeof(List<>)); public Type ListInterfaceType => GetEnumerableType(ref _listInterfaceType, typeof(IList<>)); public Type CollectionType => GetEnumerableType(ref _collectionType, typeof(Collection<>)); + public Type ReadOnlyCollectionType => GetEnumerableType(ref _readOnlyCollectionType, typeof(ReadOnlyCollection<>)); + public Type CollectionInterfaceType => GetEnumerableType(ref _collectionInterfaceType, typeof(ICollection<>)); public Type EnumerableInterfaceType => GetEnumerableType(ref _enumerableInterfaceType, typeof(IEnumerable<>)); private Type GetEnumerableType(ref Type typeField, Type openGenericEnumerableType) => typeField ?? (typeField = openGenericEnumerableType.MakeGenericType(ElementType)); + + public Type WrapperType => typeof(ReadOnlyCollectionWrapper<>).MakeGenericType(ElementType); + + public Expression GetWrapperConstruction(Expression existingItems, Expression newItemsCount) + { + // ReSharper disable once AssignNullToNotNullAttribute + return Expression.New( + WrapperType.GetConstructor(new[] { ListInterfaceType, typeof(int) }), + existingItems, + newItemsCount); + } + + public Expression GetEnumerableConversion(Expression instance) + { + if (IsArray) + { + return instance.WithToArrayCall(ElementType); + } + + if (IsReadOnlyCollection) + { + return instance.WithToReadOnlyCollectionCall(ElementType); + } + + return instance.WithToListCall(ElementType); + } } } \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/Enumerables/OverwriteEnumerablePopulationStrategy.cs b/AgileMapper/ObjectPopulation/Enumerables/OverwriteEnumerablePopulationStrategy.cs index 6e8c4b23d..d45404e66 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/OverwriteEnumerablePopulationStrategy.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/OverwriteEnumerablePopulationStrategy.cs @@ -12,7 +12,7 @@ protected override Expression GetEnumerablePopulation( { if (builder.ElementTypesAreSimple) { - if (mappingData.MapperData.TargetType.IsArray) + if (builder.TargetTypeHelper.IsReadOnly) { builder.PopulateTargetVariableFromSourceObjectOnly(); return builder; diff --git a/AgileMapper/ObjectPopulation/Enumerables/ReadOnlyCollectionWrapper.cs b/AgileMapper/ObjectPopulation/Enumerables/ReadOnlyCollectionWrapper.cs index 038c841e1..d9995e98c 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/ReadOnlyCollectionWrapper.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/ReadOnlyCollectionWrapper.cs @@ -3,10 +3,10 @@ using System; using System.Collections; using System.Collections.Generic; + using System.Collections.ObjectModel; #if !NET_STANDARD using System.Diagnostics.CodeAnalysis; #endif - using System.Linq; using Extensions; /// @@ -16,98 +16,115 @@ /// The type of item stored in the collection. public class ReadOnlyCollectionWrapper : IList { - private readonly IEnumerable _existingItems; - private readonly T[] _newItems; + private static readonly ReadOnlyCollection _emptyReadOnlyCollection = new ReadOnlyCollection(Enumerable.EmptyArray); + + private readonly T[] _items; private int _index; /// /// Initializes a new instance of the ReadOnlyCollectionWrapper{T} class. /// - /// The existing items to retain in the final collection. + /// + /// A read-only IList containing the existing items to retain in the final collection. + /// /// The number of new items to be added to the existing items. - public ReadOnlyCollectionWrapper(IEnumerable existingItems, int numberOfNewItems) + public ReadOnlyCollectionWrapper(IList existingItems, int numberOfNewItems) { - // TODO: Handle if numberOfNewItems == 0 - _existingItems = existingItems; - _newItems = new T[numberOfNewItems]; - _index = 0; + var hasExistingItems = existingItems != null; + + if (hasExistingItems) + { + _index = existingItems.Count; + } + else if (numberOfNewItems == 0) + { + _items = Enumerable.EmptyArray; + return; + } + + _items = new T[_index + numberOfNewItems]; + + if (hasExistingItems) + { + _items.CopyFrom(existingItems); + } } -#region IList Members + #region IList Members /// /// Determines the index of a specific item. /// /// The object to locate in the /// The index of item if found; otherwise, -1. -#region ExcludeFromCodeCoverage + #region ExcludeFromCodeCoverage #if !NET_STANDARD [ExcludeFromCodeCoverage] #endif -#endregion - public int IndexOf(T item) => Array.IndexOf(_newItems, item, 0, _newItems.Length); + #endregion + public int IndexOf(T item) => Array.IndexOf(_items, item, 0, _index); /// /// Inserts an item at the specified index /// /// The zero-based index at which item should be inserted. /// The object to insert. -#region ExcludeFromCodeCoverage + #region ExcludeFromCodeCoverage #if !NET_STANDARD [ExcludeFromCodeCoverage] #endif -#endregion - public void Insert(int index, T item) => ((IList)_newItems).Insert(index, item); + #endregion + public void Insert(int index, T item) => ((IList)_items).Insert(index, item); /// /// Removes the item at the specified index. /// /// The zero-based index of the item to remove. -#region ExcludeFromCodeCoverage + #region ExcludeFromCodeCoverage #if !NET_STANDARD [ExcludeFromCodeCoverage] #endif -#endregion - public void RemoveAt(int index) => ((IList)_newItems).RemoveAt(index); + #endregion + public void RemoveAt(int index) => ((IList)_items).RemoveAt(index); /// /// Gets or sets the element at the specified index. /// /// The zero-based index of the element to get or set. /// The element at the specified index. -#region ExcludeFromCodeCoverage + #region ExcludeFromCodeCoverage #if !NET_STANDARD [ExcludeFromCodeCoverage] #endif -#endregion + #endregion public T this[int index] { - get { return _newItems[index]; } - set { _newItems[index] = value; } + get { return _items[index]; } + set { _items[index] = value; } } -#endregion + #endregion -#region ICollection Members + #region ICollection Members /// /// Gets the number of elements contained in the collection. /// -#region ExcludeFromCodeCoverage + #region ExcludeFromCodeCoverage #if !NET_STANDARD [ExcludeFromCodeCoverage] #endif -#endregion - public int Count => _newItems.Length + (_existingItems?.Count() ?? 0); + #endregion + public int Count => _items.Length; /// /// Gets a value indicating whether the collection is read-only. /// -#region ExcludeFromCodeCoverage + #region ExcludeFromCodeCoverage #if !NET_STANDARD [ExcludeFromCodeCoverage] #endif -#endregion + #endregion public bool IsReadOnly => true; /// @@ -116,7 +133,7 @@ public T this[int index] /// The object to add. public void Add(T item) { - _newItems[_index] = item; + _items[_index] = item; ++_index; } @@ -125,11 +142,11 @@ public void Add(T item) /// /// The object to locate. /// True if item is found in the collection, otherwise false. -#region ExcludeFromCodeCoverage + #region ExcludeFromCodeCoverage #if !NET_STANDARD [ExcludeFromCodeCoverage] #endif -#endregion + #endregion public bool Contains(T item) => IndexOf(item) != -1; /// @@ -140,12 +157,12 @@ public void Add(T item) /// collection. The array must have zero-based indexing. /// /// The zero-based index in array at which copying begins. -#region ExcludeFromCodeCoverage + #region ExcludeFromCodeCoverage #if !NET_STANDARD [ExcludeFromCodeCoverage] #endif -#endregion - public void CopyTo(T[] array, int arrayIndex) => ((ICollection)_newItems).CopyTo(array, arrayIndex); + #endregion + public void CopyTo(T[] array, int arrayIndex) => ((ICollection)_items).CopyTo(array, arrayIndex); /// /// Removes the first occurrence of a specific object from the collection. @@ -155,63 +172,68 @@ public void Add(T item) /// True if the item was successfully removed from the collection, otherwise false. /// This method also returns false if item is not found in the original collection. /// -#region ExcludeFromCodeCoverage + #region ExcludeFromCodeCoverage #if !NET_STANDARD [ExcludeFromCodeCoverage] #endif -#endregion - public bool Remove(T item) => ((ICollection)_newItems).Remove(item); + #endregion + public bool Remove(T item) => ((ICollection)_items).Remove(item); /// /// Removes all items from the collection. /// -#region ExcludeFromCodeCoverage + #region ExcludeFromCodeCoverage #if !NET_STANDARD [ExcludeFromCodeCoverage] #endif -#endregion - public void Clear() => ((ICollection)_newItems).Clear(); + #endregion + public void Clear() => ((ICollection)_items).Clear(); -#endregion + #endregion -#region IEnumerable members + #region IEnumerable members /// /// Returns an enumerator that iterates through the collection. /// /// An enumerator that can be used to iterate through the collection. -#region ExcludeFromCodeCoverage + #region ExcludeFromCodeCoverage #if !NET_STANDARD [ExcludeFromCodeCoverage] #endif -#endregion - public IEnumerator GetEnumerator() => ((IEnumerable)_newItems).GetEnumerator(); + #endregion + public IEnumerator GetEnumerator() => ((IEnumerable)_items).GetEnumerator(); /// /// Returns an enumerator that iterates through a collection. /// /// An IEnumerator object that can be used to iterate through the collection. -#region ExcludeFromCodeCoverage + #region ExcludeFromCodeCoverage #if !NET_STANDARD [ExcludeFromCodeCoverage] #endif -#endregion - IEnumerator IEnumerable.GetEnumerator() => _newItems.GetEnumerator(); + #endregion + IEnumerator IEnumerable.GetEnumerator() => _items.GetEnumerator(); -#endregion + #endregion /// /// Returns an array containing the contents of the . /// /// An array containing the contents of the . - public T[] ToArray() - { - if (_existingItems == null) - { - return _newItems; - } + public T[] ToArray() => _items; - return _existingItems.Concat(_newItems).ToArray(); + /// + /// Returns a ReadOnlyCollection containing the contents of the . + /// + /// + /// A ReadOnlyCollection containing the contents of the . + /// + public ReadOnlyCollection ToReadOnlyCollection() + { + return (_items != null) + ? new ReadOnlyCollection(_items) + : _emptyReadOnlyCollection; } } } \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/Enumerables/SourceEnumerableAdapterBase.cs b/AgileMapper/ObjectPopulation/Enumerables/SourceEnumerableAdapterBase.cs index 1d288c333..5525857a2 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/SourceEnumerableAdapterBase.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/SourceEnumerableAdapterBase.cs @@ -20,6 +20,6 @@ protected SourceEnumerableAdapterBase(EnumerablePopulationBuilder builder) public virtual Expression GetSourceValue() => Builder.MapperData.SourceObject; public virtual bool UseReadOnlyTargetWrapper => - TargetTypeHelper.IsArray && !SourceTypeHelper.IsEnumerableInterface; + TargetTypeHelper.IsReadOnly && !SourceTypeHelper.IsEnumerableInterface; } } \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/ExistingOrDefaultValueDataSourceFactory.cs b/AgileMapper/ObjectPopulation/ExistingOrDefaultValueDataSourceFactory.cs index 2b776551b..eb59c6e21 100644 --- a/AgileMapper/ObjectPopulation/ExistingOrDefaultValueDataSourceFactory.cs +++ b/AgileMapper/ObjectPopulation/ExistingOrDefaultValueDataSourceFactory.cs @@ -10,11 +10,11 @@ internal class ExistingOrDefaultValueDataSourceFactory : IDataSourceFactory public static readonly IDataSourceFactory Instance = new ExistingOrDefaultValueDataSourceFactory(); public IDataSource Create(IMemberMapperData mapperData) - => new ExistingMemberValueOrEmptyDataSource(mapperData); + => new ExistingMemberValueOrDefaultDataSource(mapperData); - private class ExistingMemberValueOrEmptyDataSource : DataSourceBase + private class ExistingMemberValueOrDefaultDataSource : DataSourceBase { - public ExistingMemberValueOrEmptyDataSource(IMemberMapperData mapperData) + public ExistingMemberValueOrDefaultDataSource(IMemberMapperData mapperData) : base(mapperData.SourceMember, GetValue(mapperData), mapperData) { } @@ -50,6 +50,7 @@ private static bool FallbackToNull(IBasicMapperData mapperData) if (dictionaryTargetMember.HasEnumerableEntries) { + // TODO: Test coverage - target dictionary with enumerable entries return false; } diff --git a/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs b/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs index 7c5365e8d..5329fc3bf 100644 --- a/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs +++ b/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs @@ -103,6 +103,8 @@ private Expression GetMappingBlock(IList mappingExpressions, Mapping var mapperData = mappingExtras.MapperData; + AdjustForSingleExpressionBlockIfApplicable(ref mappingExpressions); + if (mappingExpressions[0].NodeType != ExpressionType.Block) { if (mappingExpressions[0].NodeType == ExpressionType.MemberAccess) @@ -117,7 +119,7 @@ private Expression GetMappingBlock(IList mappingExpressions, Mapping var assignedValue = ((BinaryExpression)objectAssignment).Right; returnExpression = GetReturnExpression(assignedValue, mappingExtras); - if (mappingExpressions.Count == 1) + if (mappingExpressions.HasOne()) { return returnExpression; } @@ -137,6 +139,21 @@ private Expression GetMappingBlock(IList mappingExpressions, Mapping return mappingBlock; } + private static void AdjustForSingleExpressionBlockIfApplicable(ref IList mappingExpressions) + { + if (!mappingExpressions.HasOne() || (mappingExpressions[0].NodeType != ExpressionType.Block)) + { + return; + } + + var block = (BlockExpression)mappingExpressions[0]; + + if (block.Expressions.HasOne()) + { + mappingExpressions = block.Expressions; + } + } + private static Expression GetReturnExpression(Expression returnValue, MappingExtras mappingExtras) { return (mappingExtras.MapToNullCondition != null) diff --git a/AgileMapper/ObjectPopulation/ObjectMapperData.cs b/AgileMapper/ObjectPopulation/ObjectMapperData.cs index fd8e5743a..5aca906cc 100644 --- a/AgileMapper/ObjectPopulation/ObjectMapperData.cs +++ b/AgileMapper/ObjectPopulation/ObjectMapperData.cs @@ -84,11 +84,7 @@ private ObjectMapperData( } else { - if (this.TargetMemberIsEnumerableElement()) - { - TargetTypeHasNotYetBeenMapped = TargetTypeWillNotBeMappedAgain = false; - } - else + if (!this.TargetMemberIsEnumerableElement()) { TargetTypeHasNotYetBeenMapped = IsTargetTypeFirstMapping(parent); TargetTypeWillNotBeMappedAgain = IsTargetTypeLastMapping(parent); @@ -353,13 +349,12 @@ public bool MappedObjectCachingNeeded private bool IsMappedObjectCachingNeeded() { - if (!TargetTypeHasNotYetBeenMapped || !TargetTypeWillNotBeMappedAgain) + if (MapperContext.UserConfigurations.DisableObjectTracking(this)) { - return true; + return false; } - return _childMapperDatas.Any(childMapperData => childMapperData.MappedObjectCachingNeeded) || - Context.NeedsSubMapping; + return !TargetTypeHasNotYetBeenMapped || !TargetTypeWillNotBeMappedAgain; } public bool TargetTypeHasNotYetBeenMapped { get; } diff --git a/AgileMapper/TypeConversion/ToFormattedStringConverter.cs b/AgileMapper/TypeConversion/ToFormattedStringConverter.cs index 150c3bc82..4dbb620c8 100644 --- a/AgileMapper/TypeConversion/ToFormattedStringConverter.cs +++ b/AgileMapper/TypeConversion/ToFormattedStringConverter.cs @@ -18,6 +18,7 @@ public ToFormattedStringConverter(Type sourceValueType, string formattingString) if (_toStringMethod == null) { + // TODO: Test coverage - unformattable source type throw new NotSupportedException( "No ToString method taking a formatting string exists on type " + sourceValueType.GetFriendlyName()); }