Можете ли вы получить DbContext из DbSet?

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

Однако, если я отключу 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? Мне не нравится делать это параметром - такое ощущение, что это не нужно.

5 ответов

Решение

Да, вы можете получить 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));
}

Объяснение:

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

Обновление от ygoe

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

С Entity Framework Core (протестировано с Версией 2.1) вы можете получить текущий контекст, используя

// DbSet<MyModel> myDbSet
var context = myDbSet.GetService<ICurrentDbContext>().Context;

Как получить DbContext из DbSet в EntityFramework Core 2.0

Почему вы делаете это на 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();
}

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

Мой вариант использования немного отличается, но я также хочу решить эту проблему для метода dbsetextension, который я назвал Save(), который будет выполнять добавление или изменение в наборе баз данных по мере необходимости в зависимости от того, соответствует ли сохраняемый элемент элементу в dbset.

это решение работает для EF 6.4.4, полученного на основе ответа smartcaveman

      public static DbContext GetContext<TEntity>(this DbSet<TEntity> dbSet) where TEntity : class
{
    var internalSetPropString = "System.Data.Entity.Internal.Linq.IInternalSetAdapter.InteralSet";
    var bfnpi = BindingFlags.NonPublic | BindingFlags.Instance;
    var bfpi = BindingFlags.Public | BindingFlags.Instance;
    var internalSet = dbSet.GetType().GetProperty(internalSetPropString, bfnpi).GetValue(dbSet);
    var internalContext = internalSet.GetType().BaseType.GetField("_internalContext", bfnpi).GetValue(internalSet);
    var ownerProperty = internalContext.GetType().GetProperty("Owner", bfpi);
    var dbContext = (dbContext)ownerProperty.GetValue(internalContext);
    return dbContext;
}

мой вариант использования в DbSetExtensions

      //yes I have another overload where i pass the context in. but this is more fun
public static void Save<T>(this DbSet<T> dbset, Expresssion<Fun<T, bool>> func, T item) where T :class
{
    var context = dbset.GetContext(); //<--
    var entity = dbset.FrirstOrDefault(func);
    if(entity == null) 
        dbset.Add(item);
    else 
    {
        var entry = context.Entry(entity);
        entry.CurrentValues.SetValues(item);
        entry.State = EntityState.Modified; 
    }
}

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

      db.AppUsers.Save(a => a.emplid == appuser.emplid, appuser);
db.SaveChangesAsync();
Другие вопросы по тегам