Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions DbExceptionClassifier/Common/IDbExceptionClassifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ public interface IDbExceptionClassifier
public bool IsNumericOverflowError(DbException exception);
public bool IsUniqueConstraintError(DbException exception);
public bool IsMaxLengthExceededError(DbException exception);
public bool IsDeadlockError(DbException exception) => false;
}
}
8 changes: 7 additions & 1 deletion DbExceptionClassifier/MySQL/MySQLExceptionClassifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,10 @@ public bool IsMaxLengthExceededError(DbException exception)
var errorCode = GetErrorCode(exception);
return errorCode == MySqlErrorCode.DataTooLong;
}
}

public bool IsDeadlockError(DbException exception)
{
var errorCode = GetErrorCode(exception);
return errorCode is MySqlErrorCode.LockDeadlock or MySqlErrorCode.XARBDeadlock;
}
}
5 changes: 4 additions & 1 deletion DbExceptionClassifier/Oracle/OracleExceptionClassifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public class OracleExceptionClassifier : IDbExceptionClassifier
private const int CannotInsertNull = 1400;
private const int CannotUpdateToNull = 1407;
private const int UniqueConstraintViolation = 1;
private const int DeadLock = 60;
private const int IntegrityConstraintViolation = 2291;
private const int ChildRecordFound = 2292;
private const int NumericOverflow = 1438;
Expand All @@ -23,4 +24,6 @@ public class OracleExceptionClassifier : IDbExceptionClassifier
public bool IsUniqueConstraintError(DbException exception) => exception is OracleException { Number: UniqueConstraintViolation };

public bool IsMaxLengthExceededError(DbException exception) => exception is OracleException { Number: NumericOrValueError };
}

public bool IsDeadlockError(DbException exception) => exception is OracleException { Number: DeadLock };
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ public class PostgreSQLExceptionClassifier : IDbExceptionClassifier
public bool IsNumericOverflowError(DbException exception) => exception is PostgresException { SqlState: PostgresErrorCodes.NumericValueOutOfRange };
public bool IsUniqueConstraintError(DbException exception) => exception is PostgresException { SqlState: PostgresErrorCodes.UniqueViolation };
public bool IsMaxLengthExceededError(DbException exception) => exception is PostgresException { SqlState: PostgresErrorCodes.StringDataRightTruncation };
}
public bool IsDeadlockError(DbException exception) => exception is PostgresException { SqlState: PostgresErrorCodes.DeadlockDetected };
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public class SqlServerExceptionClassifier : IDbExceptionClassifier
{
private const int ReferenceConstraint = 547;
private const int CannotInsertNull = 515;
private const int Deadlock = 1205;
private const int CannotInsertDuplicateKeyUniqueIndex = 2601;
private const int CannotInsertDuplicateKeyUniqueConstraint = 2627;
private const int ArithmeticOverflow = 8115;
Expand All @@ -21,4 +22,5 @@ public class SqlServerExceptionClassifier : IDbExceptionClassifier
public bool IsNumericOverflowError(DbException exception) => exception is SqlException { Number: ArithmeticOverflow };
public bool IsUniqueConstraintError(DbException exception) => exception is SqlException { Number: CannotInsertDuplicateKeyUniqueConstraint or CannotInsertDuplicateKeyUniqueIndex };
public bool IsMaxLengthExceededError(DbException exception) => exception is SqlException { Number: StringOrBinaryDataWouldBeTruncated or StringOrBinaryDataWouldBeTruncated2019 };
}
public bool IsDeadlockError(DbException exception) => exception is SqlException { Number: Deadlock };
}
2 changes: 1 addition & 1 deletion DbExceptionClassifier/Sqlite/SqliteExceptionClassifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ public bool IsUniqueConstraintError(DbException exception) => exception is Sqlit
};

public bool IsMaxLengthExceededError(DbException exception) => exception is SqliteException { SqliteExtendedErrorCode: SQLITE_TOOBIG };
}
}
3 changes: 2 additions & 1 deletion EntityFramework.Exceptions/Common/ExceptionFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ internal static Exception Create<T>(ExceptionProcessorInterceptor<T>.DatabaseErr
ExceptionProcessorInterceptor<T>.DatabaseError.NumericOverflow => new NumericOverflowException("Numeric overflow", exception.InnerException, entries),
ExceptionProcessorInterceptor<T>.DatabaseError.ReferenceConstraint => new ReferenceConstraintException("Reference constraint violation", exception.InnerException, entries),
ExceptionProcessorInterceptor<T>.DatabaseError.UniqueConstraint => new UniqueConstraintException("Unique constraint violation", exception.InnerException, entries),
ExceptionProcessorInterceptor<T>.DatabaseError.DeadLock => new DeadlockException("Deadlock", exception.InnerException, entries),
_ => null,
};
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ protected internal enum DatabaseError
CannotInsertNull,
MaxLength,
NumericOverflow,
ReferenceConstraint
ReferenceConstraint,
DeadLock,
}

