Can I map the first child in a collection efficiently with AutoMapper & EF Core? #3943
-
I would like to project the first child in a collection to another type with AutoMapper & EF, however my first attempt at this results in SQL which performs a sub-query for each property I'm selecting from it, plus an extra sub-query to see if it exists or not. Another attempt, where I manually map the collection to my type and then do the FirstOrDefault() - the SQL for this is better, selecting from Address only once. Is there a way to achieve something like this with AutoMapper, so I don't need to do the manual part? Thanks Example code (AutoMapper 11.0.1, EF Core 6.0.3) using AutoMapper;
using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
{
using var context = new PersonContext();
context.Database.EnsureCreated();
// Auto map both Person and Address
var mapper = new MapperConfiguration(x =>
{
x.CreateMap<Person, PersonDto>()
.ForMember(dto => dto.FirstAddress,
o => o.MapFrom(entity => entity.Addresses
.OrderBy(a => a.Id)
.FirstOrDefault()));
x.CreateMap<Address, AddressDto>();
});
var people = context.People.ProjectTo<PersonDto>(mapper).ToList();
// Auto map Person, manually map Addresses
mapper = new MapperConfiguration(x =>
{
x.CreateMap<Person, PersonDto>()
.ForMember(dto => dto.FirstAddress,
o => o.MapFrom(entity => entity.Addresses
.OrderBy(a => a.Id)
.Select(a => new AddressDto { Line1 = a.Line1, Line2 = a.Line2 })
.FirstOrDefault()));
});
people = context.People.ProjectTo<PersonDto>(mapper).ToList();
}
public class PersonContext : DbContext
{
public DbSet<Person> People { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseSqlServer("Server=.;Database=BugDemo;Trusted_Connection=True;")
.LogTo(x => Debug.WriteLine(x), LogLevel.Information);
}
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Address> Addresses { get; set; }
}
public class PersonDto
{
public string Name { get; set; }
public AddressDto FirstAddress { get; set; }
}
public class Address
{
public int Id { get; set; }
public string Line1 { get; set; }
public string Line2 { get; set; }
public Person Person { get; set; }
}
public class AddressDto
{
public string Line1 { get; set; }
public string Line2 { get; set; }
} First query SQL SELECT [p].[Name], CASE
WHEN NOT (EXISTS (
SELECT 1
FROM [Address] AS [a]
WHERE [p].[Id] = [a].[PersonId])) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END, (
SELECT TOP(1) [a0].[Line1]
FROM [Address] AS [a0]
WHERE [p].[Id] = [a0].[PersonId]
ORDER BY [a0].[Id]), (
SELECT TOP(1) [a1].[Line2]
FROM [Address] AS [a1]
WHERE [p].[Id] = [a1].[PersonId]
ORDER BY [a1].[Id])
FROM [People] AS [p] Second query SQL SELECT [p].[Name], [t0].[Line1], [t0].[Line2], [t0].[c]
FROM [People] AS [p]
LEFT JOIN (
SELECT [t].[Line1], [t].[Line2], [t].[c], [t].[PersonId]
FROM (
SELECT [a].[Line1], [a].[Line2], 1 AS [c], [a].[PersonId], ROW_NUMBER() OVER(PARTITION BY [a].[PersonId] ORDER BY [a].[Id]) AS [row]
FROM [Address] AS [a]
) AS [t]
WHERE [t].[row] <= 1
) AS [t0] ON [p].[Id] = [t0].[PersonId] |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 4 replies
-
Check the execution plan. That looks good to me. You can set |
Beta Was this translation helpful? Give feedback.
Check the execution plan. That looks good to me. You can set
DoNotAllowNull
forFirstAddress
, but this is really an EF Core question.You should also check the SQL execution plan. Perhaps the optimizer can handle that query better than it looks.