diff --git a/AgileMapper.UnitTests.Orms.EfCore2/WhenCreatingProjections.cs b/AgileMapper.UnitTests.Orms.EfCore2/WhenCreatingProjections.cs index a9162a669..c3c76a426 100644 --- a/AgileMapper.UnitTests.Orms.EfCore2/WhenCreatingProjections.cs +++ b/AgileMapper.UnitTests.Orms.EfCore2/WhenCreatingProjections.cs @@ -1,5 +1,6 @@ namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2 { + using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -67,5 +68,80 @@ public Task ShouldMapAQueryableAsAnEnumerable() result.ShouldHaveSingleItem().Value.ShouldBeTrue(); }); } + + // See https://github.com/agileobjects/AgileMapper/issues/204 + [Fact] + public void ShouldHandleEnumerableQueryableMappingUnmappableElements() + { + var source = + new[] { new PublicBool { Value = true } } + .AsQueryable().Where(b => b.Value); + + var result = source.Project().To().First(); + + result.ShouldNotBeNull().CanWrite.ShouldBeFalse(); + } + + // See https://github.com/agileobjects/AgileMapper/issues/204 + [Fact] + public void ShouldIgnoreConfiguredServiceProviderInEnumerableQueryableMapping() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .UseServiceProvider(new Issue204.ServiceProvider()); + + mapper.WhenMapping + .From() + .To() + .Map(ctx => ctx.GetService().HasAccess()) + .To(dto => dto.CanWrite); + + var source = + new List { new Issue204.Entity { Id = 1 } } + .AsQueryable().Where(e => e.Id == 1); + + var result = source.ProjectUsing(mapper).To().First(); + + result.ShouldNotBeNull(); + result.Id.ShouldBe(1); + result.CanWrite.ShouldBeFalse(); + } + } + + #region Helper Classes + + private static class Issue204 + { + public class ServiceProvider : IServiceProvider + { + public object GetService(Type serviceType) + => Activator.CreateInstance(serviceType); + } + + public class User + { + public bool HasAccess() => true; + } + + public class Entity + { + public long Id { get; set; } + } + + public class Dto + { + public bool CanWrite { get; set; } + } + + public class EntityDto + { + public long Id { get; set; } + + public bool CanWrite { get; set; } + } + } + + #endregion } } diff --git a/AgileMapper.UnitTests/Dictionaries/WhenMappingToNewDictionaryMembers.cs b/AgileMapper.UnitTests/Dictionaries/WhenMappingToNewDictionaryMembers.cs index 57cbe0dad..9066b94be 100644 --- a/AgileMapper.UnitTests/Dictionaries/WhenMappingToNewDictionaryMembers.cs +++ b/AgileMapper.UnitTests/Dictionaries/WhenMappingToNewDictionaryMembers.cs @@ -341,7 +341,7 @@ public void ShouldFlattenAComplexTypeCollectionToANestedObjectDictionaryImplemen // See https://github.com/agileobjects/AgileMapper/issues/200 [Fact] - public void ShouldMapListDictionaries() + public void ShouldMapBetweenListDictionaries() { var source = new PublicField>> { @@ -364,7 +364,7 @@ public void ShouldMapListDictionaries() } [Fact] - public void ShouldMapArrayDictionaries() + public void ShouldMapBetweenArrayDictionaries() { var source = new PublicField> { @@ -384,7 +384,7 @@ public void ShouldMapArrayDictionaries() .Value .ShouldNotBeNull(); - resultDictionary.Count.ShouldBe(3); ; + resultDictionary.Count.ShouldBe(3); resultDictionary["1"].ShouldHaveSingleItem().ShouldBe(1L); resultDictionary["2"].Length.ShouldBe(2); @@ -397,6 +397,46 @@ public void ShouldMapArrayDictionaries() resultDictionary["3"][2].ShouldBe(3L); } + // See https://github.com/agileobjects/AgileMapper/issues/203 + [Fact] + public void ShouldMapBetweenComplexTypeArrayDictionaries() + { + var source = new PublicField> + { + Value = new Dictionary + { + ["1"] = new[] { new Address { Line1 = "1.1.1" } }, + ["2"] = new[] + { + new Address { Line1 = "2.1.1" }, + new Address { Line1 = "2.2.1", Line2 = "2.2.2" } + } + } + }; + + var result = Mapper.Map(source) + .ToANew>>(); + + var resultDictionary = result + .ShouldNotBeNull() + .Value + .ShouldNotBeNull(); + + resultDictionary.Count.ShouldBe(2); + var address11 = resultDictionary["1"].ShouldHaveSingleItem().ShouldNotBeNull(); + address11.Line1.ShouldBe("1.1.1"); + address11.Line2.ShouldBeNull(); + + resultDictionary["2"].Length.ShouldBe(2); + var address21 = resultDictionary["2"][0].ShouldNotBeNull(); + address21.Line1.ShouldBe("2.1.1"); + address21.Line2.ShouldBeNull(); + + var address22 = resultDictionary["2"][1].ShouldNotBeNull(); + address22.Line1.ShouldBe("2.2.1"); + address22.Line2.ShouldBe("2.2.2"); + } + #region Helper Members private static class Issue97 diff --git a/AgileMapper.UnitTests/WhenMappingToNewEnumerableMembers.cs b/AgileMapper.UnitTests/WhenMappingToNewEnumerableMembers.cs index 7e873c529..8400194bf 100644 --- a/AgileMapper.UnitTests/WhenMappingToNewEnumerableMembers.cs +++ b/AgileMapper.UnitTests/WhenMappingToNewEnumerableMembers.cs @@ -2,7 +2,9 @@ { using System.Collections.Generic; using System.Collections.ObjectModel; +#if !NETCOREAPP1_0 using System.ComponentModel; +#endif using System.Linq; using Common; using TestClasses; diff --git a/AgileMapper/AgileMapper.csproj b/AgileMapper/AgileMapper.csproj index 1e143265b..88b116af3 100644 --- a/AgileMapper/AgileMapper.csproj +++ b/AgileMapper/AgileMapper.csproj @@ -20,8 +20,8 @@ $(PackageTargetFallback);dnxcore50 MIT ./Icon.png - - Fixing Dictionary-of-collections mapping, re: #200 -- Updating to ReadableExpressions v2.5.1 + - Fixing complex type collection Dictionary mapping, re: #203 +- Removing service injections from projections, re: #204 diff --git a/AgileMapper/Extensions/Internal/ExpressionExtensions.CanBeProjected.cs b/AgileMapper/Extensions/Internal/ExpressionExtensions.CanBeProjected.cs index e33939805..20cbe290c 100644 --- a/AgileMapper/Extensions/Internal/ExpressionExtensions.CanBeProjected.cs +++ b/AgileMapper/Extensions/Internal/ExpressionExtensions.CanBeProjected.cs @@ -62,6 +62,16 @@ private static bool IsNonSourceMappingDataMember(MemberExpression memberAccess) return false; } + protected override Expression VisitMethodCall(MethodCallExpression methodCall) + { + if (methodCall.Method.DeclaringType == typeof(IServiceProviderAccessor)) + { + _isNotProjectable = true; + } + + return base.VisitMethodCall(methodCall); + } + protected override Expression VisitParameter(ParameterExpression parameter) { if (_lambdaParameters.IndexOf(parameter) > 0) diff --git a/AgileMapper/MappingRuleSetSettings.cs b/AgileMapper/MappingRuleSetSettings.cs index 45d62c50a..d3369e77b 100644 --- a/AgileMapper/MappingRuleSetSettings.cs +++ b/AgileMapper/MappingRuleSetSettings.cs @@ -23,6 +23,7 @@ public static MappingRuleSetSettings ForInMemoryMapping( CheckDerivedSourceTypes = true, AllowGuardedBindings = allowGuardedBindings, AllowCloneEntityKeyMapping = allowCloneEntityKeyMapping, + RemoveEmptyElementMappings = true, GuardAccessTo = value => true, ExpressionIsSupported = value => true, AllowObjectTracking = true, @@ -52,6 +53,8 @@ public static MappingRuleSetSettings ForInMemoryMapping( public bool AllowCloneEntityKeyMapping { get; set; } + public bool RemoveEmptyElementMappings { get; set; } + public Func GuardAccessTo { get; set; } public Func ExpressionIsSupported { get; set; } diff --git a/AgileMapper/Members/Dictionaries/DictionaryTargetMember.cs b/AgileMapper/Members/Dictionaries/DictionaryTargetMember.cs index 61ffc236f..30aaa39de 100644 --- a/AgileMapper/Members/Dictionaries/DictionaryTargetMember.cs +++ b/AgileMapper/Members/Dictionaries/DictionaryTargetMember.cs @@ -190,7 +190,8 @@ private Expression GetKey(IMemberMapperData mapperData) private Expression GetDictionaryAccess(IMemberMapperData mapperData) { - var parentContextAccess = mapperData.GetAppropriateMappingContextAccess(typeof(object), _rootDictionaryMember.Type); + var parentContextAccess = mapperData + .GetAppropriateMappingContextAccess(typeof(object), _rootDictionaryMember.Type); if (parentContextAccess.NodeType != Parameter) { @@ -207,7 +208,8 @@ private Expression GetDictionaryAccess(IMemberMapperData mapperData) return dictionaryMapperData.TargetInstance; } - public override bool CheckExistingElementValue => !HasObjectEntries && !HasSimpleEntries; + public override bool CheckExistingElementValue + => !(HasObjectEntries || HasSimpleEntries || HasEnumerableEntries); public override Expression GetHasDefaultValueCheck(IMemberMapperData mapperData) { @@ -346,7 +348,7 @@ public DictionaryTargetMember WithTypeOf(Member sourceMember) public override void MapCreating(Type sourceType) { - if (CreateNonDictionaryChildMembers(sourceType)) + if (DoNotFlattenSourceObjects(sourceType)) { _createDictionaryChildMembers = false; } @@ -354,15 +356,14 @@ public override void MapCreating(Type sourceType) base.MapCreating(sourceType); } - private bool CreateNonDictionaryChildMembers(Type sourceType) + private bool DoNotFlattenSourceObjects(Type sourceType) { - // If this DictionaryTargetMember represents an object-typed dictionary - // entry and we're mapping from a source of type object, we switch from - // mapping to flattened entries to mapping entire objects: - return HasObjectEntries && - this.IsEnumerableElement() && - (MemberChain[Depth - 2] == _rootDictionaryMember.LeafMember) && - (sourceType == typeof(object)); + // If this target Dictionary member's type matches the type of source + // objects being mapped into it, we switch from flattening source + // objects into Dictionary entries to mapping entire objects: + return this.IsEnumerableElement() && + (ValueType == sourceType) && + (MemberChain[Depth - 2] == _rootDictionaryMember.LeafMember); } #region ExcludeFromCodeCoverage diff --git a/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/DictionaryPopulationBuilder.cs b/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/DictionaryPopulationBuilder.cs index 1551da816..f3a848076 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/DictionaryPopulationBuilder.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/DictionaryPopulationBuilder.cs @@ -124,7 +124,7 @@ private Expression AssignDictionaryEntry( return GetPopulation(loopData, dictionaryEntryMember, mappingData); } - mappingData = GetMappingData(mappingData); + mappingData = GetEntryMappingData(mappingData); if (dictionaryEntryMember.HasComplexEntries) { @@ -275,14 +275,12 @@ private Expression GetPopulation( return populationExpression; } - private IObjectMappingData GetMappingData(IObjectMappingData mappingData) + private IObjectMappingData GetEntryMappingData(IObjectMappingData mappingData) { var sourceElementType = _wrappedBuilder.Context.SourceElementType; var targetElementType = _targetDictionaryMember.GetElementType(sourceElementType); - mappingData = ObjectMappingDataFactory.ForElement(sourceElementType, targetElementType, mappingData); - - return mappingData; + return ObjectMappingDataFactory.ForElement(sourceElementType, targetElementType, mappingData); } private static Expression GetDerivedTypeMapping( diff --git a/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationBuilder.cs b/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationBuilder.cs index f4d1989e7..f4900bda7 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationBuilder.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationBuilder.cs @@ -321,12 +321,15 @@ private void CreateSourceTypeHelper(Expression sourceValue) #region Target Variable Population - public void PopulateTargetVariableFromSourceObjectOnly(IObjectMappingData mappingData = null) - => AssignTargetVariableTo(GetSourceOnlyReturnValue(mappingData)); + public void PopulateTargetVariableFromSourceObjectOnly(IObjectMappingData enumerableMappingData = null) + => AssignTargetVariableTo(GetSourceOnlyReturnValue(enumerableMappingData)); - private Expression GetSourceOnlyReturnValue(IObjectMappingData mappingData) + private Expression GetSourceOnlyReturnValue(IObjectMappingData enumerableMappingData) { - var convertedSourceItems = _sourceItemsSelector.SourceItemsProjectedToTargetType(mappingData).GetResult(); + var convertedSourceItems = _sourceItemsSelector + .SourceItemsProjectedToTargetType(enumerableMappingData) + .GetResult(); + var returnValue = ConvertForReturnValue(convertedSourceItems); return returnValue; @@ -510,7 +513,7 @@ public void RemoveAllTargetItems() _populationExpressions.Add(GetTargetMethodCall("Clear")); } - public void AddNewItemsToTargetVariable(IObjectMappingData mappingData) + public void AddNewItemsToTargetVariable(IObjectMappingData enumerableMappingData) { if (TargetElementsAreSimple && Context.ElementTypesAreTheSame && TargetTypeHelper.IsList) { @@ -518,7 +521,7 @@ public void AddNewItemsToTargetVariable(IObjectMappingData mappingData) return; } - BuildPopulationLoop(GetElementPopulation, mappingData); + BuildPopulationLoop(GetElementPopulation, enumerableMappingData); } public void BuildPopulationLoop( @@ -535,9 +538,11 @@ public void BuildPopulationLoop( _populationExpressions.AddUnlessNullOrEmpty(populationLoop); } - private Expression GetElementPopulation(IPopulationLoopData loopData, IObjectMappingData mappingData) + private Expression GetElementPopulation( + IPopulationLoopData loopData, + IObjectMappingData enumerableMappingData) { - var elementMapping = loopData.GetElementMapping(mappingData); + var elementMapping = loopData.GetElementMapping(enumerableMappingData); if (elementMapping == Constants.EmptyExpression) { @@ -568,24 +573,26 @@ private bool InsertSourceObjectElementNullCheck(IPopulationLoopData loopData, ou return sourceElement.Type == typeof(object); } - public Expression GetElementConversion(Expression sourceElement, IObjectMappingData mappingData) + public Expression GetElementConversion( + Expression sourceElement, + IObjectMappingData enumerableMappingData) { if (TargetElementsAreSimple) { return GetSimpleElementConversion(sourceElement); } - var targetMember = mappingData.MapperData.TargetMember; + var targetMember = enumerableMappingData.MapperData.TargetMember; Expression existingElementValue; - if (targetMember.CheckExistingElementValue && mappingData.MapperData.TargetCouldBePopulated()) + if (targetMember.CheckExistingElementValue && enumerableMappingData.MapperData.TargetCouldBePopulated()) { - var existingElementValueCheck = targetMember.GetAccessChecked(mappingData.MapperData); + var existingElementValueCheck = targetMember.GetAccessChecked(enumerableMappingData.MapperData); if (existingElementValueCheck.Variables.Any()) { - return GetValueCheckedElementMapping(sourceElement, existingElementValueCheck, mappingData); + return GetValueCheckedElementMapping(sourceElement, existingElementValueCheck, enumerableMappingData); } existingElementValue = existingElementValueCheck; @@ -595,7 +602,7 @@ public Expression GetElementConversion(Expression sourceElement, IObjectMappingD existingElementValue = Context.TargetElementType.ToDefaultExpression(); } - return GetElementMapping(sourceElement, existingElementValue, mappingData); + return GetElementMapping(sourceElement, existingElementValue, enumerableMappingData); } private static Expression GetValueCheckedElementMapping( @@ -797,7 +804,8 @@ internal SourceItemsSelector(EnumerablePopulationBuilder builder) _builder = builder; } - public SourceItemsSelector SourceItemsProjectedToTargetType(IObjectMappingData mappingData = null) + public SourceItemsSelector SourceItemsProjectedToTargetType( + IObjectMappingData enumerableMappingData = null) { var context = _builder.Context; var sourceEnumerableValue = _builder._sourceAdapter.GetSourceValues(); @@ -811,7 +819,7 @@ public SourceItemsSelector SourceItemsProjectedToTargetType(IObjectMappingData m _result = _builder.GetSourceItemsProjection( sourceEnumerableValue, - sourceElement => _builder.GetElementConversion(sourceElement, mappingData)); + sourceElement => _builder.GetElementConversion(sourceElement, enumerableMappingData)); return this; } diff --git a/AgileMapper/ObjectPopulation/MapperDataContext.cs b/AgileMapper/ObjectPopulation/MapperDataContext.cs index f6287ea91..2e4ae3bc7 100644 --- a/AgileMapper/ObjectPopulation/MapperDataContext.cs +++ b/AgileMapper/ObjectPopulation/MapperDataContext.cs @@ -95,6 +95,9 @@ private void BubbleRuntimeTypedMappingNeededToEntryPoint() public bool UseLocalVariable { get; } + public bool RemoveEmptyEntryPointMappings + => _mapperData.RuleSet.Settings.RemoveEmptyElementMappings; + public bool UseMappingTryCatch => _mapperData.RuleSet.Settings.UseTryCatch && (_mapperData.IsEntryPoint || !IsPartOfUserStructMapping()); diff --git a/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs b/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs index eee848757..1428322e8 100644 --- a/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs +++ b/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs @@ -49,12 +49,12 @@ public Expression Create(IObjectMappingData mappingData) AddPopulationsAndCallbacks(context); - if (NothingIsBeingMapped(context)) + if (RemoveEmptyMappings(context) && NothingIsBeingMapped(context)) { return mapperData.IsEntryPoint ? mapperData.TargetObject : Constants.EmptyExpression; } - CompleteMappingBlock: + CompleteMappingBlock: InsertShortCircuitReturns(context); var mappingBlock = GetMappingBlock(context); @@ -319,6 +319,16 @@ private static bool NothingIsBeingMapped(MappingCreationContext context) return objectNewing.Arguments.None() && (objectNewing.Type != typeof(object)); } + private static bool RemoveEmptyMappings(MappingCreationContext context) + { + if (context.MapperData.TargetMemberIsEnumerableElement()) + { + return context.RuleSet.Settings.RemoveEmptyElementMappings; + } + + return true; + } + private Expression GetMappingBlock(MappingCreationContext context) { var mappingExpressions = context.MappingExpressions; diff --git a/AgileMapper/ObjectPopulation/MappingFactory.cs b/AgileMapper/ObjectPopulation/MappingFactory.cs index 9074d8629..5c94cd8b2 100644 --- a/AgileMapper/ObjectPopulation/MappingFactory.cs +++ b/AgileMapper/ObjectPopulation/MappingFactory.cs @@ -6,10 +6,10 @@ using System.Linq.Expressions; #endif using Caching.Dictionaries; - using Extensions; using Extensions.Internal; using Members; using Members.MemberExtensions; + using NetStandardPolyfills; internal static class MappingFactory { @@ -88,7 +88,7 @@ public static Expression GetElementMapping( { var mapperData = mappingData.MapperData; - if (CreateElementMappingDataFor(mapperData)) + if (CreateElementMappingDataFor(mapperData, sourceElementValue)) { mappingData = ObjectMappingDataFactory.ForElement(mappingData); } @@ -103,16 +103,19 @@ public static Expression GetElementMapping( return GetElementMapping(mappingData, sourceElementValue, targetElementValue); } - private static bool CreateElementMappingDataFor(IQualifiedMemberContext context) + private static bool CreateElementMappingDataFor( + ObjectMapperData mapperData, + Expression sourceElementValue) { - if (!context.TargetMemberIsEnumerableElement()) + if (!mapperData.TargetMemberIsEnumerableElement()) { return true; } - if (context.TargetMember.IsEnumerable) + if (mapperData.TargetMember.IsEnumerable) { - return !context.TargetMember.ElementType.IsSimple(); + return !mapperData.EnumerablePopulationBuilder.TargetElementsAreSimple && + sourceElementValue.Type.IsAssignableTo(mapperData.SourceMember.ElementType); } return false; diff --git a/Directory.Build.props b/Directory.Build.props index f2e031791..252555c70 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -11,9 +11,9 @@ true git https://github.com/AgileObjects/AgileMapper - 1.7.1 - 1.7.1.0 - 1.7.1.0 + 1.7.2 + 1.7.2.0 + 1.7.2.0 \ No newline at end of file diff --git a/NuGet/AgileObjects.AgileMapper.1.7.2.nupkg b/NuGet/AgileObjects.AgileMapper.1.7.2.nupkg new file mode 100644 index 000000000..2de752837 Binary files /dev/null and b/NuGet/AgileObjects.AgileMapper.1.7.2.nupkg differ diff --git a/docs/src/query-projection/Configuration.md b/docs/src/query-projection/Configuration.md index c44858844..acb332f4b 100644 --- a/docs/src/query-projection/Configuration.md +++ b/docs/src/query-projection/Configuration.md @@ -68,6 +68,8 @@ Because projections are performed by [IQueryProvider](https://docs.microsoft.com - Object [factories](/configuration/Object-Construction) +- [Injected](/configuration/Dependency-Injection) values + - Null [mapping results](/configuration/Null-Results) Most notably, [callbacks](/configuration/Mapping-Callbacks), [object tracking](/configuration/Mapped-Object-Tracking) and [derived type pairing](/configuration/Pairing-Derived-Types) are not supported - although you can [project to derived types](/query-projection/Derived-Types) conditionally.