You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何扩展或重写UserManager.FindByEmailAsync()以避免重复数据库查询?

问题

当前登录流程中,先用_userManager.FindByEmailAsync()获取用户,之后还要再次查询数据库,因为Points和Tokens字段需要通过Sum()计算。想知道能不能扩展_userManager.FindByEmailAsync(),让它在查询时就完成Sum计算,避免重复调用数据库?

尝试过的操作

  • 手动投影查询用户部分字段,传入CheckPasswordSignInAsync()验证失败:
var userFromRepo = await _userManager.Users.Select(p => new Core.Entities.User { Id = p.Id, Email = p.Email, UserName = p.UserName}).SingleOrDefaultAsync(p => p.Email == loginDto.Email);

验证失败,说明CheckPasswordSignInAsync()需要除Email、UserName、Id之外的其他字段,但不清楚具体是哪些。

  • 查询完整User对象,验证成功但无法直接计算Sum:
var userFromRepo = await _userManager.Users.SingleOrDefaultAsync(p => p.Email == loginDto.Email);

这种方式能通过密码验证,但没法直接在这个查询里完成Points和Tokens的Sum()计算。

  • 尝试扩展UserManager但失败:
public static User FindByEmail(this UserManager<User> input, string email) {
  return input.Users.Select(p => new User {
    Id = p.Id,
      Email = p.Email,
      UserName = p.UserName
  }).SingleOrDefault(x => x.Email == email);
}

当前完整登录方法:

[AllowAnonymous]
[HttpPost("login")]
public async Task<IActionResult> Login(LoginDto loginDto) {
  
  var userFromRepo = await _userManager.FindByEmailAsync(loginDto.Email);

  if (userFromRepo == null) return Unauthorized(new ApiResponse(401));

  if (userFromRepo.IsActive == false) return NotFound(new ApiResponse(404, "This account is no longer active."));

  var result = await _signInManager.CheckPasswordSignInAsync(userFromRepo, loginDto.Password, false);

  if (!result.Succeeded) return Unauthorized(new ApiResponse(401));

  var user = await _dbContext.Users
    .Select(p => new {
        Id = p.Id,
        Email = p.Email,
        UserName = p.UserName,
        Hosted = p.Hosted,
        Instructed = p.Instructed,
        Attended = p.Attended,
        IsBoarded = p.IsBoarded,
        IsActive = p.IsActive,
        Likers = p.LikersCount,
        Rating = p.Rating,
        YearStarted = p.YearStarted,
        YearsInstructing = p.YearsInstructing,
        YearsPracticing = System.DateTime.Now.Year - p.YearStarted,
        Certification = p.Certification.ToString(),
        CreatedDate = p.CreatedDate,
        PhotoUrl = p.IsBoarded ? p.UserPhoto : "assets/images/user.png",
        Age = p.DateOfBirth.CalculateAge(),
        DateOfBirth = p.DateOfBirth,
        ExperienceLevel = p.ExperienceLevel.GetEnumName(),
        Points = p.UserPoints.Sum(p => p.Points),
        Tokens = p.Tokens.Sum(p => p.Tokens),
        Token = _tokenService.CreateToken(userFromRepo, (from userRole in p.UserRoles join role in _dbContext.Roles on userRole.RoleId equals role.Id select role.Name).ToList()),
        IsInstructor = p.IsInstructor
    })
    .FirstOrDefaultAsync(p => p.Id == userFromRepo.Id);

  if (user == null) return NotFound(new ApiResponse(404));

  return Ok(user);
}
解决方案

1. 明确CheckPasswordSignInAsync()的必填字段

CheckPasswordSignInAsync()用于验证密码哈希,必须拿到用户的**PasswordHash字段**;如果系统启用了账户锁定功能,还需要LockoutEnabled、LockoutEnd字段。投影查询时必须包含这些字段,才能通过验证。

2. 合并查询,单次获取所有数据

无需扩展UserManager,直接通过_userManager.Users构建一次查询,同时获取密码验证所需字段和计算好Sum值的用户数据,彻底避免两次数据库调用。

修改后的登录方法示例:

[AllowAnonymous]
[HttpPost("login")]
public async Task<IActionResult> Login(LoginDto loginDto) {
    // 单次查询获取验证所需字段+计算Sum后的用户数据
    var queryResult = await _userManager.Users
        .Where(p => p.Email == loginDto.Email)
        .Select(p => new {
            // 密码验证必需的核心字段
            AuthUser = new User {
                Id = p.Id,
                Email = p.Email,
                UserName = p.UserName,
                PasswordHash = p.PasswordHash,
                IsActive = p.IsActive,
                LockoutEnabled = p.LockoutEnabled,
                LockoutEnd = p.LockoutEnd
            },
            // 需要返回的用户详情,包含Sum计算结果
            UserProfile = new {
                Id = p.Id,
                Email = p.Email,
                UserName = p.UserName,
                Hosted = p.Hosted,
                Instructed = p.Instructed,
                Attended = p.Attended,
                IsBoarded = p.IsBoarded,
                IsActive = p.IsActive,
                Likers = p.LikersCount,
                Rating = p.Rating,
                YearStarted = p.YearStarted,
                YearsInstructing = p.YearsInstructing,
                YearsPracticing = System.DateTime.Now.Year - p.YearStarted,
                Certification = p.Certification.ToString(),
                CreatedDate = p.CreatedDate,
                PhotoUrl = p.IsBoarded ? p.UserPhoto : "assets/images/user.png",
                Age = p.DateOfBirth.CalculateAge(),
                DateOfBirth = p.DateOfBirth,
                ExperienceLevel = p.ExperienceLevel.GetEnumName(),
                Points = p.UserPoints.Sum(p => p.Points),
                Tokens = p.Tokens.Sum(p => p.Tokens),
                Roles = p.UserRoles.Join(_dbContext.Roles, ur => ur.RoleId, r => r.Id, (ur, r) => r.Name).ToList(),
                IsInstructor = p.IsInstructor
            }
        })
        .FirstOrDefaultAsync();

    if (queryResult == null) return Unauthorized(new ApiResponse(401));

    var authUser = queryResult.AuthUser;
    if (!authUser.IsActive) return NotFound(new ApiResponse(404, "该账户已停用"));

    // 执行密码验证
    var signInResult = await _signInManager.CheckPasswordSignInAsync(authUser, loginDto.Password, false);
    if (!signInResult.Succeeded) return Unauthorized(new ApiResponse(401));

    // 生成JWT Token并合并到返回数据
    var jwtToken = _tokenService.CreateToken(authUser, queryResult.UserProfile.Roles);
    var returnData = new {
        queryResult.UserProfile,
        Token = jwtToken
    };

    return Ok(returnData);
}

3. 扩展UserManager失败的原因

你编写的扩展方法返回的User对象仅包含Id、Email、UserName,缺少PasswordHash等验证必需字段,因此传入CheckPasswordSignInAsync()会验证失败。即便要扩展,也必须确保投影包含核心验证字段,但直接合并查询的方式更简洁高效,无需额外扩展。


内容的提问来源于stack exchange,提问作者chuckd

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 16:05:53