Вопрос по c#, dependency-injection, dbcontext, entity-framework, asp.net – Один DbContext на веб-запрос ... почему?

362

Я читал много статей, объясняющих, как настроить Entity Framework.DbContext так что только один создается и используется для каждого веб-запроса HTTP с использованием различных структур DI.

Почему это хорошая идея в первую очередь? Какие преимущества вы получаете, используя этот подход? Есть определенные ситуации, когда это было бы хорошей идеей? Есть ли вещи, которые вы можете сделать с помощью этой техники, которые вы не можете делать при создании экземпляраDbContexts за вызов метода репозитория?

Геддари вmehdi.me/ambient-dbcontext-in-ef6 вызывает экземпляр DbContext для каждого метода хранилища, вызывает антипаттерн. Цитата: «Делая так, вы теряете практически все функции, которые Entity Framework предоставляет через DbContext, включая его кэш 1-го уровня, его карту идентификации, его единицу работы, а также отслеживание изменений и отложенную загрузку . Способность & Quot; Отличная статья с отличными предложениями для обработки жизненного цикла DBContexts. Определенно стоит прочитать. Christoph

Ваш Ответ

9   ответов
9

что если вы собираетесь использовать DbContext в однопоточном приложении, вам потребуется больше памяти. Например, моему веб-приложению в Azure (один очень маленький экземпляр) требуется еще 150 МБ памяти, и у меня около 30 пользователей в час. Application sharing DBContext in HTTP Request

Вот реальный пример изображения: приложение было развернуто в 12 вечера

Возможно, идея состоит в том, чтобы поделиться контекстом для одного запроса. Если мы обращаемся к различным репозиториям и классам DBSet и хотим, чтобы операции с ними были транзакционными, это должно быть хорошим решением. Посмотрите на проект с открытым исходным кодомmvcforum.com Я думаю, что это делается в их реализации шаблона проектирования Unit Of Work.
516

NOTE: This answer talks about the Entity Framework's DbContext it is applicable to any sort of Unit of Work implementation, such as LINQ to SQL's DataContext, and NHibernate's ISession.

Позвольте начать с повторения Йену:DbContext для всего приложения это плохая идея. Единственная ситуация, в которой это имеет смысл, - это когда у вас есть однопоточное приложение и база данных, которая используется только этим единственным экземпляром приложения.DbContext не является потокобезопасным и, так какDbContext кэширует данные, они довольно быстро устаревают. Это доставит вам массу неприятностей, когда несколько пользователей / приложений будут работать с этой базой данных одновременно (что, конечно, очень часто). Но я ожидаю, что вы уже знаете это и просто хотите знать, почему бы просто не внедрить новый экземпляр (то есть с временным образом жизни)DbContext в тех, кто нуждается в этом. (для получения дополнительной информации о том, почему одинDbContext или даже по контексту на тему - плохо, читайэтот ответ).

Позвольте мне начать с того, что регистрацияDbContext как переходный процесс может работать, но, как правило, вы хотите иметь один экземпляр такой единицы работы в определенной области. В веб-приложении может оказаться целесообразным определить такую область на границах веб-запроса; таким образом, на веб-запрос стиль жизни. Это позволяет вам позволить целому набору объектов работать в одном контексте. Другими словами, они работают в рамках одной бизнес-операции.

Если у вас нет цели, чтобы набор операций работал в одном и том же контексте, в этом случае переходный образ жизни - это хорошо, но есть несколько вещей, на которые стоит обратить внимание:

Since every object gets its own instance, every class that changes the state of the system, needs to call _context.SaveChanges() (otherwise changes would get lost). This can complicate your code, and adds a second responsibility to the code (the responsibility of controlling the context), and is a violation of the Single Responsibility Principle. You need to make sure that entities [loaded and saved by a DbContext] never leave the scope of such a class, because they can't be used in the context instance of another class. This can complicate your code enormously, because when you need those entities, you need to load them again by id, which could also cause performance problems. Since DbContext implements IDisposable, you probably still want to Dispose all created instances. If you want to do this, you basically have two options. You need to dispose them in the same method right after calling context.SaveChanges() in that case the business logic takes ownership of an object it gets passed on from the outside. The second option is to Dispose all created instances on the boundary of the Http Request in that case you still need some sort of scoping to let the container know when those instances need to be Disposed.

Другой вариант заключается вnot ввестиDbContext совсем. Вместо этого вы вводитеDbContextFactory который может создать новый экземпляр (я использовал этот подход в прошлом). Таким образом, бизнес-логика явно контролирует контекст. Если может выглядеть так:

public void SomeOperation()
{
    using (var context = this.contextFactory.CreateNew())
    {
        var entities = this.otherDependency.Operate(
            context, "some value");

        context.Entities.InsertOnSubmit(entities);

        context.SaveChanges();
    }
}