/// <inheritdoc />
Expand Down Expand Up @@ -59,6 +60,7 @@ public void CommandFailed(DbCommand command, CommandErrorEventData eventData)
if (exceptionClassifier.IsCannotInsertNullError(dbException)) return DatabaseError.CannotInsertNull;
if (exceptionClassifier.IsUniqueConstraintError(dbException)) return DatabaseError.UniqueConstraint;
if (exceptionClassifier.IsReferenceConstraintError(dbException)) return DatabaseError.ReferenceConstraint;
if (exceptionClassifier.IsDeadlockError(dbException)) return DatabaseError.DeadLock;

return null;
}
Expand Down Expand Up @@ -94,9 +96,9 @@ private void SetConstraintDetails(DbContext context, UniqueConstraintException e
{
var indexes = context.Model.GetEntityTypes().SelectMany(x => x.GetDeclaredIndexes().Where(index => index.IsUnique));

var mappedIndexes = indexes.SelectMany(index => index.GetMappedTableIndexes(),
var mappedIndexes = indexes.SelectMany(index => index.GetMappedTableIndexes(),
(index, tableIndex) => new IndexDetails(tableIndex.Name, tableIndex.Table.SchemaQualifiedName, index.Properties));

var primaryKeys = context.Model.GetEntityTypes().SelectMany(x =>
{
var primaryKey = x.FindPrimaryKey();
Expand Down Expand Up @@ -152,4 +154,4 @@ private void SetConstraintDetails(DbContext context, ReferenceConstraintExceptio
exception.SchemaQualifiedTableName = match.SchemaQualifiedTableName;
}
}
}
}
25 changes: 24 additions & 1 deletion EntityFramework.Exceptions/Common/Exceptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,27 @@ public ReferenceConstraintException(string message, Exception innerException, IR
public string ConstraintName { get; internal set; }
public IReadOnlyList<string> ConstraintProperties { get; internal set; }
public string SchemaQualifiedTableName { get; internal set; }
}
}

public class DeadlockException : DbUpdateException
{
public DeadlockException()
{
}

public DeadlockException(string message) : base(message)
{
}

public DeadlockException(string message, Exception innerException) : base(message, innerException)
{
}

public DeadlockException(string message, IReadOnlyList<EntityEntry> entries) : base(message, entries)
{
}

public DeadlockException(string message, Exception innerException, IReadOnlyList<EntityEntry> entries) : base(message, innerException, entries)
{
}
}
34 changes: 31 additions & 3 deletions EntityFramework.Exceptions/Tests/DatabaseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public virtual async Task UniqueColumnViolationSameNamesIndexesInDifferentSchema
{
Name = "Rope Access"
});

SameNameIndexesContext.IncidentCategories.Add(new EFExceptionSchema.Entities.Incidents.Category
{
Name = "Rope Access"
Expand Down Expand Up @@ -109,7 +109,7 @@ public virtual async Task PrimaryKeyViolationThrowsUniqueConstraintException()
Assert.False(string.IsNullOrEmpty(uniqueConstraintException.ConstraintName));
Assert.False(string.IsNullOrEmpty(uniqueConstraintException.SchemaQualifiedTableName));
Assert.NotEmpty(uniqueConstraintException.ConstraintProperties);
Assert.Contains<string>(nameof(Product.Id), uniqueConstraintException.ConstraintProperties);
Assert.Contains<string>(nameof(Product.Id), uniqueConstraintException.ConstraintProperties);
}
}

Expand Down Expand Up @@ -348,6 +348,34 @@ public async Task NotHandledViolationReThrowsOriginalException()
await Assert.ThrowsAsync<DbUpdateException>(() => DemoContext.SaveChangesAsync());
}

