Skip to content

Commit eb84582

Browse files
committed
Add DbCommandInterceptors
1 parent a5cab23 commit eb84582

20 files changed

+416
-91
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace EntityFramework.Exceptions.Common;
2+
3+
public enum DatabaseError
4+
{
5+
UniqueConstraint,
6+
CannotInsertNull,
7+
MaxLength,
8+
NumericOverflow,
9+
ReferenceConstraint
10+
}

EntityFramework.Exceptions.Common/ExceptionFactory.cs

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,26 @@
11
using Microsoft.EntityFrameworkCore;
22
using System;
33
using System.Collections.Generic;
4-
using System.Data.Common;
54
using Microsoft.EntityFrameworkCore.ChangeTracking;
65

76
namespace EntityFramework.Exceptions.Common;
87

98
static class ExceptionFactory
109
{
11-
internal static Exception Create<T>(ExceptionProcessorInterceptor<T>.DatabaseError error, DbUpdateException exception, IReadOnlyList<EntityEntry> entries) where T : DbException
10+
internal static Exception Create(DatabaseError error, DbUpdateException exception, IReadOnlyList<EntityEntry> entries)
1211
{
1312
return error switch
1413
{
15-
ExceptionProcessorInterceptor<T>.DatabaseError.CannotInsertNull when entries.Count > 0 => new CannotInsertNullException("Cannot insert null", exception.InnerException, entries),
16-
ExceptionProcessorInterceptor<T>.DatabaseError.CannotInsertNull when entries.Count == 0 => new CannotInsertNullException("Cannot insert null", exception.InnerException),
17-
ExceptionProcessorInterceptor<T>.DatabaseError.MaxLength when entries.Count > 0 => new MaxLengthExceededException("Maximum length exceeded", exception.InnerException, entries),
18-
ExceptionProcessorInterceptor<T>.DatabaseError.MaxLength when entries.Count == 0 => new MaxLengthExceededException("Maximum length exceeded", exception.InnerException),
19-
ExceptionProcessorInterceptor<T>.DatabaseError.NumericOverflow when entries.Count > 0 => new NumericOverflowException("Numeric overflow", exception.InnerException, entries),
20-
ExceptionProcessorInterceptor<T>.DatabaseError.NumericOverflow when entries.Count == 0 => new NumericOverflowException("Numeric overflow", exception.InnerException),
21-
ExceptionProcessorInterceptor<T>.DatabaseError.ReferenceConstraint when entries.Count > 0 => new ReferenceConstraintException("Reference constraint violation", exception.InnerException, entries),
22-
ExceptionProcessorInterceptor<T>.DatabaseError.ReferenceConstraint when entries.Count == 0 => new ReferenceConstraintException("Reference constraint violation", exception.InnerException),
23-
ExceptionProcessorInterceptor<T>.DatabaseError.UniqueConstraint when entries.Count > 0 => new UniqueConstraintException("Unique constraint violation", exception.InnerException, entries),
24-
ExceptionProcessorInterceptor<T>.DatabaseError.UniqueConstraint when entries.Count == 0 => new UniqueConstraintException("Unique constraint violation", exception.InnerException),
14+
DatabaseError.CannotInsertNull when entries.Count > 0 => new CannotInsertNullException("Cannot insert null", exception.InnerException, entries),
15+
DatabaseError.CannotInsertNull when entries.Count == 0 => new CannotInsertNullException("Cannot insert null", exception.InnerException),
16+
DatabaseError.MaxLength when entries.Count > 0 => new MaxLengthExceededException("Maximum length exceeded", exception.InnerException, entries),
17+
DatabaseError.MaxLength when entries.Count == 0 => new MaxLengthExceededException("Maximum length exceeded", exception.InnerException),
18+
DatabaseError.NumericOverflow when entries.Count > 0 => new NumericOverflowException("Numeric overflow", exception.InnerException, entries),
19+
DatabaseError.NumericOverflow when entries.Count == 0 => new NumericOverflowException("Numeric overflow", exception.InnerException),
20+
DatabaseError.ReferenceConstraint when entries.Count > 0 => new ReferenceConstraintException("Reference constraint violation", exception.InnerException, entries),
21+
DatabaseError.ReferenceConstraint when entries.Count == 0 => new ReferenceConstraintException("Reference constraint violation", exception.InnerException),
22+
DatabaseError.UniqueConstraint when entries.Count > 0 => new UniqueConstraintException("Unique constraint violation", exception.InnerException, entries),
23+
DatabaseError.UniqueConstraint when entries.Count == 0 => new UniqueConstraintException("Unique constraint violation", exception.InnerException),
2524
_ => null,
2625
};
2726
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
using Microsoft.EntityFrameworkCore;
2+
using Microsoft.EntityFrameworkCore.Diagnostics;
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Data.Common;
6+
using System.Diagnostics;
7+
using System.Linq;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
11+
namespace EntityFramework.Exceptions.Common;
12+
13+
public abstract class ExceptionProcessorDbCommandInterceptor<T> : DbCommandInterceptor where T : DbException
14+
{
15+
private List<IndexDetails> uniqueIndexDetailsList;
16+
private List<ForeignKeyDetails> foreignKeyDetailsList;
17+
18+
protected abstract DatabaseError? GetDatabaseError(T dbException);
19+
20+
/// <inheritdoc />
21+
public override void CommandFailed(DbCommand command, CommandErrorEventData eventData)
22+
{
23+
ProcessException(eventData, eventData.Exception as DbUpdateException);
24+
25+
base.CommandFailed(command, eventData);
26+
}
27+
28+
/// <inheritdoc />
29+
public override Task CommandFailedAsync(DbCommand command, CommandErrorEventData eventData,
30+
CancellationToken cancellationToken = new CancellationToken())
31+
{
32+
ProcessException(eventData, eventData.Exception as DbUpdateException);
33+
34+
return base.CommandFailedAsync(command, eventData, cancellationToken);
35+
}
36+
37+
[StackTraceHidden]
38+
private void ProcessException(CommandErrorEventData eventData, DbUpdateException dbUpdateException)
39+
{
40+
if (dbUpdateException == null || eventData.Exception.GetBaseException() is not T providerException) return;
41+
42+
var error = GetDatabaseError(providerException);
43+
44+
if (error == null) return;
45+
46+
var exception = ExceptionFactory.Create(error.Value, dbUpdateException, dbUpdateException.Entries);
47+
48+
switch (exception)
49+
{
50+
case UniqueConstraintException uniqueConstraint when eventData.Context != null:
51+
SetConstraintDetails(eventData.Context, uniqueConstraint, providerException);
52+
break;
53+
case ReferenceConstraintException referenceConstraint when eventData.Context != null:
54+
SetConstraintDetails(eventData.Context, referenceConstraint, providerException);
55+
break;
56+
}
57+
58+
throw exception;
59+
}
60+
61+
private void SetConstraintDetails(DbContext context, UniqueConstraintException exception,
62+
Exception providerException)
63+
{
64+
if (uniqueIndexDetailsList == null)
65+
{
66+
var indexes = context.Model.GetEntityTypes()
67+
.SelectMany(x => x.GetDeclaredIndexes().Where(index => index.IsUnique));
68+
69+
var mappedIndexes = indexes.SelectMany(index => index.GetMappedTableIndexes(),
70+
(index, tableIndex) =>
71+
new IndexDetails(tableIndex.Name, tableIndex.Table.SchemaQualifiedName, index.Properties));
72+
73+
var primaryKeys = context.Model.GetEntityTypes().SelectMany(x =>
74+
{
75+
var primaryKey = x.FindPrimaryKey();
76+
if (primaryKey is null)
77+
{
78+
return Array.Empty<IndexDetails>();
79+
}
80+
81+
var primaryKeyName = primaryKey.GetName();
82+
83+
if (primaryKeyName is null)
84+
{
85+
return Array.Empty<IndexDetails>();
86+
}
87+
88+
return new[]
89+
{ new IndexDetails(primaryKeyName, x.GetSchemaQualifiedTableName(), primaryKey.Properties) };
90+
});
91+
92+
uniqueIndexDetailsList = mappedIndexes
93+
.Union(primaryKeys)
94+
.ToList();
95+
}
96+
97+
var matchingIndexes = uniqueIndexDetailsList.Where(index =>
98+
providerException.Message.Contains(index.Name, StringComparison.OrdinalIgnoreCase)).ToList();
99+
var match = matchingIndexes.Count == 1
100+
? matchingIndexes[0]
101+
: matchingIndexes.FirstOrDefault(index =>
102+
providerException.Message.Contains(index.SchemaQualifiedTableName, StringComparison.OrdinalIgnoreCase));
103+
104+
if (match != null)
105+
{
106+
exception.ConstraintName = match.Name;
107+
exception.ConstraintProperties = match.Properties.Select(property => property.Name).ToList();
108+
exception.SchemaQualifiedTableName = match.SchemaQualifiedTableName;
109+
}
110+
}
111+
112+
private void SetConstraintDetails(DbContext context, ReferenceConstraintException exception,
113+
Exception providerException)
114+
{
115+
if (foreignKeyDetailsList == null)
116+
{
117+
var keys = context.Model.GetEntityTypes().SelectMany(x => x.GetDeclaredForeignKeys());
118+
119+
var mappedConstraints = keys.SelectMany(index => index.GetMappedConstraints(),
120+
(index, constraint) => new { constraint, index.Properties });
121+
122+
foreignKeyDetailsList = mappedConstraints.Select(arg =>
123+
new ForeignKeyDetails(arg.constraint.Name, arg.constraint.Table.SchemaQualifiedName,
124+
arg.Properties))
125+
.ToList();
126+
}
127+
128+
var matchingForeignKeys = foreignKeyDetailsList.Where(foreignKey =>
129+
providerException.Message.Contains(foreignKey.Name, StringComparison.OrdinalIgnoreCase)).ToList();
130+
var match = matchingForeignKeys.Count == 1
131+
? matchingForeignKeys[0]
132+
: matchingForeignKeys.FirstOrDefault(foreignKey =>
133+
providerException.Message.Contains(foreignKey.SchemaQualifiedTableName,
134+
StringComparison.OrdinalIgnoreCase));
135+
136+
if (match != null)
137+
{
138+
exception.ConstraintName = match.Name;
139+
exception.ConstraintProperties = match.Properties.Select(property => property.Name).ToList();
140+
exception.SchemaQualifiedTableName = match.SchemaQualifiedTableName;
141+
}
142+
}
143+
}

EntityFramework.Exceptions.Common/ExceptionProcessorInterceptor.cs

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,6 @@ public abstract class ExceptionProcessorInterceptor<T> : SaveChangesInterceptor
1515
private List<IndexDetails> uniqueIndexDetailsList;
1616
private List<ForeignKeyDetails> foreignKeyDetailsList;
1717

18-
protected internal enum DatabaseError
19-
{
20-
UniqueConstraint,
21-
CannotInsertNull,
22-
MaxLength,
23-
NumericOverflow,
24-
ReferenceConstraint
25-
}
26-
2718
protected abstract DatabaseError? GetDatabaseError(T dbException);
2819

2920
/// <inheritdoc />

EntityFramework.Exceptions.MySQL.Pomelo/MySQL.Pomelo.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ Use this package if you use Pomelo.EntityFrameworkCore.MySql Entity Framework Co
1616
</PropertyGroup>
1717

1818
<ItemGroup>
19+
<Compile Include="..\EntityFramework.Exceptions.MySQL\ExceptionProcessorExtensions.cs">
20+
<Link>ExceptionProcessorExtensions.cs</Link>
21+
</Compile>
22+
<Compile Include="..\EntityFramework.Exceptions.MySQL\MySqlExceptionProcessorDbCommandInterceptor.cs">
23+
<Link>MySqlExceptionProcessorDbCommandInterceptor.cs</Link>
24+
</Compile>
1925
<Compile Include="..\EntityFramework.Exceptions.MySQL\MySqlExceptionProcessorInterceptor.cs" Link="MySqlExceptionProcessorInterceptor.cs" />
2026
</ItemGroup>
2127

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using Microsoft.EntityFrameworkCore;
2+
3+
#if POMELO
4+
namespace EntityFramework.Exceptions.MySQL.Pomelo;
5+
#else
6+
namespace EntityFramework.Exceptions.MySQL;
7+
#endif
8+
9+
public static class ExceptionProcessorExtensions
10+
{
11+
public static DbContextOptionsBuilder UseExceptionProcessor(this DbContextOptionsBuilder self)
12+
{
13+
return self
14+
.AddInterceptors(new MySqlExceptionProcessorInterceptor())
15+
.AddInterceptors(new MySqlExceptionProcessorDbCommandInterceptor());
16+
}
17+
18+
public static DbContextOptionsBuilder<TContext> UseExceptionProcessor<TContext>(this DbContextOptionsBuilder<TContext> self) where TContext : DbContext
19+
{
20+
return self
21+
.AddInterceptors(new MySqlExceptionProcessorInterceptor())
22+
.AddInterceptors(new MySqlExceptionProcessorDbCommandInterceptor());
23+
}
24+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using System;
2+
using EntityFramework.Exceptions.Common;
3+
4+
#if POMELO
5+
using MySqlConnector;
6+
namespace EntityFramework.Exceptions.MySQL.Pomelo;
7+
#else
8+
using MySql.Data.MySqlClient;
9+
namespace EntityFramework.Exceptions.MySQL;
10+
#endif
11+
12+
class MySqlExceptionProcessorDbCommandInterceptor : ExceptionProcessorDbCommandInterceptor<MySqlException>
13+
{
14+
protected override DatabaseError? GetDatabaseError(MySqlException dbException)
15+
{
16+
17+
#if POMELO
18+
return dbException.ErrorCode switch
19+
#else
20+
return (MySqlErrorCode)dbException.Number switch
21+
#endif
22+
{
23+
MySqlErrorCode.ColumnCannotBeNull => DatabaseError.CannotInsertNull,
24+
MySqlErrorCode.DuplicateKeyEntry=> DatabaseError.UniqueConstraint,
25+
MySqlErrorCode.WarningDataOutOfRange => DatabaseError.NumericOverflow,
26+
MySqlErrorCode.DataTooLong => DatabaseError.MaxLength,
27+
MySqlErrorCode.NoReferencedRow => DatabaseError.ReferenceConstraint,
28+
MySqlErrorCode.RowIsReferenced => DatabaseError.ReferenceConstraint,
29+
MySqlErrorCode.NoReferencedRow2 => DatabaseError.ReferenceConstraint,
30+
MySqlErrorCode.RowIsReferenced2 => DatabaseError.ReferenceConstraint,
31+
_ => null
32+
};
33+
}
34+
}

EntityFramework.Exceptions.MySQL/MySqlExceptionProcessorInterceptor.cs

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using System;
22
using EntityFramework.Exceptions.Common;
3-
using Microsoft.EntityFrameworkCore;
43

54
#if POMELO
65
using MySqlConnector;
@@ -32,17 +31,4 @@ class MySqlExceptionProcessorInterceptor : ExceptionProcessorInterceptor<MySqlEx
3231
_ => null
3332
};
3433
}
35-
}
36-
37-
public static class ExceptionProcessorExtensions
38-
{
39-
public static DbContextOptionsBuilder UseExceptionProcessor(this DbContextOptionsBuilder self)
40-
{
41-
return self.AddInterceptors(new MySqlExceptionProcessorInterceptor());
42-
}
43-
44-
public static DbContextOptionsBuilder<TContext> UseExceptionProcessor<TContext>(this DbContextOptionsBuilder<TContext> self) where TContext : DbContext
45-
{
46-
return self.AddInterceptors(new MySqlExceptionProcessorInterceptor());
47-
}
4834
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using Microsoft.EntityFrameworkCore;
2+
3+
namespace EntityFramework.Exceptions.Oracle;
4+
5+
public static class ExceptionProcessorExtensions
6+
{
7+
public static DbContextOptionsBuilder UseExceptionProcessor(this DbContextOptionsBuilder self)
8+
{
9+
return self
10+
.AddInterceptors(new OracleExceptionProcessorInterceptor())
11+
.AddInterceptors(new OracleExceptionProcessorDbCommandInterceptor());
12+
}
13+
14+
public static DbContextOptionsBuilder<TContext> UseExceptionProcessor<TContext>(this DbContextOptionsBuilder<TContext> self)
15+
where TContext : DbContext
16+
{
17+
return self
18+
.AddInterceptors(new OracleExceptionProcessorInterceptor())
19+
.AddInterceptors(new OracleExceptionProcessorDbCommandInterceptor());
20+
}
21+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using EntityFramework.Exceptions.Common;
2+
using Oracle.ManagedDataAccess.Client;
3+
4+
namespace EntityFramework.Exceptions.Oracle;
5+
6+
class OracleExceptionProcessorDbCommandInterceptor : ExceptionProcessorDbCommandInterceptor<OracleException>
7+
{
8+
private const int CannotInsertNull = 1400;
9+
private const int UniqueConstraintViolation = 1;
10+
private const int IntegrityConstraintViolation = 2291;
11+
private const int ChildRecordFound = 2292;
12+
private const int NumericOverflow = 1438;
13+
private const int NumericOrValueError = 12899;
14+
15+
protected override DatabaseError? GetDatabaseError(OracleException dbException)
16+
{
17+
return dbException.Number switch
18+
{
19+
IntegrityConstraintViolation => DatabaseError.ReferenceConstraint,
20+
ChildRecordFound => DatabaseError.ReferenceConstraint,
21+
CannotInsertNull => DatabaseError.CannotInsertNull,
22+
NumericOrValueError => DatabaseError.MaxLength,
23+
NumericOverflow => DatabaseError.NumericOverflow,
24+
UniqueConstraintViolation => DatabaseError.UniqueConstraint,
25+
_ => null
26+
};
27+
}
28+
}

0 commit comments

Comments
 (0)