< Summary

Information
Class: ArturRios.Data.MongoDb.Repositories.MongoDocumentRepository<T>
Assembly: ArturRios.Data.MongoDb
File(s): D:\Repositories\dotnet-data\src\ArturRios.Data.MongoDb\Repositories\MongoDocumentRepository.cs
Line coverage
73%
Covered lines: 152
Uncovered lines: 55
Coverable lines: 207
Total lines: 347
Line coverage: 73.4%
Branch coverage
61%
Covered branches: 26
Total branches: 42
Branch coverage: 61.9%
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%2266.66%
InsertMany(...)50%2266.66%
DeleteMany(...)50%2266.66%
Replace(...)100%4493.33%
ReplaceOne(...)50%22100%
Guarded(...)100%1170%
InsertOneAsync(...)100%22100%
InsertManyAsync(...)50%22100%
DeleteManyAsync(...)50%22100%
ReplaceAsync()25%27833.33%
GuardedAsync()100%1140%
Fail(...)100%22100%

File(s)

D:\Repositories\dotnet-data\src\ArturRios.Data.MongoDb\Repositories\MongoDocumentRepository.cs

#LineLine coverage
 1using System.Linq.Expressions;
 2using ArturRios.Data.MongoDb.Exceptions;
 3using ArturRios.Data.MongoDb.Interfaces;
 4using ArturRios.Output;
 5using MongoDB.Bson;
 6using MongoDB.Bson.Serialization;
 7using MongoDB.Driver;
 8
 9namespace ArturRios.Data.MongoDb.Repositories;
 10
 11/// <summary>
 12///     MongoDB implementation of the document repository contracts. Runs against the
 13///     <see cref="MongoContext" /> collection and enlists in its ambient session so operations
 14///     participate in a unit-of-work transaction. Failures are returned as <see cref="DataOutput{T}" />.
 15/// </summary>
 16/// <typeparam name="T">The document type.</typeparam>
 17/// <param name="context">The Mongo context.</param>
 1718public class MongoDocumentRepository<T>(MongoContext context)
 19    : IDocumentRepository<T>, IAsyncDocumentRepository<T> where T : Document
 20{
 21    /// <summary>Message prefix returned when an operation fails.</summary>
 22    protected const string OperationFailedMessage = "A data-access error occurred:";
 23
 24    /// <summary>Message returned on an optimistic-concurrency conflict.</summary>
 25    protected const string ConcurrencyMessage =
 26        "Concurrency conflict: the document was modified or removed by another process.";
 27
 28    // Cached: the serialized BSON element name for VersionedDocument.Version on T
 29    // (respects any element-name convention the consumer registered).
 230    private static readonly string VersionElementName =
 231        BsonClassMap.LookupClassMap(typeof(T)).AllMemberMaps
 432            .FirstOrDefault(m => m.MemberName == nameof(VersionedDocument.Version))?.ElementName
 233        ?? nameof(VersionedDocument.Version);
 34
 35    /// <summary>The collection for <typeparamref name="T" />.</summary>
 4636    protected IMongoCollection<T> Collection => context.GetCollection<T>();
 37
 4538    private IClientSessionHandle? Session => context.Session;
 39
 40    /// <inheritdoc />
 41    public Task<DataOutput<IEnumerable<T>>> GetAllAsync(CancellationToken ct = default) =>
 642        GuardedAsync<IEnumerable<T>>(async () => await FindFluent(FilterDefinition<T>.Empty).ToListAsync(ct));
 43
 44    /// <inheritdoc />
 45    public Task<DataOutput<T?>> GetByIdAsync(string id, CancellationToken ct = default) =>
 646        GuardedAsync<T?>(async () => await FindFluent(IdFilter(id)).FirstOrDefaultAsync(ct));
 47
 48    /// <inheritdoc />
 49    public Task<DataOutput<IEnumerable<T>>> FindAsync(Expression<Func<T, bool>> predicate,
 50        CancellationToken ct = default) =>
 251        GuardedAsync<IEnumerable<T>>(async () => await FindFluent(Builders<T>.Filter.Where(predicate)).ToListAsync(ct));
 52
 53    /// <inheritdoc />
 54    public Task<DataOutput<string>> CreateAsync(T document, CancellationToken ct = default) =>
 655        GuardedAsync(async () =>
 656        {
 657            EnsureId(document);
 658            await InsertOneAsync(document, ct);
 659            return document.Id;
 1260        });
 61
 62    /// <inheritdoc />
 63    public Task<DataOutput<IEnumerable<string>>> CreateRangeAsync(IEnumerable<T> documents,
 64        CancellationToken ct = default) =>
 265        GuardedAsync<IEnumerable<string>>(async () =>
 266        {
 267            var list = documents.ToList();
 1468            foreach (var d in list)
 469            {
 470                EnsureId(d);
 471            }
 272
 273            await InsertManyAsync(list, ct);
 674            return list.Select(d => d.Id).ToList();
 475        });
 76
 77    /// <inheritdoc />
 78    public Task<DataOutput<T>> UpdateAsync(T document, CancellationToken ct = default) =>
 179        GuardedAsync(async () =>
 180        {
 181            await ReplaceAsync(document, ct);
 182            return document;
 283        });
 84
 85    /// <inheritdoc />
 86    public Task<DataOutput<IEnumerable<T>>>
 87        UpdateRangeAsync(IEnumerable<T> documents, CancellationToken ct = default) =>
 088        GuardedAsync<IEnumerable<T>>(async () =>
 089        {
 090            var list = documents.ToList();
 091            foreach (var d in list)
 092            {
 093                await ReplaceAsync(d, ct);
 094            }
 095
 096            return list;
 097        });
 98
 99    /// <inheritdoc />
 100    public Task<DataOutput<string>> DeleteAsync(T document, CancellationToken ct = default) =>
 1101        GuardedAsync(async () =>
 1102        {
 1103            await DeleteManyAsync(IdFilter(document.Id), ct);
 1104            return document.Id;
 2105        });
 106
 107    /// <inheritdoc />
 108    public Task<DataOutput<IEnumerable<string>>> DeleteRangeAsync(IEnumerable<string> ids,
 109        CancellationToken ct = default) =>
 1110        GuardedAsync<IEnumerable<string>>(async () =>
 1111        {
 1112            var idList = ids.ToList();
 1113            await DeleteManyAsync(Builders<T>.Filter.In(d => d.Id, idList), ct);
 1114            return idList;
 2115        });
 116
 117    /// <inheritdoc />
 1118    public IQueryable<T> Query() => Collection.AsQueryable();
 119
 120    /// <inheritdoc />
 121    public DataOutput<IEnumerable<T>> GetAll() =>
 8122        Guarded(IEnumerable<T> () => FindFluent(FilterDefinition<T>.Empty).ToList());
 123
 124    /// <inheritdoc />
 125    public DataOutput<T?> GetById(string id) =>
 12126        Guarded<T?>(() => FindFluent(IdFilter(id)).FirstOrDefault());
 127
 128    /// <inheritdoc />
 129    public DataOutput<IEnumerable<T>> Find(Expression<Func<T, bool>> predicate) =>
 2130        Guarded(IEnumerable<T> () => FindFluent(Builders<T>.Filter.Where(predicate)).ToList());
 131
 132    /// <inheritdoc />
 7133    public DataOutput<string> Create(T document) => Guarded(() =>
 7134    {
 7135        EnsureId(document);
 7136        InsertOne(document);
 6137        return document.Id;
 13138    });
 139
 140    /// <inheritdoc />
 2141    public DataOutput<IEnumerable<string>> CreateRange(IEnumerable<T> documents) => Guarded(IEnumerable<string> () =>
 2142    {
 2143        var list = documents.ToList();
 14144        foreach (var d in list)
 4145        {
 4146            EnsureId(d);
 4147        }
 2148
 2149        InsertMany(list);
 6150        return list.Select(d => d.Id).ToList();
 4151    });
 152
 153    /// <inheritdoc />
 5154    public DataOutput<T> Update(T document) => Guarded(() =>
 5155    {
 5156        Replace(document);
 3157        return document;
 8158    });
 159
 160    /// <inheritdoc />
 0161    public DataOutput<IEnumerable<T>> UpdateRange(IEnumerable<T> documents) => Guarded(IEnumerable<T> () =>
 0162    {
 0163        var list = documents.ToList();
 0164        foreach (var d in list)
 0165        {
 0166            Replace(d);
 0167        }
 0168
 0169        return list;
 0170    });
 171
 172    /// <inheritdoc />
 1173    public DataOutput<string> Delete(T document) => Guarded(() =>
 1174    {
 1175        DeleteMany(IdFilter(document.Id));
 1176        return document.Id;
 2177    });
 178
 179    /// <inheritdoc />
 1180    public DataOutput<IEnumerable<string>> DeleteRange(IEnumerable<string> ids) => Guarded(IEnumerable<string> () =>
 1181    {
 1182        var idList = ids.ToList();
 1183        DeleteMany(Builders<T>.Filter.In(d => d.Id, idList));
 1184        return idList;
 2185    });
 186
 187    // --- session-aware driver helpers (sync) ---
 17188    private static FilterDefinition<T> IdFilter(string id) => Builders<T>.Filter.Eq(d => d.Id, id);
 189
 190    private static void EnsureId(T document)
 21191    {
 21192        if (string.IsNullOrEmpty(document.Id))
 19193        {
 19194            document.Id = ObjectId.GenerateNewId().ToString();
 19195        }
 21196    }
 197
 198    private IFindFluent<T, T> FindFluent(FilterDefinition<T> filter) =>
 18199        Session is { } s ? Collection.Find(s, filter) : Collection.Find(filter);
 200
 201    private void InsertOne(T document)
 7202    {
 7203        if (Session is { } s)
 0204        {
 0205            Collection.InsertOne(s, document);
 0206        }
 207        else
 7208        {
 7209            Collection.InsertOne(document);
 6210        }
 6211    }
 212
 213    private void InsertMany(IEnumerable<T> documents)
 2214    {
 2215        if (Session is { } s)
 0216        {
 0217            Collection.InsertMany(s, documents);
 0218        }
 219        else
 2220        {
 2221            Collection.InsertMany(documents);
 2222        }
 2223    }
 224
 225    private void DeleteMany(FilterDefinition<T> filter)
 2226    {
 2227        if (Session is { } s)
 0228        {
 0229            Collection.DeleteMany(s, filter);
 0230        }
 231        else
 2232        {
 2233            Collection.DeleteMany(filter);
 2234        }
 2235    }
 236
 237    // Replace with optimistic-concurrency handling for VersionedDocument.
 238    private void Replace(T document)
 5239    {
 5240        if (document is VersionedDocument versioned)
 4241        {
 4242            var expected = versioned.Version;
 4243            versioned.Version = expected + 1;
 4244            var filter = Builders<T>.Filter.And(IdFilter(document.Id),
 4245                Builders<T>.Filter.Eq(VersionElementName, expected));
 4246            var result = ReplaceOne(filter, document);
 4247            if (result.MatchedCount == 0)
 2248            {
 2249                versioned.Version = expected; // roll back the in-memory bump on a failed (stale) update
 2250                throw new MongoConcurrencyException();
 251            }
 252
 0253            return;
 254        }
 255
 1256        ReplaceOne(IdFilter(document.Id), document);
 3257    }
 258
 259    private ReplaceOneResult ReplaceOne(FilterDefinition<T> filter, T document) =>
 5260        Session is { } s ? Collection.ReplaceOne(s, filter, document) : Collection.ReplaceOne(filter, document);
 261
 262    /// <summary>Runs a synchronous operation, converting failures to envelope errors.</summary>
 263    protected static DataOutput<TResult> Guarded<TResult>(Func<TResult> operation)
 27264    {
 265        try
 27266        {
 27267            return DataOutput<TResult>.New.WithData(operation());
 268        }
 0269        catch (OperationCanceledException)
 0270        {
 0271            throw;
 272        }
 3273        catch (Exception ex)
 3274        {
 3275            return Fail<TResult>(ex);
 276        }
 27277    }
 278
 279    // --- session-aware driver helpers (async) ---
 280    private Task InsertOneAsync(T document, CancellationToken ct) =>
 6281        Session is { } s
 6282            ? Collection.InsertOneAsync(s, document, null, ct)
 6283            : Collection.InsertOneAsync(document, null, ct);
 284
 285    private Task InsertManyAsync(IEnumerable<T> documents, CancellationToken ct) =>
 2286        Session is { } s
 2287            ? Collection.InsertManyAsync(s, documents, null, ct)
 2288            : Collection.InsertManyAsync(documents, null, ct);
 289
 290    private Task DeleteManyAsync(FilterDefinition<T> filter, CancellationToken ct) =>
 2291        Session is { } s ? Collection.DeleteManyAsync(s, filter, null, ct) : Collection.DeleteManyAsync(filter, ct);
 292
 293    private async Task ReplaceAsync(T document, CancellationToken ct)
 1294    {
 1295        if (document is VersionedDocument versioned)
 0296        {
 0297            var expected = versioned.Version;
 0298            versioned.Version = expected + 1;
 0299            var filter = Builders<T>.Filter.And(IdFilter(document.Id),
 0300                Builders<T>.Filter.Eq(VersionElementName, expected));
 0301            var result = Session is { } s
 0302                ? await Collection.ReplaceOneAsync(s, filter, document, cancellationToken: ct)
 0303                : await Collection.ReplaceOneAsync(filter, document, cancellationToken: ct);
 0304            if (result.MatchedCount == 0)
 0305            {
 0306                versioned.Version = expected; // roll back the in-memory bump on a failed (stale) update
 0307                throw new MongoConcurrencyException();
 308            }
 309
 0310            return;
 311        }
 312
 1313        var idFilter = IdFilter(document.Id);
 1314        if (Session is { } session)
 0315        {
 0316            await Collection.ReplaceOneAsync(session, idFilter, document, cancellationToken: ct);
 0317        }
 318        else
 1319        {
 1320            await Collection.ReplaceOneAsync(idFilter, document, cancellationToken: ct);
 1321        }
 1322    }
 323
 324    /// <summary>Runs an asynchronous operation, converting failures to envelope errors.</summary>
 325    protected static async Task<DataOutput<TResult>> GuardedAsync<TResult>(Func<Task<TResult>> operation)
 18326    {
 327        try
 18328        {
 18329            return DataOutput<TResult>.New.WithData(await operation());
 330        }
 0331        catch (OperationCanceledException)
 0332        {
 0333            throw;
 334        }
 0335        catch (Exception ex)
 0336        {
 0337            return Fail<TResult>(ex);
 338        }
 18339    }
 340
 341    /// <summary>Maps an exception to an error envelope.</summary>
 3342    protected static DataOutput<TResult> Fail<TResult>(Exception ex) => ex switch
 3343    {
 2344        MongoConcurrencyException => DataOutput<TResult>.New.WithError(ConcurrencyMessage),
 1345        _ => DataOutput<TResult>.New.WithError($"{OperationFailedMessage} {ex.GetBaseException().Message}")
 3346    };
 347}

Methods/Properties

.ctor(ArturRios.Data.MongoDb.MongoContext)
.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>)
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)