chuckd
chuckd

Reputation: 14610

How do I extend or override UserManager.FindByEmail() to have it return only what I want?

My login method is below and you'll see I have to call my user again after I fetch it with _userManager.FindByEmailAsync() because there are a couple of fields Points and Tokens that are calculated as a Sum()

Question - Is there any way I can extend _userManager.FindByEmailAsync() so that it will run the Sum as I do in the .Select() call, so I don't have a redundant call to the DB?

I tried to call:

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);

and pass it into CheckPasswordSignInAsync() but the result failed, so it's looking for something other than Email, UserName, or Id. But what?

If I just call this to return the whole User:

var userFromRepo = await _userManager.Users.SingleOrDefaultAsync(p => p.Email == loginDto.Email);

It works but I can't run the Sum() calls for Point and Token

I also tried extending UserManager like this, with no luck.

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);
}

Here is my login method.

[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);
}

Upvotes: 3

Views: 225

Answers (3)

Manie Botha
Manie Botha

Reputation: 5

You do not have to call the user again the user already exist on userfromrepo.

Just preform your logic on your current user object, userfromrepo.column name = what ever value. It will not update the database. Just the current user object and return it.

Upvotes: -2

chuckd
chuckd

Reputation: 14610

To solve my issue I just changed this code below. Now I only fetch once from the DB, instead of 3 times.

Maybe someone who knows UserManager can weigh in on the difference between what I had and what I have now in terms of security?

// Old Code
var userFromRepo = await _userManager.FindByEmailAsync(loginDto.Email);  

// New code     
var user = await _userManager.Users
    .Include(p => p.TokensPoints)
    .Include(p => p.UserRoles)
        .ThenInclude(r => r.Role)
.SingleOrDefaultAsync(x => x.NormalizedEmail == loginDto.Email);

next I just map the fields I want to the DTO being returned to the client.

Upvotes: 0

Brando Zhang
Brando Zhang

Reputation: 28397

If you want to extend the _userManager.FindByEmailAsync method, you need to create a custom class which inherts the IUserEmailStore, since the FindByEmailAsync method will call the IUserEmailStore's FindByEmailAsync method.

The source codes for the _userManager is like this:

public virtual async Task<TUser?> FindByEmailAsync(string email)
{
    ThrowIfDisposed();
    IUserEmailStore<TUser> store = GetEmailStore();
    Microsoft.AspNetCore.Shared.ArgumentNullThrowHelper.ThrowIfNull(email, "email");
    email = NormalizeEmail(email);
    TUser val = await store.FindByEmailAsync(email, CancellationToken).ConfigureAwait(continueOnCapturedContext: false);
    if (val == null && Options.Stores.ProtectPersonalData)
    {
        ILookupProtectorKeyRing service = _services.GetService<ILookupProtectorKeyRing>();
        ILookupProtector protector = _services.GetService<ILookupProtector>();
        if (service != null && protector != null)
        {
            foreach (string allKeyId in service.GetAllKeyIds())
            {
                string normalizedEmail = protector.Protect(allKeyId, email);
                val = await store.FindByEmailAsync(normalizedEmail, CancellationToken).ConfigureAwait(continueOnCapturedContext: false);
                if (val != null)
                {
                    return val;
                }
            }
        }
    }

    return val;
}

Inside the new implemented IUserEmailStore class ,you could modify the FindByEmailAsync like below:

_dbContext.Users
            .Where(u => u.Email == email)
            .Select(p => new UserDto
            {
                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 = 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(up => up.Points),
                Tokens = p.Tokens.Sum(t => t.Tokens),
                Token = _tokenService.CreateToken(p, 
                    (from userRole in p.UserRoles
                     join role in _dbContext.Roles on userRole.RoleId equals role.Id
                     select role.Name).ToList()),
                IsInstructor = p.IsInstructor
            })
            .FirstOrDefaultAsync();

Then register it inside the program.cs:

builder.Services.AddTransient<IUserEmailStore<ApplicationUser>, CustomUserEmailStore>();

More details about how to use it, you could refer to this article.

Upvotes: -1

Related Questions