Can I share a List<T> property across multiple queries via a tracking DbContext?

David Thielen 2,281 Reputation points
2024-04-01T11:06:30.0166667+00:00

Hi all;

Will the following work? And if so, will it provide a significant performance improvement.

My AppUser object includes:

public class AppUser 
{
    public int Id { get; private set; }
    // lots of other properties
    public List<Tag>? Tags { get; set; }
}

The total number of tags is presently 22 and is unlikely to grow beyond 200. So I read all of them in and cache them for any call where I need the tag(s).

Would it work to have a singleton service that creates a DbContext on first use and keeps that DbContext for the life of my application. And this is a DbContext with tracking on. And on startup it reads in all the tags. As follows:

async Task Startup() {
    dbContext = await TrackingDbFactory.CreateDbContextAsync();
    tags = await dbContext.Tags.ToListAsync();
}

Then when I need to read in an AppUser, I do:

async Task<AppUser?> GetUserAsync(int id){
    return await dbConect.Include(u => u.Tags).FirstOrDefaultAsync(u => u.Id == id);
}

In the above case, will it re-read the AppUser.Tasks from the database? Or will the DbContext use the tags list it read in earlier and re-use those already read in objects?

I do know it will need to read the AppUserTags join table. But not also reading the Tags table again would be a performance improvement. And I have 3 other list properties I would do this for, so the total performance savings would be decent.

This seems to work, but I don’t know Entity Framework well enough to test this thoroughly. So, will this work consistently, not re-reading the Tags table?

And are there any problems with doing this? Keeping a DbContext object for the life of the app server, having the tracking for those saved of Tag objects for the life of the app server, etc.?

thanks - dave

Entity Framework Core
Entity Framework Core
A lightweight, extensible, open-source, and cross-platform version of the Entity Framework data access technology.
696 questions
0 comments No comments
{count} votes

Accepted answer
  1. Michael Taylor 48,486 Reputation points
    2024-04-01T15:00:38.1266667+00:00

    That isn't going to work. Objects loaded from a DbContext are tied to that context. If you create a new DbContext and then Include some data then it will look to see if the current context has loaded the data (which it only does once) and then load it if necessary. This is by design and you cannot prevent that. Of course the alternative is lazy loading but that isn't recommended. The reason it works this way is because it doesn't know what you may or may not want to edit so everything is loaded as tracking by default and therefore there is a proxy tied to it. If you make any changes that need to be applied back to the DB then it has to do so on the same context it was created on.

    It is also not recommended that you ever create a DbContext and leave it open for the life of your app. That is just wasting a DB connection. Open your context (or have it passed in as part of DI), do your work and dispose of it. Leaving a context open actually makes things worse because by default if you make changes to the objects obtained by the context then every time you call SaveChanges it'll try to push them out. If they fail then they are still modified and will be attempted the next time. This causes you to see saves fail even though the data you thought you were changing were valid. Cleaning up a context that has "broken" modifications is a pain, or at least used to be.

    Firstly I'd have to ask the question how often do tags change. If they almost never change then lazy load them the first time they are needed. This is what a wrapper service or equivalent is for. Using a context directly in your code, while common in sample code, is prone to issues in a more professional codebase. Instead have your app call a service/repo/whatever that uses the context behind the scenes. Add caching here. When the app asks for a tag (or tags) the first time then load them from your context as no tracking. Then cache the results into a memory cache, static field, whatever works for your app. On subsequent calls to get the tags then it can pull from cache.

    For the remaining code you have to decide how performant loading the tags really are. In general I recommend that you go ahead and use Include to load the related tags for the objects you're loading. Honestly it is a join and the DB is going to return that data quickly. You aren't going to notice a perf hit. But if your app needs to display a list of tags then it calls your service which just loads the data once. Since the app isn't going to be modifying the list of tags then it doesn't need to worry about mapping the data back to a context.

    Updating an object is a special case. If you include the tags on load then you can add and remove tags and save the changes and everything works. Again, perf probably isn't an issue. However if you do need to avoid loading the tags then don't include them when loading the object to be edited from the context. Use your service from earlier to grab the tags that you need and set the tag IDs on your object (or however you're storing the data) to populate them. Note that you cannot attach the shared tags to your parent object because they are shared. However in most cases you're using a relationship table (for many to many) and only need the "ID" of the tag and since that is a primitive you can set it without issue.


1 additional answer

Sort by: Most helpful
  1. Bruce (SqlWork.com) 56,531 Reputation points
    2024-04-01T15:57:44.93+00:00

    EF only knows of entries stored in the dbcontext. it will know nothing entities stored in List<>. It also will not use cached entities for a query, only for insert and update.

    also as you want to use Tags as a join table it more efficient to have the sql engine do the join rather than fetch the two tables locally and join in the client.

    0 comments No comments