Skip to content

Commit 76fc1d6

Browse files
committed
Do not process low-level command failed when the operation is SaveChanges. Fixes #96
1 parent 56827ce commit 76fc1d6

3 files changed

Lines changed: 29 additions & 10 deletions

File tree

EntityFramework.Exceptions/Common/ExceptionFactory.cs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,16 @@ internal static Exception Create<T>(ExceptionProcessorInterceptor<T>.DatabaseErr
1111
{
1212
return error switch
1313
{
14-
ExceptionProcessorInterceptor<T>.DatabaseError.CannotInsertNull => new CannotInsertNullException("Cannot insert null", exception.InnerException, entries),
15-
ExceptionProcessorInterceptor<T>.DatabaseError.MaxLength => new MaxLengthExceededException("Maximum length exceeded", exception.InnerException, entries),
16-
ExceptionProcessorInterceptor<T>.DatabaseError.NumericOverflow => new NumericOverflowException("Numeric overflow", exception.InnerException, entries),
17-
ExceptionProcessorInterceptor<T>.DatabaseError.ReferenceConstraint => new ReferenceConstraintException("Reference constraint violation", exception.InnerException, entries),
18-
ExceptionProcessorInterceptor<T>.DatabaseError.UniqueConstraint => new UniqueConstraintException("Unique constraint violation", exception.InnerException, entries),
19-
ExceptionProcessorInterceptor<T>.DatabaseError.Deadlock => new DeadlockException("Deadlock", exception.InnerException, entries),
14+
ExceptionProcessorInterceptor<T>.DatabaseError.CannotInsertNull => new CannotInsertNullException("Cannot insert null", exception, entries),
15+
ExceptionProcessorInterceptor<T>.DatabaseError.MaxLength => new MaxLengthExceededException("Maximum length exceeded", exception, entries),
16+
ExceptionProcessorInterceptor<T>.DatabaseError.NumericOverflow => new NumericOverflowException("Numeric overflow", exception, entries),
17+
ExceptionProcessorInterceptor<T>.DatabaseError.ReferenceConstraint => new ReferenceConstraintException("Reference constraint violation", exception, entries),
18+
ExceptionProcessorInterceptor<T>.DatabaseError.UniqueConstraint => new UniqueConstraintException("Unique constraint violation", exception, entries),
19+
// DeadlockException intentionally has no InnerException. EF Core's ExecutionStrategy uses
20+
// CallOnWrappedException to unwrap through DbUpdateException and check InnerException for
21+
// transient errors. Setting a transient provider exception as InnerException would cause
22+
// the execution strategy to wrap DeadlockException in InvalidOperationException.
23+
ExceptionProcessorInterceptor<T>.DatabaseError.Deadlock => new DeadlockException("Deadlock", null, entries),
2024
_ => null,
2125
};
2226
}

EntityFramework.Exceptions/Common/ExceptionProcessorInterceptor.cs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,24 @@ public void SaveChangesFailed(DbContextErrorEventData eventData)
4343
/// <inheritdoc />
4444
public void CommandFailed(DbCommand command, CommandErrorEventData eventData)
4545
{
46-
ProcessException(eventData.Exception, eventData.Context);
46+
// Skip SaveChanges commands — CommandFailed fires before SaveChangesFailed and only receives
47+
// the raw provider exception (no DbUpdateException, no Entries). Let SaveChangesFailed handle
48+
// these so the typed exception preserves the full exception chain and entity entries.
49+
if (eventData.CommandSource != CommandSource.SaveChanges)
50+
{
51+
ProcessException(eventData.Exception, eventData.Context);
52+
}
4753
}
4854

4955
/// <inheritdoc />
5056
public Task CommandFailedAsync(DbCommand command, CommandErrorEventData eventData, CancellationToken cancellationToken = new CancellationToken())
5157
{
52-
ProcessException(eventData.Exception, eventData.Context);
58+
// See comment in CommandFailed.
59+
if (eventData.CommandSource != CommandSource.SaveChanges)
60+
{
61+
ProcessException(eventData.Exception, eventData.Context);
62+
}
63+
5364
return Task.CompletedTask;
5465
}
5566

@@ -75,7 +86,7 @@ private void ProcessException(Exception eventException, DbContext eventContext)
7586
if (error == null) return;
7687

7788
var updateException = eventException as DbUpdateException;
78-
var exception = ExceptionFactory.Create(error.Value, eventException, updateException?.Entries);
89+
var exception = ExceptionFactory.Create(error.Value, providerException, updateException?.Entries);
7990

8091
switch (exception)
8192
{

EntityFramework.Exceptions/Tests/DatabaseTests.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Microsoft.EntityFrameworkCore;
55
using MySql.EntityFrameworkCore.Extensions;
66
using System;
7+
using System.Data.Common;
78
using System.Linq;
89
using System.Threading.Tasks;
910
using Xunit;
@@ -22,7 +23,7 @@ protected DatabaseTests(DemoContext demoContext, SameNameIndexesContext sameName
2223
DemoContext = demoContext;
2324
SameNameIndexesContext = sameNameIndexesContext;
2425

25-
isMySql = MySQLDatabaseFacadeExtensions.IsMySql(DemoContext.Database);
26+
isMySql = DemoContext.Database.IsMySql();
2627
isSqlite = demoContext.Database.IsSqlite();
2728
}
2829

@@ -35,6 +36,9 @@ public virtual async Task UniqueColumnViolationThrowsUniqueConstraintException()
3536
var uniqueConstraintException = Assert.Throws<UniqueConstraintException>(() => DemoContext.SaveChanges());
3637
await Assert.ThrowsAsync<UniqueConstraintException>(() => DemoContext.SaveChangesAsync());
3738

39+
Assert.IsAssignableFrom<DbException>(uniqueConstraintException.InnerException);
40+
Assert.NotEmpty(uniqueConstraintException.Entries);
41+
3842
if (!isSqlite)
3943
{
4044
Assert.False(string.IsNullOrEmpty(uniqueConstraintException.ConstraintName));

0 commit comments

Comments
 (0)