< Summary

Information
Class: ArturRios.Data.Relational.Core.Repositories.RelationalErrors
Assembly: ArturRios.Data.Relational.Core
File(s): /home/runner/work/dotnet-data/dotnet-data/src/ArturRios.Data.Relational.Core/Repositories/RelationalErrors.cs
Line coverage
84%
Covered lines: 33
Uncovered lines: 6
Coverable lines: 39
Total lines: 127
Line coverage: 84.6%
Branch coverage
57%
Covered branches: 15
Total branches: 26
Branch coverage: 57.6%
Method coverage
83%
Covered methods: 5
Fully covered methods: 4
Total methods: 6
Method coverage: 83.3%
Full method coverage: 66.6%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
IsUniqueViolation(...)0%4260%
Describe(...)85.71%151484.61%
IsUniqueViolation(...)50%22100%
IsIntegrityViolation(...)50%44100%
Matches(...)100%11100%

File(s)

/home/runner/work/dotnet-data/dotnet-data/src/ArturRios.Data.Relational.Core/Repositories/RelationalErrors.cs

#LineLine coverage
 1using System.Data.Common;
 2using Microsoft.EntityFrameworkCore;
 3
 4namespace ArturRios.Data.Relational.Core.Repositories;
 5
 6/// <summary>
 7///     Provider-independent classification of relational failures, and the caller-safe
 8///     messages that replace provider text. Provider messages name constraints, indexes,
 9///     columns, SQL fragments and conflicting values, so they are used for classification
 10///     only and are never returned to the caller. The full exception is still logged by
 11///     EF Core's own diagnostics, where operators can read it.
 12/// </summary>
 13public static class RelationalErrors
 14{
 15    /// <summary>Message returned when an optimistic-concurrency conflict is detected.</summary>
 16    public const string ConcurrencyMessage =
 17        "Concurrency conflict: the record was modified or removed by another process.";
 18
 19    /// <summary>Message returned when a write violates a unique constraint.</summary>
 20    public const string UniqueViolationMessage = "Conflict: a record with the same unique value already exists.";
 21
 22    /// <summary>Message returned when a write violates a non-unique integrity rule.</summary>
 23    public const string IntegrityViolationMessage = "Conflict: the operation violates a data-integrity rule.";
 24
 25    /// <summary>Message returned when the failure is transient and the operation may be retried.</summary>
 26    public const string TransientMessage = "The data store is temporarily unavailable. Please retry.";
 27
 28    /// <summary>Message returned when the failure has no caller-actionable classification.</summary>
 29    public const string GenericMessage = "A data-access error occurred.";
 30
 31    // Markers for a unique/duplicate-key violation, for providers that report no SQLSTATE
 32    // or a class-level one (MySQL reports 23000 for every integrity violation).
 233    private static readonly string[] UniqueViolationMarkers =
 234    [
 235        "unique constraint", // PostgreSQL, Oracle, SQLite ("UNIQUE constraint failed: ...")
 236        "unique index", // SQL Server, SQLite
 237        "unique key", // SQL Server ("Violation of UNIQUE KEY constraint ...")
 238        "duplicate key", // SQL Server
 239        "duplicate entry" // MySQL / MariaDB
 240    ];
 41
 42    // Markers for the remaining integrity violations: foreign key, not null, check.
 243    private static readonly string[] IntegrityViolationMarkers =
 244    [
 245        "constraint failed", // SQLite ("FOREIGN KEY constraint failed", "NOT NULL ...")
 246        "foreign key",
 247        "check constraint",
 248        "not-null",
 249        "cannot be null",
 250        "null value in column"
 251    ];
 52
 53    /// <summary>
 54    ///     Detects a unique-constraint (duplicate-key) violation anywhere in the exception chain.
 55    ///     Recognised by SQLSTATE where the provider reports one, and by provider-neutral
 56    ///     duplicate-key wording otherwise.
 57    /// </summary>
 58    /// <param name="ex">The exception caught by a repository or query guard.</param>
 59    /// <returns><c>true</c> when the failure is a unique-constraint violation.</returns>
 60    public static bool IsUniqueViolation(Exception ex)
 61    {
 062        for (var current = ex; current is not null; current = current.InnerException)
 63        {
 064            if (current is DbException db && IsUniqueViolation(db))
 65            {
 066                return true;
 67            }
 68        }
 69
 070        return false;
 71    }
 72
 73    /// <summary>
 74    ///     Classifies a failure and returns the caller-safe message for it. Never includes
 75    ///     provider text.
 76    /// </summary>
 77    /// <param name="ex">The exception caught by a repository or query guard.</param>
 78    /// <returns>One of the caller-safe messages exposed by this class.</returns>
 79    public static string Describe(Exception ex)
 80    {
 3881        if (ex is DbUpdateConcurrencyException)
 82        {
 483            return ConcurrencyMessage;
 84        }
 85
 12886        for (var current = ex; current is not null; current = current.InnerException)
 87        {
 4088            if (current is TimeoutException)
 89            {
 090                return TransientMessage;
 91            }
 92
 4093            if (current is not DbException db)
 94            {
 95                continue;
 96            }
 97
 2098            if (IsUniqueViolation(db))
 99            {
 8100                return UniqueViolationMessage;
 101            }
 102
 12103            if (IsIntegrityViolation(db))
 104            {
 2105                return IntegrityViolationMessage;
 106            }
 107
 10108            if (db.IsTransient)
 109            {
 0110                return TransientMessage;
 111            }
 112        }
 113
 24114        return GenericMessage;
 115    }
 116
 117    private static bool IsUniqueViolation(DbException db) =>
 20118        db.SqlState == "23505" || // PostgreSQL unique_violation
 20119        Matches(db.Message, UniqueViolationMarkers);
 120
 121    private static bool IsIntegrityViolation(DbException db) =>
 12122        db.SqlState?.StartsWith("23", StringComparison.Ordinal) == true || // SQLSTATE integrity class
 12123        Matches(db.Message, IntegrityViolationMarkers);
 124
 125    private static bool Matches(string message, string[] markers) =>
 170126        markers.Any(marker => message.Contains(marker, StringComparison.OrdinalIgnoreCase));
 127}