1

I have a problem with the concept of scope in dependency injection. I have registered my db context as a scope and And I save the user activity in a table using an asynchronous method without using "await".

    // In Startup:

    services.AddScoped<IDbContext, StorageSystemDbContext>();

    services.AddScoped<IUserActivityService,UserActivityService>();
    

    // In UserActivityService:

    public async void LogUserActivityAsync(string controllerName, string actionName, ActionType actionType = ActionType.View, string data = "", string description = "")
    {
        await InsertAsync(new UserActivity
        {
            ControllerName = controllerName,
            ActionName = actionName,
            ActionType = actionType,
            CreatedDateTime = DateTime.Now,
            Description = description,
            UserId = (await _workContext.CurrentUserAsync())?.Id
        });
    }

    //In Controller:

    _userActivityService.LogUserActivityAsync(CurrentControllerName, CurrentActionName,data);

I get the following error when I call same action twice immediately:

InvalidOperationException: A second operation was started on this context before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.

I expected a new db context to be created with the second request, depending on the type of db context dependency registration, but according to this error, a new context was not created for the second request and used the previous one.

What is the reason for this?

I'm using Asp Net.Core MVC and EF in .Net Core 5

  • Are you using Blazor? Otherwise as a Scoped dependency, you will get a new DbContext for each Http Request. – David Browne - Microsoft Jan 06 '22 at 19:51
  • I'm using Asp Net.core mvc and EF in .net core 5 – hossein sedaghat Jan 06 '22 at 19:59
  • You used async void. That's a huge red flag. There are very few situations where it makes sense to do that. This is not one of them. I suggest you [watch Nick Chapsas video](https://www.youtube.com/watch?v=lQu-eBIIh-w) which explains why that is so bad, and/or read [Stephen Cleary's article](https://learn.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming). – mason Jan 06 '22 at 20:01
  • I do not want the program thread to wait for this method to complete. will changing the void to Task solve the problem? – hossein sedaghat Jan 06 '22 at 20:06
  • The only real justification to leave an `async` call un-awaited is that you intend to resume after something like a WaitAll(). It is not intended to be used as a "fire and forget" background thread. Seriously, do some reading of the recommended materials provided. There are very valid reasons ***not*** to approach it this way. – Steve Py Jan 06 '22 at 21:37
  • The right way to push work out of the HTTP request is to use a Hosted Service to process the background tasks. https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-6.0&tabs=visual-studio You can add data to a ConcurrentQueue, or a BlockingCollection and process it in the background – David Browne - Microsoft Jan 06 '22 at 23:53

2 Answers2

2

An injected DbContext into a service regardless of scoping will be one single reference when constructor injected. Calling multiple methods in that service will always use the same instance. AddedScoped with ASP.Net will scope the services (and DbContext) to the web request. This is the recommended scoping for a DbContext to ensure any entities loaded during a request can ensure that they are all tracked by the same DbContext instance and that DbContext should be alive for the life of that request. (i.e. to provided lazy loading support if needed) A Transient scoped dependency would mean the DbContext passed to 2 different services would be distinct references. This leads to problems where Service A calls another service to retrieve entities that it wants to associate with an entity it loaded and is trying to update. These entities are associated to a different DbContext resulting in errors or issues like duplicate data being created.

Even with a transient scope DbContext you would still have the exact same problem trying to run two calls from the same service in parallel, and there are many good reasons referenced in the comments not to use un-awaited async calls to do so. Even if your intention is to await multiple calls together, the only way to enable something like would be to internally scope the DbContext within the method call itself. This would typically involve injecting a DbContextFactory type class rather than a DbContext into the service, where the DbContextFactory is a dependency that can initialize and provide a new DbContext; Then:

using (var context = _contextFactory.Create())
{
     // operations with DbContext. (context)
}

Even then you need to consider the DB synchronization guards like row and table locks / deadlocks which could rear their heads if you have a significant number of operations happening in parallel. Keep in mind with web applications the web server can be responding to a significant number of requests in parallel, each of which could be kicking off these processes at any time. (Works fine during development with 1 client, crawls/dies out in the real world.)

Steve Py
  • 26,149
  • 3
  • 25
  • 43
0

I found the answer here: https://stackoverflow.com/a/44121808/4604557

If for some reason you want to run parallel database operations (and think you can avoid deadlocks, concurrency conflicts etc.), make sure each one has its own DbContext instance. Note however, that parallelization is mainly useful for CPU-bound processes, not IO-bound processes like database interaction. Maybe you can benefit from parallel independent read operations but I would certainly never execute parallel write processes. Apart from deadlocks etc. it also makes it much harder to run all operations in one transaction.