EF Eager Загрузка включает в себя повторяющиеся объекты - PullRequest
0 голосов
/ 07 апреля 2011

У меня есть объект User и объект Role в отношениях «многие ко многим».Они внедряются в экземпляры репозитория, чтобы иметь возможность выполнять отложенную загрузку после удаления DbContext (т. Е. Вне уровня репозитория) следующим образом:

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.Roles

IRepository repository = new Repository();
ICollection users = repository.GetAllUsers();
foreach (User user in users)
{
    foreach (Role role in user.Roles)
    {
        ...
    }
}

Проблема возникает независимо от того, включена ли EF Lazy Loading и не помечено ли свойство User.Roles как виртуальное.

Но если я не буду загружать роли в Repository.GetAllUsers (), как показано ниже, и пусть лениво загруженное свойство Roles вызывает Repository.GetRolesByUserId (UserId), то повторяющиеся сущности ролей не возвращаются.

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 (), которое приводит к тому, что коллекция ролей заполняется дважды.

Я подтвердил, что использованиеСвойство r.Roles заполняется дважды при перечислении объекта IQueryable.например, при звонке .ToList().Это означает, что для обхода этого поведения нет способа избежать внесения изменений в тело get () свойства Roles.Что означает размещение специфической логики EF на вашем доменном уровне и больше не делает ее независимой от данных.

Есть ли способ предотвратить это?Или есть лучший способ добиться отложенной загрузки после удаления DbContext (вне слоя Repository).

1 Ответ

1 голос
/ 07 апреля 2011

Возможно, что-то подобное может сработать:

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

Но это уродливая хитрость в моих глазах.

...