Entity Framework Core - Best way to access a collection on an entity?

asp.net-core entity-framework-core

Question

I have a simple ASP.NET Core 2.0 + Entity Framework Core project:

I have an entity called ApplicationUser, which extends Microsoft.AspNet.Identity.IdentityUser, and the ApplicationUser has a collection of another entity called Books.

However, when I try to read the collection of Books from the ApplicationUser after retrieving it from UserManager:

ApplicationUser user = await _userManager.GetUserAsync(User);
return user.Books;

I get an empty collection.

If I do this instead:

ApplicationUser user = await _userManager.GetUserAsync(User);
return _context.Users.Include(d => d.Books).FirstOrDefault(u => u == user);

Then I get the correct collection of Books.

I suspect this is because the ApplicationUser I retrieved from the UserManager lacks the proper DbContext. My question is, is there a better way of doing this? Like somehow being able to just get the collection from the user without having to go through the DbContext with LINQ? Would there be significant performance gain?

Finally, what would be a good read about EF Core to get an understanding of how it works under the hood?

1
2
4/4/2018 9:09:02 PM

Accepted Answer

First of, if you want to access data from your database, you must use DBContext. Secondly, the only way you can still access the database from another object is if the object was of type IQueryable that was returned directly or indirectly from the database.

For instance, the following would return IQueryable<User>:

var users = _context.Users.Where(u => u == user);

This means you can access the users variable and access the database something like this:

var books = users.Select(u => u.Books);

Notice the _userManager.GetUserAsync(User) function does not return an IQueryable. This means that the ApplicationUser user will only contain what it was meant to fetch (the user object). Simply using user.Books will not help it fetch data from the database. Moreover, if I am not mistaken, GetUserAsync gets its data from the current claims principal, not the database. Either way, when you attempt to fetch the user, entity framework will not automatically fetch Books as well. This is as Igor mentioned due to lazy loading. https://docs.microsoft.com/en-us/ef/core/querying/related-data

As for how optimized your ways are, you can check the exact SQL query being run in your console. The above way I have shown will surely run a single query since ef core will put those lines together and understand exactly what query to run. A straightforward way to access the same data would be something like

_context.Books.Where(b => b.User == user)

If this is all you plan to use user for, it would have a little better performance to use _userManager.GetUserId(), since it would only get one value from your claims. Then you can use:

_context.Books.Where(b => b.UserId == userId)
2
4/5/2018 7:06:38 AM

Popular Answer

Adding an answer here for EF Core 2.1 (Identity 4+).

This answer is based on Lazy Loading. For other ways, see the end of the post.

Tseng made a comment on the question, and at the time it was for EF 2.1 preview. 2.1 is RTM now, and that same link he posted is really your answer, if you upgrade to EF Core 2.1.

It's really easy to use too. Following the direction for Lazy Loading with Proxies on the same link Tseng mentioned, you basically do this (assuming you've got your models defined properly with 'virtual' as per the link says):

  1. Install the Nuget package Microsoft.EntityFrameworkCore.Proxies.
  2. In your .AddDbContext<...>(...) or in your OnConfiguring(), add a call to .UseLazyLoadingProxies() For example, in Startup.cs:

        services.AddDbContextPool<ApplicationDbContext>(options =>
                options.UseMySql(Configuration.GetConnectionString("ApplicationDbContext"))
                       .UseLazyLoadingProxies()
    
        );
    

Rebuild. That's it. There are other ways mentioned too besides using Proxies, but this is the simplest.

To retrieve data using this:

var user = await _userManager.GetUserAsync(ApplicationUser);
var addresses = user.UserAddresses;

with models similar to:

public class ApplicationUser : IdentityUser<int>
{
    public ApplicationUser()
    {
        UserAddresses = new List<UserAddress>();
    }

    public virtual ICollection<UserAddress> UserAddresses { get; set; }
}

public class UserAddress
{
    public int UserAddressId { get; set; }
    public int Id { get; set; }
    ...
    public virtual ApplicationUser User { get; set; }
}

If you prefer to use Eager Loading or Explicit Loading, the same link has the details. I just do not go over them here.



Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow