< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.Data.Persistence.ScopeCascades
Assembly: ArturRios.Heimdall.Data
File(s): /home/ci/agent/workspace/heimdall-api-docs_main/app/src/Infrastructure/ArturRios.Heimdall.Data/Persistence/ScopeCascades.cs
Line coverage
100%
Covered lines: 28
Uncovered lines: 0
Coverable lines: 28
Total lines: 78
Line coverage: 100%
Branch coverage
100%
Covered branches: 2
Total branches: 2
Branch coverage: 100%
Method coverage
100%
Covered methods: 4
Fully covered methods: 4
Total methods: 4
Method coverage: 100%
Full method coverage: 100%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
MarkMembersDeletedAsync()100%11100%
DeleteMembersAsync()100%11100%
DeleteInBatchesAsync()100%22100%

File(s)

/home/ci/agent/workspace/heimdall-api-docs_main/app/src/Infrastructure/ArturRios.Heimdall.Data/Persistence/ScopeCascades.cs

#LineLine coverage
 1using ArturRios.Data.Relational.Core.Entities;
 2using ArturRios.Heimdall.Data.Configuration;
 3using ArturRios.Heimdall.Domain.Enums;
 4using ArturRios.Heimdall.Domain.Persistence;
 5using Microsoft.EntityFrameworkCore;
 6
 7namespace ArturRios.Heimdall.Data.Persistence;
 8
 9/// <summary>
 10///     <see cref="IScopeCascades" /> on EF Core's <c>ExecuteUpdate</c> and <c>ExecuteDelete</c>: one
 11///     <c>UPDATE … WHERE NOT is_deleted</c> per kind of member for a logical delete, and
 12///     <c>DELETE … WHERE id = ANY(…)</c> per batch of ids for a hard one.
 13/// </summary>
 14/// <remarks>
 15///     <para>
 16///         Nothing in the context intercepts a save to stamp these rows: <c>UpdatedAt</c> and the
 17///         deletion columns were always set by the handler before a tracked save, and the one hook the
 18///         base context has — the concurrency stamp of a versioned entity — applies to none of these
 19///         types. So the statements set exactly what the tracked saves did. The database's own
 20///         triggers on these tables (the public-id guards) fire for set-based statements as for any
 21///         other.
 22///     </para>
 23///     <para>
 24///         A hard delete goes by the ids its caller tombstoned, not by the scope, so exactly the rows
 25///         with a tombstone are removed. Each statement carries at most <see cref="BatchSize" /> of
 26///         them, so neither a parameter nor a statement grows with the tenant; the rows referencing them
 27///         (<c>SCOPE_USER</c>, tokens, two-factor rows, credentials) go by their foreign keys'
 28///         <c>ON DELETE CASCADE</c>.
 29///     </para>
 30/// </remarks>
 1431public sealed class ScopeCascades(AppDbContext context) : IScopeCascades
 32{
 33    /// <summary>How many ids one hard-delete statement carries.</summary>
 34    public const int BatchSize = 2000;
 35
 36    public async Task MarkMembersDeletedAsync(long scopeId, DateTime at)
 37    {
 38        const int administrative = (int)DeletionKinds.Administrative;
 39
 840        await context.Persons
 841            .Where(x => !x.IsDeleted && context.ScopeUsers.Any(member => member.PersonId == x.Id && member.ScopeId == sc
 1642            .ExecuteUpdateAsync(set => set
 1643                .SetProperty(x => x.IsDeleted, true)
 1644                .SetProperty(x => x.DeletedAt, at)
 1645                .SetProperty(x => x.DeletionKind, administrative)
 1646                .SetProperty(x => x.UpdatedAt, at));
 47
 848        await context.GoogleUsers
 849            .Where(x => x.ScopeId == scopeId && !x.IsDeleted)
 1650            .ExecuteUpdateAsync(set => set
 1651                .SetProperty(x => x.IsDeleted, true)
 1652                .SetProperty(x => x.DeletedAt, at)
 1653                .SetProperty(x => x.DeletionKind, administrative)
 1654                .SetProperty(x => x.UpdatedAt, at));
 55
 756        await context.Applications
 757            .Where(x => x.ScopeId == scopeId && !x.IsDeleted)
 1458            .ExecuteUpdateAsync(set => set
 1459                .SetProperty(x => x.IsDeleted, true)
 1460                .SetProperty(x => x.UpdatedAt, at));
 761    }
 62
 63    public async Task DeleteMembersAsync(ScopeMemberIds members)
 64    {
 465        await DeleteInBatchesAsync(context.Applications, members.ApplicationIds);
 466        await DeleteInBatchesAsync(context.GoogleUsers, members.GoogleUserIds);
 467        await DeleteInBatchesAsync(context.Persons, members.PersonIds);
 468    }
 69
 70    private static async Task DeleteInBatchesAsync<T>(DbSet<T> set, IReadOnlyCollection<long> ids)
 71        where T : Entity<long>
 72    {
 4473        foreach (var batch in ids.Chunk(BatchSize))
 74        {
 1075            await set.Where(x => batch.Contains(x.Id)).ExecuteDeleteAsync();
 76        }
 1277    }
 78}