EF 预加载包含重复实体
我有一个处于多对多关系的用户实体和角色实体。它们被注入 Repository 实例,以便能够在 DbContext 被释放后(即在 Repository 层之外)执行延迟加载,如下所示:
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; }
}
运行以下代码时,对于返回的每个 User 实体,user 中的每个 Role 实体都有重复对.Roles
IRepository repository = new Repository();
ICollection users = repository.GetAllUsers();
foreach (User user in users)
{
foreach (Role role in user.Roles)
{
...
}
}
无论是否启用 EF 延迟加载,以及是否将 User.Roles 属性标记为虚拟,都会出现该问题。
但是,如果我不按如下方式在 Repository.GetAllUsers() 中急切加载 Roles 并让延迟加载的 Roles 属性调用 Repository.GetRolesByUserId(UserId),则不会返回重复的 Role 实体。
public ICollection<User> GetAllUsers()
{
using (var db = CreateContext())
{
// No eager loading
return db.Users.ToList();
}
}
如果我将 User.Roles 属性更改为始终访问存储库,则不会返回重复的角色实体。
public ICollection<Role> Roles
{
get { return (_roles = Repository.GetRolesByUserId(UserId)); }
set { _roles = value; }
}
看起来调用db.Users.Include(u => u.Roles)
会触发User.Roles属性的get()操作,从而导致Roles集合被填充两次。
我已经确认,当枚举 IQueryable 对象时,User.Roles 属性实际上会被填充两次。例如,当调用 .ToList()
时。这意味着,为了解决此行为,无法避免对 Roles 属性的 get() 主体进行更改。这意味着将 EF 特定逻辑放入域层中,并且不再使其与数据无关。
有没有办法防止这种情况发生?或者是否有更好的方法来在 DbContext 被释放后(在存储库层之外)实现延迟加载。
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).
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
也许这样的东西可以工作:
但在我看来,这是一个丑陋的编程技巧。
Perhaps something like this could work:
But it's an ugly trick-programming in my eyes.