Плюсом этого является то, что вы управляете жизньюDbContext явно, и это легко настроить. Это также позволяет вам использовать один контекст в определенной области, что имеет явные преимущества, такие как выполнение кода в одной бизнес-транзакции и возможность передавать объекты, поскольку они происходят из одного и того же объекта.DbContext.

Недостатком является то, что вам придется обойтиDbContext от метода к методу (который называется инъекцией метода). Следует отметить, что в некотором смысле это решение является таким же, как «область действия» подход, но теперь область действия контролируется в самом коде приложения (и, возможно, повторяется много раз). Это приложение, которое отвечает за создание и распоряжение единицей работы. ПосколькуDbContext создается после построения графа зависимостей, внедрение Constructor Injection отсутствует, и вам нужно отложить метод Injection, если вам нужно передать контекст из одного класса в другой.

Инъекция метода не так уж и плоха, но когда бизнес-логика усложняется и в нее вовлекается больше классов, вам придется передавать ее от метода к методу и от класса к классу, что может значительно усложнить код (я уже видел) это в прошлом). Однако для простого приложения этот подход подойдет.

Из-за недостатков, этот фабричный подход для больших систем, может быть полезен другой подход, в котором вы позволяете контейнеру или коду инфраструктуры /Корень композиции управлять единицей работы. Это стиль, о котором ваш вопрос.

Позволяя контейнеру и / или инфраструктуре обрабатывать это, код вашего приложения не загрязняется необходимостью создания (необязательно) фиксации и удаления экземпляра UoW, что делает бизнес-логику простой и чистой (просто единая ответственность). Есть некоторые трудности с этим подходом. Например, вы фиксировали и утилизировали экземпляр?

Распоряжение единицей работы может быть сделано в конце веб-запроса. Однако многие людиincorrectly Предположим, что это также место для фиксации единицы работы. Однако в этот момент в приложении вы просто не можете точно определить, что единица работы должна быть фактически зафиксирована. например Если код бизнес-уровня вызвал исключение, которое было обнаружено выше в стеке вызовов, вы определенноdon't хочу совершить.

Реальное решение снова состоит в том, чтобы явно управлять некоторой областью действия, но на этот раз сделайте это внутри корня композиции. Абстрагирование всей бизнес-логики зашаблон команды / обработчика, вы сможете написать декоратор, который можно обернуть вокруг каждого обработчика команд, который позволяет это делать. Пример:

class TransactionalCommandHandlerDecorator<TCommand>
    : ICommandHandler<TCommand>
{
    readonly DbContext context;
    readonly ICommandHandler<TCommand> decorated;

    public TransactionCommandHandlerDecorator(
        DbContext context,
        ICommandHandler<TCommand> decorated)
    {
        this.context = context;
        this.decorated = decorated;
    }

    public void Handle(TCommand command)
    {
        this.decorated.Handle(command);

        context.SaveChanges();
    } 
}

Это гарантирует, что вам нужно написать этот код инфраструктуры только один раз. Любой твердотельный DI-контейнер позволяет вам настроить такой декоратор, чтобы обернуть всеICommandHandler<T> реализации в согласованном порядке.

Вау - спасибо за подробный ответ. Если бы я мог дважды проголосовать, я бы сделал это. Выше вы говорите "... не намерены позволять целому набору операций работать в одном и том же контексте, в этом случае переходный образ жизни в порядке ..." Что вы имеете в виду под «переходным», в частности? Andrew
Но вы не передаете контекст декорированному классу, как декорированный класс может работать с тем же контекстом, который был переданTransactionCommandHandlerDecorator? например, если украшенный класс будетInsertCommandHandler класс, как он мог зарегистрировать операцию вставки в контекст (DbContext в EF)?
@Andrew: «Переходный»; представляет собой концепцию внедрения зависимостей, которая означает, что если служба настроена как временная, новый экземпляр службы создается каждый раз, когда она внедряется в потребителя.
+1 Вы поверите, что я написал всеthis answer прежде чем на самом деле читать это? Кстати, IMO, я думаю, что для вас важно в конце обсудить удаление DbContext (хотя здорово, что вы остаетесь вне зависимости от контейнера)
@ user981375: для операций CRUD вы можете создать общийCreateCommand<TEnity> и общийCreateCommandHandler<TEntity> : ICommandHandler<CreateCommand<TEntity>> (и сделать то же самое для обновления и удаления, и имел одинGetByIdQuery<TEntity> запрос). Тем не менее, вы должны спросить себя, является ли эта модель полезной абстракцией для операций CRUD, или она просто добавляет сложности. Тем не менее, вы можете извлечь выгоду из возможности легко добавить сквозные задачи (через декораторов) с помощью этой модели. Вам придется взвесить все за и против.
22

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

