How to use the ExecutionStrategy properly?

We are using the ExecutionStrategy and have this helper method in our db context:

public Task<T> ExecuteWithinTransactionAsync<T>(Func<IDbContextTransaction, Task<T>> operation, string operationInfo)
{
    int counter = 0;
    return Database.CreateExecutionStrategy().ExecuteAsync(RunOperationWithinTransaction);

    async Task<T> RunOperationWithinTransaction()
    {
        counter++;
        if (counter > 1)
        {
            Logger.Log(LogLevel.Warn, $"Executing ({counter}. time) transaction for {operationInfo}.");

            ClearChangeTracker();
        }

        using (var transaction = await Database.BeginTransactionAsync(IsolationLevel.Serializable))
        {
            return await operation.Invoke(transaction);
        }
    }
}

We than use ExecuteWithinTransactionAsync when calling complex/fragile business logic which should be executed in a serializable transaction reliably. We are using Postgres so it can happen that our transaction will be aborted due to serialization issues. The execution strategy detects it and retries the operation. That works nicely. But EF still keeps the old cache from the previous execution. That's why we introduced ClearChangeTracker which looks like this:

private void ClearChangeTracker()
{
    ChangeTracker.DetectChanges();
    foreach (var entity in ChangeTracker.Entries().ToList())
    {
        entity.State = EntityState.Detached;
    }
}

And this seemed to have worked properly, until we found a case where it didn't work anymore. When we add new entities to a navigation property list, these entities won't be removed on the next try. For instance

var parent = context.Parents.FirstOrDefault(p => p.Id == 1);
if (parent.Children.Any()) 
{
    throw new Exception("Parent already has a child"); // This exception is thrown on the second try
}
parent.Children.Add(new Child());
context.SaveChangesAsync();

So if the last line context.SaveChangesAsync() fails, and the whole operation is re-run, parent.Children already contains the new child added in parent.Children.Add(new Child()); and I didn't find any way to remove that item from EF.

However, if we remove the check (if (parent.Children.Any())), if the item already exists or not, and just try adding it a second time, it's only stored once in the DB.

I was trying to figure out how to clear the DbContext properly, but most of the time, the answer was just to create a new DbContext. However, that's not an option, since the DbContext is needed for the ExecutionStrategy. That's why I wanted to know, what's the suggested way to used the ExecutionStrategy and having a clean DbContext on every retry.

Further technical details

  • EF Core version: 1.1.2
  • Database Provider: Npgsql.EntityFrameworkCore.PostgreSQL (1.1.1)
  • Operating system: Windows 10, Dockerized in Linux

1 answer

  • answered 2017-10-11 11:04 Gert Arnold

    In ef-core 2.0.0, this new feature DbContext pooling was introduced. For it to work properly, DbContext instances are now able to reset their internal state, so they can be handed out as "new". The reset method can be called like so (inside your DbContext):

    ((IDbContextPoolable)this.ResetState();
    

    So if you can upgrade to ef-core 2.0.0, go for it. Not only to benefit from this new feature, it's more mature in many ways.

    Disclaimer: this method is intended for internal use, so the API may change in the future.