diff --git a/src/AutoMapper/QueryableExtensions/ExpressionBuilder.cs b/src/AutoMapper/QueryableExtensions/ExpressionBuilder.cs index ea0207f0cf..84a16a93b9 100644 --- a/src/AutoMapper/QueryableExtensions/ExpressionBuilder.cs +++ b/src/AutoMapper/QueryableExtensions/ExpressionBuilder.cs @@ -204,13 +204,11 @@ protected override Expression VisitNew(NewExpression node) } } - private List CreateMemberBindings(ExpressionRequest request, - TypeMap typeMap, - Expression instanceParameter, TypePairCount typePairCount, LetPropertyMaps letPropertyMaps) + private List CreateMemberBindings(ExpressionRequest request, TypeMap typeMap, Expression instanceParameter, TypePairCount typePairCount, LetPropertyMaps letPropertyMaps) { var bindings = new List(); - - foreach (var propertyMap in typeMap.GetPropertyMaps().Where(pm => pm.CanResolveValue() && ReflectionHelper.CanBeSet(pm.DestinationProperty))) + foreach (var propertyMap in typeMap.GetPropertyMaps().Where(pm => + (!pm.ExplicitExpansion || request.MembersToExpand.Contains(pm.DestinationProperty)) && pm.CanResolveValue() && ReflectionHelper.CanBeSet(pm.DestinationProperty))) { letPropertyMaps.Push(propertyMap); @@ -222,44 +220,33 @@ private List CreateMemberBindings(ExpressionRequest request, void CreateMemberBinding(PropertyMap propertyMap) { var result = ResolveExpression(propertyMap, request.SourceType, instanceParameter, letPropertyMaps); - - if(propertyMap.ExplicitExpansion && !request.MembersToExpand.Contains(propertyMap.DestinationProperty)) + var propertyTypeMap = _configurationProvider.ResolveTypeMap(result.Type, propertyMap.DestinationPropertyType); + var propertyRequest = new ExpressionRequest(result.Type, propertyMap.DestinationPropertyType, request.MembersToExpand, request); + if(propertyRequest.AlreadyExists) { return; } - var propertyTypeMap = _configurationProvider.ResolveTypeMap(result.Type, - propertyMap.DestinationPropertyType); - var propertyRequest = new ExpressionRequest(result.Type, propertyMap.DestinationPropertyType, request.MembersToExpand, request); - - if(!propertyRequest.AlreadyExists) + var binder = Binders.FirstOrDefault(b => b.IsMatch(propertyMap, propertyTypeMap, result)); + if(binder == null) { - var binder = Binders.FirstOrDefault(b => b.IsMatch(propertyMap, propertyTypeMap, result)); - - if(binder == null) - { - var message = - $"Unable to create a map expression from {propertyMap.SourceMember?.DeclaringType?.Name}.{propertyMap.SourceMember?.Name} ({result.Type}) to {propertyMap.DestinationProperty.DeclaringType?.Name}.{propertyMap.DestinationProperty.Name} ({propertyMap.DestinationPropertyType})"; - - throw new AutoMapperMappingException(message, null, typeMap.Types, typeMap, propertyMap); - } - - var bindExpression = binder.Build(_configurationProvider, propertyMap, propertyTypeMap, - propertyRequest, result, typePairCount, letPropertyMaps); - - - if (bindExpression != null) - { - var rhs = propertyMap.ValueTransformers - .Concat(typeMap.ValueTransformers) - .Concat(typeMap.Profile.ValueTransformers) - .Where(vt => vt.IsMatch(propertyMap)) - .Aggregate(bindExpression.Expression, (current, vtConfig) => ToType(ReplaceParameters(vtConfig.TransformerExpression, ToType(current, vtConfig.ValueType)), propertyMap.DestinationPropertyType)); + var message = + $"Unable to create a map expression from {propertyMap.SourceMember?.DeclaringType?.Name}.{propertyMap.SourceMember?.Name} ({result.Type}) to {propertyMap.DestinationProperty.DeclaringType?.Name}.{propertyMap.DestinationProperty.Name} ({propertyMap.DestinationPropertyType})"; + throw new AutoMapperMappingException(message, null, typeMap.Types, typeMap, propertyMap); + } + var bindExpression = binder.Build(_configurationProvider, propertyMap, propertyTypeMap, propertyRequest, result, typePairCount, letPropertyMaps); + if(bindExpression == null) + { + return; + } + var rhs = propertyMap.ValueTransformers + .Concat(typeMap.ValueTransformers) + .Concat(typeMap.Profile.ValueTransformers) + .Where(vt => vt.IsMatch(propertyMap)) + .Aggregate(bindExpression.Expression, (current, vtConfig) => ToType(ReplaceParameters(vtConfig.TransformerExpression, ToType(current, vtConfig.ValueType)), propertyMap.DestinationPropertyType)); - bindExpression = bindExpression.Update(rhs); + bindExpression = bindExpression.Update(rhs); - bindings.Add(bindExpression); - } - } + bindings.Add(bindExpression); } } diff --git a/src/IntegrationTests/CustomMapFrom/MapObjectPropertyFromSubQuery.cs b/src/IntegrationTests/CustomMapFrom/MapObjectPropertyFromSubQuery.cs index 7fa1b389ef..f7bf24adde 100644 --- a/src/IntegrationTests/CustomMapFrom/MapObjectPropertyFromSubQuery.cs +++ b/src/IntegrationTests/CustomMapFrom/MapObjectPropertyFromSubQuery.cs @@ -11,6 +11,100 @@ namespace AutoMapper.IntegrationTests using System.Linq.Expressions; using QueryableExtensions; + public class MapObjectPropertyFromSubQueryExplicitExpansion : AutoMapperSpecBase + { + protected override MapperConfiguration Configuration => new MapperConfiguration(cfg => + { + cfg.CreateMap() + .ForMember(d => d.Price, o => + { + o.MapFrom(source => source.Articles.Where(x => x.IsDefault && x.NationId == 1 && source.ECommercePublished).FirstOrDefault()); + o.ExplicitExpansion(); + }); + cfg.CreateMap() + .ForMember(d => d.RegionId, o => o.MapFrom(s => s.NationId)); + }); + + [Fact] + public void Should_map_ok() + { + using(var context = new ClientContext()) + { + var projection = context.Products.ProjectTo(Configuration); + var counter = new FirstOrDefaultCounter(); + counter.Visit(projection.Expression); + counter.Count.ShouldBe(0); + var productModel = projection.First(); + productModel.Price.ShouldBeNull(); + productModel.Id.ShouldBe(1); + } + } + + class FirstOrDefaultCounter : ExpressionVisitor + { + public int Count; + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + if(node.Method.Name == "FirstOrDefault") + { + Count++; + } + return base.VisitMethodCall(node); + } + } + + public partial class Article + { + public int Id { get; set; } + public int ProductId { get; set; } + public bool IsDefault { get; set; } + public short NationId { get; set; } + public virtual Product Product { get; set; } + } + + public partial class Product + { + public int Id { get; set; } + public string Name { get; set; } + public bool ECommercePublished { get; set; } + public virtual ICollection
Articles { get; set; } + public int Value { get; } + public virtual List
OtherArticles { get; } + } + + public class PriceModel + { + public int Id { get; set; } + public short RegionId { get; set; } + public bool IsDefault { get; set; } + } + + public class ProductModel + { + public int Id { get; set; } + public PriceModel Price { get; set; } + } + + class Initializer : DropCreateDatabaseAlways + { + protected override void Seed(ClientContext context) + { + context.Products.Add(new Product { ECommercePublished = true, Articles = new[] { new Article { IsDefault = true, NationId = 1, ProductId = 1 } } }); + } + } + + class ClientContext : DbContext + { + protected override void OnModelCreating(DbModelBuilder modelBuilder) + { + Database.SetInitializer(new Initializer()); + } + + public DbSet Products { get; set; } + } + } + public class MapObjectPropertyFromSubQuery : AutoMapperSpecBase { protected override MapperConfiguration Configuration => new MapperConfiguration(cfg=>