• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Giorgi / EntityFramework.Exceptions / 23549203342

25 Mar 2026 03:28PM UTC coverage: 91.209% (-0.2%) from 91.429%
23549203342

push

github

Giorgi
Do not process low-level command failed when the operation is SaveChanges. Fixes #96

128 of 151 branches covered (84.77%)

Branch coverage included in aggregate %.

17 of 19 new or added lines in 2 files covered. (89.47%)

1 existing line in 1 file now uncovered.

204 of 213 relevant lines covered (95.77%)

36.45 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.61
/EntityFramework.Exceptions/Common/ExceptionProcessorInterceptor.cs
1
using System;
2
using System.Collections.Generic;
3
using System.Data.Common;
4
using System.Diagnostics;
5
using System.Linq;
6
using System.Threading;
7
using System.Threading.Tasks;
8
using DbExceptionClassifier.Common;
9
using Microsoft.EntityFrameworkCore;
10
using Microsoft.EntityFrameworkCore.Diagnostics;
11

12
namespace EntityFramework.Exceptions.Common;
13

14
public abstract class ExceptionProcessorInterceptor<TProviderException>(IDbExceptionClassifier exceptionClassifier)
10✔
15
    : IDbCommandInterceptor, ISaveChangesInterceptor where TProviderException : DbException