[Fact]
public virtual async Task Deadlock()
{
var p1 = DemoContext.Products.Add(new() { Name = "Test1" });
var p2 = DemoContext.Products.Add(new() { Name = "Test2" });

await DemoContext.SaveChangesAsync();

var id1 = p1.Entity.Id;
var id2 = p2.Entity.Id;

using var controlContext = new DemoContext(DemoContext.Options);
using var transaction1 = await DemoContext.Database.BeginTransactionAsync();
using var transaction2 = await controlContext.Database.BeginTransactionAsync();

await DemoContext.Products.Where(c => c.Id == id1)
.ExecuteUpdateAsync(c => c.SetProperty(p => p.Name, "Test11"));

await controlContext.Products.Where(c => c.Id == id2)
.ExecuteUpdateAsync(c => c.SetProperty(p => p.Name, "Test21"));

await Assert.ThrowsAsync<DeadlockException>(() => Task.WhenAll(Task.Run(() => DemoContext.Products
.Where(c => c.Id == id2)
.ExecuteUpdateAsync(c => c.SetProperty(p => p.Name, "Test22"))), controlContext.Products
.Where(c => c.Id == id1)
.ExecuteUpdateAsync(c => c.SetProperty(p => p.Name, "Test12"))));
}

public virtual void Dispose()
{
CleanupContext();
Expand All @@ -360,4 +388,4 @@ protected void CleanupContext()
entityEntry.State = EntityState.Detached;
}
}
}
}
7 changes: 3 additions & 4 deletions EntityFramework.Exceptions/Tests/DemoContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ namespace EntityFramework.Exceptions.Tests;

public class DemoContext : DbContext
{
public DemoContext(DbContextOptions options) : base(options)
{
}
public DemoContext(DbContextOptions options) : base(options) => Options = options;

public DbContextOptions Options { get; }
public DbSet<Customer> Customers { get; set; }
public DbSet<Product> Products { get; set; }
public DbSet<ProductSale> ProductSales { get; set; }
Expand Down Expand Up @@ -55,4 +54,4 @@ public class Customer
{
public int Id { get; set; }
public string Fullname { get; set; }
}
}
12 changes: 9 additions & 3 deletions EntityFramework.Exceptions/Tests/OracleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ public class OracleTests : DatabaseTests, IClassFixture<OracleTestContextFixture
public OracleTests(OracleTestContextFixture fixture) : base(fixture.DemoContext)
{
}

[Fact(Skip = "Skipping as oracle can't trigger deadlock.")]
public override Task Deadlock()
{
return Task.CompletedTask;
}
}

public class OracleTestContextFixture : DemoContextFixture<OracleContainer>
Expand All @@ -20,9 +26,9 @@ static OracleTestContextFixture()
Container = new OracleBuilder().Build();
}

protected override DbContextOptionsBuilder<DemoContext> BuildDemoContextOptions(DbContextOptionsBuilder<DemoContext> builder, string connectionString)
protected override DbContextOptionsBuilder<DemoContext> BuildDemoContextOptions(DbContextOptionsBuilder<DemoContext> builder, string connectionString)
=> builder.UseOracle(connectionString).UseExceptionProcessor();

protected override DbContextOptionsBuilder BuildSameNameIndexesContextOptions(DbContextOptionsBuilder builder, string connectionString)
protected override DbContextOptionsBuilder BuildSameNameIndexesContextOptions(DbContextOptionsBuilder builder, string connectionString)
=> builder.UseOracle(connectionString).UseExceptionProcessor();
}
}
10 changes: 8 additions & 2 deletions EntityFramework.Exceptions/Tests/SqliteTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,22 @@ public override Task NumericOverflowViolationThrowsNumericOverflowExceptionThrou
{
return Task.CompletedTask;
}

[Fact(Skip = "Skipping as SQLite no deadlock.")]
public override Task Deadlock()
{
return Task.CompletedTask;
}
}

public class SqliteDemoContextFixture : DemoContextFixture<IContainer>
{
private const string ConnectionString = "DataSource=file::memory:?cache=shared";

protected override DbContextOptionsBuilder<DemoContext> BuildDemoContextOptions(DbContextOptionsBuilder<DemoContext> builder, string connectionString)
protected override DbContextOptionsBuilder<DemoContext> BuildDemoContextOptions(DbContextOptionsBuilder<DemoContext> builder, string connectionString)
=> builder.UseSqlite(ConnectionString).UseExceptionProcessor();

protected override DbContextOptionsBuilder BuildSameNameIndexesContextOptions(DbContextOptionsBuilder builder, string connectionString)
protected override DbContextOptionsBuilder BuildSameNameIndexesContextOptions(DbContextOptionsBuilder builder, string connectionString)
=> builder.UseSqlite(ConnectionString).UseExceptionProcessor();
}
}
Loading