System.ObjectDisposedException: Cannot access a disposed object, ASP.NET Core 3.1

I am writing an API using SignalR in ASP.NET Core 3.1. I am totally new to .NET Core, and am pretty new to SignalR too. I am having an issue executing MongoDB (Atlas) queries which are running in transactions. It appears that the transaction sessions are expiring before the queries are executing. I’m quite sure it’s a async/await issue, but don’t seem to be able to fix it.

My Hub method looks like this:

public async Task<bool> UpdateProfile(object profileDto)
{
  try
  {
    ProfileDTO profile = ((JsonElement) profileDto).ToObject<ProfileDTO>();
    _profile.UpdateProfile(profile);
    return true;
  }
  catch
  {
    return false;
  }
}

And the _profile.UpdateProfile() method looks like this:
public void UpdateProfile(ProfileDTO profileDto)
{
  _databaseService.ExecuteInTransaction(async session =>
  {
    var profile = _mapper.Map<ProfileModel>(profileDto);
    var existingUser = (await _userCollectionService
        .FindAsync(session, user => user.Profille.Sub == profileDto.sub)
      ).FirstOrDefault();

    if (existingUser == null)
    {
      var newUser = new UserModel();
      newUser.Profille = profile;
      await _userCollectionService.CreateAsync(session, newUser);
    }
    else
    {
      existingUser.Profille = profile;
      // the error occurs on the following line
      await _userCollectionService.UpdateAsync(session, existingUser.Id, existingUser);
    }
  });
}

My ExecuteInTransaction() method is an attempt to generalise the transaction/session process, and looks like this:
public async void ExecuteInTransaction(DatabaseAction databaseAction)
{
  using (var session = await Client.StartSessionAsync())
  {
    try
    {
      session.StartTransaction();
      databaseAction(session);
      await session.CommitTransactionAsync();
    }
    catch (Exception e)
    {
      await session.AbortTransactionAsync();
      throw e;
    }
  }
}

I have indicated the line in the UpdateProfile() where the error occurs. The full error looks like this:

System.ObjectDisposedException: Cannot access a disposed object.
Object name: ‘MongoDB.Driver.Core.Bindings.CoreSessionHandle’.
at MongoDB.Driver.Core.Bindings.WrappingCoreSession.ThrowIfDisposed()
at MongoDB.Driver.Core.Bindings.WrappingCoreSession.get_IsInTransaction()
at MongoDB.Driver.ClientSessionHandle.get_IsInTransaction()
at MongoDB.Driver.MongoCollectionImpl1.CreateBulkWriteOperation(IClientSessionHandle session, IEnumerable1 requests, BulkWriteOptions options)
at MongoDB.Driver.MongoCollectionImpl1.BulkWriteAsync(IClientSessionHandle session, IEnumerable1 requests, BulkWriteOptions options, CancellationToken cancellationToken)
at MongoDB.Driver.MongoCollectionBase1.ReplaceOneAsync(FilterDefinition1 filter, TDocument replacement, ReplaceOptions options, Func3 bulkWriteAsync) at IndistinctChatter.API.Services.Business.Profile.<>c__DisplayClass5_0.<<UpdateProfile>b__0>d.MoveNext() in /Volumes/Data/Users/marknorgate/Documents/Development/source/indistinct-chatter/api-dotnet/Services/Business/Profile.cs:line 48 --- End of stack trace from previous location where exception was thrown --- at System.Threading.Tasks.Task.<>c.<ThrowAsync>b__139_1(Object state) at System.Threading.QueueUserWorkItemCallback.<>c.<.cctor>b__6_0(QueueUserWorkItemCallback quwi) at System.Threading.ExecutionContext.RunForThreadPoolUnsafe[TState](ExecutionContext executionContext, Action1 callback, TState& state)
at System.Threading.QueueUserWorkItemCallback.Execute()
at System.Threading.ThreadPoolWorkQueue.Dispatch()
at System.Threading._ThreadPoolWaitCallback.PerformWaitCallback()

So it seems the session object is expiring before the await _userCollectionService.UpdateAsync(session, existingUser.Id, existingUser); line is executing. The line await session.CommitTransactionAsync(); is executing first.

Where am I going wrong? I feel a Task coming on…

Answers:

Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.

Method 1

Your DatabaseAction is not an asynchronous delegate. It seems to be Action<T> but what you really need is Func<T, Task>. So when you do async session => that code gets turned into an async void signature. Due to it being void, you cannot await it. This leads to your async code being scheduled to a threadpool thread and the call immediately returns. This has the knock on effect that await session.CommitTransactionAsync() commits while potentially, your delegate hasn’t even started running yet. Your code inside ExecuteInTransaction is now considered “done” and exits, disposing your session along the way due to using block.

To fix this, you need to change your DatabaseAction‘s signature and then await databaseAction(session);

Method 2

Ok, I have found a work around, although I am not entirely happy with the solution.

I discovered the AutoResetEvent(), so modified my code like this:

public async void ExecuteInTransaction(DatabaseAction databaseAction)
{
  AutoResetEvent autoResetEvent = new AutoResetEvent(false);

  using var session = await Client.StartSessionAsync();

  try
  {
    session.StartTransaction();
    databaseAction(session, autoResetEvent);
    autoResetEvent.WaitOne();
    await session.CommitTransactionAsync();
  }
  catch (Exception e)
  {
    await session.AbortTransactionAsync();
    throw e;
  }
}

and
public void UpdateProfile(ProfileDTO profileDto)
{
  _databaseService.ExecuteInTransaction(async (session, autoResetEvent) =>
  {
    var profile = _mapper.Map<ProfileModel>(profileDto);
    var existingUser = (await _userCollectionService
        .FindAsync(session, user => user.Profille.Sub == profileDto.sub)
      ).FirstOrDefault();

    if (existingUser == null)
    {
      var newUser = new UserModel();
      newUser.Profille = profile;
      await _userCollectionService.CreateAsync(session, newUser);
    }
    else
    {
      existingUser.Profille = profile;
      await _userCollectionService.UpdateAsync(session, existingUser.Id, existingUser);
    }

    autoResetEvent.Set();
  });
}

Seems to work, but it is an extra step at the end of each database operation that I need to remember. If anyone can improve on this I would be pleased to hear about it!


All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x