How can I avoid fetching my user 2 times in this controller method?

221 Views Asked by At

I have this method below that gets the current user, but it fetches the user 2 times, which seems a little redundant, so I'd like to see if there is a way I can reduce it to just once.

The first "User" fetch comes from "FindByEmailFromClaimsPrinciple()" and then the second comes from "_dbContext.Users"

Problem - I need User properties(email, username, roles) to pass to "CreateToken(User user)" for the claims. So it seems like I have no option but to fetch database data twice!

Quasi Solution - I can rework the extension method to only fetch the Id, UserName, and Email and that will save me a little DB bandwidth, but trying to see if I can reduce 2 fetches to 1.

Question - Is there a way I can refactor this code so that I only need to fetch User 1 time? More specifically, get my 2 User properties (most importantly roles), without having to get my whole User first, as it's a fairly large entity with ~30 columns.

[HttpGet]
public async Task <IActionResult> GetCurrentUser() {
  var userFromRepo = await _userManager.FindByEmailFromClaimsPrinciple(HttpContext.User);
  if (userFromRepo == null) return Unauthorized(new ApiResponse(401));

  var token = await _tokenService.CreateToken(userFromRepo);

  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,
        Likers = p.Likers.Count(),
        Rating = p.Rating,
        CreatedDate = p.CreatedDate,
        PhotoUrl = p.IsBoarded ? p.UserPhotos.FirstOrDefault(p => p.IsMain).Url : "assets/images/user.png",
        Age = p.DateOfBirth.CalculateAge(),
        DateOfBirth = p.DateOfBirth,
        ExperienceLevel = p.ExperienceLevel.GetEnumName(),
        Points = p.UserPoints.Sum(p => p.Points),
        Tokens = p.UserTokens.Sum(p => p.Tokens),
        Memberships = p.YogabandMemberships.Count(x => x.Status == YogabandMemberStatus.Active),
        Token = token,
        IsInstructor = p.IsInstructor,
    })
    .FirstOrDefaultAsync(p => p.Id == userFromRepo.Id);

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

  return Ok(user);
}

Here is CreateToken

public async Task <string> CreateToken(User user) {
  var claims = new List <Claim> {
    new Claim(JwtRegisteredClaimNames.Email, user.Email),
    new Claim(JwtRegisteredClaimNames.GivenName, user.UserName),
    new Claim(JwtRegisteredClaimNames.NameId, user.Id.ToString()),
  };

  // GetRolesAync requires a User entity
  var roles = await _userManager.GetRolesAsync(user);

  claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));

  var creds = new SigningCredentials(_key, SecurityAlgorithms.HmacSha512Signature);

  var tokenDescriptor = new SecurityTokenDescriptor {
    Subject = new ClaimsIdentity(claims),
      Expires = DateTime.Now.AddDays(365),
      SigningCredentials = creds,
      Issuer = _config["Token:Issuer"]
  };

  var tokenHandler = new JwtSecurityTokenHandler();

  var token = tokenHandler.CreateToken(tokenDescriptor);

  return tokenHandler.WriteToken(token);
}

Here is the extension method

public static async Task<User> FindByEmailFromClaimsPrinciple(this UserManager<User> input, ClaimsPrincipal user)
{
    var email = user?.Claims?.FirstOrDefault(x => x.Type == ClaimTypes.Email)?.Value;
    return await input.Users.SingleOrDefaultAsync(x => x.Email == email);
}

1

There are 1 best solutions below

9
AudioBubble On
  1. Step 1 : Create a token when User Registers or Logs in.

  2. Step 2 : Use HttpContext on remaining controllers to get the current user.

In the above GetCurrentUser() method, you can mark [Authorize] attribute and get the current user directly from HttpContext than getting it from DB.