I'm aware of two different scenarios in which exceptions can be produced when working with an Entity Framework DbContext
:
- Enumerating a query (could throw a
EntityCommandExecutionException
)
- Calling
SaveChanges
(could throw a DbUpdateException
)
Within a single instance of DbContext
, I'm wanting to catch these exceptions, try to recover if applicable, and then repeat the operation.
Specifically, if a call to SaveChanges
throws an exception because of a deadlock, I would like to retry the call to SaveChanges
. I already know how to detect this situation and perform the retry.
I saw this answer here, which indicates that an SQL connection shouldn't be used after a deadlock. This indicates that I should restart the entire DbContext
and higher-level operation to recover from such exceptions.
What I'm not sure about is whether it's safe to continue using the DbContext
after it has thrown an exception such as this. Will it enter an unusable state? Will it still work but not function correctly? Will SaveChanges
no longer occur transactionally?
If you don't supply the DbContext
with an already opened SQL connection, the DbContext
will open and close the connection for you when you call SaveChanges
. In that case there is no danger in keeping the DbContext
around, except of course that the entities the DbContext
holds on to might be in an invalid state (because this could be the reason that the SQL exception was thrown).
Here's an example of a DbContext
that is suppied by an opened SQL connection and transaction:
using (var connection = new SqlConnection("my connection"))
{
connection.Open();
using (var transaction = connection.BeginTransaction())
{
using (var context = new DbContext(connection))
{
// Do useful stuff.
context.SaveChanges();
}
transaction.Commit();
}
}
If you supply the DbContext
with a SqlConnection
that runs in the context of a transaction, this answer holds.
Note that Entity Framework will not create a nested transaction. It simply checks whether the connection is "enlisted in user transaction". If SaveChanges
already runs in a transaction, no transaction is started. Entity Framework however is unable to detect if the database has aborted the transaction because of a severe failure (such as a database deadlock). So if a first call to SaveChanges
fails with something like a deadlock and you catch and recall SaveChanges
, Entity Framework still thinks it is running inside a transaction.
This means that this second call is executed without a transaction and this means that when the operation fails halfway, the already executed statements will NOT be rolled back since there is no transaction to rollback.
The problem of the torn SaveChanges
operation could have been prevented if Entity Framework used nested transactions, but it still wouldn't solve the general problem of consistency.
Entity Framework creates connections and transactions for us when we do not supply them explicitly. We only need/want to supply a connection and transaction explicitly when the call to SaveChanges
is part of a bigger overall transaction. So even if EF created a nested transaction for us and committed this before returning from SaveChanges
, we're in trouble if we call SaveChanges
a second time, since this 'nested' transaction actually isn't nested at all. When EF commits this 'nested' transaction, it actually commits the only transaction there is, which means that the entire operation we needed to be atomic is torn; all changes done by SaveChanges
are committed, while the operations that might came after this call didn't run. Obviously this is not a good place to be.
So moral of the story is that either you let Entity Framework handle connections and transactions for you and you can redo calls to SaveChanges
without risk, or you handle transactions yourself and will have to fail fast when the database throws an exception; you shouldn't call SaveChanges
again.