Да, Эндрю, вот что он имел в виду. Совместное использование контекста только для однопоточных настольных приложений.
Как насчет совместного использования контекста для одного запроса. Таким образом, для одного запроса мы можем иметь доступ к разным репозиториям и совершать транзакции между ними, используя один и тот же контекст?
Вы имеете в виду, что разделение его между HTTP-запросами никогда не является хорошей идеей? Andrew
23

contradicting Рекомендации Microsoft и многие люди используют DbContexts совершенно по-разному.

One recommendation is to "Dispose DbContexts as soon as posible" because having a DbContext Alive occupies valuable resources like db connections etc.... The other states that One DbContext per request is highly reccomended

Они противоречат друг другу, потому что если ваш Запрос в значительной степени не связан с Db, то ваш DbContext сохраняется без причины. Таким образом, тратить ваш DbContext на жизнь бесполезно, пока ваш запрос просто ожидает выполнения случайных вещей ...

Так много людей, которые следуютrule 1 имеют свои DbContexts внутри их"Repository pattern" и создатьa new Instance per Database Query такX*DbContext по запросу

Они просто получают свои данные и располагают контекстом как можно скорее. Это считаетсяMANY люди приемлемая практика. Хотя это имеет преимущество в том, что вы занимаете ресурсы БД за минимальное время, оно явно жертвует всемиUnitOfWork а такжеCaching конфеты EF может предложить.

Поддерживать в живых ни одногоmultipurpose Экземпляр DbContext максимизирует преимуществаCaching но так как DbContextnot thread safe и каждый веб-запрос выполняется в собственном потоке, DbContext per Require, то естьlongest Вы можете сохранить это.

Таким образом, рекомендация команды EF об использовании 1 ДБ контекста на запрос явно основана на том факте, что в веб-приложении UnitOfWork, скорее всего, будет в пределах одного запроса, и этот запрос имеет один поток. Таким образом, один DbContext на запрос подобен идеальному преимуществу UnitOfWork и Caching.

But во многих случаях это не так. я полагаюLogging отдельный UnitOfWork, таким образом, имеющий новый DbContext для входа в систему после запросаasync threads вполне приемлемо

Таким образом, в конечном итоге оказывается, что время жизни DbContext ограничено этими двумя параметрами.UnitOfWork а такжеThread

Честно говоря, ваши HTTP-запросы должны заканчиваться довольно быстро (несколько мс). Если они идут дольше, вам может потребоваться выполнить некоторую фоновую обработку с помощью внешнего планировщика заданий, чтобы запрос мог немедленно вернуться. Тем не менее, ваша архитектура также не должна полагаться на HTTP. В целом, хороший ответ, хотя.
3

так это то, что он выравнивает единицу работы (в том виде, в каком ее видит пользователь, то есть отправку страницы) с единицей работы в смысле ORM.

Таким образом, вы можете сделать передачу всей страницы транзакционной, чего нельзя было бы сделать, если бы вы представляли методы CRUD при каждом создании нового контекста.

12

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

Лично я создаю экземпляры DbContext, когда это необходимо - обычно присоединяемые к бизнес-компонентам, которые имеют возможность воссоздавать контекст при необходимости. Таким образом, я контролирую процесс, а не навязываю мне один экземпляр. Мне также не нужно создавать DbContext при каждом запуске контроллера, независимо от того, используется ли он на самом деле. Затем, если я все еще хочу иметь экземпляры для каждого запроса, я могу создать их в CTOR (через DI или вручную) или создать их по мере необходимости в каждом методе контроллера. Лично я обычно использую последний подход, чтобы избежать создания экземпляров DbContext, когда они на самом деле не нужны.

Это зависит от того, под каким углом ты смотришь на это тоже. Для меня экземпляр на запрос никогда не имел смысла. Действительно ли DbContext входит в Http-запрос? С точки зрения поведения, это неправильное место. Ваши бизнес-компоненты должны создавать ваш контекст, а не запрос Http. Затем вы можете создавать или отбрасывать свои бизнес-компоненты по мере необходимости и никогда не беспокоиться о времени существования контекста.

