question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Exploring Scoped vs Transactional (`IDbContextFactory`) `DbContexts`

See original GitHub issue

Ask a question

I am currently upgrading my framework to EfCore 6.0. In doing so, I have been taking the time to examine best practices to ensure that I am doing everything properly for my Blazor server-side application.

Currently, all my components and DbContext instances are scoped to the user. I am concerned about the memory utilization this may incur as more and more users adopt my application (🤞), but I have not been able to definitively ascertain this is an actual concern yet. I mention this as part of me is wrestling with the notion of avoiding premature optimization in my codebase, and solving a problem that does not actually exist yet.

OK, so with that tidbit aside, I started to do some performance analysis around scoped vs. transactional operations, which I share with you below. When I say “transactional,” I primarily mean the use of IDbContextFactory (and pooled ones at that, as we’ll see), but in my tests I use that to mean direct activation of a DbContext. Essentially, “transactional” means something that has to be created/disposed during an operation rather than pulled from (scoped) memory.

What’s beneficial and elegant about my current design is that all IQueryable<T> instances are defined once per DbContext and then scoped to the user, along with the DbContext that created them. In effect, this caches the query but also adds the memory overhead which is the concern I shared earlier.

Switching everything to be transactional/IDbContextFactory would be very time consuming in my application, particularly all the stored IQueryable<T> queries that I have defined that are subsequently scoped to the user.

However, upon further inspection, I could take a whack at everything that is non-queryable, that is, writable, or anything involving a DbContext.SaveChangesAsync.

So then the thought struck me, and that leads me to my question (which I will share in a bit, I promise!):

How about using a singleton DbContext for all queries (reads), and using IDbContextFactory for everything else (writes)?

To do this, there are two identified issues I would need to do:

  1. Mark all root DbSet<T> queries as AsNoTracking.
  2. Turn off thread checking to ward off the InvalidOperationException that occurs when same-thread access occurs.

There may be others, but I wanted to throw the thought out there here to see if there is anything else to consider.

The Question

So the question is: is it considered OK to use a singleton-scoped DbContext in a Blazor server-side application to handle all the queries (reads) of the application, while IDbContextFactory handles all the modifications/operations (writes)?

Follow up: Is there anything really stopping this from happening from a design perspective? That is, are there limits to the amount of queries that one DbContext can process, assuming the thread-checking is disabled?

Include your code

The other aspect here that is driving me towards this compromise in my application design, is that I did some benchmarking, and what I found was surprising with a very basic in-memory DbContext. You can find the code here:

https://github.com/Mike-E-angelo/Stash/tree/master/EfCore.ScopedVsTransaction

Simply returning an empty DbSet<T> as an array returned the following metrics:

Method Mean Error StdDev Ratio RatioSD Gen 0 Gen 1 Gen 2 Allocated
Scoped 2.489 us 0.0358 us 0.0334 us baseline 0.1945 - - 2 KB
Pooled 2.906 us 0.0244 us 0.0216 us 1.17x slower 0.02x 0.2022 - - 2 KB
Transactional 22.751 us 0.3772 us 0.5163 us 9.22x slower 0.27x 1.7700 0.0305 - 14 KB

Here, Scoped refers to caching the IQueryable<T> in memory (ala what happens when scoping to user), Pooled is using a PooledDbContextFactory, and Transactional is straight-up activating a new DbContext.

Looks like Scoped and Pooled are pretty much even here, and I probably would have continued to move toward a pooled IDbContextFactory model for all of my codebase until I appended a few expressions and saw the following:

Method Mean Error StdDev Ratio RatioSD Gen 0 Gen 1 Gen 2 Allocated
Scoped 18.67 us 0.174 us 0.163 us baseline 0.6714 - - 6 KB
Pooled 31.57 us 0.622 us 0.950 us 1.71x slower 0.07x 1.2207 - - 10 KB
Transactional 61.67 us 0.738 us 0.690 us 3.30x slower 0.05x 2.8076 - - 23 KB

It would seem that the more expressions added, the more and more Scoped wins. And my codebase contains a lot of very complex queries containing a lot of expressions. All of which work amazing, btw. 😁 All of which is due to your amazing work over there.

So, seeing this is what got me going down this path and considering/contemplating a singleton DbContext to handle the reads of my application, while IDbContextFactory handles the writes.

Also, while I am at this. Keep in mind that my DbContext above is completely empty, and the simplest of operations generate 2KB-23KB of allocations. To me, this seems a tad excessive and wanted to ensure this is a known issue and/or if I am doing something fundamentally wrong in my tests. I am using the In-Memory provider, which I would expect to be pretty lean in such a scenario, but pointing this out just in case.

To close, I would really like to express my sincere gratitude for all your efforts out there. EfCore is really great, and the team there has been really helpful in attending to my questions/issues. I am a huge fan of this project and all your efforts. What you have made here definitely deserves a unicorn as a mascot, indeed. 🦄

Thank you for any assistance/insight you can provide. 👍

Include stack traces

NA

Include verbose output

NA

Include provider and version information

EF Core version: 6.0.0-rc.1.21416.1 Database provider: Microsoft.EntityFrameworkCore.InMemory Target framework: (e.g. .NET 5.0) net6.0 rc1 Operating system: Windows 10 IDE: Visual Studio 2022 Preview 3.1

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Comments:15 (7 by maintainers)

github_iconTop GitHub Comments

1reaction
rojicommented, Aug 22, 2021

Sounds good, am happy I could help. Also always interested if you find odd perf tidbits that could be optimization opportunities.

1reaction
Mike-E-angelocommented, Aug 22, 2021

use EF Core’s compiled query feature - this compiles a (fully composed) query once, and gives you back a function which you can invoke multiple times with different DbContext instances.

EXCELLENT. That is indeed the missing piece here in my world. Allow me to look into this in addition to your benchmarks, and I will get back to you here when I have a better understanding.

Thank you for taking the time to provide the above valuable information and for the informative discussion, @roji. It is much appreciated. On a weekend no less. 😁

Read more comments on GitHub >

github_iconTop Results From Across the Web

What is the best to inject DbContext transient or scoped
If there are cases when you need several different instances of the context in the same scope - consider using DbContext factory approach:...
Read more >
IDbContextFactory vs Scoped Lifetime DbContext : r/csharp
My current understanding is that scoped dbcontext outweights IDbContextFactory in consistency-critical app, like applied in Unit of Work pattern ...
Read more >
DbContext Lifetime, Configuration, and Initialization
This example registers a DbContext subclass called ApplicationDbContext as a scoped service in the ASP.NET Core application service provider ( ...
Read more >
IDbContextFactory vs Scoped Lifetime DbContext : r/dotnet
My current understanding is that scoped dbcontext outweights IDbContextFactory in consistency-critical app, like applied in Unit of Work pattern ...
Read more >
Using Transactions - EF Core
You can use the DbContext.Database API to begin, commit, and rollback transactions. The following example shows two SaveChanges operations ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found