Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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: "40P01" };
}
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 };
}
3 changes: 2 additions & 1 deletion DbExceptionClassifier/Sqlite/SqliteExceptionClassifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ public bool IsUniqueConstraintError(DbException exception) => exception is Sqlit
};

public bool IsMaxLengthExceededError(DbException exception) => exception is SqliteException { SqliteExtendedErrorCode: SQLITE_TOOBIG };
}
public bool IsDeadlockError(DbException exception)=> exception is SqliteException { SqliteExtendedErrorCode: SQLITE_LOCKED_SHAREDCACHE };
}
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;
}
}
}
}
29 changes: 28 additions & 1 deletion EntityFramework.Exceptions/Common/Exceptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,31 @@ 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)
{
}

public string ConstraintName { get; internal set; }
public IReadOnlyList<string> ConstraintProperties { get; internal set; }
public string SchemaQualifiedTableName { get; internal set; }
}
41 changes: 38 additions & 3 deletions EntityFramework.Exceptions/Tests/DatabaseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
{
Name = "Rope Access"
});

SameNameIndexesContext.IncidentCategories.Add(new EFExceptionSchema.Entities.Incidents.Category
{
Name = "Rope Access"
Expand Down Expand Up @@ -109,7 +109,7 @@
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 All @@ -118,7 +118,7 @@
{
DemoContext.Products.Add(new Product());

Assert.Throws<CannotInsertNullException>(() => DemoContext.SaveChanges());

Check failure on line 121 in EntityFramework.Exceptions/Tests/DatabaseTests.cs

View workflow job for this annotation

GitHub Actions / Build and analyze

EntityFramework.Exceptions.Tests.PostgreSQLTests.RequiredColumnViolationThrowsCannotInsertNullException

Assert.Throws() Failure: Exception type was not an exact match Expected: typeof(EntityFramework.Exceptions.Common.CannotInsertNullException) Actual: typeof(Npgsql.PostgresException) ---- Npgsql.PostgresException : 25P02: current transaction is aborted, commands ignored until end of transaction block

Check failure on line 121 in EntityFramework.Exceptions/Tests/DatabaseTests.cs

View workflow job for this annotation

GitHub Actions / Build

EntityFramework.Exceptions.Tests.PostgreSQLTests.RequiredColumnViolationThrowsCannotInsertNullException

Assert.Throws() Failure: Exception type was not an exact match Expected: typeof(EntityFramework.Exceptions.Common.CannotInsertNullException) Actual: typeof(Npgsql.PostgresException) ---- Npgsql.PostgresException : 25P02: current transaction is aborted, commands ignored until end of transaction block
await Assert.ThrowsAsync<CannotInsertNullException>(() => DemoContext.SaveChangesAsync());
}

Expand Down Expand Up @@ -181,7 +181,7 @@
DemoContext.Products.Add(product);
DemoContext.ProductSales.Add(new ProductSale { Price = 3141.59265m, Product = product });

Assert.Throws<NumericOverflowException>(() => DemoContext.SaveChanges());

Check failure on line 184 in EntityFramework.Exceptions/Tests/DatabaseTests.cs

View workflow job for this annotation

GitHub Actions / Build and analyze

EntityFramework.Exceptions.Tests.PostgreSQLTests.NumericOverflowViolationThrowsNumericOverflowException

Assert.Throws() Failure: Exception type was not an exact match Expected: typeof(EntityFramework.Exceptions.Common.NumericOverflowException) Actual: typeof(System.InvalidOperationException) ---- System.InvalidOperationException : The association between entity types 'Product' and 'ProductPriceHistory' has been severed, but the relationship is either marked as required or is implicitly required because the foreign key is not nullable. If the dependent/child entity should be deleted when a required relationship is severed, configure the relationship to use cascade deletes. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the key values.

Check failure on line 184 in EntityFramework.Exceptions/Tests/DatabaseTests.cs

View workflow job for this annotation

GitHub Actions / Build

EntityFramework.Exceptions.Tests.PostgreSQLTests.NumericOverflowViolationThrowsNumericOverflowException

Assert.Throws() Failure: Exception type was not an exact match Expected: typeof(EntityFramework.Exceptions.Common.NumericOverflowException) Actual: typeof(System.InvalidOperationException) ---- System.InvalidOperationException : The association between entity types 'Product' and 'ProductPriceHistory' has been severed, but the relationship is either marked as required or is implicitly required because the foreign key is not nullable. If the dependent/child entity should be deleted when a required relationship is severed, configure the relationship to use cascade deletes. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the key values.
await Assert.ThrowsAsync<NumericOverflowException>(() => DemoContext.SaveChangesAsync());
}

Expand Down Expand Up @@ -348,6 +348,41 @@
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 = DemoContext.Database.BeginTransactionAsync();
using var transaction2 = controlContext.Database.BeginTransactionAsync();

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

await Assert.ThrowsAsync<DeadlockException>(async () =>
{
await controlContext.Products.Where(c => c.Id == id2)
.ExecuteUpdateAsync(c => c.SetProperty(p => p.Name, "Test21"));

var task1 = Task.Run(() => DemoContext.Products.Where(c => c.Id == id2)
.ExecuteUpdateAsync(c => c.SetProperty(p => p.Name, "Test22")));

await Task.Delay(100);

var task2 = controlContext.Products.Where(c => c.Id == id1)
.ExecuteUpdateAsync(c => c.SetProperty(p => p.Name, "Test12"));

await Task.WhenAll(task1, task2);
});
}

public virtual void Dispose()
{
CleanupContext();
Expand All @@ -360,4 +395,4 @@
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();
}
}
Loading