If you’re using Scoped Lifetime in a Dependency Injection containers, beware! You may be run into race conditions due to a lack of thread safety.
I was doing a live stream on Domain Events over on my YouTube Channel where I was taking advantage of Scoped Lifetime. After the stream, I realized Scoped Lifetime and thread safety isn’t mentioned much and I’m not sure why? It could be because developers aren’t using multiple threads within the top-level request (HTTP request or service bus message invocation). Or it’s because developers are writing thread-safe code by default. I tend to think it’s the former.
Either way, I figured it would be worthwhile providing an example of using Scoped Lifetime and how you can run into issues if you aren’t writing thread-safe code.
Check out my YouTube channel where I created a video that accompanies this blog post.
I’m going to first create ClassC which will have a method NotThreadSafe where it will take a string as a parameter, set that to a private member, then return the length of that private member.
The NotThreadSafe() method on ClassC is not thread-safe because setting the private member and returning its length are two different operations. If it’s called concurrently by two different threads, it could return the length of the last thread that set the _state member. Ultimately this is a race condition.
To demonstrate this I’m going to create a couple more classes that illustrate this race condition in a way this is more similar to what you’d actually have in a project with dependencies.
ClassB will take a dependency on ClassC, and I’ll create a top-level ClassA which will take a dependency on both ClassB and ClassC.
The thing to notice here is that ClassA uses a Task.WhenAll. I’m not immediately awaiting the result of the Task. Instead, I’m going to fire off both calls asynchronously and then wait for all of them to finish. What this means is that multiple threads could be used. This is ultimately what can cause the race condition.
Running this code as-is is so simplistic that you won’t likely hit the race condition. In order to make it more likely to hit, I’m going to add a delay.
Now to prove the race condition, here’s a little xUnit test.
This test fails because ClassB sets the _state member second, but finishes first. ClassA finishes last and returns _state.length which is now blank because ClassB has already finished.
Scoped Lifetime in Dependency Injection Containers
If you’re using Scoped Lifetime in a DI container, do you run into thread safety issues? Do you think about thread-safety or are you not concerned as you do not typically use multiple threads within a top-level request such as an ASP.NET Core HTTP request. Let me know in the comments or on Twitter.