在使用 AutoMapper 进行映射时,是否有一种有效的方法可以仅保留子集合的单个元素?
Is there an efficient way to only keep a single element of a child collection when mapping using AutoMapper?
我在我的 .NET 5.0 项目中配置了 AutoMapper,其中包含一个实体 (Setting
) 及其 DTO (SettingByProfileDto)
) 之间的映射。所述实体具有另一个实体的子集合 (SettingValue
)(一对多)。第一个实体的子集合(SettingValues
)映射到 DTO 内的单个项目(另一个 DTO:SettingValueDto
),因为我只需要一个特定的此列表中的项目。
对于映射配置,我使用以下几行:
int profileId = default;
profile.CreateMap<Setting, SettingByProfileDto>()
.ForMember(dto => dto.SettingValue, opt =>
{
opt.MapFrom(ss =>
ss.SettingValues
.FirstOrDefault(ssv => ssv.ProfileId == profileId)
);
});
当我想检索第一个实体时,我使用 AutoMapper ProjectTo
方法来仅请求 DTO 具有的字段。我给 ProjectTo
方法 profileId
参数的值,这样映射就可以知道必须在哪个 ID 上完成过滤器:
// ...
.Where(ss => ss.Id == request.Id)
.ProjectTo<SettingByProfileDto>(
_mapper.ConfigurationProvider,
new { profileId = request.ProfileId },
dest => dest.SettingValue
)
// ...
查询结果和映射都是正确的。但是,发送到数据库以获取结果的查询似乎没有得到优化。
这是结果查询:
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
这里是AutoMapper生成然后转换为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)
我尝试用 Where
替换映射配置中的 FirstOrDefault
子句(条件相同),在这种情况下,生成的查询将使用 LEFT JOIN
避免每个字段重复一个 WHERE
。但是,通过这种方式,我无法将子集合映射到单个项目,而只能映射到另一个集合(dto)。
我的问题如下:
- 有没有更好(更高效?)的方法来实现我想要的(只保留子集合中的一项)?
- 上面的查询是否被认为是优化的?如果是这样,我将能够继续我目前的做法
感谢您的帮助!
AutoMapper 投影通过使用生成的映射选择器注入 Select
调用来工作,或者仅在目标类型与源类型不匹配的查询表达式树中注入选择器。
问题是这些注入到哪里了。例如,在你的例子中它生成了这样的东西(伪代码,MapTo
只是标记映射注入点)
SettingValue = source.SettingValues
.FirstOrDefault(ssv => ssv.ProfileId == profileId)
.MapTo<SettingByProfileDto>()
这里的谓词版本 FirstOrDefault
它别无选择(好吧,相对而言,请继续阅读),但即使您将其重写为等效的 Where(predicate)
+ FirstOrDefault()
链,它仍然在最后注入映射
SettingValue = source.SettingValues
.Where(ssv => ssv.ProfileId == profileId)
.FirstOrDefault()
.MapTo<SettingByProfileDto>()
为时已晚,这就是 EF Core 生成低效查询的原因。
现在,人们可能认为这是 EF Core 查询翻译缺陷。但是如果映射是在 before the FirstOrDefault()
call
SettingValue = source.SettingValues
.Where(ssv => ssv.ProfileId == profileId)
.MapTo<SettingByProfileDto>()
.FirstOrDefault()
然后 Core 生成最佳翻译。
我没有找到强制 AM 这样做的方法,而且透明地发生这一切也很好。所以我写了一些自定义扩展。它所做的是插入 AutoMapper 管道并适当地转换以下 Enumerable
扩展方法(谓词和非谓词重载)- First
、FirstOrDefault
、Last
、LastOrDefault
, Single
, SingleOrDefault
.
这是源代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using AutoMapper.Internal;
using AutoMapper.QueryableExtensions;
using AutoMapper.QueryableExtensions.Impl;
namespace AutoMapper
{
public static class SingeResultQueryMapper
{
public static IMapperConfigurationExpression AddSingleResultQueryMapping(this IMapperConfigurationExpression config)
{
config.Advanced.QueryableBinders.Insert(0, new Binder());
config.Advanced.QueryableResultConverters.Insert(0, new ResultConverter());
return config;
}
static string[] TargetMethodNames => new[]
{
nameof(Enumerable.First),
nameof(Enumerable.FirstOrDefault),
nameof(Enumerable.Last),
nameof(Enumerable.LastOrDefault),
nameof(Enumerable.Single),
nameof(Enumerable.SingleOrDefault),
};
static HashSet<MethodInfo> TargetMethods { get; } =
(from method in typeof(Enumerable).GetTypeInfo().DeclaredMethods
join name in TargetMethodNames
on method.Name equals name
select method).ToHashSet();
static bool IsTarget(IMemberMap propertyMap) =>
propertyMap.SourceType != propertyMap.DestinationType &&
propertyMap.ProjectToCustomSource is null &&
propertyMap.CustomMapExpression?.Body is MethodCallExpression call &&
call.Method.IsGenericMethod &&
TargetMethods.Contains(call.Method.GetGenericMethodDefinition());
class ResultConverter : IExpressionResultConverter
{
public bool CanGetExpressionResolutionResult(ExpressionResolutionResult expressionResolutionResult, IMemberMap propertyMap)
=> IsTarget(propertyMap);
public ExpressionResolutionResult GetExpressionResolutionResult(ExpressionResolutionResult expressionResolutionResult, IMemberMap propertyMap, LetPropertyMaps letPropertyMaps)
=> new(propertyMap.CustomMapExpression.ReplaceParameters(propertyMap.CheckCustomSource(expressionResolutionResult, letPropertyMaps)));
}
class Binder : IExpressionBinder
{
public bool IsMatch(PropertyMap propertyMap, TypeMap propertyTypeMap, ExpressionResolutionResult result)
=> IsTarget(propertyMap);
public MemberAssignment Build(IConfigurationProvider configuration, PropertyMap propertyMap, TypeMap propertyTypeMap, ExpressionRequest request, ExpressionResolutionResult result, IDictionary<ExpressionRequest, int> typePairCount, LetPropertyMaps letPropertyMaps)
{
var call = (MethodCallExpression)result.ResolutionExpression;
var selectors = configuration.ExpressionBuilder.CreateMapExpression(
new(propertyMap.SourceType, propertyMap.DestinationType, request.MembersToExpand, request),
typePairCount, letPropertyMaps.New());
if (selectors == null) return null;
var query = call.Arguments[0];
var method = call.Method.GetGenericMethodDefinition();
if (call.Arguments.Count > 1)
{
// Predicate version of the method
// Convert query.Method(predicate) to query.Where(predicate).Method()
query = Expression.Call(typeof(Enumerable), nameof(Enumerable.Where), new[] { propertyMap.SourceType }, query, call.Arguments[1]);
method = TargetMethods.First(m => m.Name == method.Name && m.GetParameters().Length == 1);
}
method = method.MakeGenericMethod(propertyMap.DestinationType);
foreach (var selector in selectors)
query = Expression.Call(typeof(Enumerable), nameof(Enumerable.Select), new[] { selector.Parameters[0].Type, selector.ReturnType }, query, selector);
call = Expression.Call(method, query);
return Expression.Bind(propertyMap.DestinationMember, call);
}
}
}
}
只需将它放在配置AutoMapper的项目中的代码文件中,然后使用提供的配置助手扩展方法(类似于Expression Mapping AutoMapper扩展)启用它,就像这样
var mapper = new MapperConfiguration(cfg => {
cfg.AddSingleResultQueryMapping();
// The rest ...
}).CreateMapper();
或与 DI
services.AddAutoMapper(cfg => {
cfg.AddSingleResultQueryMapping();
}, /* assemblies with profiles */);
仅此而已。现在您的原始 DTO、映射和 ProjectTo
将产生最佳的 SQL 查询翻译(单个 LEFT OUTER JOIN
)。
我在我的 .NET 5.0 项目中配置了 AutoMapper,其中包含一个实体 (Setting
) 及其 DTO (SettingByProfileDto)
) 之间的映射。所述实体具有另一个实体的子集合 (SettingValue
)(一对多)。第一个实体的子集合(SettingValues
)映射到 DTO 内的单个项目(另一个 DTO:SettingValueDto
),因为我只需要一个特定的此列表中的项目。
对于映射配置,我使用以下几行:
int profileId = default;
profile.CreateMap<Setting, SettingByProfileDto>()
.ForMember(dto => dto.SettingValue, opt =>
{
opt.MapFrom(ss =>
ss.SettingValues
.FirstOrDefault(ssv => ssv.ProfileId == profileId)
);
});
当我想检索第一个实体时,我使用 AutoMapper ProjectTo
方法来仅请求 DTO 具有的字段。我给 ProjectTo
方法 profileId
参数的值,这样映射就可以知道必须在哪个 ID 上完成过滤器:
// ...
.Where(ss => ss.Id == request.Id)
.ProjectTo<SettingByProfileDto>(
_mapper.ConfigurationProvider,
new { profileId = request.ProfileId },
dest => dest.SettingValue
)
// ...
查询结果和映射都是正确的。但是,发送到数据库以获取结果的查询似乎没有得到优化。
这是结果查询:
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
这里是AutoMapper生成然后转换为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)
我尝试用 Where
替换映射配置中的 FirstOrDefault
子句(条件相同),在这种情况下,生成的查询将使用 LEFT JOIN
避免每个字段重复一个 WHERE
。但是,通过这种方式,我无法将子集合映射到单个项目,而只能映射到另一个集合(dto)。
我的问题如下:
- 有没有更好(更高效?)的方法来实现我想要的(只保留子集合中的一项)?
- 上面的查询是否被认为是优化的?如果是这样,我将能够继续我目前的做法
感谢您的帮助!
AutoMapper 投影通过使用生成的映射选择器注入 Select
调用来工作,或者仅在目标类型与源类型不匹配的查询表达式树中注入选择器。
问题是这些注入到哪里了。例如,在你的例子中它生成了这样的东西(伪代码,MapTo
只是标记映射注入点)
SettingValue = source.SettingValues
.FirstOrDefault(ssv => ssv.ProfileId == profileId)
.MapTo<SettingByProfileDto>()
这里的谓词版本 FirstOrDefault
它别无选择(好吧,相对而言,请继续阅读),但即使您将其重写为等效的 Where(predicate)
+ FirstOrDefault()
链,它仍然在最后注入映射
SettingValue = source.SettingValues
.Where(ssv => ssv.ProfileId == profileId)
.FirstOrDefault()
.MapTo<SettingByProfileDto>()
为时已晚,这就是 EF Core 生成低效查询的原因。
现在,人们可能认为这是 EF Core 查询翻译缺陷。但是如果映射是在 before the FirstOrDefault()
call
SettingValue = source.SettingValues
.Where(ssv => ssv.ProfileId == profileId)
.MapTo<SettingByProfileDto>()
.FirstOrDefault()
然后 Core 生成最佳翻译。
我没有找到强制 AM 这样做的方法,而且透明地发生这一切也很好。所以我写了一些自定义扩展。它所做的是插入 AutoMapper 管道并适当地转换以下 Enumerable
扩展方法(谓词和非谓词重载)- First
、FirstOrDefault
、Last
、LastOrDefault
, Single
, SingleOrDefault
.
这是源代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using AutoMapper.Internal;
using AutoMapper.QueryableExtensions;
using AutoMapper.QueryableExtensions.Impl;
namespace AutoMapper
{
public static class SingeResultQueryMapper
{
public static IMapperConfigurationExpression AddSingleResultQueryMapping(this IMapperConfigurationExpression config)
{
config.Advanced.QueryableBinders.Insert(0, new Binder());
config.Advanced.QueryableResultConverters.Insert(0, new ResultConverter());
return config;
}
static string[] TargetMethodNames => new[]
{
nameof(Enumerable.First),
nameof(Enumerable.FirstOrDefault),
nameof(Enumerable.Last),
nameof(Enumerable.LastOrDefault),
nameof(Enumerable.Single),
nameof(Enumerable.SingleOrDefault),
};
static HashSet<MethodInfo> TargetMethods { get; } =
(from method in typeof(Enumerable).GetTypeInfo().DeclaredMethods
join name in TargetMethodNames
on method.Name equals name
select method).ToHashSet();
static bool IsTarget(IMemberMap propertyMap) =>
propertyMap.SourceType != propertyMap.DestinationType &&
propertyMap.ProjectToCustomSource is null &&
propertyMap.CustomMapExpression?.Body is MethodCallExpression call &&
call.Method.IsGenericMethod &&
TargetMethods.Contains(call.Method.GetGenericMethodDefinition());
class ResultConverter : IExpressionResultConverter
{
public bool CanGetExpressionResolutionResult(ExpressionResolutionResult expressionResolutionResult, IMemberMap propertyMap)
=> IsTarget(propertyMap);
public ExpressionResolutionResult GetExpressionResolutionResult(ExpressionResolutionResult expressionResolutionResult, IMemberMap propertyMap, LetPropertyMaps letPropertyMaps)
=> new(propertyMap.CustomMapExpression.ReplaceParameters(propertyMap.CheckCustomSource(expressionResolutionResult, letPropertyMaps)));
}
class Binder : IExpressionBinder
{
public bool IsMatch(PropertyMap propertyMap, TypeMap propertyTypeMap, ExpressionResolutionResult result)
=> IsTarget(propertyMap);
public MemberAssignment Build(IConfigurationProvider configuration, PropertyMap propertyMap, TypeMap propertyTypeMap, ExpressionRequest request, ExpressionResolutionResult result, IDictionary<ExpressionRequest, int> typePairCount, LetPropertyMaps letPropertyMaps)
{
var call = (MethodCallExpression)result.ResolutionExpression;
var selectors = configuration.ExpressionBuilder.CreateMapExpression(
new(propertyMap.SourceType, propertyMap.DestinationType, request.MembersToExpand, request),
typePairCount, letPropertyMaps.New());
if (selectors == null) return null;
var query = call.Arguments[0];
var method = call.Method.GetGenericMethodDefinition();
if (call.Arguments.Count > 1)
{
// Predicate version of the method
// Convert query.Method(predicate) to query.Where(predicate).Method()
query = Expression.Call(typeof(Enumerable), nameof(Enumerable.Where), new[] { propertyMap.SourceType }, query, call.Arguments[1]);
method = TargetMethods.First(m => m.Name == method.Name && m.GetParameters().Length == 1);
}
method = method.MakeGenericMethod(propertyMap.DestinationType);
foreach (var selector in selectors)
query = Expression.Call(typeof(Enumerable), nameof(Enumerable.Select), new[] { selector.Parameters[0].Type, selector.ReturnType }, query, selector);
call = Expression.Call(method, query);
return Expression.Bind(propertyMap.DestinationMember, call);
}
}
}
}
只需将它放在配置AutoMapper的项目中的代码文件中,然后使用提供的配置助手扩展方法(类似于Expression Mapping AutoMapper扩展)启用它,就像这样
var mapper = new MapperConfiguration(cfg => {
cfg.AddSingleResultQueryMapping();
// The rest ...
}).CreateMapper();
或与 DI
services.AddAutoMapper(cfg => {
cfg.AddSingleResultQueryMapping();
}, /* assemblies with profiles */);
仅此而已。现在您的原始 DTO、映射和 ProjectTo
将产生最佳的 SQL 查询翻译(单个 LEFT OUTER JOIN
)。