< Summary

Information
Class: Elsa.Persistence.EFCore.DbExceptionClassifier
Assembly: Elsa.Persistence.EFCore.Common
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Persistence.EFCore.Common/DbExceptionClassifier.cs
Line coverage
51%
Covered lines: 50
Uncovered lines: 47
Coverable lines: 97
Total lines: 173
Line coverage: 51.5%
Branch coverage
48%
Covered branches: 46
Total branches: 94
Branch coverage: 48.9%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%210%
IsTransient(...)50%5460%
IsDuplicateKey(...)100%11100%
IsSqlServerTransientException(...)0%2040%
IsMySqlTransientException(...)0%7280%
IsDuplicateKeyException(...)38.88%663671.42%
IsSqlClientException(...)75%44100%
GetErrorNumbers()87.5%171685.71%
GetIntProperty(...)66.66%141275%
HasProperty(...)100%210%
GetStringProperty(...)50%22100%
EnumerateExceptions()50%9875%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Persistence.EFCore.Common/DbExceptionClassifier.cs

#LineLine coverage
 1namespace Elsa.Persistence.EFCore;
 2
 3internal static class DbExceptionClassifier
 4{
 05    private static readonly HashSet<int> SqlServerTransientErrorNumbers =
 06    [
 07        -2,
 08        64,
 09        233,
 010        1205,
 011        4060,
 012        10928,
 013        10929,
 014        40197,
 015        40501,
 016        40613,
 017        49918,
 018        49919,
 019        49920,
 020    ];
 21
 022    private static readonly HashSet<int> MySqlTransientErrorNumbers =
 023    [
 024        1205, // ER_LOCK_WAIT_TIMEOUT
 025        1213, // ER_LOCK_DEADLOCK
 026    ];
 27
 28    public static bool IsTransient(string providerName, Exception exception)
 29    {
 1130        if (providerName.Contains("SqlServer", StringComparison.OrdinalIgnoreCase))
 031            return EnumerateExceptions(exception).Any(IsSqlServerTransientException);
 32
 1133        if (providerName.Contains("MySql", StringComparison.OrdinalIgnoreCase))
 034            return EnumerateExceptions(exception).Any(IsMySqlTransientException);
 35
 1136        return false;
 37    }
 38
 39    public static bool IsDuplicateKey(Exception exception)
 40    {
 241        return EnumerateExceptions(exception).Any(IsDuplicateKeyException);
 42    }
 43
 44    private static bool IsSqlServerTransientException(Exception exception)
 45    {
 046        if (!IsSqlClientException(exception))
 047            return false;
 48
 049        return GetErrorNumbers(exception).Any(SqlServerTransientErrorNumbers.Contains)
 050               || exception.Message.Contains("deadlock", StringComparison.OrdinalIgnoreCase);
 51    }
 52
 53    private static bool IsMySqlTransientException(Exception exception)
 54    {
 055        var type = exception.GetType();
 056        var typeNamespace = type.Namespace ?? string.Empty;
 057        var isMySqlException = type.Name.Equals("MySqlException", StringComparison.OrdinalIgnoreCase)
 058                               && (typeNamespace.Equals("MySqlConnector", StringComparison.OrdinalIgnoreCase)
 059                                   || typeNamespace.Equals("MySql.Data.MySqlClient", StringComparison.OrdinalIgnoreCase)
 60
 061        return isMySqlException && GetErrorNumbers(exception).Any(MySqlTransientErrorNumbers.Contains);
 62    }
 63
 64    private static bool IsDuplicateKeyException(Exception exception)
 65    {
 466        var type = exception.GetType();
 467        var typeName = type.Name;
 468        var typeNamespace = type.Namespace ?? string.Empty;
 469        var errorNumbers = GetErrorNumbers(exception).ToList();
 70
 671        if (IsSqlClientException(exception) && errorNumbers.Any(number => number is 2601 or 2627))
 272            return true;
 73
 274        if (typeName.Contains("MySql", StringComparison.OrdinalIgnoreCase) && errorNumbers.Contains(1062))
 075            return true;
 76
 277        if (typeName.Contains("Sqlite", StringComparison.OrdinalIgnoreCase))
 78        {
 79            // Microsoft.Data.Sqlite exposes the extended result code when it is
 80            // available. It must take precedence over the base code: a base 19
 81            // can describe a non-duplicate constraint such as NOT NULL.
 082            if (HasProperty(exception, "SqliteExtendedErrorCode"))
 083                return GetIntProperty(exception, "SqliteExtendedErrorCode") is 1555 or 2067;
 84
 085            return GetIntProperty(exception, "SqliteErrorCode") == 19;
 86        }
 87
 288        if (typeName.Contains("Oracle", StringComparison.OrdinalIgnoreCase) && errorNumbers.Contains(1))
 089            return true;
 90
 291        if (GetStringProperty(exception, "SqlState") == "23505")
 092            return true;
 93
 294        return typeNamespace.Contains("Data", StringComparison.OrdinalIgnoreCase)
 295               && (exception.Message.Contains("duplicate key", StringComparison.OrdinalIgnoreCase)
 296               || exception.Message.Contains("unique constraint", StringComparison.OrdinalIgnoreCase)
 297               || exception.Message.Contains("UNIQUE constraint failed", StringComparison.OrdinalIgnoreCase)
 298               || exception.Message.Contains("ORA-00001", StringComparison.OrdinalIgnoreCase));
 99    }
 100
 101    private static bool IsSqlClientException(Exception exception)
 102    {
 4103        var type = exception.GetType();
 4104        return type.Name.Equals("SqlException", StringComparison.OrdinalIgnoreCase)
 4105               && type.Namespace?.Contains("SqlClient", StringComparison.OrdinalIgnoreCase) == true;
 106    }
 107
 108    private static IEnumerable<int> GetErrorNumbers(object source)
 109    {
 4110        if (GetIntProperty(source, "Number") is { } number)
 2111            yield return number;
 112
 4113        if (GetIntProperty(source, "SqliteErrorCode") is { } sqliteErrorCode)
 0114            yield return sqliteErrorCode;
 115
 4116        if (GetIntProperty(source, "SqliteExtendedErrorCode") is { } sqliteExtendedErrorCode)
 0117            yield return sqliteExtendedErrorCode;
 118
 4119        var errors = source.GetType().GetProperty("Errors")?.GetValue(source);
 4120        if (errors is not System.Collections.IEnumerable errorCollection)
 2121            yield break;
 122
 12123        foreach (var error in errorCollection)
 124        {
 4125            if (error is null)
 126                continue;
 127
 4128            if (GetIntProperty(error, "Number") is { } errorNumber)
 4129                yield return errorNumber;
 130        }
 2131    }
 132
 133    private static int? GetIntProperty(object source, string name)
 134    {
 16135        var value = source.GetType().GetProperty(name)?.GetValue(source);
 16136        return value switch
 16137        {
 6138            int number => number,
 0139            short number => number,
 0140            long number when number is >= int.MinValue and <= int.MaxValue => (int)number,
 10141            _ => null
 16142        };
 143    }
 144
 0145    private static bool HasProperty(object source, string name) => source.GetType().GetProperty(name) is not null;
 146
 147    private static string? GetStringProperty(object source, string name)
 148    {
 2149        return source.GetType().GetProperty(name)?.GetValue(source) as string;
 150    }
 151
 152    private static IEnumerable<Exception> EnumerateExceptions(Exception exception)
 153    {
 2154        var stack = new Stack<Exception>();
 2155        stack.Push(exception);
 156
 4157        while (stack.Count > 0)
 158        {
 4159            var current = stack.Pop();
 4160            yield return current;
 161
 2162            if (current is AggregateException aggregateException)
 163            {
 0164                foreach (var inner in aggregateException.InnerExceptions)
 0165                    stack.Push(inner);
 166            }
 2167            else if (current.InnerException is not null)
 168            {
 2169                stack.Push(current.InnerException);
 170            }
 2171        }
 0172    }
 173}