Repository navigation
IDbCommandInterceptor.CommandFailed does not capture all command exceptions #35229
Description
Context
When using EF Core together with Sql Server, we catch exceptions from the database (mostly SqlExceptions) and convert them to domain-specific custom exceptions. Originally we were doing this as a try/catch inside an override of SaveChangesAsync in our DbContext.
This approach worked fine for most cases, however when we execute a stored procedure via a call like DbContext.Database.ExecuteSqlInterpolatedAsync, it does not use SaveChangesAsync, and so exceptions were not being converted in those cases.
In an attempt to create one encapsulated thing that handles the exception handling, I implemented an IDbCommandInterceptor using the CommandFailed and CommandFailedAsync methods.
This appeared to work as we wanted and handled regular calls (made via SaveChangesAsync) and stored procedure calls (and other ad-hoc calls) made via DbContext.Database.
However, we've come across an edge case where the IDbCommandInterceptor approach is not working.
- When changes to multiple entities have been made and then
SaveChangesAsyncis called, EF can decide to make some optimisations. This includes executing a batch of statements in a single database call (which makes sense to do). - When it does this, it appears that if the first statement of the batch causes an exception, then everything works as expected - the call fails and the exception is passed to the
CommandFailedAsyncof the interceptor. - However, if the first statement succeeds and returns a result set (such as a new timestamp version column value), but then one of the subsequent statements throws an exception, then the
CommandFailedAsyncof the interceptor is not called.- The end result is still an exception coming from
DbContext.SaveChangesAsync, it just isn't captured intoCommandFailedAsync.
- The end result is still an exception coming from
After debugging into the code, it appears that this is because:
- the batch statement is being executing as a Db Reader (which makes sense)
- the result
DbDataReaderis only fully processed (via callingDbDataReader.NextResultAsync) after the handling for the interceptor is already passed. (NextResultAsyncis called inAffectedCountModificationCommandBatch.csin theConsumeAsyncmethod)
(Note: I found this blog post helpful in getting to this understanding: https://www.dbdelta.com/the-curious-case-of-undetected-sql-exceptions/)
My Question
After all the context, my question is:
Is this expected behaviour?
After looking at what the code is doing, it makes sense that in its current form that it behaves this way, but when I was reading about interceptors I had the impression that all command exceptions would be captured into the CommandFailed methods.
If it is intended to work this way, perhaps some additional documentation in the Interceptors page explaining that not all exceptions will be captured would help?
But ideally of course, it would be great if interceptors could be updated/enhanced so that an exception that appears as part of processing the DbDataReader would also be caught.
Issue Reproduction code
As this involves both C# code and Sql Server code and to convince EF that it needs to use its batch command optimisation, this would be quite difficult to create a reproduction for. However, hopefully the explanation above makes sense and is straight-forward to understand.
Existing Issues Raised
None found on this topic.
Work-around
In order to get the behaviour we're after, we have changed our code to have handlers in both the interceptor and within our override of SaveChangesAsync. This was not a difficult change to make (if a little less neat) but we've only just found out, with a bunch of debugging, that we needed this change and why.
Alternative Work-around not used
An alternative that was considered was to process the DbDataReader within the ReaderExecutedAsync similar to what is described within this issue.
However this involves reading the entire results of every query into memory, which could defeat the purpose of using a reader for large results sets, and it has more risk of inadvertently affecting some other part of how EF works. Another minor point is that DataTable.Load does not support async calls, so more custom code would be required to implement that.
Version information
EF Core version: <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.11" />
Target framework: net8.0
Activity
ajcvickers commentedon Nov 29, 2024
Note for team: this is an interesting case. So far, we only do
CommandFailedwhen the error happens on the call to actually execute the command. If this succeeds, but then there is a failure reading the result set, then we handle this differently. This usually makes sense for queries, but I agree with the poster that this is both confusion and a potential pit of failure for usingSaveChangeswith theCommandFailedinterceptor.AdCodicem commentedon Oct 16, 2026
The query side has the same shape, and can be shown without SQL Server: an error the database raises after the first row (so from
DbDataReader.Read, not fromExecuteReader) reaches noIDbCommandInterceptormethod. The interceptor seesReaderExecuted,DataReaderClosingandDataReaderDisposing, as for a successful query; only the logger getsQueryIterationFailed.Repro (SQLite, in memory): https://github.com/AdCodicem/dotnet-upstream-repros/tree/main/efcore-interceptor-reader-error. SQLite raises
integer overflowforabs(-9223372036854775808)on the row it reaches, so the same two rows ordered one way or the other move the error fromExecuteReadertoRead:Same output on 10.0.13 and 11.0.0-rc.1.26425.128. On the query side,
RelationalCommand.ExecuteReaderreportsCommandErrorfrom itscatcharoundDbCommand.ExecuteReader(efcore/src/EFCore.Relational/Storage/RelationalCommand.cs
Lines 576 to 599 in 48293cd
Readis caught by the enumerator and only logged asQueryIterationFailed(efcore/src/EFCore.Relational/Query/Internal/SingleQueryingEnumerable.cs
Lines 244 to 252 in 48293cd
So whether
CommandFailedfires for the same server error depends on where the provider surfaces it, which an interceptor written to translate or count database errors cannot tell from the outside. If raisingCommandFailedthere is not wanted, could theCommandFaileddocs (and the "Command failures" row of https://learn.microsoft.com/ef/core/logging-events-diagnostics/interceptors) say that it covers only errors raised while executing the command, not those raised while EF Core reads the results, and point toISaveChangesInterceptor.SaveChangesFailedandCoreEventId.QueryIterationFailedfor the rest?Investigation and write-up assisted by Claude (Anthropic); the linked repro was run on Windows 11 Pro (10.0.26200) with .NET SDK 10.0.402 and Microsoft.EntityFrameworkCore.Sqlite 10.0.13, and with .NET SDK 11.0.100-rc.1.26425.128 and Microsoft.EntityFrameworkCore.Sqlite 11.0.0-rc.1.26425.128, and I reviewed this comment.
Report command failures while reading results
Shivansh-Gaur2 commentedon Oct 25, 2026
@ajcvickers, PR #39405 proposes reporting provider
Read/ReadAsyncandNextResult/NextResultAsyncfailures through the existingCommandFailedcallbacks. Cancellations useCommandCanceled; EF row materialization exceptions remain on the query-iteration path, andQueryIterationFailedis preserved.The PR covers streaming and buffered consumption, the
SaveChangesresult-advancement case, cancellation, and the materialization boundary, including the SQLiteabs(long.MinValue)reproduction. Logging, event-specific diagnostic listeners, and interceptors are tested independently. The focused relational suites pass 157 tests, and the SQLite command-interception suite passes 300 tests.Could you confirm whether this is the direction the team wants for the issue?
Report command failures while reading results (dotnet#39405)