[UPDATED] - To return List instead of IAsyncEnumerable
As mentioned by other in the comments, your code needs to be refactored to move the call out of the constructor and fix the deadlock. The following are hypothetical example implementations based upon your code database call that returns IAsyncEnumerable. The code should be refactored to an async methd as suggested by the first example provided below - however, I've included second example following the constructor "anti-pattern" in the sample code from your question that should also work.
The "Cached" example method utilizes SpinWait
as opposed to using a traditional lock
, which substantially improves performance. You may want to consider making the following variables static
if your use case requires state to be maintained across multiple instantiations (i.e. new
-ing multiple instances) of the class. For example, if you're using the class with dependency injection as a Scoped
or Transient
dependency as opposed to Singleton
, you would need to add the static keyword as follows to maintain state:
private static List<people> _list1 = new();
private static volatile int _interLock1 = 0;
Please note that the following examples have not been compiled or tested. Should you run into any issues that require assistance - let me know.
Example Using Async Methods Instead of Property/Ctor Anti-Pattern
Requires reference to Nuget package System.Linq.Async.
public class DatabaseRegistries
{
private MyDbContext _context; //ctor injected database context
public DatabaseRegistries(MyDbContext context) =>_context = context;
// NO CACHE EXAMPLE
public async Task<List<people>> GetPeople(CancellationToken token = default)
{
// Example returning List from IAsyncEnumerable w/o caching
return await context.peopleAsyncEnumerable().ToListAsync(token);
}
// CACHE EXAMPLE
private List<people> _list1 = new();
private volatile int _interLock1 = 0; //need separate int variable for each cache list
public async Task<List<people>> GetPeople_Cached(CancellationToken token = default)
{
if (_list1.Count == 0)
{
// acquire exclusive lock
for (SpinWait sw = new SpinWait(); Interlocked.CompareExchange(ref _interLock1, 1, 0) == 1; sw.SpinOnce()) ;
// populate cache if empty
if (_list1.Count == 0) _list1.AddRange(await _context.peopleAsyncEnumerable().ToListAsync(token));
// release exclusive clock
_interLock1 = 0;
}
return _list1;
}
}
Alternative Ctor Anti-Pattern w/o Deadlocking
While an anti-pattern, the implementation GetPeople_Cached()
could be used in the constructor to load the List without causing a deadlock. I have no means to test, but I'm fairly confident it will not result in deadlocking.
public class DatabaseRegistries
{
private MyDbContext _context; //ctor injected database context
public DatabaseRegistries(MyDbContext context)
{
_context = context;
GetPeople_Cached().ConfigureAwait(false).GetAwaiter().GetResult();
}
// List
public IReadOnlyList<people> List => _list;
// change to non-static for most up-to-date results per instatiation
private static List<people> _list = new();
// CACHE EXAMPLE
private static volatile int _interLock1 = 0; //need separate int variable for each cache list
private async Task GetPeople_Cached(CancellationToken token = default)
{
if (_list.Count == 0)
{
// acquire exclusive lock
for (SpinWait sw = new SpinWait(); Interlocked.CompareExchange(ref _interLock1, 1, 0) == 1; sw.SpinOnce()) ;
// populate cache if empty
if (_list.Count == 0) _list.AddRange(await _context.peopleAsyncEnumerable().ToListAsync(token));
// release exclusive clock
_interLock1 = 0;
}
}
}