I have AutoMapper configured in my .NET 5.0 project with a map between an entity (Setting) and its DTO (SettingByProfileDto). The said entity has a child collection of another entity (SettingValue) (one to many). The child collection (SettingValues) of the first entity is mapped to a single item (another DTO : SettingValueDto) inside the DTO because I only need a specific item from this list.
For the mapping configuration, I use the following lines :
int profileId = default;
profile.CreateMap<Setting, SettingByProfileDto>()
.ForMember(dto => dto.SettingValue, opt =>
{
opt.MapFrom(ss =>
ss.SettingValues
.FirstOrDefault(ssv => ssv.ProfileId == profileId)
);
});
When I want to retrieve the first entity I use the AutoMapper ProjectTo method in order to only request the fields the DTO has. I give to the ProjectTo method the value of the profileId parameter so the mapping can know on which Id the filter has to be done :
// ...
.Where(ss => ss.Id == request.Id)
.ProjectTo<SettingByProfileDto>(
_mapper.ConfigurationProvider,
new { profileId = request.ProfileId },
dest => dest.SettingValue
)
// ...
The result of the query and the mapping are both correct. However, the query sent to the database to fetch the results seems to be poorly optimized.
Here is the resulting query :
SELECT [s8].[Description], [s8].[DisplayName], [s8].[Id], [s8].[Name], CASE
WHEN (
SELECT TOP(1) [s].[Id]
FROM [SettingValue] AS [s]
WHERE ([s8].[Id] = [s].[SettingId]) AND ([s].[ProfileId] = @__profileId_1)) IS NULL THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END, (
SELECT TOP(1) [s2].[Id]
FROM [SettingValue] AS [s2]
WHERE ([s8].[Id] = [s2].[SettingId]) AND ([s2].[ProfileId] = @__profileId_1)), (
SELECT TOP(1) [s3].[ProfileId]
FROM [SettingValue] AS [s3]
WHERE ([s8].[Id] = [s3].[SettingId]) AND ([s3].[ProfileId] = @__profileId_1)), (
SELECT TOP(1) [s4].[SettingId]
FROM [SettingValue] AS [s4]
WHERE ([s8].[Id] = [s4].[SettingId]) AND ([s4].[ProfileId] = @__profileId_1)), (
SELECT TOP(1) [s7].[Value]
FROM [SettingValue] AS [s7]
WHERE ([s8].[Id] = [s7].[SettingId]) AND ([s7].[ProfileId] = @__profileId_1))
FROM [Setting] AS [s8]
WHERE [s8].[Id] = @__request_Id_0
ORDER BY (SELECT 1)
OFFSET @__p_2 ROWS FETCH NEXT @__p_3 ROWS ONLY
And here is the query expression that is generated by AutoMapper and then transformed to SQL :
DbSet<Setting>()
.AsNoTracking()
.Where(ss => ss.Id == __request_Id_0)
.Select(dtoSetting => new Object_1800281414___SettingValue_Description_DisplayName_Id_Name{
__SettingValue = dtoSetting.SettingValues
.FirstOrDefault(ssv => ssv.ProfileId == __profileId_1),
Description = dtoSetting.Description,
DisplayName = dtoSetting.DisplayName,
Id = dtoSetting.Id,
Name = dtoSetting.Name,
}
)
.Select(dtoLet => new SettingByProfileDto{
Description = dtoLet.Description,
DisplayName = dtoLet.DisplayName,
Id = dtoLet.Id,
Name = dtoLet.Name,
SettingValue = dtoLet.__SettingValue == null ? null : new SettingValueDto{
Id = dtoLet.__SettingValue.Id,
ProfileId = dtoLet.__SettingValue.ProfileId,
SettingId = dtoLet.__SettingValue.SettingId,
Value = dtoLet.__SettingValue.Value
}
}
)
.Skip(__p_2)
.Take(__p_3)
I tried to replace the FirstOrDefault clause in the mapping configuration by a Where (with the same condition) and in this case, the query generated will use a LEFT JOIN which avoid repeating one WHERE per field. However, with this way, I can't map the child collection to a single item but only another collection (of dto).
My questions are the following :
- Is there a better (more efficient?) way to achieve what I want (only keep one item of the child collection) ?
- Is the above query considered optimized ? If so, I would be able to continue with my current way of doing
Thanks for your help !