Reputation: 75
I have a User entity and Role entity in a many-to-many relationship. They are injected with Repository instances to be able do lazy loading after the DbContext has been disposed (i.e. outside the Repository layer) like so:
public class User
{
public int UserId { get; set; }
public string UserName { get; set; }
// Lazy loaded property
public ICollection<Role> Roles
{
get { return _roles ?? (_roles = Repository.GetRolesByUserId(UserId)); }
set { _roles = value; }
}
private ICollection<Role> _roles;
public IRepository Repository { private get; set; }
}
public class Role
{
public int RoleId { get; set; }
public string Name { get; set; }
// Lazy loaded property
public ICollection<User> Users
{
get { return _users ?? (_users = Repository.GetUsersByRoleId(RoleId)); }
set { _users = value; }
}
private ICollection<User> _users;
public IRepository Repository { private get; set; }
}
public class Repository : IRepository
{
public ICollection<User> GetAllUsers()
{
using (var db = CreateContext())
{
// Using 'Include' to eager load the Roles collection for each User
return db.Users.Include(u => u.Roles).ToList();
}
}
public ICollection<Role> GetRolesByUserId(int userId)
{
using (var db = CreateContext())
{
return db.Roles.Where(r => r.Users.Any(u => u.UserId == userId))
.ToList();
}
}
public ICollection<User> GetUsersByRoleId(int roleId)
{
using (var db = CreateContext())
{
return db.Users.Where(u => u.Roles.Any(r => r.RoleId == roleId))
.ToList();
}
}
private CustomContext CreateContext()
{
var db = new CustomContext();
((IObjectContextAdapter)db).ObjectContext.ObjectMaterialized += OnObjectMaterialized;
return db;
}
private void OnObjectMaterialized(object sender, ObjectMaterializedEventArgs args)
{
if (args.Entity is User)
{
(args.Entity as User).Repository = this;
}
if (args.Entity is Role)
{
(args.Entity as Role).Repository = this;
}
}
}
public class CustomContext : DbContext
{
public CustomContext()
: base()
{
Configuration.LazyLoadingEnabled = false;
}
public DbSet<User> Users { get; set; }
public DbSet<Role> Roles { get; set; }
}
When running the following code, for each User entity returned, there are duplicate pairs for each Role entity in user.Roles
IRepository repository = new Repository();
ICollection users = repository.GetAllUsers();
foreach (User user in users)
{
foreach (Role role in user.Roles)
{
...
}
}
The problem occurs regardless of whether or not EF Lazy Loading is enabled, and whether or not the User.Roles property is marked as virtual.
But if I do not eager load Roles in Repository.GetAllUsers() as below and let the lazy-loaded Roles property call Repository.GetRolesByUserId(UserId), then no duplicate Role entities are returned.
public ICollection<User> GetAllUsers()
{
using (var db = CreateContext())
{
// No eager loading
return db.Users.ToList();
}
}
If I change the User.Roles property to always hit the Repository, then no duplicate Role entities are returned.
public ICollection<Role> Roles
{
get { return (_roles = Repository.GetRolesByUserId(UserId)); }
set { _roles = value; }
}
It looks like calling db.Users.Include(u => u.Roles)
triggers the User.Roles property's get() action which causes the Roles collection to be populated twice.
I've confirmed that the User.Roles property actually gets populated twice when the IQueryable object is enumerated. e.g. when calling .ToList()
. This means, in order to work around this behavipur, there's no way to avoid making changes to the Roles property's get() body. Which means putting EF specific logic in your Domain layer and no longer making it data agnostic.
Is there a way to prevent this from happening? Or is there a better way to achieve lazy-loading after the DbContext has been disposed (outside the Repository layer).
Upvotes: 0
Views: 1128
Reputation: 177133
Perhaps something like this could work:
public class Repository : IRepository
{
public bool RunningEagerLoading { get; set; } // false by default
public ICollection<User> GetAllUsers()
{
using (var db = CreateContext())
{
try
{
RunningEagerLoading = true;
return db.Users.Include(u => u.Roles).ToList();
// Materializing (by ToList()) is important here,
// deferred loading would not work
}
finally
// to make sure RunningEagerLoading is reset even after exceptions
{
RunningEagerLoading = false;
}
}
}
// ...
}
public class User
{
// ...
public ICollection<Role> Roles
{
get
{
if (Repository.RunningEagerLoading)
return _roles; // Eager loading cares for creating collection
else
return _roles ?? (_roles = Repository.GetRolesByUserId(UserId));
}
set { _roles = value; }
}
private ICollection<Role> _roles;
public IRepository Repository { private get; set; }
}
But it's an ugly trick-programming in my eyes.
Upvotes: 1