如何使用UserManager在IdentityUser上加载导航属性

46

我已经扩展了IdentityUser,将用户地址作为导航属性包含在内。但是当使用UserManager.FindByEmailAsync获取用户时,导航属性没有填充。ASP.NET Identity Core是否有像Entity Framework的Include()一样填充导航属性的方法,还是我需要手动完成?

我是这样设置导航属性的:

public class MyUser : IdentityUser
{
    public int? AddressId { get; set; }

    [ForeignKey(nameof(AddressId))]
    public virtual Address Address { get; set; }
}

public class Address
{
    [Key]
    public int Id { get; set; }
    public string Street { get; set; }
    public string Town { get; set; }
    public string Country { get; set; }
}

据我所知,没有内置的方法,你需要手动完成。至少当我遇到同样的问题时,我没有找到一个内置的方法。如果确实有一种方法,我很乐意接受更正。 - Christoph Fink
顺便提一下:由于约定优于配置,[ForeignKey(nameof(AddressId))] 不是必需的。 - Camilo Terevinto
1
@CamiloTerevinto:它不是必需的,确实如此。但是,我总是添加属性只是为了明确表达。我不喜欢依赖于"魔法"约定,这也使代码更容易理解。 - Chris Pratt
同样地,对于那些可能不熟悉EF Core及其约定的人来说,这一点尤为重要。 - Mourndark
1
啊!我已经苦思冥想很久了,但即使在Core 3.1中仍然无法正常工作。文档似乎表明这应该可以工作-请参见https://learn.microsoft.com/en-us/aspnet/core/security/authentication/customize-identity-model?view=aspnetcore-3.0#add-all-navigation-properties-但实际上并没有。 - Jedidja
6个回答

48

很遗憾,您必须手动执行此操作或创建自己的IUserStore<IdentityUser>,在FindByEmailAsync方法中加载相关数据:

public class MyStore : IUserStore<IdentityUser>, // the rest of the interfaces
{
    // ... implement the dozens of methods
    public async Task<IdentityUser> FindByEmailAsync(string normalizedEmail, CancellationToken token)
    {
        return await context.Users
            .Include(x => x.Address)
            .SingleAsync(x => x.Email == normalizedEmail);
    }
}

当然,仅为此实现整个存储并不是最佳选择。

但是,您也可以直接查询存储:

UserManager<IdentityUser> userManager; // DI injected

var user = await userManager.Users
    .Include(x => x.Address)
    .SingleAsync(x => x.NormalizedEmail == email);

使用 SingleOrDefaultAsync 而不是 SingleAsync。 - Mahmood Garibov

22

简短回答:你不能。不过,有几个选项:

  1. 稍后显式加载关系:

    await context.Entry(user).Reference(x => x.Address).LoadAsync();
    

    这当然需要发出额外的查询,但您可以继续通过UserManager拉取用户。

  2. 只需使用上下文。您不必须使用UserManager。它只是使某些事情变得更简单。您始终可以回退到通过上下文直接查询:

  3. var user = context.Users.Include(x => x.Address).SingleOrDefaultAsync(x=> x.Id == User.Identity.GetUserId());
    

    FWIW,您的导航属性不需要使用virtual。这是用于延迟加载的,但EF Core当前不支持。 (尽管EF Core 2.1目前在预览版中,但实际上将支持延迟加载。)无论如何,往往懒惰加载都是一个坏主意,因此您仍应坚持急切或明确地加载您的关系。


1
感谢您提供了关于延迟加载的技巧。这个项目最初是基于EF6开发的,所以在代码库和我的头脑中都有很多遗留问题! - Mourndark

12

运行良好,但是如果你有一对多的关系,请注意循环。 - Usama Aziz

5
我发现在UserManager类上编写一个扩展非常有用。
public static async Task<MyUser> FindByUserAsync(
    this UserManager<MyUser> input,
    ClaimsPrincipal user )
{
    return await input.Users
        .Include(x => x.InverseNavigationTable)
        .SingleOrDefaultAsync(x => x.NormalizedUserName == user.Identity.Name.ToUpper());
}

0

0
我知道这是一个旧帖子,但我通过使用Camilo Terevinto提供的第一个选项解决了这个问题,但稍作修改并继承了UserManager,并重写了GetUserAsync方法,以添加我需要加载所需属性的一些逻辑。
public class UserService : UserManager<MyUser> {
    private readonly ApplicationDbContext context;

    public UserService(ApplicationDbContext context, IUserStore<MyUser> store, IOptions<IdentityOptions> optionsAccessor, IPasswordHasher<MyUser> passwordHasher, IEnumerable<IUserValidator<MyUser>> userValidators, IEnumerable<IPasswordValidator<MyUser>> passwordValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, IServiceProvider services, ILogger<UserManager<MyUser>> logger) : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger) {
        this.context = context;
    }

    public override async Task<MyUser?> GetUserAsync(ClaimsPrincipal principal) {
        var user = await base.GetUserAsync(principal);

        if (user is null)
            return user;

        await context.Entry(user).Reference(x => x.Address).LoadAsync();

        return user;
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接