如何扩展或重写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
相关产品推荐
相关产品推荐

