使用 AutoMapper 的 ProjectTo 和扩展选项时,每个请求都会增加内存

Posted

技术标签:

【中文标题】使用 AutoMapper 的 ProjectTo 和扩展选项时,每个请求都会增加内存【英文标题】:Memory increases with each request when using AutoMapper's ProjectTo with expand option 【发布时间】:2021-12-20 17:31:58 【问题描述】:

我正在构建一个基于 CQRS/MediatR 的 .NET 5 REST API,我注意到在对我的应用程序进行压力测试时内存呈线性增加。我做了一些分析,发现来自命名空间System.Linq.Expression 的大量对象实例占用了所有空间。所有这些实例都与AM的MapperConfiguration有关。

我使用 AutoMapper 将我的实体映射到 DTO,为此我主要使用以下 ProjectTo 扩展方法:

public static IQueryable<TDestination> ProjectTo<TDestination>(this IQueryable source, IConfigurationProvider configuration, object parameters, params Expression<Func<TDestination, object>>[] membersToExpand);

经过一些测试,我注意到只有在ProjectTo 方法中提供membersToExpand 时才会出现内存问题。

当提供“扩展”并调用相关端点时,就像每次创建映射的表达式但从未释放过一样。当它们停留时,它们会在内存中累积并在调用查询时添加。

这是比较两个快照的内存状态截图:

对端点的一次调用后的第一个 五次后的第二次调用同一个端点(不做任何更改)

我不介意内存使用情况,认为它可能是 AM 使用的某种缓存以获得更好的响应时间,但问题是使用的内存越多,我的 API 响应请求的速度就越慢(开始时的 4ms 到压力测试后的 90ms)。我想 AM 很难在这么多存储的表达式中进行搜索。一旦我停止使用“扩展”系统,响应又是瞬时的(4 毫秒)。

最后一点是,当应用程序池被回收(并且内存被清除)时,API 以 4ms 的速度再次全速响应(带有扩展)。

我在网上搜索了几个小时,看看我是否错过了与缓存或其他东西相关的特定 AM 配置,但一无所获。

是否有人对此行为有任何想法、类似经历或更多信息?

PS : 如您所见,我将 AM 与 DI 一起使用(使用 DI 包和.AddAutoMapper 方法)

下面是我的应用程序中的一些代码示例:

RoleByProfileDtoRoleDto 没有什么特别之处,ReverseMap 仅用于“翻译”目的):

    public class RoleByProfileDto : RoleDto
    
        public ProfileRoleDto ProfileRole  get; set; 

        public void Mapping(AM.Profile profile)
        
            int profileId = default;

            profile.CreateMap<Role, RoleByProfileDto>()
                .ForMember(dto =>
                    dto.ProfileRole, opts =>
                        opts.MapFrom(r => r.ProfileRoles.FirstOrDefault(pr => pr.ProfileId == profileId))
                )
                .IncludeBase<Role, RoleDto>()
                .ReverseMap();
        
    

上述请求的处理程序:

    public class GetRoleByProfileAndIdQueryHandler : IRequestHandler<GetRoleByProfileAndIdQuery, RoleByProfileDto>
    
        private readonly IApplicationDbContext _context;
        private readonly AM.IMapper _mapper;

        public GetRoleByProfileAndIdQueryHandler(IApplicationDbContext context, AM.IMapper mapper)
        
            _context = context;
            _mapper = mapper;
        

        public async Task<RoleByProfileDto> Handle(GetRoleByProfileAndIdQuery request, CancellationToken cancellationToken)
        
            var dto = await _context.Roles
                .Where(r =>
                    r.Id == request.RoleId
                    && r.ProfileRoles.Any(pr =>
                        pr.ProfileId == request.ProfileId
                    )
                )
                .ProjectTo(
                    _mapper.ConfigurationProvider,
                    new  profileId = request.ProfileId ,
                    request.Expand.GetExpandMemberList(ExpandMappings)
                )
                .FirstOrDefaultAsync(cancellationToken);

            if (dto == null)
            
                throw new NotFoundException(
                    nameof(Role),
                    new List<string>()  nameof(Profile), nameof(Role) ,
                    new List<object>()  request.ProfileId, request.RoleId 
                );
            

            return dto;
        

        private static readonly IReadOnlyDictionary<RoleByProfileAndIdExpand, Expression<Func<RoleByProfileDto, object>>> ExpandMappings =
            new Dictionary<RoleByProfileAndIdExpand, Expression<Func<RoleByProfileDto, object>>>
            
                 RoleByProfileAndIdExpand.ProfileRole, d => d.ProfileRole 
            ;
    

用于构建提供给ProjectTo 的扩展数组的方法:

        public static Expression<Func<TDto, object>>[] GetExpandMemberList<TEnum, TDto>(
            this IList<TEnum> selectedExpands,
            IReadOnlyDictionary<TEnum, Expression<Func<TDto, object>>> mappings
        )
            where TEnum : Enum
            where TDto : BaseDto
        
            if (selectedExpands != null && selectedExpands.Any())
                return selectedExpands.Select(e => mappings[e]).ToArray();
            else
                return Array.Empty<Expression<Func<TDto, object>>>();
        

【问题讨论】:

何时调用RoleByProfileDto::Mapping 试试MyGet 构建。 @Stefan 映射是由 AM 直接在 ProjectTo 方法内完成的。这里类型是推断出来的,因为我们在展开部分使用它。 @Lucian Bargaoanu 我会尽快尝试并给您反馈。 是的,很久以前就修复了。我预计 11.0 之前不会有重大变化。检查the upgrade guide。 【参考方案1】:

此漏洞已在 MyGet 构建中修复。详情here.

【讨论】:

关于 AutoMapper 11.0 版何时可用的任何想法?如果可能的话,我想避免使用预发布,但内存泄漏是不可取的。最后一个问题,您认为在生产中使用最后一个预发布版本是否安全?我猜每个构建都经过测试。 与任何版本一样安全。:) 我当然在使用它。没有针对某个版本的特殊测试。

以上是关于使用 AutoMapper 的 ProjectTo 和扩展选项时,每个请求都会增加内存的主要内容,如果未能解决你的问题,请参考以下文章

EF Core 相关的千倍性能之差: AutoMapper ProjectTo VS Mapster ProjectToType

Automapper 无法在单元测试项目中映射外键属性

“ProjectTo”和 Nullable 类型之后的“Where”子句?

aufomaper Queryable Extensions ProjectTo

AutoMapper的使用

AutoMapper用法