| | | 1 | | using System.Data.Common; |
| | | 2 | | using ArturRios.Data.Relational.Core.Configuration; |
| | | 3 | | using ArturRios.Output; |
| | | 4 | | using Dapper; |
| | | 5 | | using Microsoft.EntityFrameworkCore; |
| | | 6 | | using Microsoft.EntityFrameworkCore.Storage; |
| | | 7 | | |
| | | 8 | | namespace ArturRios.Data.Dapper; |
| | | 9 | | |
| | | 10 | | /// <summary> |
| | | 11 | | /// Dapper-backed read-only query executor. Runs against the <see cref="BaseDbContext" />'s |
| | | 12 | | /// connection and enlists in its ambient transaction, so Dapper reads and EF writes share one |
| | | 13 | | /// connection and one unit-of-work transaction. Failures are returned as <see cref="DataOutput{T}" />. |
| | | 14 | | /// </summary> |
| | | 15 | | /// <param name="context">The application's <see cref="BaseDbContext" />.</param> |
| | 15 | 16 | | public class DapperSqlQuery(BaseDbContext context) : ISqlQuery, IAsyncSqlQuery |
| | | 17 | | { |
| | | 18 | | /// <summary>Message prefix returned when a query fails.</summary> |
| | | 19 | | protected const string QueryFailedMessage = "A data-access error occurred:"; |
| | | 20 | | |
| | | 21 | | /// <summary>The context's underlying database connection.</summary> |
| | 14 | 22 | | protected DbConnection Connection => context.Database.GetDbConnection(); |
| | | 23 | | |
| | | 24 | | /// <summary>The ambient database transaction, or <see langword="null" /> when none is active.</summary> |
| | 14 | 25 | | protected DbTransaction? Transaction => context.Database.CurrentTransaction?.GetDbTransaction(); |
| | | 26 | | |
| | | 27 | | /// <inheritdoc /> |
| | | 28 | | public Task<DataOutput<IEnumerable<T>>> QueryAsync<T>(string sql, object? parameters = null, |
| | | 29 | | CancellationToken ct = default) => |
| | 8 | 30 | | GuardedAsync(async () => await Connection.QueryAsync<T>(Command(sql, parameters, ct))); |
| | | 31 | | |
| | | 32 | | /// <inheritdoc /> |
| | | 33 | | public Task<DataOutput<T?>> QueryFirstOrDefaultAsync<T>(string sql, object? parameters = null, |
| | | 34 | | CancellationToken ct = default) => |
| | 2 | 35 | | GuardedAsync(async () => await Connection.QueryFirstOrDefaultAsync<T?>(Command(sql, parameters, ct))); |
| | | 36 | | |
| | | 37 | | /// <inheritdoc /> |
| | | 38 | | public Task<DataOutput<T?>> QuerySingleOrDefaultAsync<T>(string sql, object? parameters = null, |
| | | 39 | | CancellationToken ct = default) => |
| | 2 | 40 | | GuardedAsync(async () => await Connection.QuerySingleOrDefaultAsync<T?>(Command(sql, parameters, ct))); |
| | | 41 | | |
| | | 42 | | /// <inheritdoc /> |
| | | 43 | | public Task<DataOutput<T?>> ExecuteScalarAsync<T>(string sql, object? parameters = null, |
| | | 44 | | CancellationToken ct = default) => |
| | 2 | 45 | | GuardedAsync(async () => await Connection.ExecuteScalarAsync<T?>(Command(sql, parameters, ct))); |
| | | 46 | | |
| | | 47 | | /// <inheritdoc /> |
| | | 48 | | public DataOutput<IEnumerable<T>> Query<T>(string sql, object? parameters = null) => |
| | 6 | 49 | | Guarded(() => Connection.Query<T>(sql, parameters, Transaction)); |
| | | 50 | | |
| | | 51 | | /// <inheritdoc /> |
| | | 52 | | public DataOutput<T?> QueryFirstOrDefault<T>(string sql, object? parameters = null) => |
| | 4 | 53 | | Guarded(() => Connection.QueryFirstOrDefault<T?>(sql, parameters, Transaction)); |
| | | 54 | | |
| | | 55 | | /// <inheritdoc /> |
| | | 56 | | public DataOutput<T?> QuerySingleOrDefault<T>(string sql, object? parameters = null) => |
| | 2 | 57 | | Guarded(() => Connection.QuerySingleOrDefault<T?>(sql, parameters, Transaction)); |
| | | 58 | | |
| | | 59 | | /// <inheritdoc /> |
| | | 60 | | public DataOutput<T?> ExecuteScalar<T>(string sql, object? parameters = null) => |
| | 2 | 61 | | Guarded(() => Connection.ExecuteScalar<T?>(sql, parameters, Transaction)); |
| | | 62 | | |
| | | 63 | | /// <summary>Runs a synchronous query, converting failures to envelope errors.</summary> |
| | | 64 | | protected static DataOutput<TResult> Guarded<TResult>(Func<TResult> operation) |
| | 7 | 65 | | { |
| | | 66 | | try |
| | 7 | 67 | | { |
| | 7 | 68 | | return DataOutput<TResult>.New.WithData(operation()); |
| | | 69 | | } |
| | 0 | 70 | | catch (OperationCanceledException) |
| | 0 | 71 | | { |
| | 0 | 72 | | throw; |
| | | 73 | | } |
| | 2 | 74 | | catch (Exception ex) |
| | 2 | 75 | | { |
| | 2 | 76 | | return DataOutput<TResult>.New.WithError($"{QueryFailedMessage} {ex.GetBaseException().Message}"); |
| | | 77 | | } |
| | 7 | 78 | | } |
| | | 79 | | |
| | | 80 | | /// <summary>Builds a Dapper command carrying the ambient transaction and cancellation token.</summary> |
| | | 81 | | private CommandDefinition Command(string sql, object? parameters, CancellationToken ct) => |
| | 7 | 82 | | new(sql, parameters, Transaction, cancellationToken: ct); |
| | | 83 | | |
| | | 84 | | /// <summary>Runs an asynchronous query, converting failures to envelope errors.</summary> |
| | | 85 | | protected static async Task<DataOutput<TResult>> GuardedAsync<TResult>(Func<Task<TResult>> operation) |
| | 7 | 86 | | { |
| | | 87 | | try |
| | 7 | 88 | | { |
| | 7 | 89 | | return DataOutput<TResult>.New.WithData(await operation()); |
| | | 90 | | } |
| | 0 | 91 | | catch (OperationCanceledException) |
| | 0 | 92 | | { |
| | 0 | 93 | | throw; |
| | | 94 | | } |
| | 2 | 95 | | catch (Exception ex) |
| | 2 | 96 | | { |
| | 2 | 97 | | return DataOutput<TResult>.New.WithError($"{QueryFailedMessage} {ex.GetBaseException().Message}"); |
| | | 98 | | } |
| | 7 | 99 | | } |
| | | 100 | | } |