0
votes

I'm working with a standard ASP.NET Core 2.1 program, and I've been considering the problem that a great many of my controller methods require the current logged on user to be retrieved.

I noticed that the asp.net core Identity code uses a DBSet to hold the entities and that subsequent calls to it should be reading from the local entities in memory and not hitting the DB, but it appears that every time, my code requires a DB read (I know as I'm running SQL Profiler and see the select queries against AspNetUsers being run using Id as the key)

I know there's so many ways to set Identity up, and its changed over the versions that maybe I'm not doing something right, or is there a fundamental problem here that could be addressed.

I set up the default EF and Identity stores in startup.cs's ConfigureServices:

services.AddDbContext<MyDBContext>(options => options.UseSqlServer(Configuration.GetConnectionString("MyDBContext")));
services.AddIdentity<CustomIdentity, Models.Role>().AddDefaultTokenProviders().AddEntityFrameworkStores<MyDBContext>();

and read the user in each controller method:

var user = await _userManager.GetUserAsync(HttpContext.User);

in the Identity code, it seems that this method calls the UserStore FindByIdAsync method that calls FindAsync on the DBSet of users. the EF performance paper says:

It’s important to note that two different ObjectContext instances will have two different ObjectStateManager instances, meaning that they have separate object caches.

So what could be going wrong here, any suggestions why ASP.NET Core's EF calls within Userstore are not using the local DBSet of entities? Or am I thinking this wrongly - and each time a call is made to a controller, a new EF context is created?

2
ef dbcontext is scoped lifetime by default which means it is disposed automatically at the end of each request. You should not try to keep it around longer. Why do you need to get the user from the db so much? Things like user email, id, and name and roles can be claims which are serialized and persisted into the auth cookie so if you only need those things you don't need to hit the db for the user so often. You can just access HttpContext.User which is the claimsprincipal that is automatically deserialized from the auth cookie by the auth middleware. - Joe Audette
@JoeAudette I was afraid of that, why bother reading into an explicit DBset in Usermanager! I need various properties stored with the user, turning them all into claims doesn't smell right. - gbjbaanb
if you don't think the things you need to access are a good match for claims, then you could cache them in memory or session state to avoid hitting the db on every request. All things require tradeoffs. Without knowing specifically what data you are needing I can't make any recommendation about whether some or all of it would make sense as claims. - Joe Audette
@JoeAudette just custom data associated with a user, they do need to be cached, and I hoped putting them with the user would make it easier, given we have a usermanager object rather than the usual EF calls. I'll have to come up with a caching mechanism for these instead. - gbjbaanb

2 Answers

1
votes

any suggestions why ASP.NET Core's EF calls within Userstore are not using the local DBSet of entities?

Actually, FindAsync does do that. Quoting msdn (emphasis mine)...

Asynchronously finds an entity with the given primary key values. If an entity with the given primary key values exists in the context, then it is returned immediately without making a request to the store. Otherwise, a request is made to the store for an entity with the given primary key values and this entity, if found, is attached to the context and returned. If no entity is found in the context or the store, then null is returned.

So you can't avoid the initial read per request for the object. But subsequent reads in the same request won't query the store. That's the best you can do outside crazy levels of micro-optimization

0
votes

Yes. Controller's are instantiated and destroyed with each request, regardless of whether it's the same or a different user making the request. Further, the context is request-scoped, so it too is instantiated and destroyed with each request. If you query the same user multiple times during the same request, it will attempt to use the entity cache for subsequent queries, but you're likely not doing that.

That said, this is a text-book example of premature optimization. Querying a user from the database is an extremely quick query. It's just a simple select statement on a primary key - it doesn't get any more quick or simple as far as database queries go. You might be able to save a few milliseconds if you utilize memory caching, but that comes with a whole set of considerations, particularly being careful to segregate the cache by user, so that you don't accidentally bring in the wrong data for the wrong user. More to the point, memory cache is problematic for a host of reasons, so it's more typical to use distributed caching in production. Once you go there, caching doesn't really buy you anything for a simple query like this, because you're merely fetching it from the distributed cache store (which could even be a database like SQL Server) instead of your database. It only makes sense to cache complex and/or slow queries, as it's only then that retrieving it from cache actually ends up being quicker than just hitting the database again.

Long and short, don't be afraid to query the database. That's what it's there for. It's your source for data, so if you need the data, make the query. Once you have your site going, you can profile or otherwise monitor the performance, and if you notice slow or excessive queries, then you can start looking at ways to optimize. Don't worry about it until it's actually a problem.