0

I'd like to use lazy loading in my application using entity framework, to get data from the database. I read here among the answers about the DbContext that:

The context should be created per request.

If I use this way, then the following exception raises:

The ObjectContext instance has been disposed and can no longer be used for operations that require a connection.

And of course this is because I dropped the context after executing the query. Here I read that, I can explicitly load the data, but I don't want to load the data from other tables.

So what do you suggest, how should I use the DbContext to be able to properly use lazy loading?

Attila Szász
  • 707
  • 4
  • 22
  • Either don't dispose the context until all lazy loading has been done, or use eager / explicit loading. You won't be able to execute further SQL (lazy loading) without a database connection. – Johnathan Barclay Nov 27 '19 at 11:37
  • Do not drop the context yourself, use Dependency Injection (e.g. https://simpleinjector.org/index.html). – milo Nov 27 '19 at 11:41
  • 1
    @mesies: (1) No one _has to_ use DI. DI frameworks are an elective automation layer to make your life easier. It's perfectly acceptable to not use a DI framework if you so choose, and manage your own dependencies (including disposables). (2) Your suggestion to use simple injection works for web requests (as injections happen on a per-request basis), but patently fails for Windows services and local applications (note that OP is using a WPF app!), where objects and their injected dependencies are kept alive for as long as the service/application runs, which is the opposite of what you'd want. – Flater Nov 27 '19 at 13:37

2 Answers2

4

Your expectation and your current execution are contradicting one another.

I'd like to use lazy loading in my application

The ObjectContext instance has been disposed and can no longer be used

The error message clearly reveals that you are still expecting to access data (via lazy loading) after you've closed your context, which is impossible.

There are two solutions here:

1. Ensure you don't close your context until you're done lazily loading data.

This is how you're suppose to do lazy loading, by design. Assuming you're using a using statement, ensure that you only fetch data while inside the using block.

However, you will notice that in large codebases, it becomes hard to ensure that the context is kept open long enough. Which I why I urge you to consider the other option:

2. Switch to eager loading.

Lazy loading is a very simple approach and works well enough in tiny applications (e.g. when I write a short-term-usage tool to help me). However, for larger infrastructures, lazy loading starts leading to mistakes that are easy to make but painfully hard to debug.

While eager loading doesn't avoid these runtime exceptions, the exceptions you will encounter (having forgotten an Include statement) will be much easier to debug, compared to having to figure out which data was being lazily loaded at what point.

I'm not going to tell you you can't use lazy loading, but you do need to be aware of the complexities that this leads to. For sufficiently large codebases, it becomes nigh unmaintainable to keep track of whether you closed the context too soon or not.

The cost of maintaining this (and debugging all the issues that you didn't prevent) is going to vastly outweigh the slightly more verbose code required for eager loading.

Community
  • 1
  • 1
Flater
  • 12,908
  • 4
  • 39
  • 62
  • Thanks for the answer! And what would you say for the following scenario: I create a context for getting the data from the database, which I would keep alive while the app is running and for every other operation I would create separate contexts? Somewhere I read that after the time it's going to be slow, but I run a test with more than 5K queries, and the performance is the same as it was at the beginning. – Attila Szász Nov 27 '19 at 13:18
  • @AttilaSzász: Discard contexts as soon as possible, do not keep them open. The issue is that **a context caches entities**. If your long-living context has fetched and cached an entity, a different context then changes it, the long-living context doesn't see this change and it will assume that its cached entity is still up to date. This can lead to regressions where the long-living context sets values back to what it _thinks_ is current (but is actually outdated) data. We've run into issues with this that took us weeks to discover because it's impossible to debug. – Flater Nov 27 '19 at 13:28
  • @AttilaSzász: Just to clarify my example: our codebase was written as a web service class library (so we implicitly expected contexts to be discarded after every web request). However, this library was then reused in a Windows service, which stays alive and thus never discards its db context. This lead to infrequent undocumented behavior that contradicted all logging, up to a point where the changetracker (during `SaveChanges`) showed me different values than the context actually wrote to the DB. It took over a month to find the cause of this bug because the origin is nigh unobservable. – Flater Nov 27 '19 at 13:31
  • _Ensure you don't close your context until you're done lazily loading data_ - you described eager loading here ;) – Fabio Nov 27 '19 at 17:25
  • @Fabio: It seems you misunderstand what lazy loading is. It is impossible to load data (neither lazily nor eagerly) with a closed context. – Flater Nov 27 '19 at 19:52
  • @Flater, eager loading is _when you keep dbContext open until you done with loading required data_ ;) – Fabio Nov 27 '19 at 20:37
  • @Fabio: Keeping the context open while still expecting to load data is **always required, regardless of lazy/eager loading**. – Flater Nov 28 '19 at 09:55
