简体   繁体   中英

UserManager not updating custom Identity User properly

I'm using Using aspnet Identity + OpenIddict to do authentication on top of EF Core.

I extended the IdentityUser class to include two lists; StudyTags and ExpertTags , along with a few other properties:

public class ApplicationUser : IdentityUser
{
    public ApplicationUser()
    {
        StudyTags = new List<Tag>();
        ExpertTags = new List<Tag>();
    }

    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Location { get; set; }
    public List<Tag> StudyTags { get; set; }
    public List<Tag> ExpertTags { get; set; }
}

This is all well and good, however, when I want to update these lists and save them, the update only seems to persist in a certain scope. For instance, if I call the below API call twice, the first time adds the tag to the proper list, and the second time logs a message saying it's already been added. I can inspect the user and verify that the tag is in the list as expected.

    [Authorize(ActiveAuthenticationSchemes = OAuthValidationDefaults.AuthenticationScheme)]
    [HttpPost("add/tag/{tagType}/{tagName}")]
    public async Task<IActionResult> AddTagToUser(int tagType, string tagName) 
    {
        var user = await _userManager.GetUserAsync(User) as ApplicationUser;
        if(user == null) 
        {
            _logger.LogError("User not found");
            return BadRequest();
        }

        TagType type = (TagType)tagType;

        var tag = _tagService.GetTagByName(tagName);
        if(tag == null)
        {
            _logger.LogError("No tag corresponding to tagName '{0}'", tagName);
            return BadRequest();
        }

        if(type == TagType.Study)
        {
            if(user.StudyTags.Contains(tag))
            {
                _logger.LogInformation("{0} already assigned to {1}", tag.ToString(), user.ToString());
            }
            else
            {
                _logger.LogInformation("{0} added to {1}", tag.ToString(), user.ToString());
                user.StudyTags.Add(tag);
            }
        }
        else if(type == TagType.Expert)
        {
            if(user.ExpertTags.Contains(tag))
            {
                _logger.LogInformation("{0} already assigned to {1}", tag.ToString(), user.ToString());
            }
            else 
            {
                _logger.LogInformation("{0} added to {1}", tag.ToString(), user.ToString());
                user.ExpertTags.Add(tag);
            }
        }
        else
        {
            _logger.LogError("No tagType corresponding to {0}", tagType);
            return BadRequest();
        }


        var result = await _userManager.UpdateAsync(user);

        if(result.Succeeded)
        {
            return Ok(result);
        }
        else
        {
            _logger.LogError("Updating Tag failed");
            return BadRequest();
        } 
    }

However, if I just try to do a Get of the same user, the StudyTags and ExpertTags lists are empty.

    [Authorize(ActiveAuthenticationSchemes = OAuthValidationDefaults.AuthenticationScheme)]
    [HttpGet]
    public async Task<IActionResult> Get()
    {
        var user = await _userManager.GetUserAsync(User);
        if(user == null)
        {
            _logger.LogError("User not found");
            return BadRequest();
        }

        return Ok(user); 
    }

Am I doing something incorrectly? Any help would be appreciated!

Your properties should be marked as virtual. If the records exist in your database but just aren't being fetched when you run the GET method and fetch data for the user, then it's because of lazy loading. The virtual keyword will allow them to be populated when you access the user object.

This is what you want:

public virtual List<Tag> StudyTags { get; set; }
public virtual List<Tag> ExpertTags { get; set; }

UPDATE:

So it looks like what's required is a M:M relationship. Since EF Core doesn't support the auto-creation of the junction class, it'll have to be made explicitly. So in the end, your models must look like:

public class ApplicationUser : IdentityUser 
{
    //Other Properties
    public ICollection<Tag> StudyTags { get; set; }
    public ICollection<Tag> ExpertTags { get; set; }
}

public class Tag
{
    //Other Properties
    public ICollection<ApplicationUser> Users { get; set; }
}

public class UserTags 
{
    public int UserId { get; set; }
    public ApplicationUser User { get; set; }

    public int TagId { get; set; }
    public Tag Tag { get; set; }
}

And your context needs to be updated to include:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<UserTag>()
        .HasKey(x => new { x.User, x.TagId });

    modelBuilder.Entity<UserTag>()
        .HasOne(x => x.User)
        .WithMany(y => y.UserTags)
        .HasForeignKey(x => x.UserId);

    modelBuilder.Entity<UserTag>()
        .HasOne(x => x.Tag)
        .WithMany(y => y.UserTags)
        .HasForeignKey(x => x.TagId);
}

In your _userManager.GetUserAsync method you have to write the code to eagerly load the StudyTags like below:

context.User.Include(x => x.StudyTags).Include(x => x.ExpertTags);

If your property is virtual, then it will only be loaded when you access the property (lazy loading). If your property is not virtual, then it should be loaded when object holding the property is loaded.

If you want to turn off lazy loading globally you can include this in your context constructor:

this.Configuration.LazyLoadingEnabled = false; // careful though keep reading

The above switch has a global effect so if not careful it can cause performance issues since the whole object graph will be loaded every time, even though you may not need it.

To turn off lazy loading for a single property, make it NON virtual.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM