diff --git a/DbExceptionClassifier/Common/IDbExceptionClassifier.cs b/DbExceptionClassifier/Common/IDbExceptionClassifier.cs index ea15a31..86c30cd 100644 --- a/DbExceptionClassifier/Common/IDbExceptionClassifier.cs +++ b/DbExceptionClassifier/Common/IDbExceptionClassifier.cs @@ -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; } } diff --git a/DbExceptionClassifier/MySQL/MySQLExceptionClassifier.cs b/DbExceptionClassifier/MySQL/MySQLExceptionClassifier.cs index 4372d39..03714eb 100644 --- a/DbExceptionClassifier/MySQL/MySQLExceptionClassifier.cs +++ b/DbExceptionClassifier/MySQL/MySQLExceptionClassifier.cs @@ -59,4 +59,10 @@ public bool IsMaxLengthExceededError(DbException exception) var errorCode = GetErrorCode(exception); return errorCode == MySqlErrorCode.DataTooLong; } -} \ No newline at end of file + + public bool IsDeadlockError(DbException exception) + { + var errorCode = GetErrorCode(exception); + return errorCode is MySqlErrorCode.LockDeadlock or MySqlErrorCode.XARBDeadlock; + } +} diff --git a/DbExceptionClassifier/Oracle/OracleExceptionClassifier.cs b/DbExceptionClassifier/Oracle/OracleExceptionClassifier.cs index f1974f9..f1449bb 100644 --- a/DbExceptionClassifier/Oracle/OracleExceptionClassifier.cs +++ b/DbExceptionClassifier/Oracle/OracleExceptionClassifier.cs @@ -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; @@ -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 }; -} \ No newline at end of file + + public bool IsDeadlockError(DbException exception) => exception is OracleException { Number: DeadLock }; +} diff --git a/DbExceptionClassifier/PostgreSQL/PostgreSQLExceptionClassifier.cs b/DbExceptionClassifier/PostgreSQL/PostgreSQLExceptionClassifier.cs index 12cad2e..7fd52cc 100644 --- a/DbExceptionClassifier/PostgreSQL/PostgreSQLExceptionClassifier.cs +++ b/DbExceptionClassifier/PostgreSQL/PostgreSQLExceptionClassifier.cs @@ -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 }; -} \ No newline at end of file + public bool IsDeadlockError(DbException exception) => exception is PostgresException { SqlState: PostgresErrorCodes.DeadlockDetected }; +} diff --git a/DbExceptionClassifier/SqlServer/SqlServerExceptionClassifier.cs b/DbExceptionClassifier/SqlServer/SqlServerExceptionClassifier.cs index ad859a4..d0d8c40 100644 --- a/DbExceptionClassifier/SqlServer/SqlServerExceptionClassifier.cs +++ b/DbExceptionClassifier/SqlServer/SqlServerExceptionClassifier.cs @@ -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; @@ -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 }; -} \ No newline at end of file + public bool IsDeadlockError(DbException exception) => exception is SqlException { Number: Deadlock }; +} diff --git a/DbExceptionClassifier/Sqlite/SqliteExceptionClassifier.cs b/DbExceptionClassifier/Sqlite/SqliteExceptionClassifier.cs index 0a3a6fd..cafd539 100644 --- a/DbExceptionClassifier/Sqlite/SqliteExceptionClassifier.cs +++ b/DbExceptionClassifier/Sqlite/SqliteExceptionClassifier.cs @@ -19,4 +19,4 @@ public bool IsUniqueConstraintError(DbException exception) => exception is Sqlit }; public bool IsMaxLengthExceededError(DbException exception) => exception is SqliteException { SqliteExtendedErrorCode: SQLITE_TOOBIG }; -} \ No newline at end of file +} diff --git a/EntityFramework.Exceptions/Common/ExceptionFactory.cs b/EntityFramework.Exceptions/Common/ExceptionFactory.cs index 7900ea7..14979d4 100644 --- a/EntityFramework.Exceptions/Common/ExceptionFactory.cs +++ b/EntityFramework.Exceptions/Common/ExceptionFactory.cs @@ -16,7 +16,8 @@ internal static Exception Create(ExceptionProcessorInterceptor.DatabaseErr ExceptionProcessorInterceptor.DatabaseError.NumericOverflow => new NumericOverflowException("Numeric overflow", exception.InnerException, entries), ExceptionProcessorInterceptor.DatabaseError.ReferenceConstraint => new ReferenceConstraintException("Reference constraint violation", exception.InnerException, entries), ExceptionProcessorInterceptor.DatabaseError.UniqueConstraint => new UniqueConstraintException("Unique constraint violation", exception.InnerException, entries), + ExceptionProcessorInterceptor.DatabaseError.DeadLock => new DeadlockException("Deadlock", exception.InnerException, entries), _ => null, }; } -} \ No newline at end of file +} diff --git a/EntityFramework.Exceptions/Common/ExceptionProcessorInterceptor.cs b/EntityFramework.Exceptions/Common/ExceptionProcessorInterceptor.cs index 92f906e..b5f9cfb 100644 --- a/EntityFramework.Exceptions/Common/ExceptionProcessorInterceptor.cs +++ b/EntityFramework.Exceptions/Common/ExceptionProcessorInterceptor.cs @@ -23,7 +23,8 @@ protected internal enum DatabaseError CannotInsertNull, MaxLength, NumericOverflow, - ReferenceConstraint + ReferenceConstraint, + DeadLock, } /// @@ -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; } @@ -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(); @@ -152,4 +154,4 @@ private void SetConstraintDetails(DbContext context, ReferenceConstraintExceptio exception.SchemaQualifiedTableName = match.SchemaQualifiedTableName; } } -} \ No newline at end of file +} diff --git a/EntityFramework.Exceptions/Common/Exceptions.cs b/EntityFramework.Exceptions/Common/Exceptions.cs index 5a7a3ed..1123368 100644 --- a/EntityFramework.Exceptions/Common/Exceptions.cs +++ b/EntityFramework.Exceptions/Common/Exceptions.cs @@ -127,4 +127,27 @@ public ReferenceConstraintException(string message, Exception innerException, IR public string ConstraintName { get; internal set; } public IReadOnlyList ConstraintProperties { get; internal set; } public string SchemaQualifiedTableName { get; internal set; } -} \ No newline at end of file +} + +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 entries) : base(message, entries) + { + } + + public DeadlockException(string message, Exception innerException, IReadOnlyList entries) : base(message, innerException, entries) + { + } +} diff --git a/EntityFramework.Exceptions/Tests/DatabaseTests.cs b/EntityFramework.Exceptions/Tests/DatabaseTests.cs index 4ab981b..9d9a2ae 100644 --- a/EntityFramework.Exceptions/Tests/DatabaseTests.cs +++ b/EntityFramework.Exceptions/Tests/DatabaseTests.cs @@ -71,7 +71,7 @@ public virtual async Task UniqueColumnViolationSameNamesIndexesInDifferentSchema { Name = "Rope Access" }); - + SameNameIndexesContext.IncidentCategories.Add(new EFExceptionSchema.Entities.Incidents.Category { Name = "Rope Access" @@ -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(nameof(Product.Id), uniqueConstraintException.ConstraintProperties); + Assert.Contains(nameof(Product.Id), uniqueConstraintException.ConstraintProperties); } } @@ -348,6 +348,34 @@ public async Task NotHandledViolationReThrowsOriginalException() await Assert.ThrowsAsync(() => 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(() => 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(); @@ -360,4 +388,4 @@ protected void CleanupContext() entityEntry.State = EntityState.Detached; } } -} \ No newline at end of file +} diff --git a/EntityFramework.Exceptions/Tests/DemoContext.cs b/EntityFramework.Exceptions/Tests/DemoContext.cs index e17c492..d59b700 100644 --- a/EntityFramework.Exceptions/Tests/DemoContext.cs +++ b/EntityFramework.Exceptions/Tests/DemoContext.cs @@ -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 Customers { get; set; } public DbSet Products { get; set; } public DbSet ProductSales { get; set; } @@ -55,4 +54,4 @@ public class Customer { public int Id { get; set; } public string Fullname { get; set; } -} \ No newline at end of file +} diff --git a/EntityFramework.Exceptions/Tests/OracleTests.cs b/EntityFramework.Exceptions/Tests/OracleTests.cs index cf7ec8d..a99489b 100644 --- a/EntityFramework.Exceptions/Tests/OracleTests.cs +++ b/EntityFramework.Exceptions/Tests/OracleTests.cs @@ -11,6 +11,12 @@ public class OracleTests : DatabaseTests, IClassFixture @@ -20,9 +26,9 @@ static OracleTestContextFixture() Container = new OracleBuilder().Build(); } - protected override DbContextOptionsBuilder BuildDemoContextOptions(DbContextOptionsBuilder builder, string connectionString) + protected override DbContextOptionsBuilder BuildDemoContextOptions(DbContextOptionsBuilder 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(); -} \ No newline at end of file +} diff --git a/EntityFramework.Exceptions/Tests/SqliteTests.cs b/EntityFramework.Exceptions/Tests/SqliteTests.cs index 58ebb86..372036c 100644 --- a/EntityFramework.Exceptions/Tests/SqliteTests.cs +++ b/EntityFramework.Exceptions/Tests/SqliteTests.cs @@ -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 { private const string ConnectionString = "DataSource=file::memory:?cache=shared"; - protected override DbContextOptionsBuilder BuildDemoContextOptions(DbContextOptionsBuilder builder, string connectionString) + protected override DbContextOptionsBuilder BuildDemoContextOptions(DbContextOptionsBuilder 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(); } }