Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Asp.Net Core async lazy initialization per request

For many endpoints in our multi-tenant Asp.Net Core 2.2 web app, we need to get some tenant information from our database on a per-request basis (using an asynchronous database call), then reuse that information from various points in the request pipeline as required without having to make a database call each time. That is, we'd like something like a lazy async property on a per-request basis.

Based on the various docs I've read, instances of scoped services are only ever accessed by one thread at a time (the thread processing the request). So, I believe this means that I should be able to safely do something like the following:

// Registered as scoped in the DI container
public class TenantInfoRetriever : ITenantInfoRetriever
{
    private TenantInfo _tenantInfo;
    private bool _initialized;

    public async Task<TenantInfo> GetAsync()
    {
        // This code is obviously not thread safe, which is why it's important that no two threads are running here at the same time.
        if (!_initialized)
        {
            _tenantInfo = await GetTenantInfoAsync(); // this might actually legitimately return null, hence the need for the separate property "_initialized"
            _initialized = true;
        }
        return _tenantInfo;
    }

    private async Task<TenantInfo> GetTenantInfoAsync()
    {
        return await DoDatabaseCallToGetTenantInfo(); // of course this would use an injected EfContext instance (scoped service)
    }
}

To the best of my knowledge this should work because scoped services don't need to be thread-safe.

My question: are my assumptions here correct, or am I perhaps missing something important?

like image 364
sammy34 Avatar asked May 17 '26 01:05

sammy34


1 Answers

Based on the various docs I've read, instances of scoped services are only ever accessed by one thread at a time (the thread processing the request).

That's not exactly correct. When you await an operation, the thread is returned to the thread pool and when the async operation resumes, one free thread from the thread pool will be picked up. Its not necessary the exact same thread which started it.

But yes, there won't be two threads accessing it at the same time when its scoped - that is from ASP.NET Core side.

Of course, you need to take precautions of your own code too. If you spin up two tasks / threads and run them at the same time, then it still possible for it to be accessed at the same time.

If you worry it be accessed, do a double-check lock pattern (with volatile field) or locking on a mutex (object that isn't the value you modify) or use AsyncLazy<T>, i.e. from this blog post.

Examples (from https://help.semmle.com/wiki/display/CSHARP/Double-checked+lock+is+not+thread-safe)

Example 1:

string name;

public string Name
{
    get
    {
        lock (mutex)    // GOOD: Thread-safe
        {
            if (name == null)
                name = LoadNameFromDatabase();
            return name;
        }
    }
}

Example 2:

volatile string name;    // GOOD: Thread-safe

public string Name
{
    get
    {
        if (name == null)
        {
            lock (mutex)
            {
                if (name == null)
                    name = LoadNameFromDatabase();
            }
        }
        return name;
    }
}
like image 162
Tseng Avatar answered May 22 '26 00:05

Tseng



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!