From 154c0966aa5c8164ad5793a049a653e10a910f80 Mon Sep 17 00:00:00 2001 From: Steve Wilkes Date: Mon, 13 May 2019 08:22:24 +0100 Subject: [PATCH] Support for mapping (not projecting) queryables --- .../WhenCreatingProjections.cs | 19 +++++++++ .../WhenMappingToMetaMembers.cs | 33 +++++++++++++++ .../Finders/MetaMemberDataSourceFinder.cs | 40 +++++++------------ .../Internal/ExpressionExtensions.cs | 2 +- .../EnumerablePopulationBuilder.cs | 4 +- .../Enumerables/EnumerableTypeHelper.cs | 12 +++++- .../SourceEnumerableAdapterBase.cs | 2 +- .../QueryProjectionExpressionFactory.cs | 7 ++-- 8 files changed, 85 insertions(+), 34 deletions(-) diff --git a/AgileMapper.UnitTests.Orms.EfCore2/WhenCreatingProjections.cs b/AgileMapper.UnitTests.Orms.EfCore2/WhenCreatingProjections.cs index f7d7f559f..e3ab13636 100644 --- a/AgileMapper.UnitTests.Orms.EfCore2/WhenCreatingProjections.cs +++ b/AgileMapper.UnitTests.Orms.EfCore2/WhenCreatingProjections.cs @@ -1,5 +1,7 @@ namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2 { + using System.Collections.Generic; + using System.Linq; using System.Threading.Tasks; using Common; using Infrastructure; @@ -49,5 +51,22 @@ public Task ShouldReuseACachedProjectionMapper() mapper.RootMapperCountShouldBeOne(); }); } + + [Fact] + public Task ShouldMapAQueryableAsAnEnumerable() + { + return RunTest(async (context, mapper) => + { + await context.BoolItems.AddRangeAsync(new PublicBool { Value = true }, new PublicBool { Value = false }); + await context.SaveChangesAsync(); + + var result = mapper + .Map(context.BoolItems.Where(bi => bi.Value)) + .ToANew>(); + + result.ShouldNotBeNull(); + result.ShouldHaveSingleItem().Value.ShouldBeTrue(); + }); + } } } diff --git a/AgileMapper.UnitTests/WhenMappingToMetaMembers.cs b/AgileMapper.UnitTests/WhenMappingToMetaMembers.cs index 919f9f61c..8ef332a8a 100644 --- a/AgileMapper.UnitTests/WhenMappingToMetaMembers.cs +++ b/AgileMapper.UnitTests/WhenMappingToMetaMembers.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; + using System.Linq; using AgileMapper.Extensions; using Common; using TestClasses; @@ -144,6 +145,18 @@ public void ShouldPopulateAnEmptyHasEnumerableMemberNameMemberWithFalse() result.HasValue.ShouldBeFalse(); } + [Fact] + public void ShouldPopulateAHasQueryableMemberNameMember() + { + var source = new PublicField> + { + Value = new[] { new Address { Line1 = "Queryable?!" } }.AsQueryable() + }; + var result = Mapper.Map(source).ToANew>(); + + result.HasValue.ShouldBeTrue(); + } + [Fact] public void ShouldPopulateAnIntHasMemberNameMember() { @@ -266,6 +279,16 @@ public void ShouldPopulateAnEmptyFirstArrayMemberNameMemberToNull() result.FirstItem.ShouldBeNull(); } + [Fact] + public void ShouldPopulateAnEmptyFirstQueryableMemberNameMemberToNull() + { + var source = new { Items = Enumerable
.EmptyArray.AsQueryable() }; + var result = Mapper.Map(source).ToANew>>(); + + result.Items.ShouldBeEmpty(); + result.FirstItem.ShouldBeNull(); + } + [Fact] public void ShouldPopulateAFirstListMemberNameMember() { @@ -545,6 +568,16 @@ public void ShouldPopulateAListLongCountMember() result.ValueCount.ShouldBe(5L); } + [Fact] + public void ShouldPopulateAQueryableLongCountMember() + { + var source = new { Values = new[] { "1", "2", "3", "4" }.AsQueryable() }; + var result = Mapper.Map(source).ToANew>>(); + + result.Values.Count.ShouldBe(4); + result.ValueCount.ShouldBe(4L); + } + [Fact] public void ShouldNotPopulateANonNumericCountMember() { diff --git a/AgileMapper/DataSources/Finders/MetaMemberDataSourceFinder.cs b/AgileMapper/DataSources/Finders/MetaMemberDataSourceFinder.cs index 1f7cb6a0b..e581cf8d3 100644 --- a/AgileMapper/DataSources/Finders/MetaMemberDataSourceFinder.cs +++ b/AgileMapper/DataSources/Finders/MetaMemberDataSourceFinder.cs @@ -3,6 +3,11 @@ using System; using System.Collections.Generic; using System.Linq; +#if NET35 + using Microsoft.Scripting.Ast; +#else + using System.Linq.Expressions; +#endif using Extensions; using Extensions.Internal; using Members; @@ -11,11 +16,6 @@ using ObjectPopulation.Enumerables; using ReadableExpressions.Extensions; using TypeConversion; -#if NET35 - using Microsoft.Scripting.Ast; -#else - using System.Linq.Expressions; -#endif using static System.StringComparison; internal struct MetaMemberDataSourceFinder : IDataSourceFinder @@ -310,8 +310,10 @@ protected static Expression GetLinqMethodCall( Expression enumerable, EnumerableTypeHelper helper) { + var enumerableType = helper.IsQueryableInterface ? typeof(Queryable) : typeof(Enumerable); + return Expression.Call( - typeof(Enumerable) + enumerableType .GetPublicStaticMethod(methodName, parameterCount: 1) .MakeGenericMethod(helper.ElementType), enumerable); @@ -357,26 +359,16 @@ public override Expression GetAccess(Expression parentInstance) private Expression GetHasCheck(Expression queriedMemberAccess) { - if (QueriedMember.IsEnumerable) - { - return GetHasEnumerableCheck(queriedMemberAccess); - } - - var queriedMemberNotDefault = queriedMemberAccess.GetIsNotDefaultComparison(); - - if (QueriedMember.IsSimple) - { - return queriedMemberNotDefault; - } - - return queriedMemberNotDefault; + return QueriedMember.IsEnumerable + ? GetHasEnumerableCheck(queriedMemberAccess) + : queriedMemberAccess.GetIsNotDefaultComparison(); } private static Expression GetHasEnumerableCheck(Expression enumerableAccess) { var helper = new EnumerableTypeHelper(enumerableAccess.Type); - return helper.IsEnumerableInterface + return helper.IsEnumerableOrQueryable ? GetLinqMethodCall(nameof(Enumerable.Any), enumerableAccess, helper) : GetEnumerableCountCheck(enumerableAccess, helper); } @@ -470,14 +462,12 @@ private Expression GetCondition(Expression enumerableAccess, EnumerableTypeHelpe private Expression GetOrderedEnumerableAccess(Expression enumerableAccess, EnumerableTypeHelper helper) { - var elementType = _sourceMember.Type; - - if (!elementType.IsComplex()) + if (!_sourceMember.Type.IsComplex()) { return GetLinqMethodCall(LinqSelectionMethodName, enumerableAccess, helper); } - var orderMember = MapperData.GetOrderMember(elementType); + var orderMember = MapperData.GetOrderMember(_sourceMember.Type); if (orderMember == null) { @@ -559,7 +549,7 @@ public override Expression GetAccess(Expression enumerableAccess) { var helper = new EnumerableTypeHelper(enumerableAccess.Type); - var count = helper.IsEnumerableInterface + var count = helper.IsEnumerableOrQueryable ? GetLinqMethodCall(nameof(Enumerable.Count), enumerableAccess, helper) : helper.GetCountFor(enumerableAccess, MapperData.TargetMember.Type.GetNonNullableType()); diff --git a/AgileMapper/Extensions/Internal/ExpressionExtensions.cs b/AgileMapper/Extensions/Internal/ExpressionExtensions.cs index 92e6fb525..127de0cdc 100644 --- a/AgileMapper/Extensions/Internal/ExpressionExtensions.cs +++ b/AgileMapper/Extensions/Internal/ExpressionExtensions.cs @@ -386,7 +386,7 @@ public static Expression GetEmptyInstanceCreation( typeHelper = new EnumerableTypeHelper(enumerableType, elementType); } - if (typeHelper.IsEnumerableInterface) + if (typeHelper.IsEnumerableOrQueryable) { return Expression.Field(null, typeof(Enumerable<>).MakeGenericType(elementType), "Empty"); } diff --git a/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationBuilder.cs b/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationBuilder.cs index b185fd4d6..00a462a82 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationBuilder.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationBuilder.cs @@ -415,9 +415,9 @@ private Expression GetNullTargetConstruction() { var nullTargetVariableType = GetNullTargetVariableType(); - if (SourceTypeHelper.IsEnumerableInterface) + if (SourceTypeHelper.IsEnumerableOrQueryable) { - // Can't use a capacity constructor as unable to count source elements: + // Don't use a capacity constructor as source count not readily available: return Expression.New(nullTargetVariableType); } diff --git a/AgileMapper/ObjectPopulation/Enumerables/EnumerableTypeHelper.cs b/AgileMapper/ObjectPopulation/Enumerables/EnumerableTypeHelper.cs index cf353efad..d5241e0e6 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/EnumerableTypeHelper.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/EnumerableTypeHelper.cs @@ -3,6 +3,7 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.Enumerables using System; using System.Collections.Generic; using System.Collections.ObjectModel; + using System.Linq; using Extensions.Internal; using Members; using NetStandardPolyfills; @@ -24,6 +25,7 @@ internal class EnumerableTypeHelper private Type _readOnlyCollectionType; private Type _collectionInterfaceType; private Type _enumerableInterfaceType; + private Type _queryableInterfaceType; #if FEATURE_ISET private Type _setInterfaceType; #endif @@ -49,7 +51,11 @@ public bool IsDictionary public bool IsReadOnlyCollection => EnumerableType == ReadOnlyCollectionType; - public bool IsEnumerableInterface => EnumerableType == EnumerableInterfaceType; + private bool IsEnumerableInterface => EnumerableType == EnumerableInterfaceType; + + public bool IsQueryableInterface => EnumerableType == QueryableInterfaceType; + + public bool IsEnumerableOrQueryable => IsEnumerableInterface || IsQueryableInterface; public bool HasCollectionInterface => EnumerableType.IsAssignableTo(CollectionInterfaceType); @@ -61,7 +67,7 @@ public bool IsDictionary public bool IsReadOnly => IsArray || IsReadOnlyCollection; public bool IsDeclaredReadOnly - => IsReadOnly || IsEnumerableInterface || IsReadOnlyCollectionInterface(); + => IsReadOnly || IsEnumerableOrQueryable || IsReadOnlyCollectionInterface(); public bool CouldBeReadOnly() { @@ -111,6 +117,8 @@ private bool IsReadOnlyCollectionInterface() public Type EnumerableInterfaceType => GetEnumerableType(ref _enumerableInterfaceType, typeof(IEnumerable<>)); + public Type QueryableInterfaceType => GetEnumerableType(ref _queryableInterfaceType, typeof(IQueryable<>)); + #if FEATURE_ISET private Type SetInterfaceType => GetEnumerableType(ref _setInterfaceType, typeof(ISet<>)); #endif diff --git a/AgileMapper/ObjectPopulation/Enumerables/SourceEnumerableAdapterBase.cs b/AgileMapper/ObjectPopulation/Enumerables/SourceEnumerableAdapterBase.cs index 2a191d31a..4f55ccaae 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/SourceEnumerableAdapterBase.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/SourceEnumerableAdapterBase.cs @@ -24,6 +24,6 @@ protected SourceEnumerableAdapterBase(EnumerablePopulationBuilder builder) public virtual Expression GetSourceValues() => Builder.MapperData.SourceObject; public virtual bool UseReadOnlyTargetWrapper => - TargetTypeHelper.IsReadOnly && !SourceTypeHelper.IsEnumerableInterface; + TargetTypeHelper.IsReadOnly && !SourceTypeHelper.IsEnumerableOrQueryable; } } \ No newline at end of file diff --git a/AgileMapper/Queryables/QueryProjectionExpressionFactory.cs b/AgileMapper/Queryables/QueryProjectionExpressionFactory.cs index 968e208cd..eca899539 100644 --- a/AgileMapper/Queryables/QueryProjectionExpressionFactory.cs +++ b/AgileMapper/Queryables/QueryProjectionExpressionFactory.cs @@ -1,13 +1,13 @@ namespace AgileObjects.AgileMapper.Queryables { using System.Collections.Generic; - using Extensions.Internal; - using ObjectPopulation; #if NET35 using Microsoft.Scripting.Ast; #else using System.Linq.Expressions; #endif + using Extensions.Internal; + using ObjectPopulation; internal class QueryProjectionExpressionFactory : MappingExpressionFactoryBase { @@ -18,7 +18,8 @@ public override bool IsFor(IObjectMappingData mappingData) var mapperData = mappingData.MapperData; return mapperData.IsRoot && - mapperData.TargetMember.IsEnumerable && + mapperData.TargetMember.IsEnumerable && + (mappingData.MappingContext.RuleSet.Name == Constants.Project) && mapperData.SourceType.IsQueryable(); }