| | | 1 | | #region Usings |
| | | 2 | | using System.Data; |
| | | 3 | | using System.Diagnostics; |
| | | 4 | | using Microsoft.EntityFrameworkCore.Storage; |
| | | 5 | | #endregion |
| | | 6 | | |
| | | 7 | | namespace 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> |
| | | 13 | | internal 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. |
| | 279 | 32 | | if ( takeLock && dbContext.Database.CurrentTransaction is not null ) |
| | | 33 | | { |
| | 9 | 34 | | throw new InvalidOperationException( "Seed migrations run in their own transactions: call ISeedMigrator with |
| | | 35 | | } |
| | | 36 | | |
| | 270 | 37 | | if ( configuration.Migrations is not { IsSuccess: true, Result: { } migrations } ) { return LayerResponse<SeedRu |
| | | 38 | | |
| | 273 | 39 | | if ( targetMigrationId is not null && migrations.All( m => m.Id != targetMigrationId ) ) { return SeedErrors.Tar |
| | | 40 | | |
| | | 41 | | |
| | 267 | 42 | | return await SessionAsync( dbContext, |
| | 267 | 43 | | configuration, |
| | 267 | 44 | | provider, |
| | 267 | 45 | | takeLock, |
| | 267 | 46 | | () => ApplyPendingAsync( dbContext, configuration, provider, migrations, targetMigrat |
| | 267 | 47 | | async, |
| | 267 | 48 | | cancellationToken ) |
| | 267 | 49 | | .ConfigureAwait( false ); |
| | 270 | 50 | | } |
| | | 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; |
| | 381 | 65 | | var opened = false; |
| | 381 | 66 | | var locked = false; |
| | 381 | 67 | | Error? cleanup = null; |
| | | 68 | | |
| | | 69 | | try |
| | | 70 | | { |
| | 381 | 71 | | opened = await OpenAsync( dbContext, async, cancellationToken ).ConfigureAwait( false ); |
| | | 72 | | |
| | | 73 | | |
| | | 74 | | // Lock first: two runners starting together must not both create the history tables. |
| | 738 | 75 | | if ( takeLock ) { locked = await provider.AcquireLockAsync( dbContext, configuration.Options.LockTimeout, as |
| | | 76 | | |
| | 388 | 77 | | if ( takeLock && !locked ) { result = SeedErrors.LockTimeout( configuration.Options.LockTimeout ); } |
| | | 78 | | else |
| | | 79 | | { |
| | 374 | 80 | | await provider.EnsureTablesAsync( dbContext, async, cancellationToken ).ConfigureAwait( false ); |
| | | 81 | | |
| | 374 | 82 | | result = await body().ConfigureAwait( false ); |
| | | 83 | | } |
| | 381 | 84 | | } |
| | 0 | 85 | | catch ( OperationCanceledException ) { throw; } |
| | 0 | 86 | | 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. |
| | 731 | 90 | | if ( locked ) { cleanup = await TryAsync( provider, () => provider.ReleaseLockAsync( dbContext, async, Cance |
| | | 91 | | |
| | 738 | 92 | | if ( opened ) { cleanup ??= await TryAsync( provider, () => CloseAsync( dbContext, async ) ).ConfigureAwait( |
| | | 93 | | } |
| | | 94 | | |
| | | 95 | | |
| | 762 | 96 | | if ( cleanup is null ) { return result; } |
| | | 97 | | |
| | 0 | 98 | | return result.IsSuccess ? cleanup : LayerResponse<T>.Failure( [ ..result.Errors, cleanup ] ); |
| | 381 | 99 | | } |
| | | 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 | | { |
| | 15 | 110 | | if ( configuration.Migrations is not { IsSuccess: true, Result: { } migrations } ) |
| | | 111 | | { |
| | 0 | 112 | | return LayerResponse<IReadOnlyList<SeedMigrationInfo>>.Failure( configuration.Migrations ); |
| | | 113 | | } |
| | | 114 | | |
| | | 115 | | |
| | | 116 | | IReadOnlyList<SeedHistoryRow> history; |
| | 15 | 117 | | var opened = false; |
| | | 118 | | |
| | | 119 | | try |
| | | 120 | | { |
| | 15 | 121 | | opened = await OpenAsync( dbContext, async, cancellationToken ).ConfigureAwait( false ); |
| | 15 | 122 | | history = await provider.ReadHistoryAsync( dbContext, configuration.ContextKey, async, cancellationToken ).C |
| | 15 | 123 | | } |
| | 0 | 124 | | catch ( OperationCanceledException ) { throw; } |
| | 0 | 125 | | catch ( Exception ex ) { return provider.Classify( ex ) ?? Error.Unknown( ex ); } |
| | | 126 | | finally |
| | | 127 | | { |
| | 30 | 128 | | if ( opened ) { await CloseAsync( dbContext, async ).ConfigureAwait( false ); } |
| | | 129 | | } |
| | | 130 | | |
| | | 131 | | |
| | 15 | 132 | | var applied = history.ToDictionary( h => h.MigrationId, StringComparer.Ordinal ); |
| | 15 | 133 | | var embedded = migrations.ToDictionary( m => m.Id, StringComparer.Ordinal ); |
| | | 134 | | |
| | 15 | 135 | | return applied.Keys.Union( embedded.Keys ) |
| | 15 | 136 | | .Order( StringComparer.Ordinal ) |
| | 15 | 137 | | .Select( id => |
| | 15 | 138 | | { |
| | 15 | 139 | | var row = applied.GetValueOrDefault( id ); |
| | 15 | 140 | | var migration = embedded.GetValueOrDefault( id ); |
| | 15 | 141 | | |
| | 15 | 142 | | return new SeedMigrationInfo |
| | 15 | 143 | | { |
| | 15 | 144 | | Id = id, |
| | 15 | 145 | | IsApplied = row is not null, |
| | 15 | 146 | | AppliedOn = row?.AppliedOn, |
| | 15 | 147 | | ChecksumMatches = row is null || ( migration is not null && migration.C |
| | 15 | 148 | | RequiresSchemaMigration = migration?.Manifest.RequiresSchemaMigration |
| | 15 | 149 | | }; |
| | 15 | 150 | | } ) |
| | 15 | 151 | | .ToList(); |
| | 15 | 152 | | } |
| | | 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 | | { |
| | 260 | 165 | | var history = await provider.ReadHistoryAsync( dbContext, configuration.ContextKey, async, cancellationToken ). |
| | 260 | 166 | | var applied = history.ToDictionary( h => h.MigrationId, StringComparer.Ordinal ); |
| | 260 | 167 | | var newest = applied.Keys.Max( StringComparer.Ordinal ); |
| | 260 | 168 | | var warnings = migrations.Where( m => applied.TryGetValue( m.Id, out var row ) && row.Checksum != m.Checksum ).S |
| | 260 | 169 | | var ids = new List<string>(); |
| | | 170 | | |
| | | 171 | | |
| | 1186 | 172 | | foreach ( var migration in migrations.Where( m => !applied.ContainsKey( m.Id ) |
| | 260 | 173 | | && ( targetMigrationId is null || string.CompareOrdinal( m.Id, ta |
| | | 174 | | { |
| | 349 | 175 | | if ( newest is not null && string.CompareOrdinal( migration.Id, newest ) < 0 ) { warnings.Add( SeedErrors.Ou |
| | | 176 | | |
| | | 177 | | |
| | 343 | 178 | | var applyResult = await ApplyAsync( dbContext, configuration, provider, migration, async, cancellationToken |
| | | 179 | | |
| | 363 | 180 | | if ( applyResult.IsFailure ) { return LayerResponse<SeedRunResult>.Failure( applyResult ); } |
| | | 181 | | |
| | 323 | 182 | | ids.Add( migration.Id ); |
| | 323 | 183 | | } |
| | | 184 | | |
| | | 185 | | |
| | 240 | 186 | | return new SeedRunResult { Ids = ids, Warnings = warnings }; |
| | 260 | 187 | | } |
| | | 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 | | { |
| | 343 | 199 | | using var activity = DbSeederDiagnostics.ActivitySource.StartActivity( "Apply seed migration" ); |
| | 343 | 200 | | activity?.SetTag( "MigrationId", migration.Id ); |
| | | 201 | | |
| | 343 | 202 | | var startedOn = Now( configuration ); |
| | 343 | 203 | | var counts = new SeedCounts(); |
| | 343 | 204 | | var result = await CheckAsync( dbContext, configuration, migration, async, cancellationToken ).ConfigureAwait |
| | | 205 | | |
| | | 206 | | |
| | 343 | 207 | | if ( result is { IsSuccess: true, Result: { } tables } ) |
| | | 208 | | { |
| | | 209 | | try |
| | | 210 | | { |
| | 337 | 211 | | result = dbContext.Database.CurrentTransaction is not null |
| | 337 | 212 | | ? await RunAsync( dbContext, configuration, provider, migration, tables, startedOn, counts, |
| | 337 | 213 | | : await InStrategyAsync( dbContext, configuration, provider, migration, tables, startedOn, |
| | 330 | 214 | | } |
| | 0 | 215 | | catch ( OperationCanceledException ) { throw; } |
| | 21 | 216 | | catch ( Exception ex ) { result = provider.Classify( ex ) ?? Error.Unknown( ex ); } |
| | | 217 | | } |
| | | 218 | | |
| | | 219 | | |
| | 343 | 220 | | activity?.SetTag( "RowsInserted", counts.Inserted ); |
| | 343 | 221 | | activity?.SetTag( "RowsUpdated", counts.Updated ); |
| | 343 | 222 | | activity?.SetTag( "RowsDeleted", counts.Deleted ); |
| | | 223 | | |
| | 343 | 224 | | if ( result.IsSuccess ) |
| | | 225 | | { |
| | 323 | 226 | | activity?.SetStatus( ActivityStatusCode.Ok ); |
| | | 227 | | |
| | 323 | 228 | | return LayerResponse.Success(); |
| | | 229 | | } |
| | | 230 | | |
| | | 231 | | |
| | 20 | 232 | | 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 |
| | 20 | 235 | | var audit = await TryAsync( provider, () => provider.WriteAuditAsync( dbContext, Audit( configuration, migration |
| | 20 | 236 | | .ConfigureAwait( false ); |
| | | 237 | | |
| | 20 | 238 | | return LayerResponse.Failure( audit is null ? result.Errors : [ ..result.Errors, audit ] ); |
| | 343 | 239 | | } |
| | | 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 | | { |
| | 343 | 250 | | if ( migration.Manifest.RequiresSchemaMigration is { Length: > 0 } schemaMigration ) |
| | | 251 | | { |
| | 10 | 252 | | var appliedSchema = async |
| | 10 | 253 | | ? await dbContext.Database.GetAppliedMigrationsAsync( cancellationToken ).ConfigureA |
| | 10 | 254 | | : dbContext.Database.GetAppliedMigrations(); |
| | | 255 | | |
| | 16 | 256 | | if ( !appliedSchema.Contains( schemaMigration, StringComparer.Ordinal ) ) { return SeedErrors.SchemaNotReady |
| | | 257 | | } |
| | | 258 | | |
| | | 259 | | |
| | 337 | 260 | | return SeedMigrationBinder.Bind( dbContext.Model, configuration, migration ); |
| | 343 | 261 | | } |
| | | 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. |
| | 308 | 276 | | var strategy = dbContext.Database.CreateExecutionStrategy(); |
| | | 277 | | |
| | 308 | 278 | | return async |
| | 308 | 279 | | ? await strategy.ExecuteAsync( token => InTransactionAsync( true, token ), cancellationToken ).Config |
| | 308 | 280 | | : 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 | | } |
| | 301 | 310 | | } |
| | | 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 | | { |
| | 337 | 325 | | var steps = SeedStepPlanner.Plan( tables, new SeedRowWriter( configuration.Options, Now( configuration ) ), migr |
| | | 326 | | |
| | 337 | 327 | | if ( steps.IsFailure ) { return LayerResponse<IReadOnlyList<SeedBoundTable>>.Failure( steps ); } |
| | | 328 | | |
| | | 329 | | |
| | 3610 | 330 | | foreach ( var step in steps.Result! ) |
| | | 331 | | { |
| | 1475 | 332 | | switch ( step.Kind ) |
| | | 333 | | { |
| | | 334 | | case SeedStepKind.Upsert: |
| | 1236 | 335 | | var written = await provider.UpsertAsync( dbContext, step.Write!, async, cancellationToken ).Configu |
| | | 336 | | |
| | 1229 | 337 | | counts.Inserted += written.Inserted; |
| | 1229 | 338 | | counts.Updated += written.Updated; |
| | | 339 | | |
| | 1229 | 340 | | break; |
| | | 341 | | |
| | | 342 | | case SeedStepKind.Restore: |
| | 35 | 343 | | counts.Updated += await provider.RestoreAsync( dbContext, step.Keys!, step.Values, async, cancellati |
| | | 344 | | |
| | 35 | 345 | | break; |
| | | 346 | | |
| | | 347 | | case SeedStepKind.SoftDelete: |
| | 134 | 348 | | counts.Deleted += await provider.SoftDeleteAsync( dbContext, step.Keys!, step.Values, async, cancell |
| | | 349 | | |
| | 134 | 350 | | break; |
| | | 351 | | |
| | | 352 | | case SeedStepKind.Delete: |
| | 70 | 353 | | var blockers = await provider.CountReferencesAsync( dbContext, step.Keys!, async, cancellationToken |
| | | 354 | | |
| | 77 | 355 | | if ( blockers.Count > 0 ) { return SeedErrors.RowsReferenced( step.Table.DisplayName, blockers ); } |
| | | 356 | | |
| | 63 | 357 | | counts.Deleted += await provider.DeleteAsync( dbContext, step.Keys!, async, cancellationToken ).Conf |
| | | 358 | | |
| | | 359 | | break; |
| | | 360 | | } |
| | 1461 | 361 | | } |
| | | 362 | | |
| | | 363 | | |
| | 323 | 364 | | var now = Now( configuration ); |
| | | 365 | | |
| | 323 | 366 | | await provider.WriteHistoryAsync( dbContext, |
| | 323 | 367 | | new SeedHistoryRow |
| | 323 | 368 | | { |
| | 323 | 369 | | MigrationId = migration.Id, |
| | 323 | 370 | | ContextKey = configuration.ContextKey, |
| | 323 | 371 | | Checksum = migration.Checksum, |
| | 323 | 372 | | AppliedOn = now, |
| | 323 | 373 | | ProductVersion = DbSeederDiagnostics.Version |
| | 323 | 374 | | }, |
| | 323 | 375 | | async, |
| | 323 | 376 | | cancellationToken ) |
| | 323 | 377 | | .ConfigureAwait( false ); |
| | | 378 | | |
| | 323 | 379 | | await provider.WriteAuditAsync( dbContext, Audit( configuration, migration, startedOn, counts, errorCode: null ) |
| | | 380 | | |
| | | 381 | | |
| | 323 | 382 | | return LayerResponse<IReadOnlyList<SeedBoundTable>>.Success( tables ); |
| | 330 | 383 | | } |
| | | 384 | | |
| | | 385 | | private static SeedAuditRow Audit( SeedConfiguration configuration, SeedMigrationFiles migration, DateTime startedOn |
| | 343 | 386 | | => new() |
| | 343 | 387 | | { |
| | 343 | 388 | | MigrationId = migration.Id, |
| | 343 | 389 | | ContextKey = configuration.ContextKey, |
| | 343 | 390 | | Operation = Up, |
| | 343 | 391 | | StartedOn = startedOn, |
| | 343 | 392 | | FinishedOn = Now( configuration ), |
| | 343 | 393 | | Succeeded = errorCode is null, |
| | 343 | 394 | | ErrorCode = errorCode, |
| | 343 | 395 | | RowsInserted = counts.Inserted, |
| | 343 | 396 | | RowsUpdated = counts.Updated, |
| | 343 | 397 | | RowsDeleted = counts.Deleted, |
| | 343 | 398 | | OsUser = Environment.UserName, |
| | 343 | 399 | | MachineName = Environment.MachineName, |
| | 343 | 400 | | ToolVersion = DbSeederDiagnostics.Version |
| | 343 | 401 | | }; |
| | | 402 | | |
| | 1593 | 403 | | 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 | | { |
| | 420 | 407 | | if ( dbContext.Database.GetDbConnection().State == ConnectionState.Open ) { return false; } |
| | | 408 | | |
| | | 409 | | |
| | 723 | 410 | | if ( async ) { await dbContext.Database.OpenConnectionAsync( cancellationToken ).ConfigureAwait( false ); } |
| | 21 | 411 | | else { dbContext.Database.OpenConnection(); } |
| | | 412 | | |
| | 372 | 413 | | return true; |
| | 396 | 414 | | } |
| | | 415 | | |
| | | 416 | | private static async Task CloseAsync( DbContext dbContext, bool async ) |
| | | 417 | | { |
| | 723 | 418 | | if ( async ) { await dbContext.Database.CloseConnectionAsync().ConfigureAwait( false ); } |
| | 21 | 419 | | else { dbContext.Database.CloseConnection(); } |
| | 372 | 420 | | } |
| | | 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 | | { |
| | 738 | 427 | | await step().ConfigureAwait( false ); |
| | | 428 | | |
| | 738 | 429 | | return null; |
| | | 430 | | } |
| | 0 | 431 | | catch ( OperationCanceledException ) { throw; } |
| | 0 | 432 | | catch ( Exception ex ) { return provider.Classify( ex ) ?? Error.Unknown( ex ); } |
| | 738 | 433 | | } |
| | | 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 | | |
| | 308 | 445 | | public void Reset() => ( Inserted, Updated, Deleted ) = ( 0, 0, 0 ); |
| | | 446 | | } |
| | | 447 | | } |