-1

I have the code below, that is causing me a DeadLock. In the code, i need to call a Database to fill a List inside a constructor. The database call returns an IAsyncEnumerable.

public class DatabaseRegistries
{
  public List<people> List {get;}

  public DatabaseRegistries()
  {
    List = DataBaseCallReturnIEnumerableAsync().ToListAsync().GetAwaiter().GetResult();
  }

}

Any Clue on how to do it without causing a Dead Lock ?

Thanks,

Glayson

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • 1
    `async` methods represent an IO boundary, and constructors should not be performing any IO. You need to redesign your software. One option is to use a static factory method (which would be an async method) which loads the list and then passes thast loaded list to the ctor. – Dai Sep 26 '22 at 20:24
  • Also... what is `DataBaseCallReturnIEnumerableAsync`? That looks like a `static` method to access a database, which is another anti-pattern... – Dai Sep 26 '22 at 20:30
  • Does this answer your question? [Can constructors be async?](https://stackoverflow.com/questions/8145479/can-constructors-be-async) – Mohammad Aghazadeh Sep 26 '22 at 20:38

2 Answers2

1
  • async methods represent an IO boundary, and constructors should not be performing any IO, so you need to rethink/redesign your class.
  • One option is to use a static factory method (which would be an async method) which loads the list and then passes thast loaded list to the ctor.

Like so:

public class DatabaseRegistries
{
    public static async Task<DatabaseRegistries> LoadAsync( SomeDataSource data )
    {
        if( data is null ) throw new ArgumentNullException(nameof(data));
        
        List<Person> list = await data.GetPeopleToListAsync();
        return new DatabaseRegistries( list );
    }

    private DatabaseRegistries( IReadOnlyList<Person> list )
    {
        this.List = list ?? throw new ArgumentNullException(nameof(list));
    }

    public IReadOnlyList<Person> List { get; }
}
Dai
  • 141,631
  • 28
  • 261
  • 374
0

[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;
        }
    }
}