Короче говоря, что вы имеете в виду, когда говорите «возможность воссоздать контекст, если это необходимо»? Вы катите свою собственную способность отката? Можете ли вы разработать немного?
В этом случае внедрение в бизнес-объект должно быть связано с управлением жизненным циклом. На мой взгляд, бизнес-объект владеет контекстом и должен контролировать время жизни.
Лично я думаю, что это немного хлопотно, чтобы заставить DbContext в начале там. Там нет никакой гарантии, что вам даже нужно попасть в базу данных. Возможно, вы звоните сторонней службе, которая меняет состояние на этой стороне. Или, может быть, у вас есть 2 или 3 базы данных, с которыми вы работаете одновременно. Вы не будете создавать группу DbContexts в начале, на случай, если вы в конечном итоге будете их использовать. Бизнес знает данные, с которыми он работает, поэтому он связан с этим. Просто поместите TransactionScope в начало, если это необходимо. Я не думаю, что все звонки нужны. Это требует ресурсов.
Это интересный ответ, и я частично согласен с вами. На мой взгляд, DbContext не обязательно должен быть привязан к веб-запросу, но он всегда печатается на одном «запросе». как в: «бизнес-операция». И когда вы связываете контекст с бизнес-транзакцией, отмена изменений становится действительно странной. Но отсутствие его на границе веб-запроса не означает, что бизнес-компоненты (BC) должны создавать контекст; Я думаю, что это не их ответственность. Вместо этого вы можете применить определение области видимости с помощью декораторов вокруг ваших БЦ. Таким образом, вы даже можете изменить область видимости без изменения кода.
27

ал о дизайне DbContext для одного приложения / приложения, он спрашивал о дизайне запроса для каждого веб-сайта и о том, какие потенциальные выгоды могут существовать.

Я буду ссылатьсяhttp://mehdi.me/ambient-dbcontext-in-ef6/ Мехди - это фантастический ресурс

Possible performance gains.

Each DbContext instance maintains a first-level cache of all the entities its loads from the database. Whenever you query an entity by its primary key, the DbContext will first attempt to retrieve it from its first-level cache before defaulting to querying it from the database. Depending on your data query pattern, re-using the same DbContext across multiple sequential business transactions may result in a fewer database queries being made thanks to the DbContext first-level cache.

It enables lazy-loading.

If your services return persistent entities (as opposed to returning view models or other sorts of DTOs) and you'd like to take advantage of lazy-loading on those entities, the lifetime of the DbContext instance from which those entities were retrieved must extend beyond the scope of the business transaction. If the service method disposed the DbContext instance it used before returning, any attempt to lazy-load properties on the returned entities would fail (whether or not using lazy-loading is a good idea is a different debate altogether which we won't get into here). In our web application example, lazy-loading would typically be used in controller action methods on entities returned by a separate service layer. In that case, the DbContext instance that was used by the service method to load these entities would need to remain alive for the duration of the web request (or at the very least until the action method has completed).

Имейте в виду, что есть и минусы. Эта ссылка содержит много других ресурсов для чтения на эту тему.

Просто опубликовать это на тот случай, если кто-то еще наткнется на этот вопрос и не будет поглощен ответами, которые фактически не решают вопрос.

1

на которую следует обратить особое внимание в Entity Framework, - это использование комбинации создания новых объектов, отложенной загрузки и последующего использования этих новых объектов (из того же контекста). Если вы не используете IDbSet.Create (вместо только что созданного), отложенная загрузка этого объекта не будет работать, когда его извлекают из контекста, в котором он был создан. Пример:

 public class Foo {
     public string Id {get; set; }
     public string BarId {get; set; }
     // lazy loaded relationship to bar
     public virtual Bar Bar { get; set;}
 }
 var foo = new Foo {
     Id = "foo id"
     BarId = "some existing bar id"
 };
 dbContext.Set<Foo>().Add(foo);
 dbContext.SaveChanges();

 // some other code, using the same context
 var foo = dbContext.Set<Foo>().Find("foo id");
 var barProp = foo.Bar.SomeBarProp; // fails with null reference even though we have BarId set.
2

t, даже в однопоточном однопользовательском приложении, заключается в используемом им шаблоне карты идентификации. Это означает, что каждый раз, когда вы извлекаете данные, используя запрос или по идентификатору, он будет хранить извлеченные экземпляры сущности в кэше. В следующий раз, когда вы получите тот же объект, он предоставит вам кэшированный экземпляр объекта, если он доступен, с любыми изменениями, которые вы сделали в том же сеансе. Это необходимо, чтобы метод SaveChanges не заканчивался несколькими экземплярами сущностей одной и той же записи базы данных; в противном случае контекст должен был бы каким-то образом объединить данные из всех этих экземпляров сущности.

Причиной проблемы является то, что одноэлементный DbContext может стать бомбой замедленного действия, которая может в конечном итоге кэшировать всю базу данных + накладные расходы объектов .NET в памяти.

Есть способы обойти это поведение, используя только запросы Linq с.NoTracking() метод расширения. Также в наши дни компьютеры имеют много оперативной памяти. Но обычно это не желаемое поведение.

Сборщик мусора не собирается собирать экземпляры объектов, которые содержатся в активном статическом / одноэлементном объекте. Они окажутся во втором поколении кучи.
Это правильно, но вы должны предположить, что сборщик мусора будет работать, делая эту проблему более виртуальной, чем реальной.

Похожие вопросы