entity-framework-coreautomapperautomapper-13

MapFrom and ProjectTo fail to populate target property


Given the following EF Core entity class:

[PrimaryKey(nameof(CustomerID))]
[Table(nameof(Customer))]
public partial class Customer
{
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public Nullable<Int32> CustomerID { get; set; }

    public string CustomerName { get; set; }
    public string CustomerCode { get; set; }

    public Nullable<int> HOOpAddressID { get; set; }

    [ForeignKey(nameof(HOOpAddressID))]
    public OperationalAddress HeadOfficeAddress { get; set; }

    public string Nickname { get; set; }

    public bool isActive { get; set; }
}

I have defined a DTO to hold a subset of the data as follows...

public class CustomerLookupListItem
{

    public Int32 CustomerID { get; set; }

    public String CustomerName { get; set; }
    public String Nickname { get; set; }
    public String Postcode { get; set; }

    public Boolean IsActive { get; set; }
}

And, in order to use AutoMapper, I have also defined a Profile as the Postcode property of the CustomerLookupListItem is not mapped from the top level:

public class CustomerLookupListItemMappingProfile : Profile
{
    public CustomerLookupListItemMappingProfile()
    {
        CreateMap<Customer, CustomerLookupListItem>()
            .ForMember(dst => dst.Postcode, opt => opt.MapFrom(src => src.HeadOfficeAddress.PostCode));
    }
}

When I then use the ProjectTo<T> method as a part of my EF query, the value of the Postcode property in the CustomerLookupListItem is always null.

Looking at the SQL that is sent to the database, I can see that it is not being requested as a part of the query...

info: Microsoft.EntityFrameworkCore.Database.Command
      Executed DbCommand (22ms) [Parameters=[@__startsWith_0_startswith='le%' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SELECT [c].[CustomerID], [c].[CustomerName], [c].[Nickname], [c].[isActive] AS [IsActive]
      FROM [Customer] AS [c]
      LEFT JOIN [OperationalAddress] AS [o] ON [c].[HOOpAddressID] = [o].[OperationalAddressID]
      WHERE [c].[isActive] = CAST(1 AS bit) AND [o].[PostCode] LIKE @__startsWith_0_startswith ESCAPE N'\'
      ORDER BY [o].[PostCode]

I have done this (MapFrom with ProjectTo) elsewhere in my application in multiple places and it has worked every time so why does it not work in this instance?

Note: I have verified that the constructor of the CustomerLookupListItemMappingProfile is being called so the mapping exists.

EDIT #1: Including the LINQ query as requested in the comments...

public async Task<List<CustomerLookupListItem>> GetCustomerPostcodeAutocompleteAsync(String startsWith, Boolean activeOnly)
    => await dbContext.Customers.AsNoTracking()
        .Where(c => !activeOnly || c.isActive && c.HeadOfficeAddress.PostCode.StartsWith(startsWith))
        .OrderBy(c => c.HeadOfficeAddress.PostCode)
        .ProjectTo<CustomerLookupListItem>(mapper.ConfigurationProvider)
        .ToListAsync();

EDIT #2: Including DebugView content from Automapper...

dbContext.Customers.ProjectTo(mapper.ConfigurationProvider).Expression.DebugView...

.Call System.Linq.Queryable.Select(
    .Extension<Microsoft.EntityFrameworkCore.Query.EntityQueryRootExpression>,
    '(.Lambda #Lambda1<System.Func`2[MyNameSpace.CustomerLookupListItem]>))

.Lambda #Lambda1<System.Func`2[MyNameSpace.CustomerLookupListItem]>(MyNameSpace.Customer $dtoCustomer) {
    .New MyNameSpace.CustomerLookupListItem() {
        CustomerID = $dtoCustomer.CustomerID ?? .New System.Int32(),
        CustomerName = $dtoCustomer.CustomerName,
        Nickname = $dtoCustomer.Nickname,
        IsActive = $dtoCustomer.isActive
    }
}

mapperConfiguration.BuildExecutionPlantypeof(Customer), typeof(CustomerLookupListItem)).Body.DebugView...

.If ($source == .Default(System.Object)) {
    .If ($destination == .Default(System.Object)) {
        .Default(MyNameSpace.CustomerLookupListItem)}
    .Else { 
        $destination
    }
}
.Else {
    .Block(MyNameSpace.CustomerLookupListItem $typeMapDestination) {
        .Block() {
            $typeMapDestination = ($destination ?? .New MyNameSpace.CustomerLookupListItem());
            .Try {
                .Block(
                    System.Nullable`1[System.Int32] $resolvedValue, System.Int32 $mappedValue) {
                        $resolvedValue = $source.CustomerID;
                        $mappedValue = .If ($resolvedValue.HasValue) {
                            $resolvedValue.Value
                        }
                        .Else {
                            .Default(System.Int32)
                        };
                        $typeMapDestination.CustomerID = $mappedValue
                    }
            }
            .Catch (System.Exception $ex) {
                .Throw
                .Call AutoMapper.Execution.TypeMapPlanBuilder.MemberMappingError($ex, .Constant<AutoMapper.PropertyMap>(CustomerID))
            };
            .Try {
                .Block(System.String $resolvedValue) {
                    $resolvedValue = $source.CustomerName;
                    $typeMapDestination.CustomerName = $resolvedValue
                }
            }
            .Catch (System.Exception $ex) {
                .Throw
                .Call AutoMapper.Execution.TypeMapPlanBuilder.MemberMappingError($ex, .Constant<AutoMapper.PropertyMap>(CustomerName))
            };
            .Try {
                .Block(System.String $resolvedValue) {
                    $resolvedValue = $source.Nickname;
                    $typeMapDestination.Nickname = $resolvedValue
                }
            }
            .Catch (System.Exception $ex) {
                .Throw
                .Call AutoMapper.Execution.TypeMapPlanBuilder.MemberMappingError($ex, .Constant<AutoMapper.PropertyMap>(Nickname))
            };
            .Try {
                .Block(System.Boolean $resolvedValue) {
                    $resolvedValue = $source.isActive;
                    $typeMapDestination.IsActive = $resolvedValue
                }
            }
            .Catch (System.Exception $ex) {
                .Throw
                .Call AutoMapper.Execution.TypeMapPlanBuilder.MemberMappingError($ex, .Constant<AutoMapper.PropertyMap>(IsActive))
            };
            .Try {
                .Block(System.String $resolvedValue) {
                    $resolvedValue = .Block(MyNameSpace.OperationalAddress $sourceHeadOfficeAddress) {
                        .Block() {
                            $sourceHeadOfficeAddress = $source.HeadOfficeAddress;
                            .If ($sourceHeadOfficeAddress == .Default(System.Object)) {
                                .Default(System.String)
                            }
                            .Else {
                                $sourceHeadOfficeAddress.PostCode
                            }
                        }
                    };
                    $typeMapDestination.Postcode = $resolvedValue
                }
            }
            .Catch (System.Exception $ex) {
                .Throw
                .Call AutoMapper.Execution.TypeMapPlanBuilder.MemberMappingError($ex, .Constant<AutoMapper.PropertyMap>(Postcode))
            };
            $typeMapDestination
        }
    }
}

Solution

  • Summarizing from the comment discussion/debugging some possible causes for issues like this can include: