< Summary

Information
Class: ArturRios.Data.Dapper.DapperSqlQuery
Assembly: ArturRios.Data.Dapper
File(s): /home/runner/work/dotnet-data/dotnet-data/src/ArturRios.Data.Dapper/DapperSqlQuery.cs
Line coverage
84%
Covered lines: 22
Uncovered lines: 4
Coverable lines: 26
Total lines: 125
Line coverage: 84.6%
Branch coverage
100%
Covered branches: 4
Total branches: 4
Branch coverage: 100%
Method coverage
100%
Covered methods: 15
Fully covered methods: 13
Total methods: 15
Method coverage: 100%
Full method coverage: 86.6%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_Connection()100%11100%
get_Transaction()100%22100%
QueryAsync(...)100%11100%
QueryFirstOrDefaultAsync(...)100%11100%
QuerySingleOrDefaultAsync(...)100%11100%
ExecuteScalarAsync(...)100%11100%
Query(...)100%11100%
QueryFirstOrDefault(...)100%11100%
QuerySingleOrDefault(...)100%11100%
ExecuteScalar(...)100%11100%
Guarded(...)100%1166.66%
Command(...)100%11100%
GuardedAsync()100%1166.66%
Fail(...)100%22100%

File(s)

/home/runner/work/dotnet-data/dotnet-data/src/ArturRios.Data.Dapper/DapperSqlQuery.cs

#LineLine coverage
 1using System.Data.Common;
 2using ArturRios.Data.Relational.Core.Configuration;
 3using ArturRios.Data.Relational.Core.Repositories;
 4using ArturRios.Output;
 5using Dapper;
 6using Microsoft.EntityFrameworkCore;
 7using Microsoft.EntityFrameworkCore.Storage;
 8using Microsoft.Extensions.Logging;
 9
 10namespace ArturRios.Data.Dapper;
 11
 12/// <summary>
 13///     Dapper-backed read-only query executor. Runs against the <see cref="BaseDbContext" />'s
 14///     connection and enlists in its ambient transaction, so Dapper reads and EF writes share one
 15///     connection and one unit-of-work transaction. Failures are returned as <see cref="DataOutput{T}" />.
 16/// </summary>
 17/// <param name="context">The application's <see cref="BaseDbContext" />.</param>
 18/// <param name="logger">
 19///     Optional logger. Envelopes never carry provider text, so a query failure is otherwise
 20///     undiagnosable: supply a logger and the full exception, plus the SQL that produced it, is
 21///     written at <see cref="LogLevel.Error" />. Parameter values are never logged - they are the
 22///     part most likely to hold personal data. Resolved from DI when logging is registered.
 23/// </param>
 4224public class DapperSqlQuery(BaseDbContext context, ILogger<DapperSqlQuery>? logger = null)
 25    : ISqlQuery, IAsyncSqlQuery
 26{
 27    /// <summary>Message returned when a query fails with no finer classification.</summary>
 28    protected const string QueryFailedMessage = RelationalErrors.GenericMessage;
 29
 30    /// <summary>The context's underlying database connection.</summary>
 4231    protected DbConnection Connection => context.Database.GetDbConnection();
 32
 33    /// <summary>The ambient database transaction, or <see langword="null" /> when none is active.</summary>
 4234    protected DbTransaction? Transaction => context.Database.CurrentTransaction?.GetDbTransaction();
 35
 36    /// <inheritdoc />
 37    public Task<DataOutput<IEnumerable<T>>> QueryAsync<T>(string sql, object? parameters = null,
 38        CancellationToken ct = default) =>
 1639        GuardedAsync(sql, async () => await Connection.QueryAsync<T>(Command(sql, parameters, ct)).ConfigureAwait(false)
 40
 41    /// <inheritdoc />
 42    public Task<DataOutput<T?>> QueryFirstOrDefaultAsync<T>(string sql, object? parameters = null,
 43        CancellationToken ct = default) =>
 444        GuardedAsync(sql, async () => await Connection.QueryFirstOrDefaultAsync<T?>(Command(sql, parameters, ct)).Config
 45
 46    /// <inheritdoc />
 47    public Task<DataOutput<T?>> QuerySingleOrDefaultAsync<T>(string sql, object? parameters = null,
 48        CancellationToken ct = default) =>
 449        GuardedAsync(sql, async () => await Connection.QuerySingleOrDefaultAsync<T?>(Command(sql, parameters, ct)).Confi
 50
 51    /// <inheritdoc />
 52    public Task<DataOutput<T?>> ExecuteScalarAsync<T>(string sql, object? parameters = null,
 53        CancellationToken ct = default) =>
 454        GuardedAsync(sql, async () => await Connection.ExecuteScalarAsync<T?>(Command(sql, parameters, ct)).ConfigureAwa
 55
 56    /// <inheritdoc />
 57    public DataOutput<IEnumerable<T>> Query<T>(string sql, object? parameters = null) =>
 2858        Guarded(sql, () => Connection.Query<T>(sql, parameters, Transaction));
 59
 60    /// <inheritdoc />
 61    public DataOutput<T?> QueryFirstOrDefault<T>(string sql, object? parameters = null) =>
 862        Guarded(sql, () => Connection.QueryFirstOrDefault<T?>(sql, parameters, Transaction));
 63
 64    /// <inheritdoc />
 65    public DataOutput<T?> QuerySingleOrDefault<T>(string sql, object? parameters = null) =>
 466        Guarded(sql, () => Connection.QuerySingleOrDefault<T?>(sql, parameters, Transaction));
 67
 68    /// <inheritdoc />
 69    public DataOutput<T?> ExecuteScalar<T>(string sql, object? parameters = null) =>
 1670        Guarded(sql, () => Connection.ExecuteScalar<T?>(sql, parameters, Transaction));
 71
 72    /// <summary>Runs a synchronous query, converting failures to envelope errors.</summary>
 73    /// <param name="sql">The SQL being run, used as log context when a logger is configured.</param>
 74    /// <param name="operation">The query to run.</param>
 75    protected DataOutput<TResult> Guarded<TResult>(string sql, Func<TResult> operation)
 76    {
 77        try
 78        {
 2879            return DataOutput<TResult>.New.WithData(operation());
 80        }
 081        catch (OperationCanceledException)
 82        {
 083            throw;
 84        }
 1485        catch (Exception ex)
 86        {
 1487            return Fail<TResult>(ex, sql);
 88        }
 2889    }
 90
 91    /// <summary>Builds a Dapper command carrying the ambient transaction and cancellation token.</summary>
 92    private CommandDefinition Command(string sql, object? parameters, CancellationToken ct) =>
 1493        new(sql, parameters, Transaction, cancellationToken: ct);
 94
 95    /// <summary>Runs an asynchronous query, converting failures to envelope errors.</summary>
 96    /// <param name="sql">The SQL being run, used as log context when a logger is configured.</param>
 97    /// <param name="operation">The query to run.</param>
 98    protected async Task<DataOutput<TResult>> GuardedAsync<TResult>(string sql, Func<Task<TResult>> operation)
 99    {
 100        try
 101        {
 14102            return DataOutput<TResult>.New.WithData(await operation().ConfigureAwait(false));
 103        }
 0104        catch (OperationCanceledException)
 105        {
 0106            throw;
 107        }
 4108        catch (Exception ex)
 109        {
 4110            return Fail<TResult>(ex, sql);
 111        }
 14112    }
 113
 114    /// <summary>
 115    ///     Logs the failure in full when a logger is configured, and returns the caller-safe
 116    ///     envelope. Provider text names constraints, columns and SQL fragments, so it goes to
 117    ///     the log and never to the caller.
 118    /// </summary>
 119    private DataOutput<TResult> Fail<TResult>(Exception ex, string sql)
 120    {
 18121        logger?.LogError(ex, "Dapper query failed. SQL: {Sql}", sql);
 122
 18123        return DataOutput<TResult>.New.WithError(RelationalErrors.Describe(ex));
 124    }
 125}