< Summary

Information
Class: ArturRios.Data.DynamoDb.Repositories.DynamoRepository<T>
Assembly: ArturRios.Data.DynamoDb
File(s): /home/ci/agent/workspace/dotnet-data-docs_main/app/src/ArturRios.Data.DynamoDb/Repositories/DynamoRepository.cs
Line coverage
92%
Covered lines: 72
Uncovered lines: 6
Coverable lines: 78
Total lines: 222
Line coverage: 92.3%
Branch coverage
67%
Covered branches: 19
Total branches: 28
Branch coverage: 67.8%
Method coverage
100%
Covered methods: 18
Fully covered methods: 14
Total methods: 18
Method coverage: 100%
Full method coverage: 77.7%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
.cctor()100%11100%
SaveAsync(...)100%11100%
LoadAsync(...)100%11100%
LoadAsync(...)100%11100%
DeleteAsync(...)100%11100%
QueryAsync(...)100%11100%
QueryAsync(...)100%11100%
ScanAsync(...)100%11100%
SaveManyAsync(...)100%11100%
DeleteManyAsync(...)100%11100%
LoadManyAsync(...)100%11100%
GuardedAsync()100%1166.66%
GuardedProcessAsync()50%2280%
Fail(...)100%22100%
Log(...)100%44100%
Describe(...)66.66%7675%
IsRetryableServiceFault(...)57.14%151485.71%

File(s)

/home/ci/agent/workspace/dotnet-data-docs_main/app/src/ArturRios.Data.DynamoDb/Repositories/DynamoRepository.cs

#LineLine coverage
 1using System.Runtime.CompilerServices;
 2using Amazon.DynamoDBv2.DataModel;
 3using Amazon.DynamoDBv2.DocumentModel;
 4using Amazon.DynamoDBv2.Model;
 5using Amazon.Runtime;
 6using ArturRios.Data.DynamoDb.Interfaces;
 7using ArturRios.Output;
 8using Microsoft.Extensions.Logging;
 9
 10namespace ArturRios.Data.DynamoDb.Repositories;
 11
 12/// <summary>
 13///     DynamoDB implementation of <see cref="IAsyncDynamoRepository{T}" /> over the AWS object-persistence
 14///     model (<see cref="IDynamoDBContext" />). Failures are returned as <see cref="DataOutput{T}" /> /
 15///     <see cref="ProcessOutput" />; a <see cref="ConditionalCheckFailedException" /> (from
 16///     <c>[DynamoDBVersion]</c> optimistic locking) becomes a concurrency error.
 17/// </summary>
 18/// <typeparam name="T">The item type.</typeparam>
 19/// <param name="context">The DynamoDB object-persistence context.</param>
 20/// <param name="logger">
 21///     Optional logger. Envelopes never carry service text, so a failure is otherwise
 22///     undiagnosable: supply a logger and the full exception, plus the item type and the
 23///     repository method that failed, is written at <see cref="LogLevel.Error" />. Item contents
 24///     and key values are never logged. Resolved from DI when logging is registered.
 25/// </param>
 2426public class DynamoRepository<T>(IDynamoDBContext context, ILogger<DynamoRepository<T>>? logger = null)
 27    : IAsyncDynamoRepository<T> where T : class
 28{
 29    /// <summary>Message returned when an operation fails with no finer classification.</summary>
 30    protected const string OperationFailedMessage = "A data-access error occurred.";
 31
 32    /// <summary>Message returned on an optimistic-concurrency conflict.</summary>
 33    protected const string ConcurrencyMessage =
 34        "Concurrency conflict: the item was modified or removed by another process.";
 35
 36    /// <summary>Message returned when the failure is transient and the operation may be retried.</summary>
 37    protected const string TransientMessage = "The data store is temporarily unavailable. Please retry.";
 38
 39    /// <summary>
 40    ///     The DynamoDB batch-write API rejects types with a <c>[DynamoDBVersion]</c> property unless
 41    ///     version checking is explicitly skipped (batch writes have no per-item conditional-check
 42    ///     support). Optimistic concurrency remains enforced on the single-item <see cref="SaveAsync" />/
 43    ///     <see cref="DeleteAsync" /> paths.
 44    /// </summary>
 245    private static readonly BatchWriteConfig BatchSkipVersionCheckConfig = new() { SkipVersionCheck = true };
 46
 47    /// <inheritdoc />
 48    public Task<DataOutput<T>> SaveAsync(T item, CancellationToken ct = default) =>
 3049        GuardedAsync(async () =>
 3050        {
 3051            await context.SaveAsync(item, ct).ConfigureAwait(false);
 3052
 2453            return item;
 5454        });
 55
 56    /// <inheritdoc />
 57    public Task<DataOutput<T?>> LoadAsync(object hashKey, CancellationToken ct = default) =>
 858        GuardedAsync<T?>(async () => await context.LoadAsync<T>(hashKey, ct).ConfigureAwait(false));
 59
 60    /// <inheritdoc />
 61    public Task<DataOutput<T?>> LoadAsync(object hashKey, object rangeKey, CancellationToken ct = default) =>
 1262        GuardedAsync<T?>(async () => await context.LoadAsync<T>(hashKey, rangeKey, ct).ConfigureAwait(false));
 63
 64    /// <inheritdoc />
 65    public Task<ProcessOutput> DeleteAsync(T item, CancellationToken ct = default) =>
 1666        GuardedProcessAsync(async () => await context.DeleteAsync(item, ct).ConfigureAwait(false));
 67
 68    /// <inheritdoc />
 69    public Task<DataOutput<IEnumerable<T>>> QueryAsync(object hashKey, CancellationToken ct = default) =>
 470        GuardedAsync<IEnumerable<T>>(async () => await context.QueryAsync<T>(hashKey).GetRemainingAsync(ct).ConfigureAwa
 71
 72    /// <inheritdoc />
 73    public Task<DataOutput<IEnumerable<T>>> QueryAsync(object hashKey, QueryOperator op,
 74        IEnumerable<object> sortKeyValues, CancellationToken ct = default) =>
 275        GuardedAsync<IEnumerable<T>>(async () =>
 476            await context.QueryAsync<T>(hashKey, op, sortKeyValues).GetRemainingAsync(ct).ConfigureAwait(false));
 77
 78    /// <inheritdoc />
 79    public Task<DataOutput<IEnumerable<T>>> ScanAsync(IEnumerable<ScanCondition> conditions,
 80        CancellationToken ct = default) =>
 481        GuardedAsync<IEnumerable<T>>(async () => await context.ScanAsync<T>(conditions).GetRemainingAsync(ct).ConfigureA
 82
 83    /// <inheritdoc />
 84    public Task<DataOutput<IEnumerable<T>>> SaveManyAsync(IEnumerable<T> items, CancellationToken ct = default) =>
 285        GuardedAsync<IEnumerable<T>>(async () =>
 286        {
 287            var list = items.ToList();
 288            var batch = context.CreateBatchWrite<T>(BatchSkipVersionCheckConfig);
 289            batch.AddPutItems(list);
 290            await batch.ExecuteAsync(ct).ConfigureAwait(false);
 291            return list;
 492        });
 93
 94    /// <inheritdoc />
 95    public Task<ProcessOutput> DeleteManyAsync(IEnumerable<T> items, CancellationToken ct = default) =>
 296        GuardedProcessAsync(async () =>
 297        {
 298            var batch = context.CreateBatchWrite<T>(BatchSkipVersionCheckConfig);
 299            batch.AddDeleteItems(items.ToList());
 2100            await batch.ExecuteAsync(ct).ConfigureAwait(false);
 4101        });
 102
 103    /// <inheritdoc />
 104    public Task<DataOutput<IEnumerable<T>>>
 105        LoadManyAsync(IEnumerable<object> hashKeys, CancellationToken ct = default) =>
 4106        GuardedAsync<IEnumerable<T>>(async () =>
 4107        {
 4108            var batch = context.CreateBatchGet<T>();
 24109            foreach (var key in hashKeys)
 4110            {
 8111                batch.AddKey(key);
 4112            }
 4113
 4114            await batch.ExecuteAsync(ct).ConfigureAwait(false);
 4115
 4116            return batch.Results;
 8117        });
 118
 119    /// <summary>Runs an operation returning data, converting failures to envelope errors.</summary>
 120    /// <param name="operation">The operation to run.</param>
 121    /// <param name="operationName">The calling repository method, used as log context.</param>
 122    protected async Task<DataOutput<TResult>> GuardedAsync<TResult>(Func<Task<TResult>> operation,
 123        [CallerMemberName] string operationName = "")
 124    {
 125        try
 126        {
 52127            return DataOutput<TResult>.New.WithData(await operation().ConfigureAwait(false));
 128        }
 0129        catch (OperationCanceledException)
 130        {
 0131            throw;
 132        }
 6133        catch (Exception ex)
 134        {
 6135            return Fail<TResult>(ex, operationName);
 136        }
 52137    }
 138
 139    /// <summary>Runs an operation with no payload, converting failures to envelope errors.</summary>
 140    /// <param name="operation">The operation to run.</param>
 141    /// <param name="operationName">The calling repository method, used as log context.</param>
 142    protected async Task<ProcessOutput> GuardedProcessAsync(Func<Task> operation,
 143        [CallerMemberName] string operationName = "")
 144    {
 145        try
 146        {
 10147            await operation().ConfigureAwait(false);
 8148            return ProcessOutput.New;
 149        }
 0150        catch (OperationCanceledException)
 151        {
 0152            throw;
 153        }
 2154        catch (Exception ex)
 155        {
 2156            Log(ex, operationName);
 157
 2158            return ex is ConditionalCheckFailedException
 2159                ? ProcessOutput.New.WithError(ConcurrencyMessage)
 2160                : ProcessOutput.New.WithError(Describe(ex));
 161        }
 10162    }
 163
 164    /// <summary>
 165    ///     Logs the failure in full when a logger is configured, and maps it to a data-output error
 166    ///     envelope. Service text names tables, indexes, keys and request ids, so it goes to the
 167    ///     log and never to the caller.
 168    /// </summary>
 169    /// <param name="ex">The exception caught by a guard.</param>
 170    /// <param name="operationName">The repository method that failed, used as log context.</param>
 171    protected DataOutput<TResult> Fail<TResult>(Exception ex, string operationName = "")
 172    {
 6173        Log(ex, operationName);
 174
 6175        return ex switch
 6176        {
 2177            ConditionalCheckFailedException => DataOutput<TResult>.New.WithError(ConcurrencyMessage),
 4178            _ => DataOutput<TResult>.New.WithError(Describe(ex))
 6179        };
 180    }
 181
 182    private void Log(Exception ex, string operationName)
 183    {
 184        // A conditional-check failure is the expected outcome of optimistic locking, not an
 185        // operational fault: logging it at Error would fill the log with routine contention.
 8186        var level = ex is ConditionalCheckFailedException ? LogLevel.Debug : LogLevel.Error;
 187
 8188        logger?.Log(level, ex, "DynamoDB operation failed. Item: {Item}, operation: {Operation}",
 8189            typeof(T).Name, operationName);
 2190    }
 191
 192    /// <summary>Classifies a non-concurrency failure into a caller-safe message.</summary>
 193    protected static string Describe(Exception ex)
 194    {
 24195        for (var current = ex; current is not null; current = current.InnerException)
 196        {
 8197            if (current is TimeoutException || IsRetryableServiceFault(current))
 198            {
 0199                return TransientMessage;
 200            }
 201        }
 202
 4203        return OperationFailedMessage;
 204    }
 205
 206    // Throttling and server faults reach the caller under several exception types (and as a plain
 207    // AmazonDynamoDBException with a throttling error code), so classify on what the SDK reports
 208    // about the response rather than on the exception type alone.
 209    private static bool IsRetryableServiceFault(Exception ex)
 210    {
 8211        if (ex is ProvisionedThroughputExceededException or RequestLimitExceededException
 8212            or InternalServerErrorException)
 213        {
 0214            return true;
 215        }
 216
 8217        return ex is AmazonServiceException service &&
 8218               (service.Retryable is not null ||
 8219                (int)service.StatusCode == 429 ||
 8220                (int)service.StatusCode >= 500);
 221    }
 222}