< Summary

Information
Class: ArturRios.Data.MongoDb.Repositories.MongoDocumentRepository<T>
Assembly: ArturRios.Data.MongoDb
File(s): /home/runner/work/dotnet-data/dotnet-data/src/ArturRios.Data.MongoDb/Repositories/MongoDocumentRepository.cs
Line coverage
73%
Covered lines: 130
Uncovered lines: 48
Coverable lines: 178
Total lines: 403
Line coverage: 73%
Branch coverage
63%
Covered branches: 28
Total branches: 44
Branch coverage: 63.6%
Method coverage
94%
Covered methods: 36
Fully covered methods: 29
Total methods: 38
Method coverage: 94.7%
Full method coverage: 76.3%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
.cctor()50%44100%
get_Collection()100%11100%
get_Session()100%11100%
GetAllAsync(...)100%11100%
GetByIdAsync(...)100%11100%
FindAsync(...)100%11100%
CreateAsync(...)100%11100%
CreateRangeAsync(...)100%11100%
UpdateAsync(...)100%11100%
UpdateRangeAsync(...)100%210%
DeleteAsync(...)100%11100%
DeleteRangeAsync(...)100%11100%
Query()100%11100%
GetAll()100%11100%
GetById(...)100%11100%
Find(...)100%11100%
Create(...)100%11100%
CreateRange(...)100%44100%
Update(...)100%11100%
UpdateRange(...)0%620%
Delete(...)100%11100%
DeleteRange(...)100%11100%
IdFilter(...)100%11100%
EnsureId(...)100%22100%
FindFluent(...)100%22100%
InsertOne(...)50%2275%
InsertMany(...)50%2275%
DeleteMany(...)50%2275%
Replace(...)100%4481.25%
ReplaceOne(...)50%22100%
Guarded(...)100%1166.66%
InsertOneAsync(...)100%22100%
InsertManyAsync(...)50%22100%
DeleteManyAsync(...)50%22100%
ReplaceAsync()25%36823.8%
GuardedAsync()100%1133.33%
Fail(...)100%44100%

File(s)

/home/runner/work/dotnet-data/dotnet-data/src/ArturRios.Data.MongoDb/Repositories/MongoDocumentRepository.cs

#LineLine coverage
 1using System.Linq.Expressions;
 2using System.Runtime.CompilerServices;
 3using ArturRios.Data.MongoDb.Exceptions;
 4using ArturRios.Data.MongoDb.Interfaces;
 5using ArturRios.Output;
 6using Microsoft.Extensions.Logging;
 7using MongoDB.Bson;
 8using MongoDB.Bson.Serialization;
 9using MongoDB.Driver;
 10
 11namespace ArturRios.Data.MongoDb.Repositories;
 12
 13/// <summary>
 14///     MongoDB implementation of the document repository contracts. Runs against the
 15///     <see cref="MongoContext" /> collection and enlists in its ambient session so operations
 16///     participate in a unit-of-work transaction. Failures are returned as <see cref="DataOutput{T}" />.
 17/// </summary>
 18/// <typeparam name="T">The document type.</typeparam>
 19/// <param name="context">The Mongo context.</param>
 20/// <param name="logger">
 21///     Optional logger. Envelopes never carry driver text, so a failure is otherwise
 22///     undiagnosable: supply a logger and the full exception, plus the document type and the
 23///     repository method that failed, is written at <see cref="LogLevel.Error" />. Document
 24///     contents and key values are never logged. Resolved from DI when logging is registered.
 25/// </param>
 3826public class MongoDocumentRepository<T>(MongoContext context, ILogger<MongoDocumentRepository<T>>? logger = null)
 27    : IDocumentRepository<T>, IAsyncDocumentRepository<T> where T : Document
 28{
 29    /// <summary>Message returned when an operation fails with no finer classification.</summary>
 30    protected const string OperationFailedMessage = MongoErrors.GenericMessage;
 31
 32    /// <summary>Message returned when the failure is transient and the operation may be retried.</summary>
 33    protected const string TransientMessage = MongoErrors.TransientMessage;
 34
 35    /// <summary>Message returned on an optimistic-concurrency conflict.</summary>
 36    protected const string ConcurrencyMessage = MongoErrors.ConcurrencyMessage;
 37
 38    /// <summary>Message returned when a write violates a unique index.</summary>
 39    protected const string UniqueViolationMessage = MongoErrors.UniqueViolationMessage;
 40
 41    // Cached: the serialized BSON element name for VersionedDocument.Version on T
 42    // (respects any element-name convention the consumer registered).
 443    private static readonly string VersionElementName =
 444        BsonClassMap.LookupClassMap(typeof(T)).AllMemberMaps
 845            .FirstOrDefault(m => m.MemberName == nameof(VersionedDocument.Version))?.ElementName
 446        ?? nameof(VersionedDocument.Version);
 47
 48    /// <summary>The collection for <typeparamref name="T" />.</summary>
 9649    protected IMongoCollection<T> Collection => context.GetCollection<T>();
 50
 9451    private IClientSessionHandle? Session => context.Session;
 52
 53    /// <inheritdoc />
 54    public Task<DataOutput<IEnumerable<T>>> GetAllAsync(CancellationToken ct = default) =>
 1255        GuardedAsync<IEnumerable<T>>(async () => await FindFluent(FilterDefinition<T>.Empty).ToListAsync(ct).ConfigureAw
 56
 57    /// <inheritdoc />
 58    public Task<DataOutput<T?>> GetByIdAsync(string id, CancellationToken ct = default) =>
 1259        GuardedAsync<T?>(async () => await FindFluent(IdFilter(id)).FirstOrDefaultAsync(ct).ConfigureAwait(false));
 60
 61    /// <inheritdoc />
 62    public Task<DataOutput<IEnumerable<T>>> FindAsync(Expression<Func<T, bool>> predicate,
 63        CancellationToken ct = default) =>
 464        GuardedAsync<IEnumerable<T>>(async () => await FindFluent(Builders<T>.Filter.Where(predicate)).ToListAsync(ct).C
 65
 66    /// <inheritdoc />
 67    public Task<DataOutput<string>> CreateAsync(T document, CancellationToken ct = default) =>
 1268        GuardedAsync(async () =>
 1269        {
 1270            EnsureId(document);
 1271            await InsertOneAsync(document, ct).ConfigureAwait(false);
 1272            return document.Id;
 2473        });
 74
 75    /// <inheritdoc />
 76    public Task<DataOutput<IEnumerable<string>>> CreateRangeAsync(IEnumerable<T> documents,
 77        CancellationToken ct = default) =>
 478        GuardedAsync<IEnumerable<string>>(async () =>
 479        {
 480            var list = documents.ToList();
 2481            foreach (var d in list)
 482            {
 883                EnsureId(d);
 484            }
 485
 486            await InsertManyAsync(list, ct).ConfigureAwait(false);
 1287            return list.Select(d => d.Id).ToList();
 888        });
 89
 90    /// <inheritdoc />
 91    public Task<DataOutput<T>> UpdateAsync(T document, CancellationToken ct = default) =>
 292        GuardedAsync(async () =>
 293        {
 294            await ReplaceAsync(document, ct).ConfigureAwait(false);
 295            return document;
 496        });
 97
 98    /// <inheritdoc />
 99    public Task<DataOutput<IEnumerable<T>>>
 100        UpdateRangeAsync(IEnumerable<T> documents, CancellationToken ct = default) =>
 0101        GuardedAsync<IEnumerable<T>>(async () =>
 0102        {
 0103            var list = documents.ToList();
 0104            foreach (var d in list)
 0105            {
 0106                await ReplaceAsync(d, ct).ConfigureAwait(false);
 0107            }
 0108
 0109            return list;
 0110        });
 111
 112    /// <inheritdoc />
 113    public Task<DataOutput<string>> DeleteAsync(T document, CancellationToken ct = default) =>
 2114        GuardedAsync(async () =>
 2115        {
 2116            await DeleteManyAsync(IdFilter(document.Id), ct).ConfigureAwait(false);
 2117            return document.Id;
 4118        });
 119
 120    /// <inheritdoc />
 121    public Task<DataOutput<IEnumerable<string>>> DeleteRangeAsync(IEnumerable<string> ids,
 122        CancellationToken ct = default) =>
 2123        GuardedAsync<IEnumerable<string>>(async () =>
 2124        {
 2125            var idList = ids.ToList();
 2126            await DeleteManyAsync(Builders<T>.Filter.In(d => d.Id, idList), ct).ConfigureAwait(false);
 2127            return idList;
 4128        });
 129
 130    /// <inheritdoc />
 2131    public IQueryable<T> Query() => Collection.AsQueryable();
 132
 133    /// <inheritdoc />
 134    public DataOutput<IEnumerable<T>> GetAll() =>
 16135        Guarded(IEnumerable<T> () => FindFluent(FilterDefinition<T>.Empty).ToList());
 136
 137    /// <inheritdoc />
 138    public DataOutput<T?> GetById(string id) =>
 24139        Guarded<T?>(() => FindFluent(IdFilter(id)).FirstOrDefault());
 140
 141    /// <inheritdoc />
 142    public DataOutput<IEnumerable<T>> Find(Expression<Func<T, bool>> predicate) =>
 4143        Guarded(IEnumerable<T> () => FindFluent(Builders<T>.Filter.Where(predicate)).ToList());
 144
 145    /// <inheritdoc />
 18146    public DataOutput<string> Create(T document) => Guarded(() =>
 18147    {
 18148        EnsureId(document);
 18149        InsertOne(document);
 14150        return document.Id;
 18151    });
 152
 153    /// <inheritdoc />
 4154    public DataOutput<IEnumerable<string>> CreateRange(IEnumerable<T> documents) => Guarded(IEnumerable<string> () =>
 4155    {
 4156        var list = documents.ToList();
 24157        foreach (var d in list)
 4158        {
 8159            EnsureId(d);
 4160        }
 4161
 4162        InsertMany(list);
 12163        return list.Select(d => d.Id).ToList();
 4164    });
 165
 166    /// <inheritdoc />
 10167    public DataOutput<T> Update(T document) => Guarded(() =>
 10168    {
 10169        Replace(document);
 6170        return document;
 10171    });
 172
 173    /// <inheritdoc />
 0174    public DataOutput<IEnumerable<T>> UpdateRange(IEnumerable<T> documents) => Guarded(IEnumerable<T> () =>
 0175    {
 0176        var list = documents.ToList();
 0177        foreach (var d in list)
 0178        {
 0179            Replace(d);
 0180        }
 0181
 0182        return list;
 0183    });
 184
 185    /// <inheritdoc />
 2186    public DataOutput<string> Delete(T document) => Guarded(() =>
 2187    {
 2188        DeleteMany(IdFilter(document.Id));
 2189        return document.Id;
 2190    });
 191
 192    /// <inheritdoc />
 2193    public DataOutput<IEnumerable<string>> DeleteRange(IEnumerable<string> ids) => Guarded(IEnumerable<string> () =>
 2194    {
 2195        var idList = ids.ToList();
 2196        DeleteMany(Builders<T>.Filter.In(d => d.Id, idList));
 2197        return idList;
 2198    });
 199
 200    // --- session-aware driver helpers (sync) ---
 34201    private static FilterDefinition<T> IdFilter(string id) => Builders<T>.Filter.Eq(d => d.Id, id);
 202
 203    private static void EnsureId(T document)
 204    {
 46205        if (string.IsNullOrEmpty(document.Id))
 206        {
 38207            document.Id = ObjectId.GenerateNewId().ToString();
 208        }
 46209    }
 210
 211    private IFindFluent<T, T> FindFluent(FilterDefinition<T> filter) =>
 36212        Session is { } s ? Collection.Find(s, filter) : Collection.Find(filter);
 213
 214    private void InsertOne(T document)
 215    {
 18216        if (Session is { } s)
 217        {
 0218            Collection.InsertOne(s, document);
 219        }
 220        else
 221        {
 18222            Collection.InsertOne(document);
 223        }
 14224    }
 225
 226    private void InsertMany(IEnumerable<T> documents)
 227    {
 4228        if (Session is { } s)
 229        {
 0230            Collection.InsertMany(s, documents);
 231        }
 232        else
 233        {
 4234            Collection.InsertMany(documents);
 235        }
 4236    }
 237
 238    private void DeleteMany(FilterDefinition<T> filter)
 239    {
 4240        if (Session is { } s)
 241        {
 0242            Collection.DeleteMany(s, filter);
 243        }
 244        else
 245        {
 4246            Collection.DeleteMany(filter);
 247        }
 4248    }
 249
 250    // Replace with optimistic-concurrency handling for VersionedDocument.
 251    private void Replace(T document)
 252    {
 10253        if (document is VersionedDocument versioned)
 254        {
 8255            var expected = versioned.Version;
 8256            versioned.Version = expected + 1;
 8257            var filter = Builders<T>.Filter.And(IdFilter(document.Id),
 8258                Builders<T>.Filter.Eq(VersionElementName, expected));
 259
 260            ReplaceOneResult result;
 261            try
 262            {
 8263                result = ReplaceOne(filter, document);
 8264            }
 0265            catch
 266            {
 267                // The write never landed, so the in-memory bump must not survive: keeping it would
 268                // make every retry filter on a version the server never stored.
 0269                versioned.Version = expected;
 0270                throw;
 271            }
 272
 8273            if (result.MatchedCount == 0)
 274            {
 4275                versioned.Version = expected; // roll back the in-memory bump on a failed (stale) update
 4276                throw new MongoConcurrencyException();
 277            }
 278
 4279            return;
 280        }
 281
 2282        ReplaceOne(IdFilter(document.Id), document);
 2283    }
 284
 285    private ReplaceOneResult ReplaceOne(FilterDefinition<T> filter, T document) =>
 10286        Session is { } s ? Collection.ReplaceOne(s, filter, document) : Collection.ReplaceOne(filter, document);
 287
 288    /// <summary>Runs a synchronous operation, converting failures to envelope errors.</summary>
 289    /// <param name="operation">The operation to run.</param>
 290    /// <param name="operationName">The calling repository method, used as log context.</param>
 291    protected DataOutput<TResult> Guarded<TResult>(Func<TResult> operation,
 292        [CallerMemberName] string operationName = "")
 293    {
 294        try
 295        {
 58296            return DataOutput<TResult>.New.WithData(operation());
 297        }
 0298        catch (OperationCanceledException)
 299        {
 0300            throw;
 301        }
 8302        catch (Exception ex)
 303        {
 8304            return Fail<TResult>(ex, operationName);
 305        }
 58306    }
 307
 308    // --- session-aware driver helpers (async) ---
 309    private Task InsertOneAsync(T document, CancellationToken ct) =>
 12310        Session is { } s
 12311            ? Collection.InsertOneAsync(s, document, null, ct)
 12312            : Collection.InsertOneAsync(document, null, ct);
 313
 314    private Task InsertManyAsync(IEnumerable<T> documents, CancellationToken ct) =>
 4315        Session is { } s
 4316            ? Collection.InsertManyAsync(s, documents, null, ct)
 4317            : Collection.InsertManyAsync(documents, null, ct);
 318
 319    private Task DeleteManyAsync(FilterDefinition<T> filter, CancellationToken ct) =>
 4320        Session is { } s ? Collection.DeleteManyAsync(s, filter, null, ct) : Collection.DeleteManyAsync(filter, ct);
 321
 322    private async Task ReplaceAsync(T document, CancellationToken ct)
 323    {
 2324        if (document is VersionedDocument versioned)
 325        {
 0326            var expected = versioned.Version;
 0327            versioned.Version = expected + 1;
 0328            var filter = Builders<T>.Filter.And(IdFilter(document.Id),
 0329                Builders<T>.Filter.Eq(VersionElementName, expected));
 330            ReplaceOneResult result;
 331            try
 332            {
 0333                result = Session is { } s
 0334                    ? await Collection.ReplaceOneAsync(s, filter, document, cancellationToken: ct).ConfigureAwait(false)
 0335                    : await Collection.ReplaceOneAsync(filter, document, cancellationToken: ct).ConfigureAwait(false);
 0336            }
 0337            catch
 338            {
 339                // The write never landed, so the in-memory bump must not survive: keeping it would
 340                // make every retry filter on a version the server never stored.
 0341                versioned.Version = expected;
 0342                throw;
 343            }
 344
 0345            if (result.MatchedCount == 0)
 346            {
 0347                versioned.Version = expected; // roll back the in-memory bump on a failed (stale) update
 0348                throw new MongoConcurrencyException();
 349            }
 350
 0351            return;
 352        }
 353
 2354        var idFilter = IdFilter(document.Id);
 2355        if (Session is { } session)
 356        {
 0357            await Collection.ReplaceOneAsync(session, idFilter, document, cancellationToken: ct).ConfigureAwait(false);
 358        }
 359        else
 360        {
 2361            await Collection.ReplaceOneAsync(idFilter, document, cancellationToken: ct).ConfigureAwait(false);
 362        }
 2363    }
 364
 365    /// <summary>Runs an asynchronous operation, converting failures to envelope errors.</summary>
 366    /// <param name="operation">The operation to run.</param>
 367    /// <param name="operationName">The calling repository method, used as log context.</param>
 368    protected async Task<DataOutput<TResult>> GuardedAsync<TResult>(Func<Task<TResult>> operation,
 369        [CallerMemberName] string operationName = "")
 370    {
 371        try
 372        {
 36373            return DataOutput<TResult>.New.WithData(await operation().ConfigureAwait(false));
 374        }
 0375        catch (OperationCanceledException)
 376        {
 0377            throw;
 378        }
 0379        catch (Exception ex)
 380        {
 0381            return Fail<TResult>(ex, operationName);
 382        }
 36383    }
 384
 385    /// <summary>
 386    ///     Logs the failure when a logger is configured, and maps it to an error envelope.
 387    ///     Driver text names indexes, collections, key values and cluster endpoints, so it goes to
 388    ///     the log and never to the caller.
 389    /// </summary>
 390    /// <param name="ex">The exception caught by a guard.</param>
 391    /// <param name="operationName">The repository method that failed, used as log context.</param>
 392    protected DataOutput<TResult> Fail<TResult>(Exception ex, string operationName = "")
 393    {
 394        // A concurrency conflict is an expected outcome of optimistic locking, not an operational
 395        // fault: logging it at Error would fill the log with routine contention.
 8396        var level = ex is MongoConcurrencyException ? LogLevel.Debug : LogLevel.Error;
 397
 8398        logger?.Log(level, ex, "Mongo operation failed. Document: {Document}, operation: {Operation}",
 8399            typeof(T).Name, operationName);
 400
 8401        return DataOutput<TResult>.New.WithError(MongoErrors.Describe(ex));
 402    }
 403}

Methods/Properties

.ctor(ArturRios.Data.MongoDb.MongoContext,Microsoft.Extensions.Logging.ILogger`1<ArturRios.Data.MongoDb.Repositories.MongoDocumentRepository`1<T>>)
.cctor()
get_Collection()
get_Session()
GetAllAsync(System.Threading.CancellationToken)
GetByIdAsync(System.String,System.Threading.CancellationToken)
FindAsync(System.Linq.Expressions.Expression`1<System.Func`2<T,System.Boolean>>,System.Threading.CancellationToken)
CreateAsync(T,System.Threading.CancellationToken)
CreateRangeAsync(System.Collections.Generic.IEnumerable`1<T>,System.Threading.CancellationToken)
UpdateAsync(T,System.Threading.CancellationToken)
UpdateRangeAsync(System.Collections.Generic.IEnumerable`1<T>,System.Threading.CancellationToken)
DeleteAsync(T,System.Threading.CancellationToken)
DeleteRangeAsync(System.Collections.Generic.IEnumerable`1<System.String>,System.Threading.CancellationToken)
Query()
GetAll()
GetById(System.String)
Find(System.Linq.Expressions.Expression`1<System.Func`2<T,System.Boolean>>)
Create(T)
CreateRange(System.Collections.Generic.IEnumerable`1<T>)
Update(T)
UpdateRange(System.Collections.Generic.IEnumerable`1<T>)
Delete(T)
DeleteRange(System.Collections.Generic.IEnumerable`1<System.String>)
IdFilter(System.String)
EnsureId(T)
FindFluent(MongoDB.Driver.FilterDefinition`1<T>)
InsertOne(T)
InsertMany(System.Collections.Generic.IEnumerable`1<T>)
DeleteMany(MongoDB.Driver.FilterDefinition`1<T>)
Replace(T)
ReplaceOne(MongoDB.Driver.FilterDefinition`1<T>,T)
Guarded(System.Func`1<TResult>,System.String)
InsertOneAsync(T,System.Threading.CancellationToken)
InsertManyAsync(System.Collections.Generic.IEnumerable`1<T>,System.Threading.CancellationToken)
DeleteManyAsync(MongoDB.Driver.FilterDefinition`1<T>,System.Threading.CancellationToken)
ReplaceAsync()
GuardedAsync()
Fail(System.Exception,System.String)