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
}
}
}
Summarizing from the comment discussion/debugging some possible causes for issues like this can include:
.ToList()
/.AsEnumerable()
meaning automapper will not work custom mapping into the Query.