< Summary - ZhEaIsNsAaBn.Libraries coverage (affected: 23 packages)

Information
Line coverage
94%
Covered lines: 161
Uncovered lines: 9
Coverable lines: 170
Total lines: 447
Line coverage: 94.7%
Branch coverage
77%
Covered branches: 84
Total branches: 109
Branch coverage: 77%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
UpAsync()78.57%1414100%
SessionAsync()70%232081.25%
ListAsync()50%101089.65%
ApplyPendingAsync()100%1010100%
ApplyAsync()73.07%262695.65%
CheckAsync()87.5%88100%
InStrategyAsync()100%22100%
RunAsync()90.9%1111100%
Audit(...)100%11100%
Now(...)100%11100%
OpenAsync()100%44100%
CloseAsync()100%22100%
TryAsync()0%2260%
Reset()100%11100%

File(s)

/home/runner/work/ZhEaIsNsAaBn.Libraries.V1/ZhEaIsNsAaBn.Libraries.V1/src/DataBasePackages/ZhEaIsNsAaBn.DbSeeder/Running/SeedEngine.cs

#LineLine coverage
 1#region Usings
 2using System.Data;
 3using System.Diagnostics;
 4using Microsoft.EntityFrameworkCore.Storage;
 5#endregion
 6
 7namespace ZhEaIsNsAaBn.DbSeeder;
 8
 9/// <summary>
 10/// The provider-neutral runner (§6.1). One shared implementation for sync and async (EF's <c>bool async</c> pattern). E
 11/// returned: cancellation propagates, anything else goes through <see cref="ISeedDatabaseProvider.Classify"/>, else <se
 12/// </summary>
 13internal static class SeedEngine
 14{
 15    private const string Up = nameof( Up );
 16
 17
 18    // Applies the pending migrations (up to targetMigrationId when given). takeLock is false from EF's UseSeeding, wher
 19    // holds its migration lock.
 20    public static async Task<LayerResponse<SeedRunResult>> UpAsync(
 21            DbContext dbContext,
 22            SeedConfiguration configuration,
 23            ISeedDatabaseProvider provider,
 24            string? targetMigrationId,
 25            bool takeLock,
 26            bool async,
 27            CancellationToken cancellationToken
 28        )
 29    {
 30        // Standalone runs own their transactions (one per migration, failures audited). Only EF's seeding hook (takeLoc
 31        // the transaction EF already opened.
 27932        if ( takeLock && dbContext.Database.CurrentTransaction is not null )
 33        {
 934            throw new InvalidOperationException( "Seed migrations run in their own transactions: call ISeedMigrator with
 35        }
 36
 27037        if ( configuration.Migrations is not { IsSuccess: true, Result: { } migrations } ) { return LayerResponse<SeedRu
 38
 27339        if ( targetMigrationId is not null && migrations.All( m => m.Id != targetMigrationId ) ) { return SeedErrors.Tar
 40
 41
 26742        return await SessionAsync( dbContext,
 26743                                   configuration,
 26744                                   provider,
 26745                                   takeLock,
 26746                                   () => ApplyPendingAsync( dbContext, configuration, provider, migrations, targetMigrat
 26747                                   async,
 26748                                   cancellationToken )
 26749                         .ConfigureAwait( false );
 27050    }
 51
 52    /// Opens the connection, takes the lock (when asked), ensures the tables, runs <paramref name="body"/>; then releas
 53    /// Exceptions become classified errors; a clean-up failure is reported with the result, never dropped.
 54    internal static async Task<LayerResponse<T>> SessionAsync< T >(
 55            DbContext dbContext,
 56            SeedConfiguration configuration,
 57            ISeedDatabaseProvider provider,
 58            bool takeLock,
 59            Func<Task<LayerResponse<T>>> body,
 60            bool async,
 61            CancellationToken cancellationToken
 62        )
 63    {
 64        LayerResponse<T> result;
 38165        var              opened  = false;
 38166        var              locked  = false;
 38167        Error?           cleanup = null;
 68
 69        try
 70        {
 38171            opened = await OpenAsync( dbContext, async, cancellationToken ).ConfigureAwait( false );
 72
 73
 74            // Lock first: two runners starting together must not both create the history tables.
 73875            if ( takeLock ) { locked = await provider.AcquireLockAsync( dbContext, configuration.Options.LockTimeout, as
 76
 38877            if ( takeLock && !locked ) { result = SeedErrors.LockTimeout( configuration.Options.LockTimeout ); }
 78            else
 79            {
 37480                await provider.EnsureTablesAsync( dbContext, async, cancellationToken ).ConfigureAwait( false );
 81
 37482                result = await body().ConfigureAwait( false );
 83            }
 38184        }
 085        catch ( OperationCanceledException ) { throw; }
 086        catch ( Exception ex ) { result = provider.Classify( ex ) ?? Error.Unknown( ex ); }
 87        finally
 88        {
 89            // Release before closing: a pooled connection keeps a session lock until it is reused.
 73190            if ( locked ) { cleanup = await TryAsync( provider, () => provider.ReleaseLockAsync( dbContext, async, Cance
 91
 73892            if ( opened ) { cleanup ??= await TryAsync( provider, () => CloseAsync( dbContext, async ) ).ConfigureAwait(
 93        }
 94
 95
 76296        if ( cleanup is null ) { return result; }
 97
 098        return result.IsSuccess ? cleanup : LayerResponse<T>.Failure( [ ..result.Errors, cleanup ] );
 38199    }
 100
 101    /// Lists the embedded and applied migrations.
 102    public static async Task<LayerResponse<IReadOnlyList<SeedMigrationInfo>>> ListAsync(
 103            DbContext dbContext,
 104            SeedConfiguration configuration,
 105            ISeedDatabaseProvider provider,
 106            bool async,
 107            CancellationToken cancellationToken
 108        )
 109    {
 15110        if ( configuration.Migrations is not { IsSuccess: true, Result: { } migrations } )
 111        {
 0112            return LayerResponse<IReadOnlyList<SeedMigrationInfo>>.Failure( configuration.Migrations );
 113        }
 114
 115
 116        IReadOnlyList<SeedHistoryRow> history;
 15117        var                           opened = false;
 118
 119        try
 120        {
 15121            opened  = await OpenAsync( dbContext, async, cancellationToken ).ConfigureAwait( false );
 15122            history = await provider.ReadHistoryAsync( dbContext, configuration.ContextKey, async, cancellationToken ).C
 15123        }
 0124        catch ( OperationCanceledException ) { throw; }
 0125        catch ( Exception ex ) { return provider.Classify( ex ) ?? Error.Unknown( ex ); }
 126        finally
 127        {
 30128            if ( opened ) { await CloseAsync( dbContext, async ).ConfigureAwait( false ); }
 129        }
 130
 131
 15132        var applied  = history.ToDictionary( h => h.MigrationId, StringComparer.Ordinal );
 15133        var embedded = migrations.ToDictionary( m => m.Id, StringComparer.Ordinal );
 134
 15135        return applied.Keys.Union( embedded.Keys )
 15136                      .Order( StringComparer.Ordinal )
 15137                      .Select( id =>
 15138                      {
 15139                          var row       = applied.GetValueOrDefault( id );
 15140                          var migration = embedded.GetValueOrDefault( id );
 15141
 15142                          return new SeedMigrationInfo
 15143                                     {
 15144                                         Id                      = id,
 15145                                         IsApplied               = row is not null,
 15146                                         AppliedOn               = row?.AppliedOn,
 15147                                         ChecksumMatches         = row is null || ( migration is not null && migration.C
 15148                                         RequiresSchemaMigration = migration?.Manifest.RequiresSchemaMigration
 15149                                     };
 15150                      } )
 15151                      .ToList();
 15152    }
 153
 154
 155    private static async Task<LayerResponse<SeedRunResult>> ApplyPendingAsync(
 156            DbContext dbContext,
 157            SeedConfiguration configuration,
 158            ISeedDatabaseProvider provider,
 159            IReadOnlyList<SeedMigrationFiles> migrations,
 160            string? targetMigrationId,
 161            bool async,
 162            CancellationToken cancellationToken
 163        )
 164    {
 260165        var history  = await provider.ReadHistoryAsync( dbContext, configuration.ContextKey, async, cancellationToken ).
 260166        var applied  = history.ToDictionary( h => h.MigrationId, StringComparer.Ordinal );
 260167        var newest   = applied.Keys.Max( StringComparer.Ordinal );
 260168        var warnings = migrations.Where( m => applied.TryGetValue( m.Id, out var row ) && row.Checksum != m.Checksum ).S
 260169        var ids      = new List<string>();
 170
 171
 1186172        foreach ( var migration in migrations.Where( m => !applied.ContainsKey( m.Id )
 260173                                                       && ( targetMigrationId is null || string.CompareOrdinal( m.Id, ta
 174        {
 349175            if ( newest is not null && string.CompareOrdinal( migration.Id, newest ) < 0 ) { warnings.Add( SeedErrors.Ou
 176
 177
 343178            var applyResult = await ApplyAsync( dbContext, configuration, provider, migration, async, cancellationToken 
 179
 363180            if ( applyResult.IsFailure ) { return LayerResponse<SeedRunResult>.Failure( applyResult ); }
 181
 323182            ids.Add( migration.Id );
 323183        }
 184
 185
 240186        return new SeedRunResult { Ids = ids, Warnings = warnings };
 260187    }
 188
 189    /// One migration: checks, then its operations + history + audit in one transaction. A failure writes a failed audit
 190    private static async Task<LayerResponse> ApplyAsync(
 191            DbContext dbContext,
 192            SeedConfiguration configuration,
 193            ISeedDatabaseProvider provider,
 194            SeedMigrationFiles migration,
 195            bool async,
 196            CancellationToken cancellationToken
 197        )
 198    {
 343199        using var activity = DbSeederDiagnostics.ActivitySource.StartActivity( "Apply seed migration" );
 343200        activity?.SetTag( "MigrationId", migration.Id );
 201
 343202        var startedOn = Now( configuration );
 343203        var counts    = new SeedCounts();
 343204        var result    = await CheckAsync( dbContext, configuration, migration, async, cancellationToken ).ConfigureAwait
 205
 206
 343207        if ( result is { IsSuccess: true, Result: { } tables } )
 208        {
 209            try
 210            {
 337211                result = dbContext.Database.CurrentTransaction is not null
 337212                             ? await RunAsync( dbContext, configuration, provider, migration, tables, startedOn, counts,
 337213                             : await InStrategyAsync( dbContext, configuration, provider, migration, tables, startedOn, 
 330214            }
 0215            catch ( OperationCanceledException ) { throw; }
 21216            catch ( Exception ex ) { result = provider.Classify( ex ) ?? Error.Unknown( ex ); }
 217        }
 218
 219
 343220        activity?.SetTag( "RowsInserted", counts.Inserted );
 343221        activity?.SetTag( "RowsUpdated", counts.Updated );
 343222        activity?.SetTag( "RowsDeleted", counts.Deleted );
 223
 343224        if ( result.IsSuccess )
 225        {
 323226            activity?.SetStatus( ActivityStatusCode.Ok );
 227
 323228            return LayerResponse.Success();
 229        }
 230
 231
 20232        activity?.SetStatus( ActivityStatusCode.Error, result.Errors[ 0 ].Code );
 233
 234        // The transaction was rolled back; record the failure outside it (inside EF's own seeding transaction it rolls 
 20235        var audit = await TryAsync( provider, () => provider.WriteAuditAsync( dbContext, Audit( configuration, migration
 20236                        .ConfigureAwait( false );
 237
 20238        return LayerResponse.Failure( audit is null ? result.Errors : [ ..result.Errors, audit ] );
 343239    }
 240
 241    /// Schema readiness and binding (no database writes).
 242    private static async Task<LayerResponse<IReadOnlyList<SeedBoundTable>>> CheckAsync(
 243            DbContext dbContext,
 244            SeedConfiguration configuration,
 245            SeedMigrationFiles migration,
 246            bool async,
 247            CancellationToken cancellationToken
 248        )
 249    {
 343250        if ( migration.Manifest.RequiresSchemaMigration is { Length: > 0 } schemaMigration )
 251        {
 10252            var appliedSchema = async
 10253                                    ? await dbContext.Database.GetAppliedMigrationsAsync( cancellationToken ).ConfigureA
 10254                                    : dbContext.Database.GetAppliedMigrations();
 255
 16256            if ( !appliedSchema.Contains( schemaMigration, StringComparer.Ordinal ) ) { return SeedErrors.SchemaNotReady
 257        }
 258
 259
 337260        return SeedMigrationBinder.Bind( dbContext.Model, configuration, migration );
 343261    }
 262
 263    private static async Task<LayerResponse<IReadOnlyList<SeedBoundTable>>> InStrategyAsync(
 264            DbContext dbContext,
 265            SeedConfiguration configuration,
 266            ISeedDatabaseProvider provider,
 267            SeedMigrationFiles migration,
 268            IReadOnlyList<SeedBoundTable> tables,
 269            DateTime startedOn,
 270            SeedCounts counts,
 271            bool async,
 272            CancellationToken cancellationToken
 273        )
 274    {
 275        // A retrying execution strategy reruns the whole transaction, so the counts restart with it.
 308276        var strategy = dbContext.Database.CreateExecutionStrategy();
 277
 308278        return async
 308279                   ? await strategy.ExecuteAsync( token => InTransactionAsync( true, token ), cancellationToken ).Config
 308280                   : strategy.Execute( () => SyncPath.Result( InTransactionAsync( false, cancellationToken ) ) );
 281
 282
 283        async Task<LayerResponse<IReadOnlyList<SeedBoundTable>>> InTransactionAsync( bool runAsync, CancellationToken to
 284        {
 285            counts.Reset();
 286
 287            var transaction = runAsync
 288                                  ? await dbContext.Database.BeginTransactionAsync( IsolationLevel.ReadCommitted, token 
 289                                  : dbContext.Database.BeginTransaction( IsolationLevel.ReadCommitted );
 290
 291            try
 292            {
 293                var run = await RunAsync( dbContext, configuration, provider, migration, tables, startedOn, counts, runA
 294
 295                if ( run.IsFailure ) { return run; }
 296
 297
 298                if ( runAsync ) { await transaction.CommitAsync( token ).ConfigureAwait( false ); }
 299                else { transaction.Commit(); }
 300
 301                return run;
 302            }
 303            finally
 304            {
 305                // Disposing an uncommitted transaction rolls it back.
 306                if ( runAsync ) { await transaction.DisposeAsync().ConfigureAwait( false ); }
 307                else { transaction.Dispose(); }
 308            }
 309        }
 301310    }
 311
 312    /// The migration's operations, then its history and audit rows, on the current transaction.
 313    private static async Task<LayerResponse<IReadOnlyList<SeedBoundTable>>> RunAsync(
 314            DbContext dbContext,
 315            SeedConfiguration configuration,
 316            ISeedDatabaseProvider provider,
 317            SeedMigrationFiles migration,
 318            IReadOnlyList<SeedBoundTable> tables,
 319            DateTime startedOn,
 320            SeedCounts counts,
 321            bool async,
 322            CancellationToken cancellationToken
 323        )
 324    {
 337325        var steps = SeedStepPlanner.Plan( tables, new SeedRowWriter( configuration.Options, Now( configuration ) ), migr
 326
 337327        if ( steps.IsFailure ) { return LayerResponse<IReadOnlyList<SeedBoundTable>>.Failure( steps ); }
 328
 329
 3610330        foreach ( var step in steps.Result! )
 331        {
 1475332            switch ( step.Kind )
 333            {
 334                case SeedStepKind.Upsert:
 1236335                    var written = await provider.UpsertAsync( dbContext, step.Write!, async, cancellationToken ).Configu
 336
 1229337                    counts.Inserted += written.Inserted;
 1229338                    counts.Updated  += written.Updated;
 339
 1229340                    break;
 341
 342                case SeedStepKind.Restore:
 35343                    counts.Updated += await provider.RestoreAsync( dbContext, step.Keys!, step.Values, async, cancellati
 344
 35345                    break;
 346
 347                case SeedStepKind.SoftDelete:
 134348                    counts.Deleted += await provider.SoftDeleteAsync( dbContext, step.Keys!, step.Values, async, cancell
 349
 134350                    break;
 351
 352                case SeedStepKind.Delete:
 70353                    var blockers = await provider.CountReferencesAsync( dbContext, step.Keys!, async, cancellationToken 
 354
 77355                    if ( blockers.Count > 0 ) { return SeedErrors.RowsReferenced( step.Table.DisplayName, blockers ); }
 356
 63357                    counts.Deleted += await provider.DeleteAsync( dbContext, step.Keys!, async, cancellationToken ).Conf
 358
 359                    break;
 360            }
 1461361        }
 362
 363
 323364        var now = Now( configuration );
 365
 323366        await provider.WriteHistoryAsync( dbContext,
 323367                                          new SeedHistoryRow
 323368                                              {
 323369                                                  MigrationId    = migration.Id,
 323370                                                  ContextKey     = configuration.ContextKey,
 323371                                                  Checksum       = migration.Checksum,
 323372                                                  AppliedOn      = now,
 323373                                                  ProductVersion = DbSeederDiagnostics.Version
 323374                                              },
 323375                                          async,
 323376                                          cancellationToken )
 323377                      .ConfigureAwait( false );
 378
 323379        await provider.WriteAuditAsync( dbContext, Audit( configuration, migration, startedOn, counts, errorCode: null )
 380
 381
 323382        return LayerResponse<IReadOnlyList<SeedBoundTable>>.Success( tables );
 330383    }
 384
 385    private static SeedAuditRow Audit( SeedConfiguration configuration, SeedMigrationFiles migration, DateTime startedOn
 343386        => new()
 343387               {
 343388                   MigrationId  = migration.Id,
 343389                   ContextKey   = configuration.ContextKey,
 343390                   Operation    = Up,
 343391                   StartedOn    = startedOn,
 343392                   FinishedOn   = Now( configuration ),
 343393                   Succeeded    = errorCode is null,
 343394                   ErrorCode    = errorCode,
 343395                   RowsInserted = counts.Inserted,
 343396                   RowsUpdated  = counts.Updated,
 343397                   RowsDeleted  = counts.Deleted,
 343398                   OsUser       = Environment.UserName,
 343399                   MachineName  = Environment.MachineName,
 343400                   ToolVersion  = DbSeederDiagnostics.Version
 343401               };
 402
 1593403    internal static DateTime Now( SeedConfiguration configuration ) => configuration.Options.TimeProvider.GetLocalNow().
 404
 405    private static async Task<bool> OpenAsync( DbContext dbContext, bool async, CancellationToken cancellationToken )
 406    {
 420407        if ( dbContext.Database.GetDbConnection().State == ConnectionState.Open ) { return false; }
 408
 409
 723410        if ( async ) { await dbContext.Database.OpenConnectionAsync( cancellationToken ).ConfigureAwait( false ); }
 21411        else { dbContext.Database.OpenConnection(); }
 412
 372413        return true;
 396414    }
 415
 416    private static async Task CloseAsync( DbContext dbContext, bool async )
 417    {
 723418        if ( async ) { await dbContext.Database.CloseConnectionAsync().ConfigureAwait( false ); }
 21419        else { dbContext.Database.CloseConnection(); }
 372420    }
 421
 422    /// Runs a clean-up step; its failure becomes an error (reported with the run's result, never dropped silently).
 423    internal static async Task<Error?> TryAsync( ISeedDatabaseProvider provider, Func<Task> step )
 424    {
 425        try
 426        {
 738427            await step().ConfigureAwait( false );
 428
 738429            return null;
 430        }
 0431        catch ( OperationCanceledException ) { throw; }
 0432        catch ( Exception ex ) { return provider.Classify( ex ) ?? Error.Unknown( ex ); }
 738433    }
 434
 435
 436    /// Rows changed by one migration (mutable while it runs).
 437    private sealed class SeedCounts
 438    {
 439        public int Inserted { get; set; }
 440
 441        public int Updated { get; set; }
 442
 443        public int Deleted { get; set; }
 444
 308445        public void Reset() => ( Inserted, Updated, Deleted ) = ( 0, 0, 0 );
 446    }
 447}