将 AutoMapper 的 ProjectTo 与扩展选项一起使用时,内存会随着每个请求而增加

Memory increases with each request when using AutoMapper's ProjectTo with expand option

我正在构建一个基于 CQRS/MediatR 的 .NET 5 REST API,并且我注意到在对我的应用程序进行压力测试时线性内存增加。我做了一些分析,发现来自名称 space System.Linq.Expression 的大量对象实例占用了所有 space。所有这些实例都与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>>>();
        }

此漏洞已在 MyGet build. Details here 中修复。