< Summary - MechanicsSoftware — Coverage Report

Information
Class: MechanicsSoftware.Infrastructure.Persistence.AppDbContext
Assembly: MechanicsSoftware.Infrastructure
File(s): /home/runner/work/mechanics-software/mechanics-software/src/MechanicsSoftware.Infrastructure/Persistence/AppDbContext.cs
Line coverage
92%
Covered lines: 36
Uncovered lines: 3
Coverable lines: 39
Total lines: 81
Line coverage: 92.3%
Branch coverage
83%
Covered branches: 10
Total branches: 12
Branch coverage: 83.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
.ctor(...)100%11100%
OnModelCreating(...)100%11100%
OnModelCreating(...)100%11100%
SaveChangesAsync()100%11100%
PromoteNewOwnedCollectionItems()87.5%9877.77%
PromoteNewOwnedScalarsAsync()100%22100%
PromoteIfMissingAsync()50%2275%

File(s)

/home/runner/work/mechanics-software/mechanics-software/src/MechanicsSoftware.Infrastructure/Persistence/AppDbContext.cs

#LineLine coverage
 1using System.Reflection;
 2using Microsoft.EntityFrameworkCore;
 3using Microsoft.EntityFrameworkCore.ChangeTracking;
 4using MechanicsSoftware.Application.Abstractions;
 5using MechanicsSoftware.Domain.Entities;
 6using MechanicsSoftware.Domain.ValueObjects;
 7using MechanicsSoftware.Domain.Enums;
 8using MechanicsSoftware.Domain.Exceptions;
 9
 10namespace MechanicsSoftware.Infrastructure.Persistence;
 11
 12public sealed class AppDbContext : DbContext, IAppDbContext
 13{
 51014    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
 6615
 16    public DbSet<User> Users { get; set; } = null!;
 17    public DbSet<Customer> Customers { get; set; } = null!;
 18    public DbSet<Vehicle> Vehicles { get; set; } = null!;
 19    public DbSet<Part> Parts { get; set; } = null!;
 20    public DbSet<Service> Services { get; set; } = null!;
 21    public DbSet<ServiceOrder> ServiceOrders { get; set; } = null!;
 22
 23    protected override void OnModelCreating(ModelBuilder modelBuilder)
 224    {
 325        modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
 326    }
 127
 28    // Workaround for EF Core 9.0.14 + Npgsql 9.0.4: brand-new owned entities (collection items
 29    // and owned scalars) are sometimes attached as Modified instead of Added even when the
 30    // owner is loaded with .Include(...) of the navigation. The resulting UPDATE affects 0
 31    // rows and throws DbUpdateConcurrencyException. We promote those entries before saving.
 32    public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
 17033    {
 17034        ChangeTracker.DetectChanges();
 17035        PromoteNewOwnedCollectionItems();
 17036        await PromoteNewOwnedScalarsAsync(cancellationToken);
 37
 17038        var auto = ChangeTracker.AutoDetectChangesEnabled;
 17039        ChangeTracker.AutoDetectChangesEnabled = false;
 40        try
 17041        {
 17042            return await base.SaveChangesAsync(cancellationToken);
 43        }
 44        finally
 17045        {
 17046            ChangeTracker.AutoDetectChangesEnabled = auto;
 17047        }
 17048    }
 49
 50    // Owned-collection items in this domain are append-only (ServiceItems, PartItems,
 51    // StockMovements). Any Modified entry is therefore a freshly-added item. No DB round-trip.
 52    private void PromoteNewOwnedCollectionItems()
 17053    {
 133454        foreach (var entry in ChangeTracker.Entries())
 41255        {
 78256            if (entry.State != EntityState.Modified) continue;
 4257            var ownership = entry.Metadata.FindOwnership();
 8458            if (ownership is null || ownership.IsUnique) continue;
 059            entry.State = EntityState.Added;
 060        }
 17061    }
 62
 63    // Owned scalars (e.g. Budget) ARE mutated after creation (Approve/Reject), so we cannot
 64    // blindly flip Modified → Added. A single DB lookup distinguishes new from existing.
 65    private async Task PromoteNewOwnedScalarsAsync(CancellationToken cancellationToken)
 17066    {
 17067        var candidates = ChangeTracker.Entries()
 17068            .Where(e => e.State == EntityState.Modified
 17069                     && e.Metadata.FindOwnership() is { IsUnique: true })
 17070            .ToList();
 71
 53472        foreach (var entry in candidates)
 1273            await PromoteIfMissingAsync(entry, cancellationToken);
 17074    }
 75
 76    private static async Task PromoteIfMissingAsync(EntityEntry entry, CancellationToken ct)
 1277    {
 1278        if (await entry.GetDatabaseValuesAsync(ct) is null)
 079            entry.State = EntityState.Added;
 1280    }
 81}