0

Alternative to lazy loading - use DbContext instance per query.
Load only data required for current action, especially in web application, where application works in small chunks "request-response".

DbContext is just an object, it's creation is chip.

Lazy loading will quickly introduce issues for example "N + 1" queries.

With instance per query, you will be able to fully benefit of asynchronous queries, where you can executes multiple queries almost simultaneously.

var orderTask = ordersRepository.Find(orderId);  // Takes 1 second
var invoicesTask = invoiceRepository.FindBy(orderId); // Takes 2 seconds
var deliveriesTask = deliveryRepository.FindBy(orderId); // Takes 3 seconds

await Task.WhenAll(orderTask, invoicesTask, deliveriesTask); 

var order = await orderTask;
var invoices = await invoicesTask;
var deliveries = await deliveriesTask;

Three queries will be finished in 3 seconds, instead of 6, if the would be executed one by one.
Single DbContext do not support simultaneous asynchronous calls.

Fabio
  • 31,528
  • 4
  • 33
  • 72
  • "Don't use lazy loading" is an absolute, and I don't think an absolute is justified here. I'm generally not a fan of lazy loading but that doesn't mean that it can't or shouldn't be used by anyone. – Flater Nov 27 '19 at 12:37
  • Also, while "one context per query" isn't an issue, this approach fails when you take data operations into account. If you create new contexts for every data operation, you effectively disregard transactional behavior where you would commit multiple operations all at once (or none, if any of them fail). Forcing your repositories to create new contexts for queries but reuse them for operations (and sharing them between repositories) is needlessly complex. – Flater Nov 27 '19 at 12:39
  • My app is a desktop wpf application, so it's not a web app and the performance is important. If I load a record with lazy loading it's 2 ms, but if I get the same record using the eager loading with all of the dependencies, it lasts 20-30 ms, at least 10 times longer. Can you explain a bit more please why I shouldn't use it? What do you mean N+1 query issues? If it's not recommended to use lazy loading, then why is it implemented at all? :) – Attila Szász Nov 27 '19 at 12:44
  • @AttilaSzász: Your initial query is faster when lazy loading, yes. But you're not accounting for all the extra queries that are fetched when you lazily load your data. For sizable data sets (i.e. many entities), the overhead cost of each individual load becomes a notable performance impact. – Flater Nov 27 '19 at 12:48
  • @Flater: yes, you're right, but in my case, it doesn't have to make all join operations after a lazy loaded query, just in couple of cases – Attila Szász Nov 27 '19 at 13:08
  • @Flater, `dbContext.SaveChanges()` wraps all changes with one transaction, if something fail other changes will be rolled back. You create repositories around domain actions/objects - not around tables in datatable. Order repository can have method Save which takes all required data as an argument and saves it with one transaction, including address, products and so on. – Fabio Nov 27 '19 at 17:14
  • 1
    @AttilaSzász, about N+1 queries: [https://stackoverflow.com/questions/97197/what-is-the-n1-selects-problem-in-orm-object-relational-mapping](https://stackoverflow.com/questions/97197/what-is-the-n1-selects-problem-in-orm-object-relational-mapping) – Fabio Nov 27 '19 at 17:16