How to load navigation properties on an IdentityUser with UserManager
Asked Answered
M

6

47

I've extended IdentityUser to include a navigation property for the user's address, however when getting the user with UserManager.FindByEmailAsync, the navigation property isn't populated. Does ASP.NET Identity Core have some way to populate navigation properties like Entity Framework's Include(), or do I have to do it manually?

I've set up the navigation property like this:

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; }
}
Marvismarwin answered 5/2, 2018 at 13:19 Comment(5)
AFAIK there is no built-in way and you need to do it "manually". At least I did not find one, when I had the same problem. Happy to be corrected if there actually is a way...Wisent
As a side note: [ForeignKey(nameof(AddressId))] is not required due to Convention over Configuration.Sloshy
@CamiloTerevinto: It's not required, true. However, I always add the attribute just to be explicit. I don't like relying on convention "magic", and it also makes the code more obvious.Emrich
Likewise, particularly for people who may not be familiar with EF Core and its conventions.Marvismarwin
Ack! Was banging my head against the wall for ages. Even in Core 3.1 this is still not working. The docs seem to indicate this should work - see learn.microsoft.com/en-us/aspnet/core/security/authentication/… - but it doesn't.Snob
S
48

Unfortunately, you have to either do it manually or create your own IUserStore<IdentityUser> where you load related data in the FindByEmailAsync method:

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

Of course, implementing the entire store just for this isn't the best option.

You can also query the store directly, though:

UserManager<IdentityUser> userManager; // DI injected

var user = await userManager.Users
    .Include(x => x.Address)
    .SingleAsync(x => x.NormalizedEmail == email);
Sloshy answered 5/2, 2018 at 13:27 Comment(1)
use SingleOrDefaultAsync instead of SingleAsyncHyperborean
E
23

The short answer: you can't. However, there's options:

  1. Explicitly load the relation later:

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

    This will require issuing an additional query of course, but you can continue to pull the user via UserManager.

  2. Just use the context. You don't have to use UserManager. It just makes some things a little simpler. You can always fallback to querying directly via the context:

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

FWIW, you don't need virtual on your navigation property. That's for lazy-loading, which EF Core currently does not support. (Though, EF Core 2.1, currently in preview, will actually support lazy-loading.) Regardless, lazy-loading is a bad idea more often than not, so you should still stick to either eagerly or explicitly loading your relationships.

Emrich answered 5/2, 2018 at 14:27 Comment(1)
Thanks for the tip on lazy-loading. This project originated in EF6 so there are plenty of hangovers from there, in the codebase and in my head!Marvismarwin
H
12

Update for .NET 6.0 with EF Core 6.0:

You can now configure the property to be automatically included on every query.

modelBuilder.Entity<MyUser>().Navigation(e => e.Address).AutoInclude();

For more info check out: https://learn.microsoft.com/en-us/ef/core/querying/related-data/eager#model-configuration-for-auto-including-navigations

Huss answered 16/2, 2022 at 8:11 Comment(1)
Works fine, but beware of the cycles if you've one-many relationships.Oratorical
T
5

I found it useful to write an extension on the UserManager class.

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());
}
Terricolous answered 7/9, 2019 at 2:22 Comment(0)
T
0

Best Option in my case is to add a package reference to Microsoft.EntityFrameworkCore.Proxies and then in your services use the UseLazyLoadingProxies

.AddDbContext<YourDbContext>(
    b => b.UseLazyLoadingProxies()
          .UseSqlServer(myConnectionString));

More infos https://learn.microsoft.com/de-de/ef/core/querying/related-data/lazy

Tomfoolery answered 23/5, 2022 at 11:30 Comment(0)
I
0

I know this is an old post but I solved this issue by using the first option given by Camilo Terevinto but tweaked it a little and I inherited UserManager and overrode the GetUserAsync method to add in some logic I needed to load the property I needed:

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;
    }
}
Incurvate answered 30/9, 2023 at 0:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.