16
{
17
    private List<IndexDetails> uniqueIndexDetailsList;
18
    private List<ForeignKeyDetails> foreignKeyDetailsList;
19

20
    protected internal enum DatabaseError
21
    {
22
        UniqueConstraint,
23
        CannotInsertNull,
24
        MaxLength,
25
        NumericOverflow,
26
        ReferenceConstraint,
27
        Deadlock,
28
    }
29

30
    /// <inheritdoc />
31
    public void SaveChangesFailed(DbContextErrorEventData eventData)
32
    {
39✔
33
        ProcessException(eventData.Exception, eventData.Context);
39✔
34
    }
5✔
35

36
    /// <inheritdoc />
37
    public Task SaveChangesFailedAsync(DbContextErrorEventData eventData, CancellationToken cancellationToken = new CancellationToken())
38
    {
39✔
39
        ProcessException(eventData.Exception, eventData.Context);
39✔
40
        return Task.CompletedTask;
5✔
41
    }
5✔
42

43
    /// <inheritdoc />
44
    public void CommandFailed(DbCommand command, CommandErrorEventData eventData)
45
    {
58✔
46
        // Skip SaveChanges commands — CommandFailed fires before SaveChangesFailed and only receives
47
        // the raw provider exception (no DbUpdateException, no Entries). Let SaveChangesFailed handle
48
        // these so the typed exception preserves the full exception chain and entity entries.
49
        if (eventData.CommandSource != CommandSource.SaveChanges)
58✔
50
        {
27✔
51
            ProcessException(eventData.Exception, eventData.Context);
27✔
NEW
52
        }
×
53
    }
31✔
54

55
    /// <inheritdoc />
56
    public Task CommandFailedAsync(DbCommand command, CommandErrorEventData eventData, CancellationToken cancellationToken = new CancellationToken())
57
    {
63✔
58
        // See comment in CommandFailed.
59
        if (eventData.CommandSource != CommandSource.SaveChanges)
63✔
60
        {
32✔
61
            ProcessException(eventData.Exception, eventData.Context);
32✔
NEW
62
        }
×
63

64
        return Task.CompletedTask;
31✔
65
    }
31✔
66

67
    protected DatabaseError? GetDatabaseError(TProviderException dbException)
68
    {
137✔
69
        if (exceptionClassifier.IsMaxLengthExceededError(dbException)) return DatabaseError.MaxLength;
153✔
70
        if (exceptionClassifier.IsNumericOverflowError(dbException)) return DatabaseError.NumericOverflow;
133✔
71
        if (exceptionClassifier.IsCannotInsertNullError(dbException)) return DatabaseError.CannotInsertNull;
129✔
72
        if (exceptionClassifier.IsUniqueConstraintError(dbException)) return DatabaseError.UniqueConstraint;
123✔
73
        if (exceptionClassifier.IsReferenceConstraintError(dbException)) return DatabaseError.ReferenceConstraint;
95✔
74
        if (exceptionClassifier.IsDeadlockError(dbException)) return DatabaseError.Deadlock;
20✔
75

76
        return null;
10✔
77
    }
137✔
78

79
    [StackTraceHidden]
80
    private void ProcessException(Exception eventException, DbContext eventContext)
81
    {
137✔
82
        if (eventException?.GetBaseException() is not TProviderException providerException) return;
137!
83

84
        var error = GetDatabaseError(providerException);
137✔
85

86
        if (error == null) return;
147✔
87

88
        var updateException = eventException as DbUpdateException;
127✔
89
        var exception = ExceptionFactory.Create(error.Value, providerException, updateException?.Entries);
127✔
90

91
        switch (exception)
127✔
92
        {
93
            case UniqueConstraintException uniqueConstraint when eventContext != null:
34✔
94
                SetConstraintDetails(eventContext, uniqueConstraint, providerException);
34✔
95
                break;
34✔
96
            case ReferenceConstraintException referenceConstraint when eventContext != null:
40✔
97
                SetConstraintDetails(eventContext, referenceConstraint, providerException);
40✔
98
                break;
40✔
99
        }
100

101
        throw exception;
127✔
102
    }
10✔
103

104
    private void SetConstraintDetails(DbContext context, UniqueConstraintException exception, TProviderException providerException)
105
    {
34✔
106
        if (uniqueIndexDetailsList == null)
34✔
107
        {
7✔
108
            var indexes = context.Model.GetEntityTypes().SelectMany(x => x.GetDeclaredIndexes().Where(index => index.IsUnique));
50✔
109

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

113
            var primaryKeys = context.Model.GetEntityTypes().SelectMany(x =>
7✔
114
            {
24✔
115
                var primaryKey = x.FindPrimaryKey();
24✔
116
                if (primaryKey is null)
24!
117
                {
×
118
                    return Array.Empty<IndexDetails>();
×
119
                }
7✔
120

7✔
121
                var primaryKeyName = primaryKey.GetName();
24✔
122

7✔
123
                if (primaryKeyName is null)
24!
124
                {
×
125
                    return Array.Empty<IndexDetails>();
×
126
                }
7✔
127

7✔
128
                return [new IndexDetails(primaryKeyName, x.GetSchemaQualifiedTableName(), primaryKey.Properties)];
24✔
129
            });
31✔
130

131
            uniqueIndexDetailsList = mappedIndexes
7✔
132
                .Union(primaryKeys)
7✔
133
                .ToList();
7✔
134
        }
7✔
135

136
        var matchingIndexes = uniqueIndexDetailsList.Where(index => providerException.Message.Contains(index.Name, StringComparison.OrdinalIgnoreCase)).ToList();
200✔
137
        var match = matchingIndexes.Count == 1 ? matchingIndexes[0] : matchingIndexes.FirstOrDefault(index => providerException.Message.Contains(index.SchemaQualifiedTableName, StringComparison.OrdinalIgnoreCase));
36✔
138

139
        if (match != null)
34✔
140
        {
26✔
141
            exception.ConstraintName = match.Name;
26✔
142
            exception.ConstraintProperties = match.Properties.Select(property => property.Name).ToList();
52✔
143
            exception.SchemaQualifiedTableName = match.SchemaQualifiedTableName;
26✔
144
        }
26✔
145
    }
34✔
146

147
    private void SetConstraintDetails(DbContext context, ReferenceConstraintException exception, TProviderException providerException)
148
    {
40✔
149
        if (foreignKeyDetailsList == null)
40✔
150
        {
5✔
151
            var keys = context.Model.GetEntityTypes().SelectMany(x => x.GetDeclaredForeignKeys());
25✔
152

153
            var mappedConstraints = keys.SelectMany(index => index.GetMappedConstraints(), (index, constraint) => new { constraint, index.Properties });
25✔
154

155
            foreignKeyDetailsList = mappedConstraints.Select(arg => new ForeignKeyDetails(arg.constraint.Name, arg.constraint.Table.SchemaQualifiedName, arg.Properties)).ToList();
15✔
156
        }
5✔
157

158
        var matchingForeignKeys = foreignKeyDetailsList.Where(foreignKey => providerException.Message.Contains(foreignKey.Name, StringComparison.OrdinalIgnoreCase)).ToList();
120✔
159
        var match = matchingForeignKeys.Count == 1 ? matchingForeignKeys[0] : matchingForeignKeys.FirstOrDefault(foreignKey => providerException.Message.Contains(foreignKey.SchemaQualifiedTableName, StringComparison.OrdinalIgnoreCase));
40✔
160

161
        if (match != null)
40✔
162
        {
32✔
163
            exception.ConstraintName = match.Name;
32✔
164
            exception.ConstraintProperties = match.Properties.Select(property => property.Name).ToList();
64✔
165
            exception.SchemaQualifiedTableName = match.SchemaQualifiedTableName;
32✔
166
        }
32✔
167
    }
40✔
168
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc