Вы можете получить DbContext из DbSet?

В моем приложении иногда необходимо сохранить 10 000 или более строк в базе данных за одну операцию. Я обнаружил, что простое повторение и добавление каждого элемента по одному может занять более полутора часов.

Однако, если я отключу AutoDetectChangesEnabled, это займет ~ 5 секунд (именно это я хочу)

Я пытаюсь сделать метод расширения, называемый "AddRange", в DbSet, который отключит AutoDetectChangesEnabled, а затем снова включит его после завершения.

public static void AddRange<TEntity>(this DbSet<TEntity> set, DbContext con, IEnumerable<TEntity> items) where TEntity : class
    {
        // Disable auto detect changes for speed
        var detectChanges = con.Configuration.AutoDetectChangesEnabled;
        try
        {
            con.Configuration.AutoDetectChangesEnabled = false;

            foreach (var item in items)
            {
                set.Add(item);
            }
        }
        finally
        {
            con.Configuration.AutoDetectChangesEnabled = detectChanges;
        }
    }

Итак, мой вопрос: есть ли способ получить DbContext из DbSet? Мне не нравится делать его параметром - кажется, что это не должно быть лишним.

Ответы

Ответ 1

Да, вы можете получить DbContext от DbSet<TEntity>, но это решение является тяжелым отражением. Я привел пример того, как это сделать ниже.

Я протестировал следующий код и смог успешно извлечь экземпляр DbContext, из которого был создан DbSet. Обратите внимание, что, хотя он отвечает на ваш вопрос, почти наверняка будет лучшим решением вашей проблемы.

public static class HackyDbSetGetContextTrick
{ 
    public static DbContext GetContext<TEntity>(this DbSet<TEntity> dbSet)
        where TEntity: class
    { 
        object internalSet = dbSet
            .GetType()
            .GetField("_internalSet",BindingFlags.NonPublic|BindingFlags.Instance)
            .GetValue(dbSet);
        object internalContext = internalSet
            .GetType()
            .BaseType
            .GetField("_internalContext",BindingFlags.NonPublic|BindingFlags.Instance)
            .GetValue(internalSet); 
        return (DbContext)internalContext
            .GetType()
            .GetProperty("Owner",BindingFlags.Instance|BindingFlags.Public)
            .GetValue(internalContext,null); 
    } 
}

Пример использования:

using(var originalContextReference = new MyContext())
{
   DbSet<MyObject> set = originalContextReference.Set<MyObject>();
   DbContext retrievedContextReference = set.GetContext();
   Debug.Assert(ReferenceEquals(retrievedContextReference,originalContextReference));
}

Объяснение:

Согласно Reflector, DbSet<TEntity> имеет частное поле _internalSet типа InternalSet<TEntity>. Тип является внутренним для DLL EntityFramework. Он наследует от InternalQuery<TElement> (где TEntity : TElement). InternalQuery<TElement> также является внутренним для dll EntityFramework. Он имеет частное поле _internalContext типа InternalContext. InternalContext также является внутренним для EntityFramework. Однако InternalContext предоставляет общедоступное свойство DbContext, называемое Owner. Итак, если у вас есть DbSet<TEntity>, вы можете получить ссылку на владельца DbContext, обратившись к каждому из этих свойств и передав окончательный результат в DbContext.

Обновление @LoneyPixel

В EF7 есть частное поле _context непосредственно в классе, реализующее DbSet. Публиковать это поле публично не публично

Ответ 2

Почему вы делаете это на DbSet? Попробуйте сделать это вместо DbContext:

public static void AddRangeFast<T>(this DbContext context, IEnumerable<T> items) where T : class
{
    var detectChanges = context.Configuration.AutoDetectChangesEnabled;
    try
    {
        context.Configuration.AutoDetectChangesEnabled = false;
        var set = context.Set<T>();

        foreach (var item in items)
        {
            set.Add(item);
        }
    }
    finally
    {
        context.Configuration.AutoDetectChangesEnabled = detectChanges;
    }
}

Тогда его использование выполняется так же просто, как:

using (var db = new MyContext())
{
    // slow add
    db.MyObjects.Add(new MyObject { MyProperty = "My Value 1" });
    // fast add
    db.AddRangeFast(new[] {
        new MyObject { MyProperty = "My Value 2" },
        new MyObject { MyProperty = "My Value 3" },
    });
    db.SaveChanges();
}

Ответ 4

возможно, вы могли бы создать помощника, который отключил бы это для вас, а затем просто вызвать помощника из метода AddRange