Skip to content

Commit fb4b298

Browse files
authored
Merge pull request #82 from MarkusG/use-dbcommand-interceptor
2 parents c5eff6c + eaf6b22 commit fb4b298

File tree

5 files changed

+236
-37
lines changed

5 files changed

+236
-37
lines changed

EntityFramework.Exceptions.Common/ExceptionFactory.cs

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
using Microsoft.EntityFrameworkCore;
2-
using System;
1+
using System;
32
using System.Collections.Generic;
43
using System.Data.Common;
54
using Microsoft.EntityFrameworkCore.ChangeTracking;
@@ -8,20 +7,28 @@ 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<T>(ExceptionProcessorInterceptor<T>.DatabaseError error, Exception exception, IReadOnlyList<EntityEntry> entries) where T : DbException
1211
{
12+
if (entries?.Count > 0)
13+
{
14+
return error switch
15+
{
16+
ExceptionProcessorInterceptor<T>.DatabaseError.CannotInsertNull => new CannotInsertNullException( "Cannot insert null", exception.InnerException, entries),
17+
ExceptionProcessorInterceptor<T>.DatabaseError.MaxLength => new MaxLengthExceededException( "Maximum length exceeded", exception.InnerException, entries),
18+
ExceptionProcessorInterceptor<T>.DatabaseError.NumericOverflow => new NumericOverflowException( "Numeric overflow", exception.InnerException, entries),
19+
ExceptionProcessorInterceptor<T>.DatabaseError.ReferenceConstraint => new ReferenceConstraintException( "Reference constraint violation", exception.InnerException, entries),
20+
ExceptionProcessorInterceptor<T>.DatabaseError.UniqueConstraint => new UniqueConstraintException( "Unique constraint violation", exception.InnerException, entries),
21+
_ => null,
22+
};
23+
}
24+
1325
return error switch
1426
{
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),
27+
ExceptionProcessorInterceptor<T>.DatabaseError.CannotInsertNull => new CannotInsertNullException("Cannot insert null", exception.InnerException),
28+
ExceptionProcessorInterceptor<T>.DatabaseError.MaxLength => new MaxLengthExceededException("Maximum length exceeded", exception.InnerException),
29+
ExceptionProcessorInterceptor<T>.DatabaseError.NumericOverflow => new NumericOverflowException("Numeric overflow", exception.InnerException),
30+
ExceptionProcessorInterceptor<T>.DatabaseError.ReferenceConstraint => new ReferenceConstraintException("Reference constraint violation", exception.InnerException),
31+
ExceptionProcessorInterceptor<T>.DatabaseError.UniqueConstraint => new UniqueConstraintException("Unique constraint violation", exception.InnerException),
2532
_ => null,
2633
};
2734
}

EntityFramework.Exceptions.Common/ExceptionProcessorInterceptor.cs

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
using Microsoft.EntityFrameworkCore;
2-
using Microsoft.EntityFrameworkCore.Diagnostics;
31
using System;
42
using System.Collections.Generic;
53
using System.Data.Common;
64
using System.Diagnostics;
75
using System.Linq;
86
using System.Threading;
97
using System.Threading.Tasks;
8+
using Microsoft.EntityFrameworkCore;
9+
using Microsoft.EntityFrameworkCore.Diagnostics;
1010

1111
namespace EntityFramework.Exceptions.Common;
1212

13-
public abstract class ExceptionProcessorInterceptor<T> : SaveChangesInterceptor where T : DbException
13+
public abstract class ExceptionProcessorInterceptor<TProviderException> : IDbCommandInterceptor, ISaveChangesInterceptor where TProviderException : DbException
1414
{
1515
private List<IndexDetails> uniqueIndexDetailsList;
1616
private List<ForeignKeyDetails> foreignKeyDetailsList;
@@ -24,48 +24,60 @@ protected internal enum DatabaseError
2424
ReferenceConstraint
2525
}
2626

27-
protected abstract DatabaseError? GetDatabaseError(T dbException);
28-
2927
/// <inheritdoc />
30-
public override void SaveChangesFailed(DbContextErrorEventData eventData)
28+
public void SaveChangesFailed(DbContextErrorEventData eventData)
3129
{
32-
ProcessException(eventData, eventData.Exception as DbUpdateException);
30+
ProcessException(eventData.Exception, eventData.Context);
31+
}
3332

34-
base.SaveChangesFailed(eventData);
33+
/// <inheritdoc />
34+
public Task SaveChangesFailedAsync(DbContextErrorEventData eventData, CancellationToken cancellationToken = new CancellationToken())
35+
{
36+
ProcessException(eventData.Exception, eventData.Context);
37+
return Task.CompletedTask;
3538
}
3639

3740
/// <inheritdoc />
38-
public override Task SaveChangesFailedAsync(DbContextErrorEventData eventData, CancellationToken cancellationToken = new CancellationToken())
41+
public void CommandFailed(DbCommand command, CommandErrorEventData eventData)
3942
{
40-
ProcessException(eventData, eventData.Exception as DbUpdateException);
43+
ProcessException(eventData.Exception, eventData.Context);
44+
}
4145

42-
return base.SaveChangesFailedAsync(eventData, cancellationToken);
46+
/// <inheritdoc />
47+
public Task CommandFailedAsync(DbCommand command, CommandErrorEventData eventData, CancellationToken cancellationToken = new CancellationToken())
48+
{
49+
ProcessException(eventData.Exception, eventData.Context);
50+
return Task.CompletedTask;
4351
}
4452

53+
protected abstract DatabaseError? GetDatabaseError(TProviderException dbException);
54+
4555
[StackTraceHidden]
46-
private void ProcessException(DbContextErrorEventData eventData, DbUpdateException dbUpdateException)
56+
private void ProcessException(Exception eventException, DbContext eventContext)
4757
{
48-
if (dbUpdateException == null || eventData.Exception.GetBaseException() is not T providerException) return;
49-
58+
if (eventException?.GetBaseException() is not TProviderException providerException) return;
59+
5060
var error = GetDatabaseError(providerException);
5161

5262
if (error == null) return;
53-
54-
var exception = ExceptionFactory.Create(error.Value, dbUpdateException, dbUpdateException.Entries);
63+
64+
var updateException = eventException as DbUpdateException;
65+
var exception = ExceptionFactory.Create(error.Value, eventException, updateException?.Entries);
5566

5667
switch (exception)
5768
{
58-
case UniqueConstraintException uniqueConstraint when eventData.Context != null:
59-
SetConstraintDetails(eventData.Context, uniqueConstraint, providerException);
69+
case UniqueConstraintException uniqueConstraint when eventContext != null:
70+
SetConstraintDetails(eventContext, uniqueConstraint, providerException);
6071
break;
61-
case ReferenceConstraintException referenceConstraint when eventData.Context != null:
62-
SetConstraintDetails(eventData.Context, referenceConstraint, providerException);
72+
case ReferenceConstraintException referenceConstraint when eventContext != null:
73+
SetConstraintDetails(eventContext, referenceConstraint, providerException);
6374
break;
6475
}
76+
6577
throw exception;
6678
}
6779

68-
private void SetConstraintDetails(DbContext context, UniqueConstraintException exception, Exception providerException)
80+
private void SetConstraintDetails(DbContext context, UniqueConstraintException exception, TProviderException providerException)
6981
{
7082
if (uniqueIndexDetailsList == null)
7183
{
@@ -108,7 +120,7 @@ private void SetConstraintDetails(DbContext context, UniqueConstraintException e
108120
}
109121
}
110122

111-
private void SetConstraintDetails(DbContext context, ReferenceConstraintException exception, Exception providerException)
123+
private void SetConstraintDetails(DbContext context, ReferenceConstraintException exception, TProviderException providerException)
112124
{
113125
if (foreignKeyDetailsList == null)
114126
{
@@ -129,4 +141,4 @@ private void SetConstraintDetails(DbContext context, ReferenceConstraintExceptio
129141
exception.SchemaQualifiedTableName = match.SchemaQualifiedTableName;
130142
}
131143
}
132-
}
144+
}

EntityFramework.Exceptions.Tests/DatabaseTests.cs

Lines changed: 161 additions & 0 deletions
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.Linq;
78
using System.Threading.Tasks;
89
using Xunit;
910

@@ -43,6 +44,20 @@ public virtual async Task UniqueColumnViolationThrowsUniqueConstraintException()
4344
}
4445
}
4546

47+
[Fact]
48+
public virtual async Task UniqueColumnViolationThrowsUniqueConstraintExceptionThroughExecuteUpdate()
49+
{
50+
DemoContext.Products.Add(new Product { Name = "Bulk Update 1" });
51+
DemoContext.Products.Add(new Product { Name = "Bulk Update 2" });
52+
53+
await DemoContext.SaveChangesAsync();
54+
Assert.Throws<UniqueConstraintException>(() => DemoContext.Products.ExecuteUpdate(p => p.SetProperty(pp => pp.Name, "Bulk Update 1")));
55+
await Assert.ThrowsAsync<UniqueConstraintException>(async () => await DemoContext.Products.ExecuteUpdateAsync(p => p.SetProperty(pp => pp.Name, "Bulk Update 1")));
56+
await DemoContext.Products
57+
.Where(p => p.Name == "Bulk Update 1" || p.Name == "Bulk Update 2")
58+
.ExecuteDeleteAsync();
59+
}
60+
4661
[Fact]
4762
public virtual async Task UniqueColumnViolationSameNamesIndexesInDifferentSchemasSetsCorrectTableName()
4863
{
@@ -107,6 +122,17 @@ public virtual async Task RequiredColumnViolationThrowsCannotInsertNullException
107122
await Assert.ThrowsAsync<CannotInsertNullException>(() => DemoContext.SaveChangesAsync());
108123
}
109124

125+
[Fact]
126+
public virtual async Task RequiredColumnViolationThrowsCannotInsertNullExceptionThroughExecuteUpdate()
127+
{
128+
DemoContext.Products.Add(new Product { Name = "Bulk Update 1" });
129+
await DemoContext.SaveChangesAsync();
130+
131+
Assert.Throws<CannotInsertNullException>(() => DemoContext.Products.ExecuteUpdate(p => p.SetProperty(pp => pp.Name, (string)null)));
132+
await Assert.ThrowsAsync<CannotInsertNullException>(async () => await DemoContext.Products.ExecuteUpdateAsync(p => p.SetProperty(pp => pp.Name, (string)null)));
133+
await DemoContext.Products.Where(p => p.Name == "Bulk Update 1").ExecuteDeleteAsync();
134+
}
135+
110136
[Fact]
111137
public virtual async Task MaxLengthViolationThrowsMaxLengthExceededException()
112138
{
@@ -116,6 +142,38 @@ public virtual async Task MaxLengthViolationThrowsMaxLengthExceededException()
116142
await Assert.ThrowsAsync<MaxLengthExceededException>(() => DemoContext.SaveChangesAsync());
117143
}
118144

145+
[Fact]
146+
public virtual async Task MaxLengthViolationThrowsMaxLengthExceededExceptionThroughExecuteUpdate()
147+
{
148+
DemoContext.Products.Add(new Product { Name = "Bulk Update 1" });
149+
await DemoContext.SaveChangesAsync();
150+
151+
CleanupContext();
152+
153+
Assert.Throws<MaxLengthExceededException>(Query);
154+
await Assert.ThrowsAsync<MaxLengthExceededException>(QueryAsync);
155+
156+
await DemoContext.Products
157+
.Where(p => p.Name == "Bulk Update 1")
158+
.ExecuteDeleteAsync();
159+
160+
return;
161+
162+
void Query()
163+
{
164+
DemoContext.Products
165+
.Where(p => p.Name == "Bulk Update 1")
166+
.ExecuteUpdate(p => p.SetProperty(pp => pp.Name, new string('G', DemoContext.ProductNameMaxLength + 5)));
167+
}
168+
169+
async Task QueryAsync()
170+
{
171+
await DemoContext.Products
172+
.Where(p => p.Name == "Bulk Update 1")
173+
.ExecuteUpdateAsync(p => p.SetProperty(pp => pp.Name, new string('G', DemoContext.ProductNameMaxLength + 5)));
174+
}
175+
}
176+
119177
[Fact]
120178
public virtual async Task NumericOverflowViolationThrowsNumericOverflowException()
121179
{
@@ -127,6 +185,39 @@ public virtual async Task NumericOverflowViolationThrowsNumericOverflowException
127185
await Assert.ThrowsAsync<NumericOverflowException>(() => DemoContext.SaveChangesAsync());
128186
}
129187

188+
[Fact]
189+
public virtual async Task NumericOverflowViolationThrowsNumericOverflowExceptionThroughExecuteUpdate()
190+
{
191+
var product = new Product { Name = "Numeric Overflow Test 2" };
192+
DemoContext.Products.Add(product);
193+
var sale = new ProductSale { Price = 1m, Product = product };
194+
DemoContext.ProductSales.Add(sale);
195+
await DemoContext.SaveChangesAsync();
196+
197+
Assert.Throws<NumericOverflowException>(Query);
198+
await Assert.ThrowsAsync<NumericOverflowException>(QueryAsync);
199+
200+
DemoContext.Remove(sale);
201+
DemoContext.Remove(product);
202+
await DemoContext.SaveChangesAsync();
203+
204+
return;
205+
206+
void Query()
207+
{
208+
DemoContext.ProductSales
209+
.Where(s => s.Id == sale.Id)
210+
.ExecuteUpdate(s => s.SetProperty(ss => ss.Price, 3141.59265m));
211+
}
212+
213+
async Task QueryAsync()
214+
{
215+
await DemoContext.ProductSales
216+
.Where(s => s.Id == sale.Id)
217+
.ExecuteUpdateAsync(s => s.SetProperty(ss => ss.Price, 3141.59265m));
218+
}
219+
}
220+
130221
[Fact]
131222
public virtual async Task ReferenceViolationThrowsReferenceConstraintException()
132223
{
@@ -143,6 +234,46 @@ public virtual async Task ReferenceViolationThrowsReferenceConstraintException()
143234
}
144235
}
145236

237+
[Fact]
238+
public virtual async Task ReferenceViolationThrowsReferenceConstraintExceptionThroughExecuteUpdate()
239+
{
240+
var product = new Product { Name = "RefConstraint Violation 1" };
241+
DemoContext.Products.Add(product);
242+
var sale = new ProductSale { Price = 1m, Product = product };
243+
DemoContext.ProductSales.Add(sale);
244+
await DemoContext.SaveChangesAsync();
245+
246+
var exception = Assert.Throws<ReferenceConstraintException>(Query);
247+
var asyncException = await Assert.ThrowsAsync<ReferenceConstraintException>(QueryAsync);
248+
249+
if (!isSqlite)
250+
{
251+
Assert.False(string.IsNullOrEmpty(exception.ConstraintName));
252+
Assert.NotEmpty(exception.ConstraintProperties);
253+
Assert.Contains<string>(nameof(ProductSale.ProductId), exception.ConstraintProperties);
254+
255+
Assert.False(string.IsNullOrEmpty(asyncException.ConstraintName));
256+
Assert.NotEmpty(asyncException.ConstraintProperties);
257+
Assert.Contains<string>(nameof(ProductSale.ProductId), asyncException.ConstraintProperties);
258+
}
259+
260+
return;
261+
262+
void Query()
263+
{
264+
DemoContext.ProductSales
265+
.Where(s => s.Id == sale.Id)
266+
.ExecuteUpdate(s => s.SetProperty(ss => ss.ProductId, 0));
267+
}
268+
269+
async Task QueryAsync()
270+
{
271+
await DemoContext.ProductSales
272+
.Where(s => s.Id == sale.Id)
273+
.ExecuteUpdateAsync(s => s.SetProperty(ss => ss.ProductId, 0));
274+
}
275+
}
276+
146277
[Fact]
147278
public virtual async Task DatabaseUnrelatedExceptionThrowsOriginalException()
148279
{
@@ -176,6 +307,36 @@ public virtual async Task DeleteParentItemThrowsReferenceConstraintException()
176307
await Assert.ThrowsAsync<ReferenceConstraintException>(() => DemoContext.SaveChangesAsync());
177308
}
178309

310+
[Fact]
311+
public virtual async Task DeleteParentItemThrowsReferenceConstraintExceptionThroughExecuteDelete()
312+
{
313+
var product = new Product { Name = "AN2" };
314+
var productPriceHistory = new ProductPriceHistory { Product = product, Price = 15.27m, EffectiveDate = DateTimeOffset.UtcNow };
315+
DemoContext.ProductPriceHistories.Add(productPriceHistory);
316+
await DemoContext.SaveChangesAsync();
317+
318+
CleanupContext();
319+
320+
Assert.Throws<ReferenceConstraintException>(Query);
321+
await Assert.ThrowsAsync<ReferenceConstraintException>(QueryAsync);
322+
323+
return;
324+
325+
void Query()
326+
{
327+
DemoContext.Products
328+
.Where(p => p.Name == "AN2")
329+
.ExecuteDelete();
330+
}
331+
332+
async Task QueryAsync()
333+
{
334+
await DemoContext.Products
335+
.Where(p => p.Name == "AN2")
336+
.ExecuteDeleteAsync();
337+
}
338+
}
339+
179340
[Fact]
180341
public async Task NotHandledViolationReThrowsOriginalException()
181342
{

EntityFramework.Exceptions.Tests/OracleTests.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using EntityFramework.Exceptions.Oracle;
1+
using System.Threading.Tasks;
2+
using EntityFramework.Exceptions.Oracle;
23
using Microsoft.EntityFrameworkCore;
34
using Testcontainers.Oracle;
45
using Xunit;
@@ -10,6 +11,12 @@ public class OracleTests : DatabaseTests, IClassFixture<OracleTestContextFixture
1011
public OracleTests(OracleTestContextFixture fixture) : base(fixture.DemoContext)
1112
{
1213
}
14+
15+
[Fact(Skip = "Skipping until ORA-01407 is supported")]
16+
public override Task RequiredColumnViolationThrowsCannotInsertNullExceptionThroughExecuteUpdate()
17+
{
18+
return Task.CompletedTask;
19+
}
1320
}
1421

1522
public class OracleTestContextFixture : DemoContextFixture<OracleContainer>

0 commit comments

Comments
 (0)