From 6644f5f64099fa26df0921c55d3d272770e3a7ba Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Thu, 1 Aug 2024 19:59:54 -0700 Subject: [PATCH] Remove migration lock timeout Log migration lock acquisition Fixes #34196 Fixes #17578 --- .../Storage/Internal/CosmosDatabaseCreator.cs | 50 +++++-- .../Design/Internal/DbContextOperations.cs | 2 - .../Design/Internal/MigrationsOperations.cs | 3 +- .../Internal/InMemoryDatabaseCreator.cs | 51 ++++++- .../Diagnostics/RelationalEventId.cs | 14 ++ .../Diagnostics/RelationalLoggerExtensions.cs | 30 +++++ .../RelationalLoggingDefinitions.cs | 9 ++ .../RelationalDatabaseFacadeExtensions.cs | 26 +--- ...ntityFrameworkRelationalServicesBuilder.cs | 3 +- .../Migrations/HistoryRepository.cs | 6 +- .../Migrations/IHistoryRepository.cs | 6 +- src/EFCore.Relational/Migrations/IMigrator.cs | 18 +-- .../Migrations/IMigratorData.cs | 29 ---- .../Migrations/IMigratorPlugin.cs | 73 ---------- .../Internal/MigrationCommandExecutor.cs | 61 ++++----- .../Migrations/Internal/Migrator.cs | 74 ++++++----- .../Migrations/Internal/MigratorData.cs | 7 +- .../Properties/RelationalStrings.Designer.cs | 27 +++- .../Properties/RelationalStrings.resx | 58 ++++---- .../Storage/RelationalDatabaseCreator.cs | 89 ++++++++----- .../RelationalDatabaseCreatorDependencies.cs | 7 + .../Internal/SqlServerHistoryRepository.cs | 14 +- .../Internal/SqliteHistoryRepository.cs | 40 +----- src/EFCore/DbContextOptionsBuilder.cs | 44 ++++++ src/EFCore/DbContextOptionsBuilder`.cs | 88 ++++++++++++ .../Infrastructure/CoreOptionsExtension.cs | 52 ++++++++ src/EFCore/Properties/CoreStrings.Designer.cs | 30 +++-- src/EFCore/Properties/CoreStrings.resx | 60 +++++---- .../OptimisticConcurrencyCosmosTest.cs | 20 +-- .../Query/JsonQueryCosmosTest.cs | 1 + .../Storage/CosmosDatabaseCreatorTest.cs | 23 +++- .../TestUtilities/CosmosTestStore.cs | 2 +- .../Design/DesignTimeServicesTest.cs | 8 +- .../Design/MigrationScaffolderTest.cs | 8 +- .../Storage/InMemoryDatabaseCreatorTest.cs | 39 +++++- .../MigrationsInfrastructureTestBase.cs | 125 +++++++----------- .../Query/EntitySplittingQueryTestBase.cs | 18 ++- .../EntitySplitting/EntitySplittingData.cs | 16 ++- .../RelationalDatabaseFacadeExtensionsTest.cs | 4 +- .../Infrastructure/RelationalEventIdTest.cs | 9 -- .../F1FixtureBase.cs | 23 +++- .../Scaffolding/CompiledModelTestBase.cs | 3 +- .../TestModels/ConcurrencyModel/F1Context.cs | 10 +- .../MigrationsInfrastructureSqlServerTest.cs | 11 +- .../SqlServerDatabaseCreatorTest.cs | 63 +++++++-- .../TestUtilities/SqliteTestStore.cs | 5 +- 46 files changed, 821 insertions(+), 538 deletions(-) delete mode 100644 src/EFCore.Relational/Migrations/IMigratorData.cs delete mode 100644 src/EFCore.Relational/Migrations/IMigratorPlugin.cs diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs index d55b7a63f9e..75386a35107 100644 --- a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs +++ b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs @@ -18,6 +18,8 @@ public class CosmosDatabaseCreator : IDatabaseCreator private readonly IDesignTimeModel _designTimeModel; private readonly IUpdateAdapterFactory _updateAdapterFactory; private readonly IDatabase _database; + private readonly ICurrentDbContext _currentContext; + private readonly IDbContextOptions _contextOptions; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -29,12 +31,16 @@ public CosmosDatabaseCreator( ICosmosClientWrapper cosmosClient, IDesignTimeModel designTimeModel, IUpdateAdapterFactory updateAdapterFactory, - IDatabase database) + IDatabase database, + ICurrentDbContext currentContext, + IDbContextOptions contextOptions) { _cosmosClient = cosmosClient; _designTimeModel = designTimeModel; _updateAdapterFactory = updateAdapterFactory; _database = database; + _currentContext = currentContext; + _contextOptions = contextOptions; } /// @@ -55,7 +61,21 @@ public virtual bool EnsureCreated() if (created) { - Seed(); + InsertData(); + } + + var coreOptionsExtension = + _contextOptions.FindExtension() + ?? new CoreOptionsExtension(); + + var seed = coreOptionsExtension.Seeder; + if (seed != null) + { + seed(_currentContext.Context, created); + } + else if (coreOptionsExtension.AsyncSeeder != null) + { + throw new InvalidOperationException(CoreStrings.MissingSeeder); } return created; @@ -81,7 +101,21 @@ public virtual async Task EnsureCreatedAsync(CancellationToken cancellatio if (created) { - await SeedAsync(cancellationToken).ConfigureAwait(false); + await InsertDataAsync(cancellationToken).ConfigureAwait(false); + } + + var coreOptionsExtension = + _contextOptions.FindExtension() + ?? new CoreOptionsExtension(); + + var seedAsync = coreOptionsExtension.AsyncSeeder; + if (seedAsync != null) + { + await seedAsync(_currentContext.Context, created, cancellationToken).ConfigureAwait(false); + } + else if (coreOptionsExtension.Seeder != null) + { + throw new InvalidOperationException(CoreStrings.MissingSeeder); } return created; @@ -153,9 +187,9 @@ private static IEnumerable GetContainersToCreate(IModel mod /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual void Seed() + public virtual void InsertData() { - var updateAdapter = AddSeedData(); + var updateAdapter = AddModelData(); _database.SaveChanges(updateAdapter.GetEntriesToSave()); } @@ -166,14 +200,14 @@ public virtual void Seed() /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual Task SeedAsync(CancellationToken cancellationToken = default) + public virtual Task InsertDataAsync(CancellationToken cancellationToken = default) { - var updateAdapter = AddSeedData(); + var updateAdapter = AddModelData(); return _database.SaveChangesAsync(updateAdapter.GetEntriesToSave(), cancellationToken); } - private IUpdateAdapter AddSeedData() + private IUpdateAdapter AddModelData() { var updateAdapter = _updateAdapterFactory.CreateStandalone(); foreach (var entityType in _designTimeModel.Model.GetEntityTypes()) diff --git a/src/EFCore.Design/Design/Internal/DbContextOperations.cs b/src/EFCore.Design/Design/Internal/DbContextOperations.cs index 0c4ae03d1af..00d878a8176 100644 --- a/src/EFCore.Design/Design/Internal/DbContextOperations.cs +++ b/src/EFCore.Design/Design/Internal/DbContextOperations.cs @@ -1,14 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.IO; using System.Text; using Microsoft.Build.Locator; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.MSBuild; -using Microsoft.CodeAnalysis.Simplification; using Microsoft.EntityFrameworkCore.Infrastructure.Internal; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata.Internal; diff --git a/src/EFCore.Design/Design/Internal/MigrationsOperations.cs b/src/EFCore.Design/Design/Internal/MigrationsOperations.cs index 419d658cd2e..e841e680ac9 100644 --- a/src/EFCore.Design/Design/Internal/MigrationsOperations.cs +++ b/src/EFCore.Design/Design/Internal/MigrationsOperations.cs @@ -220,8 +220,7 @@ public virtual void UpdateDatabase( EnsureServices(services); var migrator = services.GetRequiredService(); - - migrator.Migrate(targetMigration: targetMigration); + migrator.Migrate(targetMigration); } _reporter.WriteInformation(DesignStrings.Done); diff --git a/src/EFCore.InMemory/Storage/Internal/InMemoryDatabaseCreator.cs b/src/EFCore.InMemory/Storage/Internal/InMemoryDatabaseCreator.cs index 654528b4a6c..69fc4d05515 100644 --- a/src/EFCore.InMemory/Storage/Internal/InMemoryDatabaseCreator.cs +++ b/src/EFCore.InMemory/Storage/Internal/InMemoryDatabaseCreator.cs @@ -12,6 +12,8 @@ namespace Microsoft.EntityFrameworkCore.InMemory.Storage.Internal; public class InMemoryDatabaseCreator : IDatabaseCreator { private readonly IDatabase _database; + private readonly ICurrentDbContext _currentContext; + private readonly IDbContextOptions _contextOptions; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -19,9 +21,14 @@ public class InMemoryDatabaseCreator : IDatabaseCreator /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public InMemoryDatabaseCreator(IDatabase database) + public InMemoryDatabaseCreator( + IDatabase database, + ICurrentDbContext currentContext, + IDbContextOptions contextOptions) { _database = database; + _currentContext = currentContext; + _contextOptions = contextOptions; } /// @@ -58,7 +65,25 @@ public virtual Task EnsureDeletedAsync(CancellationToken cancellationToken /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual bool EnsureCreated() - => Database.EnsureDatabaseCreated(); + { + var created = Database.EnsureDatabaseCreated(); + + var coreOptionsExtension = + _contextOptions.FindExtension() + ?? new CoreOptionsExtension(); + + var seed = coreOptionsExtension.Seeder; + if (seed != null) + { + seed(_currentContext.Context, created); + } + else if (coreOptionsExtension.AsyncSeeder != null) + { + throw new InvalidOperationException(CoreStrings.MissingSeeder); + } + + return created; + } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -66,8 +91,26 @@ public virtual bool EnsureCreated() /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual Task EnsureCreatedAsync(CancellationToken cancellationToken = default) - => Task.FromResult(Database.EnsureDatabaseCreated()); + public virtual async Task EnsureCreatedAsync(CancellationToken cancellationToken = default) + { + var created = Database.EnsureDatabaseCreated(); + + var coreOptionsExtension = + _contextOptions.FindExtension() + ?? new CoreOptionsExtension(); + + var seedAsync = coreOptionsExtension.AsyncSeeder; + if (seedAsync != null) + { + await seedAsync(_currentContext.Context, created, cancellationToken).ConfigureAwait(false); + } + else if (coreOptionsExtension.Seeder != null) + { + throw new InvalidOperationException(CoreStrings.MissingSeeder); + } + + return created; + } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.Relational/Diagnostics/RelationalEventId.cs b/src/EFCore.Relational/Diagnostics/RelationalEventId.cs index f487c54ff62..34189265a32 100644 --- a/src/EFCore.Relational/Diagnostics/RelationalEventId.cs +++ b/src/EFCore.Relational/Diagnostics/RelationalEventId.cs @@ -79,6 +79,7 @@ private enum Id ColumnOrderIgnoredWarning, PendingModelChangesWarning, NonTransactionalMigrationOperationWarning, + AcquiringMigrationLock, // Query events QueryClientEvaluationWarning = CoreEventId.RelationalBaseId + 500, @@ -749,6 +750,19 @@ private static EventId MakeMigrationsId(Id id) /// public static readonly EventId NonTransactionalMigrationOperationWarning = MakeMigrationsId(Id.NonTransactionalMigrationOperationWarning); + /// + /// A migration lock is being acquired. + /// + /// + /// + /// This event is in the category. + /// + /// + /// This event uses the payload when used with a . + /// + /// + public static readonly EventId AcquiringMigrationLock = MakeMigrationsId(Id.AcquiringMigrationLock); + private static readonly string _queryPrefix = DbLoggerCategory.Query.Name + "."; private static EventId MakeQueryId(Id id) diff --git a/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs b/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs index d704bcf1f1b..e86dea9a04b 100644 --- a/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs +++ b/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs @@ -2393,6 +2393,36 @@ private static string NonTransactionalMigrationOperationWarning(EventDefinitionB return d.GenerateMessage(commandText, p.Migration.GetType().ShortDisplayName()); } + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + public static void AcquiringMigrationLock( + this IDiagnosticsLogger diagnostics) + { + var definition = RelationalResources.LogAcquiringMigrationLock(diagnostics); + + if (diagnostics.ShouldLog(definition)) + { + definition.Log(diagnostics); + } + + if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled)) + { + var eventData = new EventData( + definition, + AcquiringMigrationLock); + + diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled); + } + } + + private static string AcquiringMigrationLock(EventDefinitionBase definition, EventData payload) + { + var d = (EventDefinition)definition; + return d.GenerateMessage(); + } + /// /// Logs for the event. /// diff --git a/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs b/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs index 51e0e3b1aee..9b57a07998e 100644 --- a/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs +++ b/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs @@ -358,6 +358,15 @@ public abstract class RelationalLoggingDefinitions : LoggingDefinitions [EntityFrameworkInternal] public EventDefinitionBase? LogNoMigrationsFound; + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + [EntityFrameworkInternal] + public EventDefinitionBase? LogAcquiringMigrationLock; + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs b/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs index e355e7a7c23..5d8e3bd558f 100644 --- a/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs @@ -123,13 +123,6 @@ public static void Migrate(this DatabaseFacade databaseFacade) /// /// The target migration to migrate the database to, or to migrate to the latest. /// - /// - /// The optional seed method to run after migrating the database. It will be invoked even if no migrations were applied. - /// - /// - /// The maximum amount of time that the migration lock should be held. Unless a catastrophic failure occurs, the - /// lock is released when the migration operation completes. - /// /// /// /// Note that this API is mutually exclusive with . EnsureCreated does not use migrations @@ -145,10 +138,8 @@ public static void Migrate(this DatabaseFacade databaseFacade) + " Use a migration bundle or an alternate way of executing migration operations.")] public static void Migrate( this DatabaseFacade databaseFacade, - Action? seed, - string? targetMigration = null, - TimeSpan? lockTimeout = null) - => databaseFacade.GetRelationalService().Migrate(seed, targetMigration, lockTimeout); + string? targetMigration) + => databaseFacade.GetRelationalService().Migrate(targetMigration); /// /// Asynchronously applies any pending migrations for the context to the database. Will create the database @@ -184,13 +175,6 @@ public static Task MigrateAsync( /// /// The target migration to migrate the database to, or to migrate to the latest. /// - /// - /// The optional seed method to run after migrating the database. It will be invoked even if no migrations were applied. - /// - /// - /// The maximum amount of time that the migration lock should be held. Unless a catastrophic failure occurs, the - /// lock is released when the migration operation completes. - /// /// A to observe while waiting for the task to complete. /// /// @@ -209,11 +193,9 @@ public static Task MigrateAsync( + " Use a migration bundle or an alternate way of executing migration operations.")] public static Task MigrateAsync( this DatabaseFacade databaseFacade, - Func? seed, - string? targetMigration = null, - TimeSpan? lockTimeout = null, + string? targetMigration, CancellationToken cancellationToken = default) - => databaseFacade.GetRelationalService().MigrateAsync(seed, targetMigration, lockTimeout, cancellationToken); + => databaseFacade.GetRelationalService().MigrateAsync(targetMigration, cancellationToken); /// /// Executes the given SQL against the database and returns the number of rows affected. diff --git a/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs b/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs index 2a526d3dfc2..d0ee126a44c 100644 --- a/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs +++ b/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs @@ -96,8 +96,7 @@ public static readonly IDictionary RelationalServi typeof(IAggregateMethodCallTranslatorPlugin), new ServiceCharacteristics(ServiceLifetime.Scoped, multipleRegistrations: true) }, - { typeof(IMemberTranslatorPlugin), new ServiceCharacteristics(ServiceLifetime.Scoped, multipleRegistrations: true) }, - { typeof(IMigratorPlugin), new ServiceCharacteristics(ServiceLifetime.Singleton, multipleRegistrations: true) } + { typeof(IMemberTranslatorPlugin), new ServiceCharacteristics(ServiceLifetime.Scoped, multipleRegistrations: true) } }; /// diff --git a/src/EFCore.Relational/Migrations/HistoryRepository.cs b/src/EFCore.Relational/Migrations/HistoryRepository.cs index e3d1a03e75f..78dc3b24013 100644 --- a/src/EFCore.Relational/Migrations/HistoryRepository.cs +++ b/src/EFCore.Relational/Migrations/HistoryRepository.cs @@ -197,18 +197,16 @@ protected virtual IReadOnlyList GetCreateCommands() /// /// Gets an exclusive lock on the database. /// - /// The time to wait for the lock before an exception is thrown. /// An object that can be disposed to release the lock. - public abstract IDisposable GetDatabaseLock(TimeSpan timeout); + public abstract IDisposable GetDatabaseLock(); /// /// Gets an exclusive lock on the database. /// - /// The time to wait for the lock before an exception is thrown. /// A to observe while waiting for the task to complete. /// An object that can be disposed to release the lock. /// If the is canceled. - public abstract Task GetDatabaseLockAsync(TimeSpan timeout, CancellationToken cancellationToken = default); + public abstract Task GetDatabaseLockAsync(CancellationToken cancellationToken = default); /// /// Configures the entity type mapped to the history table. diff --git a/src/EFCore.Relational/Migrations/IHistoryRepository.cs b/src/EFCore.Relational/Migrations/IHistoryRepository.cs index 4d52db38043..7bf3a461650 100644 --- a/src/EFCore.Relational/Migrations/IHistoryRepository.cs +++ b/src/EFCore.Relational/Migrations/IHistoryRepository.cs @@ -77,18 +77,16 @@ Task> GetAppliedMigrationsAsync( /// /// Gets an exclusive lock on the database. /// - /// The time to wait for the lock before an exception is thrown. /// An object that can be disposed to release the lock. - IDisposable GetDatabaseLock(TimeSpan timeout); + IDisposable GetDatabaseLock(); /// /// Gets an exclusive lock on the database. /// - /// The time to wait for the lock before an exception is thrown. /// A to observe while waiting for the task to complete. /// An object that can be disposed to release the lock. /// If the is canceled. - Task GetDatabaseLockAsync(TimeSpan timeout, CancellationToken cancellationToken = default); + Task GetDatabaseLockAsync(CancellationToken cancellationToken = default); /// /// Generates a SQL script that will create the history table. diff --git a/src/EFCore.Relational/Migrations/IMigrator.cs b/src/EFCore.Relational/Migrations/IMigrator.cs index 0b02cbe7df3..b7735b4f0f7 100644 --- a/src/EFCore.Relational/Migrations/IMigrator.cs +++ b/src/EFCore.Relational/Migrations/IMigrator.cs @@ -26,37 +26,23 @@ public interface IMigrator /// Migrates the database to either a specified target migration or up to the latest /// migration that exists in the . /// - /// - /// The optional seed method to run after migrating the database. It will be invoked even if no migrations were applied. - /// /// /// The target migration to migrate the database to, or to migrate to the latest. /// - /// - /// The maximum amount of time that the migration lock should be held. Unless a catastrophic failure occurs, the - /// lock is released when the migration operation completes. - /// /// /// See Database migrations for more information and examples. /// [RequiresUnreferencedCode("Migration generation currently isn't compatible with trimming")] [RequiresDynamicCode("Migrations operations are not supported with NativeAOT")] - void Migrate(Action? seed = null, string? targetMigration = null, TimeSpan? lockTimeout = null); + void Migrate(string? targetMigration = null); /// /// Migrates the database to either a specified target migration or up to the latest /// migration that exists in the . /// - /// - /// The optional seed method to run after migrating the database. It will be invoked even if no migrations were applied. - /// /// /// The target migration to migrate the database to, or to migrate to the latest. /// - /// - /// The maximum amount of time that the migration lock should be held. Unless a catastrophic failure occurs, the - /// lock is released when the migration operation completes. - /// /// A to observe while waiting for the task to complete. /// A task that represents the asynchronous operation /// @@ -66,9 +52,7 @@ public interface IMigrator [RequiresUnreferencedCode("Migration generation currently isn't compatible with trimming")] [RequiresDynamicCode("Migrations operations are not supported with NativeAOT")] Task MigrateAsync( - Func? seed = null, string? targetMigration = null, - TimeSpan? lockTimeout = null, CancellationToken cancellationToken = default); /// diff --git a/src/EFCore.Relational/Migrations/IMigratorData.cs b/src/EFCore.Relational/Migrations/IMigratorData.cs deleted file mode 100644 index 60531a1c699..00000000000 --- a/src/EFCore.Relational/Migrations/IMigratorData.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.Migrations; - -/// -/// A class that holds the results from the last migrations application. -/// -/// -/// See Database migrations for more information and examples. -/// -public interface IMigratorData -{ - /// - /// The migrations that were applied to the database. - /// - public IReadOnlyList AppliedMigrations { get; } - - /// - /// The migrations that were reverted from the database. - /// - public IReadOnlyList RevertedMigrations { get; } - - /// - /// The target migration. - /// if all migrations were reverted or no target migration was specified. - /// - public Migration? TargetMigration { get; } -} diff --git a/src/EFCore.Relational/Migrations/IMigratorPlugin.cs b/src/EFCore.Relational/Migrations/IMigratorPlugin.cs deleted file mode 100644 index ae06c4d968e..00000000000 --- a/src/EFCore.Relational/Migrations/IMigratorPlugin.cs +++ /dev/null @@ -1,73 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.Migrations; - -/// -/// -/// A service on the EF internal service provider that allows providers or extensions to execute logic -/// after is called. -/// -/// -/// This type is typically used by providers or extensions. It is generally not used in application code. -/// -/// -/// -/// The service lifetime is . This means a single instance -/// is used by many instances. The implementation must be thread-safe. -/// This service cannot depend on services registered as . -/// -public interface IMigratorPlugin -{ - /// - /// Called by before applying the migrations. - /// - /// The that is being migrated. - /// The that contains the result of the migrations application. - /// - /// See Database migrations for more information and examples. - /// - void Migrating(DbContext context, IMigratorData data); - - /// - /// Called by before applying the migrations. - /// - /// The that is being migrated. - /// The that contains the result of the migrations application. - /// - /// See Database migrations for more information and examples. - /// - /// A to observe while waiting for the task to complete. - /// A task that represents the asynchronous operation - /// If the is canceled. - Task MigratingAsync( - DbContext context, - IMigratorData data, - CancellationToken cancellationToken = default); - - /// - /// Called by after applying the migrations, but before the seeding action. - /// - /// The that is being migrated. - /// The that contains the result of the migrations application. - /// - /// See Database migrations for more information and examples. - /// - void Migrated(DbContext context, IMigratorData data); - - /// - /// Called by after applying the migrations, but before the seeding action. - /// - /// The that is being migrated. - /// The that contains the result of the migrations application. - /// - /// See Database migrations for more information and examples. - /// - /// A to observe while waiting for the task to complete. - /// A task that represents the asynchronous operation - /// If the is canceled. - Task MigratedAsync( - DbContext context, - IMigratorData data, - CancellationToken cancellationToken = default); -} diff --git a/src/EFCore.Relational/Migrations/Internal/MigrationCommandExecutor.cs b/src/EFCore.Relational/Migrations/Internal/MigrationCommandExecutor.cs index e545d4c13b5..4b2d78a85bc 100644 --- a/src/EFCore.Relational/Migrations/Internal/MigrationCommandExecutor.cs +++ b/src/EFCore.Relational/Migrations/Internal/MigrationCommandExecutor.cs @@ -11,21 +11,14 @@ namespace Microsoft.EntityFrameworkCore.Migrations.Internal; /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// -public class MigrationCommandExecutor : IMigrationCommandExecutor +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +public class MigrationCommandExecutor(IExecutionStrategy executionStrategy) : IMigrationCommandExecutor { - private readonly IExecutionStrategy _executionStrategy; - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public MigrationCommandExecutor(IExecutionStrategy executionStrategy) - { - _executionStrategy = executionStrategy; - } - /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -36,19 +29,21 @@ public virtual void ExecuteNonQuery( IEnumerable migrationCommands, IRelationalConnection connection) { + // TODO: Remove ToList, see #19710 + var commands = migrationCommands.ToList(); var userTransaction = connection.CurrentTransaction; if (userTransaction is not null - && (migrationCommands.Any(x => x.TransactionSuppressed) || _executionStrategy.RetriesOnFailure)) + && (commands.Any(x => x.TransactionSuppressed) || executionStrategy.RetriesOnFailure)) { throw new NotSupportedException(RelationalStrings.TransactionSuppressedMigrationInUserTransaction); } using (new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled)) { - var parameters = new ExecuteParameters(migrationCommands.ToList(), connection); + var parameters = new ExecuteParameters(commands, connection); if (userTransaction is null) { - _executionStrategy.Execute(parameters, static (_, p) => Execute(p, beginTransaction: true), verifySucceeded: null); + executionStrategy.Execute(parameters, static (_, p) => Execute(p, beginTransaction: true), verifySucceeded: null); } else { @@ -114,34 +109,28 @@ public virtual async Task ExecuteNonQueryAsync( IRelationalConnection connection, CancellationToken cancellationToken = default) { + var commands = migrationCommands.ToList(); var userTransaction = connection.CurrentTransaction; if (userTransaction is not null - && (migrationCommands.Any(x => x.TransactionSuppressed) || _executionStrategy.RetriesOnFailure)) + && (commands.Any(x => x.TransactionSuppressed) || executionStrategy.RetriesOnFailure)) { throw new NotSupportedException(RelationalStrings.TransactionSuppressedMigrationInUserTransaction); } - var transactionScope = new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled); - try - { - var parameters = new ExecuteParameters(migrationCommands.ToList(), connection); - if (userTransaction is null) - { - await _executionStrategy.ExecuteAsync( - parameters, - static (_, p, ct) => ExecuteAsync(p, beginTransaction: true, ct), - verifySucceeded: null, - cancellationToken).ConfigureAwait(false); - } - else - { - await ExecuteAsync(parameters, beginTransaction: false, cancellationToken).ConfigureAwait(false); - } + using var transactionScope = new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled); + var parameters = new ExecuteParameters(commands, connection); + if (userTransaction is null) + { + await executionStrategy.ExecuteAsync( + parameters, + static (_, p, ct) => ExecuteAsync(p, beginTransaction: true, ct), + verifySucceeded: null, + cancellationToken).ConfigureAwait(false); } - finally + else { - await transactionScope.DisposeAsyncIfAvailable().ConfigureAwait(false); + await ExecuteAsync(parameters, beginTransaction: false, cancellationToken).ConfigureAwait(false); } } diff --git a/src/EFCore.Relational/Migrations/Internal/Migrator.cs b/src/EFCore.Relational/Migrations/Internal/Migrator.cs index 4012a1a7a53..9d525dd4e95 100644 --- a/src/EFCore.Relational/Migrations/Internal/Migrator.cs +++ b/src/EFCore.Relational/Migrations/Internal/Migrator.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.EntityFrameworkCore.Diagnostics.Internal; -using Microsoft.EntityFrameworkCore.Storage; namespace Microsoft.EntityFrameworkCore.Migrations.Internal; @@ -26,11 +25,10 @@ public class Migrator : IMigrator private readonly IModelRuntimeInitializer _modelRuntimeInitializer; private readonly IDiagnosticsLogger _logger; private readonly IRelationalCommandDiagnosticsLogger _commandLogger; - private readonly IEnumerable _plugins; private readonly IMigrationsModelDiffer _migrationsModelDiffer; private readonly IDesignTimeModel _designTimeModel; private readonly string _activeProvider; - private static readonly TimeSpan _defaultLockTimeout = TimeSpan.FromHours(1); + private readonly IDbContextOptions _contextOptions; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -52,9 +50,9 @@ public Migrator( IDiagnosticsLogger logger, IRelationalCommandDiagnosticsLogger commandLogger, IDatabaseProvider databaseProvider, - IEnumerable plugins, IMigrationsModelDiffer migrationsModelDiffer, - IDesignTimeModel designTimeModel) + IDesignTimeModel designTimeModel, + IDbContextOptions contextOptions) { _migrationsAssembly = migrationsAssembly; _historyRepository = historyRepository; @@ -68,10 +66,10 @@ public Migrator( _modelRuntimeInitializer = modelRuntimeInitializer; _logger = logger; _commandLogger = commandLogger; - _plugins = plugins; _migrationsModelDiffer = migrationsModelDiffer; _designTimeModel = designTimeModel; _activeProvider = databaseProvider.Name; + _contextOptions = contextOptions; } /// @@ -80,7 +78,7 @@ public Migrator( /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual void Migrate(Action? seed, string? targetMigration, TimeSpan? lockTimeout) + public virtual void Migrate(string? targetMigration) { if (RelationalResources.LogPendingModelChanges(_logger).WarningBehavior != WarningBehavior.Ignore && HasPendingModelChanges()) @@ -99,7 +97,8 @@ public virtual void Migrate(Action? seed, string? targ { _connection.Open(); - using var _ = _historyRepository.GetDatabaseLock(lockTimeout ?? _defaultLockTimeout); + _logger.AcquiringMigrationLock(); + using var _ = _historyRepository.GetDatabaseLock(); if (!_historyRepository.Exists()) { @@ -111,28 +110,30 @@ public virtual void Migrate(Action? seed, string? targ targetMigration, out var migratorData); - foreach (var plugin in _plugins) - { - plugin.Migrating(_currentContext.Context, migratorData); - } - var commandLists = GetMigrationCommandLists(migratorData); foreach (var commandList in commandLists) { _migrationCommandExecutor.ExecuteNonQuery(commandList(), _connection); } - foreach (var plugin in _plugins) - { - plugin.Migrated(_currentContext.Context, migratorData); - } + var coreOptionsExtension = + _contextOptions.FindExtension() + ?? new CoreOptionsExtension(); + var seed = coreOptionsExtension.Seeder; if (seed != null) { - using var transaction = _connection.BeginTransaction(); - seed(_currentContext.Context, migratorData); + var context = _currentContext.Context; + var operationsPerformed = migratorData.AppliedMigrations.Count != 0 + || migratorData.RevertedMigrations.Count != 0; + using var transaction = context.Database.BeginTransaction(); + seed(context, operationsPerformed); transaction.Commit(); } + else if (coreOptionsExtension.AsyncSeeder != null) + { + throw new InvalidOperationException(CoreStrings.MissingSeeder); + } } finally { @@ -147,9 +148,7 @@ public virtual void Migrate(Action? seed, string? targ /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual async Task MigrateAsync( - Func? seed, string? targetMigration, - TimeSpan? lockTimeout = null, CancellationToken cancellationToken = default) { if (RelationalResources.LogPendingModelChanges(_logger).WarningBehavior != WarningBehavior.Ignore @@ -169,7 +168,8 @@ public virtual async Task MigrateAsync( { await _connection.OpenAsync(cancellationToken).ConfigureAwait(false); - var dbLock = await _historyRepository.GetDatabaseLockAsync(lockTimeout ?? _defaultLockTimeout, cancellationToken).ConfigureAwait(false); + _logger.AcquiringMigrationLock(); + var dbLock = await _historyRepository.GetDatabaseLockAsync(cancellationToken).ConfigureAwait(false); await using var _ = dbLock.ConfigureAwait(false); if (!await _historyRepository.ExistsAsync(cancellationToken).ConfigureAwait(false)) @@ -182,11 +182,6 @@ public virtual async Task MigrateAsync( targetMigration, out var migratorData); - foreach (var plugin in _plugins) - { - await plugin.MigratingAsync(_currentContext.Context, migratorData, cancellationToken).ConfigureAwait(false); - } - var commandLists = GetMigrationCommandLists(migratorData); foreach (var commandList in commandLists) { @@ -194,18 +189,25 @@ await _migrationCommandExecutor.ExecuteNonQueryAsync(commandList(), _connection, .ConfigureAwait(false); } - foreach (var plugin in _plugins) - { - await plugin.MigratedAsync(_currentContext.Context, migratorData, cancellationToken).ConfigureAwait(false); - } + var coreOptionsExtension = + _contextOptions.FindExtension() + ?? new CoreOptionsExtension(); - if (seed != null) + var seedAsync = coreOptionsExtension.AsyncSeeder; + if (seedAsync != null) { - var transaction = await _connection.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); + var context = _currentContext.Context; + var operationsPerformed = migratorData.AppliedMigrations.Count != 0 + || migratorData.RevertedMigrations.Count != 0; + var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); await using var __ = transaction.ConfigureAwait(false); - await seed(_currentContext.Context, migratorData, cancellationToken).ConfigureAwait(false); + await seedAsync(context, operationsPerformed, cancellationToken).ConfigureAwait(false); await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); } + else if (coreOptionsExtension.Seeder != null) + { + throw new InvalidOperationException(CoreStrings.MissingSeeder); + } } finally { @@ -213,7 +215,7 @@ await _migrationCommandExecutor.ExecuteNonQueryAsync(commandList(), _connection, } } - private IEnumerable>> GetMigrationCommandLists(IMigratorData parameters) + private IEnumerable>> GetMigrationCommandLists(MigratorData parameters) { var migrationsToApply = parameters.AppliedMigrations; var migrationsToRevert = parameters.RevertedMigrations; @@ -273,7 +275,7 @@ private IEnumerable>> GetMigrationCommandLi protected virtual void PopulateMigrations( IEnumerable appliedMigrationEntries, string? targetMigration, - out IMigratorData parameters) + out MigratorData parameters) { var appliedMigrations = new Dictionary(); var unappliedMigrations = new Dictionary(); diff --git a/src/EFCore.Relational/Migrations/Internal/MigratorData.cs b/src/EFCore.Relational/Migrations/Internal/MigratorData.cs index 97c47555d2f..6a4f0706460 100644 --- a/src/EFCore.Relational/Migrations/Internal/MigratorData.cs +++ b/src/EFCore.Relational/Migrations/Internal/MigratorData.cs @@ -13,7 +13,6 @@ public class MigratorData( IReadOnlyList appliedMigrations, IReadOnlyList revertedMigrations, Migration? targetMigration) - : IMigratorData { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -21,7 +20,7 @@ public class MigratorData( /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public IReadOnlyList AppliedMigrations { get; } = appliedMigrations; + public virtual IReadOnlyList AppliedMigrations { get; } = appliedMigrations; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -29,7 +28,7 @@ public class MigratorData( /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public IReadOnlyList RevertedMigrations { get; } = revertedMigrations; + public virtual IReadOnlyList RevertedMigrations { get; } = revertedMigrations; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -37,5 +36,5 @@ public class MigratorData( /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public Migration? TargetMigration { get; } = targetMigration; + public virtual Migration? TargetMigration { get; } = targetMigration; } diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index 3b7b9c4c49e..54aabd7a2f9 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -2108,7 +2108,7 @@ public static string UnsupportedOperatorForSqlExpression(object? nodeType, objec nodeType, expressionType); /// - /// No relational type mapping can be found for property '{entity}.{property}' and the current provider doesn't specify a default store type for the properties of type '{clrType}'. + /// No relational type mapping can be found for property '{entity}.{property}' and the current provider doesn't specify a default store type for the properties of type '{clrType}'. /// public static string UnsupportedPropertyType(object? entity, object? property, object? clrType) => string.Format( @@ -2233,6 +2233,31 @@ public static class RelationalResources private static readonly ResourceManager _resourceManager = new ResourceManager("Microsoft.EntityFrameworkCore.Properties.RelationalStrings", typeof(RelationalResources).Assembly); + /// + /// Acquiring an exclusive lock for migration application. See https://aka.ms/efcore-docs-migrations for more information if this takes too long. + /// + public static EventDefinition LogAcquiringMigrationLock(IDiagnosticsLogger logger) + { + var definition = ((RelationalLoggingDefinitions)logger.Definitions).LogAcquiringMigrationLock; + if (definition == null) + { + definition = NonCapturingLazyInitializer.EnsureInitialized( + ref ((RelationalLoggingDefinitions)logger.Definitions).LogAcquiringMigrationLock, + logger, + static logger => new EventDefinition( + logger.Options, + RelationalEventId.AcquiringMigrationLock, + LogLevel.Information, + "RelationalEventId.AcquiringMigrationLock", + level => LoggerMessage.Define( + level, + RelationalEventId.AcquiringMigrationLock, + _resourceManager.GetString("LogAcquiringMigrationLock")!))); + } + + return (EventDefinition)definition; + } + /// /// An ambient transaction has been detected, but the current provider does not support ambient transactions. See https://go.microsoft.com/fwlink/?LinkId=800142 /// diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index 7099418d2e3..84f26811cc3 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -1,17 +1,17 @@  - @@ -589,6 +589,10 @@ Queries performing '{method}' operation must have a deterministic sort order. Rewrite the query to apply an 'OrderBy' operation on the sequence before calling '{method}'. + + Acquiring an exclusive lock for migration application. See https://aka.ms/efcore-docs-migrations-lock for more information if this takes too long. + Information RelationalEventId.AcquiringMigrationLock + An ambient transaction has been detected, but the current provider does not support ambient transactions. See https://go.microsoft.com/fwlink/?LinkId=800142 Warning RelationalEventId.AmbientTransactionWarning diff --git a/src/EFCore.Relational/Storage/RelationalDatabaseCreator.cs b/src/EFCore.Relational/Storage/RelationalDatabaseCreator.cs index dbbd3b083fb..ddd5799033f 100644 --- a/src/EFCore.Relational/Storage/RelationalDatabaseCreator.cs +++ b/src/EFCore.Relational/Storage/RelationalDatabaseCreator.cs @@ -234,23 +234,40 @@ public virtual async Task EnsureDeletedAsync(CancellationToken cancellatio /// public virtual bool EnsureCreated() { - using (new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled)) + using var transactionScope = new TransactionScope( + TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled); + + var operationsPerformed = false; + if (!Exists()) + { + Create(); + CreateTables(); + operationsPerformed = true; + } + else if (!HasTables()) { - if (!Exists()) - { - Create(); - CreateTables(); - return true; - } - - if (!HasTables()) - { - CreateTables(); - return true; - } + CreateTables(); + operationsPerformed = true; } - return false; + var coreOptionsExtension = + Dependencies.ContextOptions.FindExtension() + ?? new CoreOptionsExtension(); + + var seed = coreOptionsExtension.Seeder; + if (seed != null) + { + var context = Dependencies.CurrentContext.Context; + using var transaction = context.Database.BeginTransaction(); + seed(context, operationsPerformed); + transaction.Commit(); + } + else if (coreOptionsExtension.AsyncSeeder != null) + { + throw new InvalidOperationException(CoreStrings.MissingSeeder); + } + + return operationsPerformed; } /// @@ -266,30 +283,42 @@ public virtual bool EnsureCreated() /// If the is canceled. public virtual async Task EnsureCreatedAsync(CancellationToken cancellationToken = default) { - var transactionScope = new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled); - try + using var transactionScope = new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled); + + var operationsPerformed = false; + if (!await ExistsAsync(cancellationToken).ConfigureAwait(false)) { - if (!await ExistsAsync(cancellationToken).ConfigureAwait(false)) - { - await CreateAsync(cancellationToken).ConfigureAwait(false); - await CreateTablesAsync(cancellationToken).ConfigureAwait(false); + await CreateAsync(cancellationToken).ConfigureAwait(false); + await CreateTablesAsync(cancellationToken).ConfigureAwait(false); - return true; - } + operationsPerformed = true; + } + else if (!await HasTablesAsync(cancellationToken).ConfigureAwait(false)) + { + await CreateTablesAsync(cancellationToken).ConfigureAwait(false); - if (!await HasTablesAsync(cancellationToken).ConfigureAwait(false)) - { - await CreateTablesAsync(cancellationToken).ConfigureAwait(false); + operationsPerformed = true; + } + + var coreOptionsExtension = + Dependencies.ContextOptions.FindExtension() + ?? new CoreOptionsExtension(); - return true; - } + var seedAsync = coreOptionsExtension.AsyncSeeder; + if (seedAsync != null) + { + var context = Dependencies.CurrentContext.Context; + var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); + await using var _ = transaction.ConfigureAwait(false); + await seedAsync(context, operationsPerformed, cancellationToken).ConfigureAwait(false); + await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); } - finally + else if (coreOptionsExtension.Seeder != null) { - await transactionScope.DisposeAsyncIfAvailable().ConfigureAwait(false); + throw new InvalidOperationException(CoreStrings.MissingSeeder); } - return false; + return operationsPerformed; } /// diff --git a/src/EFCore.Relational/Storage/RelationalDatabaseCreatorDependencies.cs b/src/EFCore.Relational/Storage/RelationalDatabaseCreatorDependencies.cs index 0b14d27ec79..8343a802486 100644 --- a/src/EFCore.Relational/Storage/RelationalDatabaseCreatorDependencies.cs +++ b/src/EFCore.Relational/Storage/RelationalDatabaseCreatorDependencies.cs @@ -53,6 +53,7 @@ public RelationalDatabaseCreatorDependencies( ISqlGenerationHelper sqlGenerationHelper, IExecutionStrategy executionStrategy, ICurrentDbContext currentContext, + IDbContextOptions contextOptions, IRelationalCommandDiagnosticsLogger commandLogger) { Connection = connection; @@ -62,6 +63,7 @@ public RelationalDatabaseCreatorDependencies( SqlGenerationHelper = sqlGenerationHelper; ExecutionStrategy = executionStrategy; CurrentContext = currentContext; + ContextOptions = contextOptions; CommandLogger = commandLogger; } @@ -100,6 +102,11 @@ public RelationalDatabaseCreatorDependencies( /// public IRelationalCommandDiagnosticsLogger CommandLogger { get; init; } + /// + /// Gets the context options. + /// + public IDbContextOptions ContextOptions { get; init; } + /// /// Contains the currently in use. /// diff --git a/src/EFCore.SqlServer/Migrations/Internal/SqlServerHistoryRepository.cs b/src/EFCore.SqlServer/Migrations/Internal/SqlServerHistoryRepository.cs index cf365bcc2cb..47bede43829 100644 --- a/src/EFCore.SqlServer/Migrations/Internal/SqlServerHistoryRepository.cs +++ b/src/EFCore.SqlServer/Migrations/Internal/SqlServerHistoryRepository.cs @@ -60,13 +60,13 @@ protected override bool InterpretExistsResult(object? value) /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public override IDisposable GetDatabaseLock(TimeSpan timeout) + public override IDisposable GetDatabaseLock() { var dbLock = CreateMigrationDatabaseLock(); int result; try { - result = (int)CreateGetLockCommand(timeout).ExecuteScalar(CreateRelationalCommandParameters())!; + result = (int)CreateGetLockCommand().ExecuteScalar(CreateRelationalCommandParameters())!; } catch { @@ -92,13 +92,13 @@ public override IDisposable GetDatabaseLock(TimeSpan timeout) /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public override async Task GetDatabaseLockAsync(TimeSpan timeout, CancellationToken cancellationToken = default) + public override async Task GetDatabaseLockAsync(CancellationToken cancellationToken = default) { var dbLock = CreateMigrationDatabaseLock(); int result; try { - result = (int)(await CreateGetLockCommand(timeout).ExecuteScalarAsync(CreateRelationalCommandParameters(), cancellationToken) + result = (int)(await CreateGetLockCommand().ExecuteScalarAsync(CreateRelationalCommandParameters(), cancellationToken) .ConfigureAwait(false))!; } catch @@ -119,13 +119,13 @@ public override async Task GetDatabaseLockAsync(TimeSpan timeo : dbLock; } - private IRelationalCommand CreateGetLockCommand(TimeSpan timeout) + private IRelationalCommand CreateGetLockCommand() => Dependencies.RawSqlCommandBuilder.Build(""" DECLARE @result int; -EXEC @result = sp_getapplock @Resource = '__EFMigrationsLock', @LockOwner = 'Session', @LockMode = 'Exclusive', @LockTimeout = @LockTimeout; +EXEC @result = sp_getapplock @Resource = '__EFMigrationsLock', @LockOwner = 'Session', @LockMode = 'Exclusive'; SELECT @result """, - [new SqlParameter("@LockTimeout", timeout.TotalMilliseconds)]).RelationalCommand; + []).RelationalCommand; private SqlServerMigrationDatabaseLock CreateMigrationDatabaseLock() => new( diff --git a/src/EFCore.Sqlite.Core/Migrations/Internal/SqliteHistoryRepository.cs b/src/EFCore.Sqlite.Core/Migrations/Internal/SqliteHistoryRepository.cs index 477a43a65eb..26b68e31ffa 100644 --- a/src/EFCore.Sqlite.Core/Migrations/Internal/SqliteHistoryRepository.cs +++ b/src/EFCore.Sqlite.Core/Migrations/Internal/SqliteHistoryRepository.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Text; using Microsoft.EntityFrameworkCore.Sqlite.Internal; namespace Microsoft.EntityFrameworkCore.Sqlite.Migrations.Internal; @@ -103,7 +102,7 @@ public override string GetEndIfScript() /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public override IDisposable GetDatabaseLock(TimeSpan timeout) + public override IDisposable GetDatabaseLock() { if (!InterpretExistsResult(Dependencies.RawSqlCommandBuilder.Build(CreateExistsSql(LockTableName)) .ExecuteScalar(CreateRelationalCommandParameters()))) @@ -112,8 +111,7 @@ public override IDisposable GetDatabaseLock(TimeSpan timeout) } var retryDelay = _retryDelay; - var startTime = DateTimeOffset.UtcNow; - while (DateTimeOffset.UtcNow - startTime < timeout) + while (true) { var dbLock = CreateMigrationDatabaseLock(); var insertCount = CreateInsertLockCommand(DateTimeOffset.UtcNow) @@ -123,17 +121,6 @@ public override IDisposable GetDatabaseLock(TimeSpan timeout) return dbLock; } - using var reader = CreateGetLockCommand().ExecuteReader(CreateRelationalCommandParameters()); - if (reader.Read()) - { - var timestamp = reader.DbDataReader.GetFieldValue(1); - if (DateTimeOffset.UtcNow - timestamp > timeout) - { - var id = reader.DbDataReader.GetFieldValue(0); - CreateDeleteLockCommand(id).ExecuteNonQuery(CreateRelationalCommandParameters()); - } - } - Thread.Sleep(retryDelay); if (retryDelay < TimeSpan.FromMinutes(1)) { @@ -150,7 +137,7 @@ public override IDisposable GetDatabaseLock(TimeSpan timeout) /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public override async Task GetDatabaseLockAsync(TimeSpan timeout, CancellationToken cancellationToken = default) + public override async Task GetDatabaseLockAsync(CancellationToken cancellationToken = default) { if (!InterpretExistsResult(await Dependencies.RawSqlCommandBuilder.Build(CreateExistsSql(LockTableName)) .ExecuteScalarAsync(CreateRelationalCommandParameters(), cancellationToken).ConfigureAwait(false))) @@ -159,8 +146,7 @@ public override async Task GetDatabaseLockAsync(TimeSpan timeo } var retryDelay = _retryDelay; - var startTime = DateTimeOffset.UtcNow; - while (DateTimeOffset.UtcNow - startTime < timeout) + while (true) { var dbLock = CreateMigrationDatabaseLock(); var insertCount = await CreateInsertLockCommand(DateTimeOffset.UtcNow) @@ -171,19 +157,6 @@ public override async Task GetDatabaseLockAsync(TimeSpan timeo return dbLock; } - using var reader = await CreateGetLockCommand().ExecuteReaderAsync(CreateRelationalCommandParameters(), cancellationToken) - .ConfigureAwait(false); - if (await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) - { - var timestamp = await reader.DbDataReader.GetFieldValueAsync(1).ConfigureAwait(false); - if (DateTimeOffset.UtcNow - timestamp > timeout) - { - var id = await reader.DbDataReader.GetFieldValueAsync(0).ConfigureAwait(false); - await CreateDeleteLockCommand(id).ExecuteNonQueryAsync(CreateRelationalCommandParameters(), cancellationToken) - .ConfigureAwait(false); - } - } - await Task.Delay(_retryDelay, cancellationToken).ConfigureAwait(true); if (retryDelay < TimeSpan.FromMinutes(1)) { @@ -212,11 +185,6 @@ private IRelationalCommand CreateInsertLockCommand(DateTimeOffset timestamp) """); } - private IRelationalCommand CreateGetLockCommand() - => Dependencies.RawSqlCommandBuilder.Build($""" -SELECT "Id", "Timestamp" FROM "{LockTableName}" LIMIT 1; -"""); - private IRelationalCommand CreateDeleteLockCommand(int? id = null) { var sql = $""" diff --git a/src/EFCore/DbContextOptionsBuilder.cs b/src/EFCore/DbContextOptionsBuilder.cs index c77a4a94144..25179a1f475 100644 --- a/src/EFCore/DbContextOptionsBuilder.cs +++ b/src/EFCore/DbContextOptionsBuilder.cs @@ -730,6 +730,50 @@ public virtual DbContextOptionsBuilder AddInterceptors(params IInterceptor[] int public virtual DbContextOptionsBuilder ConfigureLoggingCacheTime(TimeSpan timeSpan) => WithOption(e => e.WithLoggingCacheTime(timeSpan)); + /// + /// Configures the seed method to run after + /// is called or after migrations are applied. + /// It will be invoked even if no changes to the store were performed. + /// + /// + /// + /// The argument of the seed delegate indicates whether any store management + /// operation was performed. + /// + /// + /// It is recomended to also call with the same logic. + /// + /// + /// See Using DbContextOptions for more information and examples. + /// + /// + /// The seed method to run. + /// The same builder instance so that multiple calls can be chained. + public virtual DbContextOptionsBuilder UseSeeding(Action seed) + => WithOption(e => e.WithSeeding(seed)); + + /// + /// Configures the seed method to run after + /// is called or after migrations are applied asynchronously. + /// It will be invoked even if no changes to the store were performed. + /// + /// + /// + /// The argument of the seed delegate indicates whether any store management + /// operation was performed. + /// + /// + /// It is recomended to also call with the same logic. + /// + /// + /// See Using DbContextOptions for more information and examples. + /// + /// + /// The seed method to run. + /// The same builder instance so that multiple calls can be chained. + public virtual DbContextOptionsBuilder UseAsyncSeeding(Func seedAsync) + => WithOption(e => e.WithAsyncSeeding(seedAsync)); + /// /// Adds the given extension to the options. If an existing extension of the same type already exists, it will be replaced. /// diff --git a/src/EFCore/DbContextOptionsBuilder`.cs b/src/EFCore/DbContextOptionsBuilder`.cs index 6f7953c7d30..5e6b5a9e28f 100644 --- a/src/EFCore/DbContextOptionsBuilder`.cs +++ b/src/EFCore/DbContextOptionsBuilder`.cs @@ -629,4 +629,92 @@ public DbContextOptionsBuilder(DbContextOptions options) /// The same builder instance so that multiple calls can be chained. public new virtual DbContextOptionsBuilder ConfigureLoggingCacheTime(TimeSpan timeSpan) => (DbContextOptionsBuilder)base.ConfigureLoggingCacheTime(timeSpan); + + /// + /// Configures the seed method to run after + /// is called or after migrations are applied. + /// It will be invoked even if no changes to the store were performed. + /// + /// + /// + /// The argument of the seed delegate indicates whether any store management + /// operation was performed. + /// + /// + /// It is recomended to also call with the same logic. + /// + /// + /// See Using DbContextOptions for more information and examples. + /// + /// + /// The seed method to run. + /// The same builder instance so that multiple calls can be chained. + public new virtual DbContextOptionsBuilder UseSeeding(Action seed) + => (DbContextOptionsBuilder)base.UseSeeding((c, p) => seed(c, p)); + + /// + /// Configures the seed method to run after + /// is called or after migrations are applied. + /// It will be invoked even if no changes to the store were performed. + /// + /// + /// + /// The argument of the seed delegate indicates whether any store management + /// operation was performed. + /// + /// + /// It is recomended to also call with the same logic. + /// + /// + /// See Using DbContextOptions for more information and examples. + /// + /// + /// The seed method to run. + /// The same builder instance so that multiple calls can be chained. + public virtual DbContextOptionsBuilder UseSeeding(Action seed) + => (DbContextOptionsBuilder)base.UseSeeding((c, p) => seed((TContext)c, p)); + + /// + /// Configures the seed method to run after + /// is called or after migrations are applied asynchronously. + /// It will be invoked even if no changes to the store were performed. + /// + /// + /// + /// The argument of the seed delegate indicates whether any store management + /// operation was performed. + /// + /// + /// It is recomended to also call with the same logic. + /// + /// + /// See Using DbContextOptions for more information and examples. + /// + /// + /// The seed method to run. + /// The same builder instance so that multiple calls can be chained. + public new virtual DbContextOptionsBuilder UseAsyncSeeding(Func seedAsync) + => (DbContextOptionsBuilder)base.UseAsyncSeeding((c, p, t) => seedAsync(c, p, t)); + + /// + /// Configures the seed method to run after + /// is called or after migrations are applied asynchronously. + /// It will be invoked even if no changes to the store were performed. + /// + /// + /// + /// The argument of the seed delegate indicates whether any store management + /// operation was performed. + /// + /// + /// It is recomended to also call with the same logic. + /// + /// + /// See Using DbContextOptions for more information and examples. + /// + /// + /// The seed method to run. + /// The same builder instance so that multiple calls can be chained. + public virtual DbContextOptionsBuilder UseAsyncSeeding(Func seedAsync) + => (DbContextOptionsBuilder)base.UseAsyncSeeding((c, p, t) => seedAsync((TContext)c, p, t)); } diff --git a/src/EFCore/Infrastructure/CoreOptionsExtension.cs b/src/EFCore/Infrastructure/CoreOptionsExtension.cs index 02d7a2c4161..3c87a8f759b 100644 --- a/src/EFCore/Infrastructure/CoreOptionsExtension.cs +++ b/src/EFCore/Infrastructure/CoreOptionsExtension.cs @@ -42,6 +42,8 @@ public class CoreOptionsExtension : IDbContextOptionsExtension private DbContextOptionsExtensionInfo? _info; private IEnumerable? _interceptors; private IEnumerable? _singletonInterceptors; + private Action? _seed; + private Func? _seedAsync; private static readonly TimeSpan DefaultLoggingCacheTime = TimeSpan.FromSeconds(1); @@ -85,6 +87,8 @@ protected CoreOptionsExtension(CoreOptionsExtension copyFrom) _serviceProviderCachingEnabled = copyFrom.ServiceProviderCachingEnabled; _interceptors = copyFrom.Interceptors?.ToList(); _singletonInterceptors = copyFrom.SingletonInterceptors?.ToList(); + _seed = copyFrom._seed; + _seedAsync = copyFrom._seedAsync; if (copyFrom._replacedServices != null) { @@ -407,6 +411,36 @@ public virtual CoreOptionsExtension WithSingletonInterceptors(IEnumerable + /// Creates a new instance with all options the same as for this instance, but with the given option changed. + /// It is unusual to call this method directly. Instead use . + /// + /// The option to change. + /// A new instance with the option changed. + public virtual CoreOptionsExtension WithSeeding(Action seed) + { + var clone = Clone(); + + clone._seed = seed; + + return clone; + } + + /// + /// Creates a new instance with all options the same as for this instance, but with the given option changed. + /// It is unusual to call this method directly. Instead use . + /// + /// The option to change. + /// A new instance with the option changed. + public virtual CoreOptionsExtension WithAsyncSeeding(Func seedAsync) + { + var clone = Clone(); + + clone._seedAsync = seedAsync; + + return clone; + } + /// /// The option set from the method. /// @@ -529,6 +563,24 @@ public virtual IEnumerable? Interceptors public virtual IEnumerable? SingletonInterceptors => _singletonInterceptors; + /// + /// The option set from the + /// + /// method. + /// + public virtual Action? Seeder + => _seed; + + /// + /// The option set from the + /// + /// method. + /// + public virtual Func? AsyncSeeder + => _seedAsync; + /// /// Adds the services required to make the selected options work. This is used when there /// is no external and EF is maintaining its own service diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index 1a9d3320fa9..1aa731b3728 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -1155,7 +1155,7 @@ public static string ErrorMaterializingPropertyInvalidCast(object? entityType, o entityType, property, expectedType, actualType); /// - /// The methods '{methodName}' and '{asyncMethodName}' are not supported by the current database provider. Please contact the publisher of the database provider for more information. + /// The methods '{methodName}' and '{asyncMethodName}' are not supported by the current database provider. Please contact the publisher of the database provider for more information. /// public static string ExecuteQueriesNotSupported(object? methodName, object? asyncMethodName) => string.Format( @@ -1827,6 +1827,12 @@ public static string MemberListBindingNotSupported public static string MemberMemberBindingNotSupported => GetString("MemberMemberBindingNotSupported"); + /// + /// An asynchronous store managment operation was performed and no asynchronous seed delegate has been provided, however a synchronous seed delegate was. Set 'UseAsyncSeeding' option with a delegate equivalent to the one supplied in 'UseSeeding'. + /// + public static string MissingAsyncSeeder + => GetString("MissingAsyncSeeder"); + /// /// The specified field '{field}' could not be found for property '{2_entityType}.{1_property}'. /// @@ -1835,6 +1841,12 @@ public static string MissingBackingField(object? field, object? property, object GetString("MissingBackingField", nameof(field), "1_property", "2_entityType"), field, property, entityType); + /// + /// A synchronous store managment operation was performed and no synchronous seed delegate has been provided, however an asynchronous seed delegate was. Set 'UseSeeding' option with a delegate equivalent to the one supplied in 'UseAsyncSeeding'. + /// + public static string MissingSeeder + => GetString("MissingSeeder"); + /// /// Runtime metadata changes are not allowed when the model hasn't been marked as read-only. /// @@ -2159,14 +2171,6 @@ public static string NonIndexerEntityType(object? property, object? entityType, GetString("NonIndexerEntityType", nameof(property), nameof(entityType), nameof(type)), property, entityType, type); - /// - /// The LINQ expression '{expression}' could not be translated. Additional information: {details} See https://go.microsoft.com/fwlink/?linkid=2101038 for more information. - /// - public static string NonQueryTranslationFailedWithDetails(object? expression, object? details) - => string.Format( - GetString("NonQueryTranslationFailedWithDetails", nameof(expression), nameof(details)), - expression, details); - /// /// The collection type '{2_collectionType}' being used for navigation '{1_entityType}.{0_navigation}' does not implement 'INotifyCollectionChanged'. Any entity type configured to use the '{changeTrackingStrategy}' change tracking strategy must use collections that implement 'INotifyCollectionChanged'. Consider using 'ObservableCollection<T>' for this. /// @@ -2175,6 +2179,14 @@ public static string NonNotifyingCollection(object? navigation, object? entityTy GetString("NonNotifyingCollection", "0_navigation", "1_entityType", "2_collectionType", nameof(changeTrackingStrategy)), navigation, entityType, collectionType, changeTrackingStrategy); + /// + /// The LINQ expression '{expression}' could not be translated. Additional information: {details} See https://go.microsoft.com/fwlink/?linkid=2101038 for more information. + /// + public static string NonQueryTranslationFailedWithDetails(object? expression, object? details) + => string.Format( + GetString("NonQueryTranslationFailedWithDetails", nameof(expression), nameof(details)), + expression, details); + /// /// The foreign key {foreignKeyProperties} on the entity type '{declaringEntityType}' cannot have a required dependent end since it is not unique. /// diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index 41d62da4122..4849feeeac3 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -1,17 +1,17 @@  - @@ -1139,9 +1139,15 @@ EF Core does not support MemberMemberBinding: 'new Blog { Data = { Name = "hello world" } }'. + + An asynchronous store managment operation was performed and no asynchronous seed delegate has been provided, however a synchronous seed delegate was. Set 'UseAsyncSeeding' option with a delegate equivalent to the one supplied in 'UseSeeding'. + The specified field '{field}' could not be found for property '{2_entityType}.{1_property}'. + + A synchronous store managment operation was performed and no synchronous seed delegate has been provided, however an asynchronous seed delegate was. Set 'UseSeeding' option with a delegate equivalent to the one supplied in 'UseAsyncSeeding'. + Runtime metadata changes are not allowed when the model hasn't been marked as read-only. diff --git a/test/EFCore.Cosmos.FunctionalTests/OptimisticConcurrencyCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/OptimisticConcurrencyCosmosTest.cs index b690a982c4a..599d18467d9 100644 --- a/test/EFCore.Cosmos.FunctionalTests/OptimisticConcurrencyCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/OptimisticConcurrencyCosmosTest.cs @@ -59,39 +59,39 @@ protected override IDbContextTransaction BeginTransaction(DatabaseFacade facade) => new FakeDbContextTransaction(); public override Task Calling_Reload_on_an_Added_entity_that_is_not_in_database_is_no_op(bool async) - => CosmosTestHelpers.Instance.NoSyncTest(async, a => base.Calling_Reload_on_an_Added_entity_that_is_not_in_database_is_no_op(a)); + => CosmosTestHelpers.Instance.NoSyncTest(async, base.Calling_Reload_on_an_Added_entity_that_is_not_in_database_is_no_op); public override Task Calling_Reload_on_an_Unchanged_entity_that_is_not_in_database_detaches_it(bool async) => CosmosTestHelpers.Instance.NoSyncTest( - async, a => base.Calling_Reload_on_an_Unchanged_entity_that_is_not_in_database_detaches_it(a)); + async, base.Calling_Reload_on_an_Unchanged_entity_that_is_not_in_database_detaches_it); public override Task Calling_Reload_on_a_Modified_entity_that_is_not_in_database_detaches_it(bool async) => CosmosTestHelpers.Instance.NoSyncTest( - async, a => base.Calling_Reload_on_a_Modified_entity_that_is_not_in_database_detaches_it(a)); + async, base.Calling_Reload_on_a_Modified_entity_that_is_not_in_database_detaches_it); public override Task Calling_Reload_on_a_Deleted_entity_that_is_not_in_database_detaches_it(bool async) => CosmosTestHelpers.Instance.NoSyncTest( - async, a => base.Calling_Reload_on_a_Deleted_entity_that_is_not_in_database_detaches_it(a)); + async, base.Calling_Reload_on_a_Deleted_entity_that_is_not_in_database_detaches_it); public override Task Calling_Reload_on_a_Detached_entity_that_is_not_in_database_detaches_it(bool async) => CosmosTestHelpers.Instance.NoSyncTest( - async, a => base.Calling_Reload_on_a_Detached_entity_that_is_not_in_database_detaches_it(a)); + async, base.Calling_Reload_on_a_Detached_entity_that_is_not_in_database_detaches_it); public override Task Calling_Reload_on_an_Unchanged_entity_makes_the_entity_unchanged(bool async) - => CosmosTestHelpers.Instance.NoSyncTest(async, a => base.Calling_Reload_on_an_Unchanged_entity_makes_the_entity_unchanged(a)); + => CosmosTestHelpers.Instance.NoSyncTest(async, base.Calling_Reload_on_an_Unchanged_entity_makes_the_entity_unchanged); public override Task Calling_Reload_on_a_Modified_entity_makes_the_entity_unchanged(bool async) - => CosmosTestHelpers.Instance.NoSyncTest(async, a => base.Calling_Reload_on_a_Modified_entity_makes_the_entity_unchanged(a)); + => CosmosTestHelpers.Instance.NoSyncTest(async, base.Calling_Reload_on_a_Modified_entity_makes_the_entity_unchanged); public override Task Calling_Reload_on_a_Deleted_entity_makes_the_entity_unchanged(bool async) - => CosmosTestHelpers.Instance.NoSyncTest(async, a => base.Calling_Reload_on_a_Deleted_entity_makes_the_entity_unchanged(a)); + => CosmosTestHelpers.Instance.NoSyncTest(async, base.Calling_Reload_on_a_Deleted_entity_makes_the_entity_unchanged); public override Task Calling_Reload_on_an_Added_entity_that_was_saved_elsewhere_makes_the_entity_unchanged(bool async) => CosmosTestHelpers.Instance.NoSyncTest( - async, a => base.Calling_Reload_on_an_Added_entity_that_was_saved_elsewhere_makes_the_entity_unchanged(a)); + async, base.Calling_Reload_on_an_Added_entity_that_was_saved_elsewhere_makes_the_entity_unchanged); public override Task Calling_Reload_on_a_Detached_entity_makes_the_entity_unchanged(bool async) - => CosmosTestHelpers.Instance.NoSyncTest(async, a => base.Calling_Reload_on_a_Detached_entity_makes_the_entity_unchanged(a)); + => CosmosTestHelpers.Instance.NoSyncTest(async, base.Calling_Reload_on_a_Detached_entity_makes_the_entity_unchanged); private class FakeDbContextTransaction : IDbContextTransaction { diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/JsonQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/JsonQueryCosmosTest.cs index d803f3ec4b5..46f242cc84c 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/JsonQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/JsonQueryCosmosTest.cs @@ -7,6 +7,7 @@ namespace Microsoft.EntityFrameworkCore.Query; +[CosmosCondition(CosmosCondition.DoesNotUseTokenCredential)] public class JsonQueryCosmosTest : JsonQueryTestBase { private const string NotImplementedBindPropertyMessage diff --git a/test/EFCore.Cosmos.FunctionalTests/Storage/CosmosDatabaseCreatorTest.cs b/test/EFCore.Cosmos.FunctionalTests/Storage/CosmosDatabaseCreatorTest.cs index 5d879385922..e9f9bb666cb 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Storage/CosmosDatabaseCreatorTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Storage/CosmosDatabaseCreatorTest.cs @@ -8,7 +8,7 @@ namespace Microsoft.EntityFrameworkCore.Storage; [CosmosCondition(CosmosCondition.DoesNotUseTokenCredential)] public class CosmosDatabaseCreatorTest { - public static IEnumerable IsAsyncData = new object[][] { [false], [true] }; + public static IEnumerable IsAsyncData = [[false], [true]]; [ConditionalFact] public async Task EnsureCreated_returns_true_when_database_does_not_exist() @@ -86,20 +86,37 @@ public Task EnsureDeleted_returns_false_when_database_does_not_exist(bool async) Assert.False(a ? await creator.EnsureDeletedAsync() : creator.EnsureDeleted()); }); - private class BloggingContext(CosmosTestStore testStore) : DbContext + [ConditionalFact] + public async Task EnsureCreated_throws_for_missing_seed() + { + await using var testDatabase = await CosmosTestStore.CreateInitializedAsync("EnsureCreatedSeedTest"); + using var context = new BloggingContext(testDatabase, seed: true); + + Assert.Equal(CoreStrings.MissingSeeder, + (await Assert.ThrowsAsync(() => context.Database.EnsureCreatedAsync())).Message); + } + + private class BloggingContext(CosmosTestStore testStore, bool seed = false) : DbContext { private readonly string _connectionUri = testStore.ConnectionUri; private readonly string _authToken = testStore.AuthToken; private readonly string _name = testStore.Name; protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - => optionsBuilder + { + optionsBuilder .UseCosmos( _connectionUri, _authToken, _name, b => b.ApplyConfiguration()); + if (seed) + { + optionsBuilder.UseSeeding((_, __) => { }); + } + } + protected override void OnModelCreating(ModelBuilder modelBuilder) { } diff --git a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs index 1673d662679..521ba638f25 100644 --- a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs +++ b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs @@ -502,7 +502,7 @@ private async Task DeleteContainers(DbContext context) private static async Task SeedAsync(DbContext context) { var creator = (CosmosDatabaseCreator)context.GetService(); - await creator.SeedAsync().ConfigureAwait(false); + await creator.InsertDataAsync().ConfigureAwait(false); } public override void Dispose() diff --git a/test/EFCore.Design.Tests/Design/DesignTimeServicesTest.cs b/test/EFCore.Design.Tests/Design/DesignTimeServicesTest.cs index 94a072271cb..e9561f61d8d 100644 --- a/test/EFCore.Design.Tests/Design/DesignTimeServicesTest.cs +++ b/test/EFCore.Design.Tests/Design/DesignTimeServicesTest.cs @@ -218,10 +218,10 @@ public string GetCreateIfNotExistsScript() public string GetCreateScript() => throw new NotImplementedException(); - public IDisposable GetDatabaseLock(TimeSpan timeout) + public IDisposable GetDatabaseLock() => throw new NotImplementedException(); - public Task GetDatabaseLockAsync(TimeSpan timeout, CancellationToken cancellationToken = default) + public Task GetDatabaseLockAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); public string GetDeleteScript(string migrationId) @@ -290,10 +290,10 @@ public void Create() public Task CreateAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public IDisposable GetDatabaseLock(TimeSpan timeout) + public IDisposable GetDatabaseLock() => throw new NotImplementedException(); - public Task GetDatabaseLockAsync(TimeSpan timeout, CancellationToken cancellationToken = default) + public Task GetDatabaseLockAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); public string GetDeleteScript(string migrationId) diff --git a/test/EFCore.Design.Tests/Migrations/Design/MigrationScaffolderTest.cs b/test/EFCore.Design.Tests/Migrations/Design/MigrationScaffolderTest.cs index 7cab26ed8f3..866b54c997f 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/MigrationScaffolderTest.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/MigrationScaffolderTest.cs @@ -123,9 +123,9 @@ var migrationAssembly services.GetRequiredService>(), services.GetRequiredService(), services.GetRequiredService(), - services.GetServices(), services.GetRequiredService(), - services.GetRequiredService()))); + services.GetRequiredService(), + services.GetRequiredService()))); } // ReSharper disable once UnusedTypeParameter @@ -182,10 +182,10 @@ public void Create() public Task CreateAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public IDisposable GetDatabaseLock(TimeSpan timeout) + public IDisposable GetDatabaseLock() => throw new NotImplementedException(); - public Task GetDatabaseLockAsync(TimeSpan timeout, CancellationToken cancellationToken = default) + public Task GetDatabaseLockAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); } diff --git a/test/EFCore.InMemory.Tests/Storage/InMemoryDatabaseCreatorTest.cs b/test/EFCore.InMemory.Tests/Storage/InMemoryDatabaseCreatorTest.cs index 6b4fb66192a..44985085f8f 100644 --- a/test/EFCore.InMemory.Tests/Storage/InMemoryDatabaseCreatorTest.cs +++ b/test/EFCore.InMemory.Tests/Storage/InMemoryDatabaseCreatorTest.cs @@ -46,7 +46,28 @@ private static InMemoryDatabaseCreator CreateDatabaseCreator(IServiceProvider se optionsBuilder.UseInMemoryDatabase(nameof(InMemoryDatabaseCreatorTest)); var contextServices = InMemoryTestHelpers.Instance.CreateContextServices(serviceProvider, optionsBuilder.Options); - return new InMemoryDatabaseCreator(contextServices.GetRequiredService()); + return new InMemoryDatabaseCreator( + contextServices.GetRequiredService(), + contextServices.GetRequiredService(), + contextServices.GetRequiredService()); + } + + [ConditionalFact] + public void EnsureCreated_throws_for_missing_seed() + { + using var context = new FraggleContext( asyncSeed: true); + + Assert.Equal(CoreStrings.MissingSeeder, + Assert.Throws(() => context.Database.EnsureCreated()).Message); + } + + [ConditionalFact] + public async Task EnsureCreatedAsync_throws_for_missing_seed() + { + using var context = new FraggleContext(seed: true); + + Assert.Equal(CoreStrings.MissingSeeder, + (await Assert.ThrowsAsync(() => context.Database.EnsureCreatedAsync())).Message); } [ConditionalFact] @@ -100,15 +121,27 @@ private static async Task Delete_clears_all_in_memory_data_test(bool async) } } - private class FraggleContext : DbContext + private class FraggleContext(bool seed = false, bool asyncSeed = false) : DbContext { // ReSharper disable once UnusedAutoPropertyAccessor.Local public DbSet Fraggles { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - => optionsBuilder + { + optionsBuilder .UseInternalServiceProvider(InMemoryFixture.DefaultServiceProvider) .UseInMemoryDatabase(nameof(FraggleContext)); + + if (seed) + { + optionsBuilder.UseSeeding((_, __) => { }); + } + + if (asyncSeed) + { + optionsBuilder.UseAsyncSeeding((_, __, ___) => Task.CompletedTask); + } + } } private class Fraggle diff --git a/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsInfrastructureTestBase.cs b/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsInfrastructureTestBase.cs index f5baf42669c..0b617c99581 100644 --- a/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsInfrastructureTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsInfrastructureTestBase.cs @@ -3,8 +3,6 @@ // ReSharper disable InconsistentNaming -using Microsoft.EntityFrameworkCore.Diagnostics.Internal; - namespace Microsoft.EntityFrameworkCore.Migrations; #nullable disable @@ -19,6 +17,7 @@ protected MigrationsInfrastructureTestBase(TFixture fixture) Fixture = fixture; Fixture.TestStore.CloseConnection(); Fixture.TestSqlLoggerFactory.Clear(); + Fixture.ResetCounts(); } protected string Sql { get; private set; } @@ -71,12 +70,9 @@ public virtual void Can_apply_all_migrations() GiveMeSomeTime(db); - MigrationsInfrastructureFixtureBase.MigratorPlugin.ResetCounts(); - db.Database.Migrate((c, d) => - { - c.Add(new MigrationsInfrastructureFixtureBase.Foo { Id = 1, Bar = 10, Description = "Test" }); - c.SaveChanges(); - }); + Assert.Equal(0, Fixture.SeedCallCount); + + db.Database.Migrate(); var history = db.GetService(); Assert.Collection( @@ -89,12 +85,8 @@ public virtual void Can_apply_all_migrations() x => Assert.Equal("00000000000006_Migration6", x.MigrationId), x => Assert.Equal("00000000000007_Migration7", x.MigrationId)); - Assert.NotNull(db.Find(1)); - - Assert.Equal(1, MigrationsInfrastructureFixtureBase.MigratorPlugin.MigratingCallCount); - Assert.Equal(1, MigrationsInfrastructureFixtureBase.MigratorPlugin.MigratedCallCount); - Assert.Equal(0, MigrationsInfrastructureFixtureBase.MigratorPlugin.MigratingAsyncCallCount); - Assert.Equal(0, MigrationsInfrastructureFixtureBase.MigratorPlugin.MigratedAsyncCallCount); + Assert.Equal(1, Fixture.SeedCallCount); + Assert.Equal(0, Fixture.SeedAsyncCallCount); } [ConditionalFact] @@ -105,12 +97,9 @@ public virtual async Task Can_apply_all_migrations_async() await GiveMeSomeTimeAsync(db); - MigrationsInfrastructureFixtureBase.MigratorPlugin.ResetCounts(); - await db.Database.MigrateAsync(async (c, d, ct) => - { - c.Add(new MigrationsInfrastructureFixtureBase.Foo { Id = 1, Bar = 10, Description = "Test" }); - await c.SaveChangesAsync(ct); - }); + Assert.Equal(0, Fixture.SeedAsyncCallCount); + + await db.Database.MigrateAsync(); var history = db.GetService(); Assert.Collection( @@ -123,12 +112,8 @@ await history.GetAppliedMigrationsAsync(), x => Assert.Equal("00000000000006_Migration6", x.MigrationId), x => Assert.Equal("00000000000007_Migration7", x.MigrationId)); - Assert.NotNull(await db.FindAsync(1)); - - Assert.Equal(0, MigrationsInfrastructureFixtureBase.MigratorPlugin.MigratingCallCount); - Assert.Equal(0, MigrationsInfrastructureFixtureBase.MigratorPlugin.MigratedCallCount); - Assert.Equal(1, MigrationsInfrastructureFixtureBase.MigratorPlugin.MigratingAsyncCallCount); - Assert.Equal(1, MigrationsInfrastructureFixtureBase.MigratorPlugin.MigratedAsyncCallCount); + Assert.Equal(0, Fixture.SeedCallCount); + Assert.Equal(1, Fixture.SeedAsyncCallCount); } [ConditionalFact] @@ -139,7 +124,7 @@ public virtual void Can_apply_range_of_migrations() GiveMeSomeTime(db); - db.Database.Migrate(null, "Migration6"); + db.Database.Migrate("Migration6"); var history = db.GetService(); Assert.Collection( @@ -161,7 +146,7 @@ public virtual void Can_apply_one_migration() GiveMeSomeTime(db); var migrator = db.GetService(); - migrator.Migrate(targetMigration: "Migration1"); + migrator.Migrate("Migration1"); var history = db.GetService(); Assert.Collection( @@ -181,8 +166,8 @@ public virtual void Can_revert_all_migrations() GiveMeSomeTime(db); var migrator = db.GetService(); - migrator.Migrate(targetMigration: "Migration5"); - migrator.Migrate(targetMigration: Migration.InitialDatabase); + migrator.Migrate("Migration5"); + migrator.Migrate(Migration.InitialDatabase); var history = db.GetService(); Assert.Empty(history.GetAppliedMigrations()); @@ -197,8 +182,8 @@ public virtual void Can_revert_one_migrations() GiveMeSomeTime(db); var migrator = db.GetService(); - migrator.Migrate(targetMigration: "Migration5"); - migrator.Migrate(targetMigration: "Migration4"); + migrator.Migrate("Migration5"); + migrator.Migrate("Migration4"); var history = db.GetService(); Assert.Collection( @@ -221,7 +206,7 @@ public virtual void Can_apply_one_migration_in_parallel() { using var context = Fixture.CreateContext(); var migrator = context.GetService(); - migrator.Migrate(targetMigration: "Migration1"); + migrator.Migrate("Migration1"); }); var history = db.GetService(); @@ -242,7 +227,7 @@ await Parallel.ForAsync(0, Environment.ProcessorCount, async (i, _) => { using var context = Fixture.CreateContext(); var migrator = context.GetService(); - await migrator.MigrateAsync(targetMigration: "Migration1"); + await migrator.MigrateAsync("Migration1"); }); var history = db.GetService(); @@ -257,13 +242,13 @@ public virtual void Can_apply_second_migration_in_parallel() using var db = Fixture.CreateContext(); db.Database.EnsureDeleted(); GiveMeSomeTime(db); - db.GetService().Migrate(targetMigration: "Migration1"); + db.GetService().Migrate("Migration1"); Parallel.For(0, Environment.ProcessorCount, i => { using var context = Fixture.CreateContext(); var migrator = context.GetService(); - migrator.Migrate(targetMigration: "Migration2"); + migrator.Migrate("Migration2"); }); var history = db.GetService(); @@ -279,13 +264,13 @@ public virtual async Task Can_apply_second_migration_in_parallel_async() using var db = Fixture.CreateContext(); await db.Database.EnsureDeletedAsync(); await GiveMeSomeTimeAsync(db); - await db.GetService().MigrateAsync(targetMigration: "Migration1"); + await db.GetService().MigrateAsync("Migration1"); await Parallel.ForAsync(0, Environment.ProcessorCount, async (i, _) => { using var context = Fixture.CreateContext(); var migrator = context.GetService(); - await migrator.MigrateAsync(targetMigration: "Migration2"); + await migrator.MigrateAsync("Migration2"); }); var history = db.GetService(); @@ -468,11 +453,19 @@ public abstract class MigrationsInfrastructureFixtureBase public new RelationalTestStore TestStore => (RelationalTestStore)base.TestStore; + public int SeedCallCount { get; private set; } + public int SeedAsyncCallCount { get; private set; } + + public void ResetCounts() + { + SeedCallCount = 0; + SeedAsyncCallCount = 0; + } + protected override IServiceCollection AddServices(IServiceCollection serviceCollection) { TestStore.UseConnectionString = true; - return base.AddServices(serviceCollection) - .AddSingleton(); + return base.AddServices(serviceCollection); } protected override string StoreName @@ -504,9 +497,19 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con } public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) - => base.AddOptions(builder).ConfigureWarnings(e => e - .Log(RelationalEventId.PendingModelChangesWarning) - .Log(RelationalEventId.NonTransactionalMigrationOperationWarning) + => base.AddOptions(builder) + .UseSeeding((context, migrated) => + { + SeedCallCount++; + }) + .UseAsyncSeeding((context, migrated, token) => + { + SeedAsyncCallCount++; + return Task.CompletedTask; + }) + .ConfigureWarnings(e => e + .Log(RelationalEventId.PendingModelChangesWarning) + .Log(RelationalEventId.NonTransactionalMigrationOperationWarning) ); protected override bool ShouldLogCategory(string logCategory) @@ -519,42 +522,6 @@ public class Foo public string Description { get; set; } } - public class MigratorPlugin : IMigratorPlugin - { - public static int MigratedCallCount { get; private set; } - public static int MigratedAsyncCallCount { get; private set; } - public static int MigratingCallCount { get; private set; } - public static int MigratingAsyncCallCount { get; private set; } - - public static void ResetCounts() - { - MigratedCallCount = 0; - MigratedAsyncCallCount = 0; - MigratingCallCount = 0; - MigratingAsyncCallCount = 0; - } - - public void Migrated(DbContext context, IMigratorData data) - { - MigratedCallCount++; - } - - public Task MigratedAsync(DbContext context, IMigratorData data, CancellationToken cancellationToken) - { - MigratedAsyncCallCount++; - return Task.CompletedTask; - } - - public void Migrating(DbContext context, IMigratorData data) - => MigratingCallCount++; - - public Task MigratingAsync(DbContext context, IMigratorData data, CancellationToken cancellationToken = default) - { - MigratingAsyncCallCount++; - return Task.CompletedTask; - } - } - [DbContext(typeof(MigrationsContext))] [Migration("00000000000001_Migration1")] private class Migration1 : Migration diff --git a/test/EFCore.Relational.Specification.Tests/Query/EntitySplittingQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/EntitySplittingQueryTestBase.cs index c30158fe590..c600f3deeeb 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/EntitySplittingQueryTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/EntitySplittingQueryTestBase.cs @@ -2920,11 +2920,24 @@ protected async Task InitializeContextFactoryAsync(Action onModelC { wc.Log(RelationalEventId.ForeignKeyTpcPrincipalWarning); }), - shouldLogCategory: _ => true, seed: c => SeedAsync(c)); + shouldLogCategory: _ => true); protected virtual EntitySplittingContext CreateContext() => ContextFactory.CreateContext(); + protected override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + => base.AddOptions(builder) + .UseSeeding((c, _) => + { + EntitySplittingData.Instance.AddSeedData((EntitySplittingContext)c); + c.SaveChanges(); + }) + .UseAsyncSeeding((c, _, t) => + { + EntitySplittingData.Instance.AddSeedData((EntitySplittingContext)c); + return c.SaveChangesAsync(t); + }); + public void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) => facade.UseTransaction(transaction.GetDbTransaction()); @@ -2947,9 +2960,6 @@ protected virtual void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity(); } - protected virtual Task SeedAsync(EntitySplittingContext context) - => EntitySplittingData.Instance.Seed(context); - public override async Task DisposeAsync() { await base.DisposeAsync(); diff --git a/test/EFCore.Relational.Specification.Tests/TestModels/EntitySplitting/EntitySplittingData.cs b/test/EFCore.Relational.Specification.Tests/TestModels/EntitySplitting/EntitySplittingData.cs index c92256cf0a3..24d0458b431 100644 --- a/test/EFCore.Relational.Specification.Tests/TestModels/EntitySplitting/EntitySplittingData.cs +++ b/test/EFCore.Relational.Specification.Tests/TestModels/EntitySplitting/EntitySplittingData.cs @@ -276,15 +276,25 @@ private void WireUp() } } - public Task Seed(EntitySplittingContext context) + public void AddSeedData(EntitySplittingContext context) { + try + { + if (context.Set().AsNoTracking().Any()) + { + return; + } + } + catch + { + return; + } + // Seed data cannot contain any store generated value, // or recreate instances when calling AddRange context.AddRange(_entityOnes); context.AddRange(_entityTwos); context.AddRange(_entityThrees); context.AddRange(_baseEntities); - - return context.SaveChangesAsync(); } } diff --git a/test/EFCore.Relational.Tests/Extensions/RelationalDatabaseFacadeExtensionsTest.cs b/test/EFCore.Relational.Tests/Extensions/RelationalDatabaseFacadeExtensionsTest.cs index a3dc6bb70a2..92e7f123544 100644 --- a/test/EFCore.Relational.Tests/Extensions/RelationalDatabaseFacadeExtensionsTest.cs +++ b/test/EFCore.Relational.Tests/Extensions/RelationalDatabaseFacadeExtensionsTest.cs @@ -361,10 +361,10 @@ public void Create() public Task CreateAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public IDisposable GetDatabaseLock(TimeSpan timeout) + public IDisposable GetDatabaseLock() => throw new NotImplementedException(); - public Task GetDatabaseLockAsync(TimeSpan timeout, CancellationToken cancellationToken = default) + public Task GetDatabaseLockAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); } diff --git a/test/EFCore.Relational.Tests/Infrastructure/RelationalEventIdTest.cs b/test/EFCore.Relational.Tests/Infrastructure/RelationalEventIdTest.cs index fe1ee28da16..6cf00809dc8 100644 --- a/test/EFCore.Relational.Tests/Infrastructure/RelationalEventIdTest.cs +++ b/test/EFCore.Relational.Tests/Infrastructure/RelationalEventIdTest.cs @@ -144,15 +144,6 @@ public string GenerateScript( MigrationsSqlGenerationOptions options = MigrationsSqlGenerationOptions.Default) => throw new NotImplementedException(); - public void Migrate(Action seed, string targetMigration, TimeSpan? lockTimeout) - => throw new NotImplementedException(); - - public Task MigrateAsync(Func seed, - string targetMigration, - TimeSpan? lockTimeout, - CancellationToken cancellationToken = default) - => throw new NotImplementedException(); - public bool HasPendingModelChanges() => throw new NotImplementedException(); } diff --git a/test/EFCore.Specification.Tests/F1FixtureBase.cs b/test/EFCore.Specification.Tests/F1FixtureBase.cs index 0daf63bb51f..8da3c33f3e4 100644 --- a/test/EFCore.Specification.Tests/F1FixtureBase.cs +++ b/test/EFCore.Specification.Tests/F1FixtureBase.cs @@ -17,6 +17,26 @@ protected override bool UsePooling public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) => base.AddOptions(builder) .UseModel(CreateModelExternal()) + .UseSeeding((c, _) => + { + if (c.Set().Count() != 0) + { + return; + } + + F1Context.AddSeedData((F1Context)c); + c.SaveChanges(); + }) + .UseAsyncSeeding(async (c, _, t) => + { + if (await c.Set().CountAsync(t) != 0) + { + return; + } + + F1Context.AddSeedData((F1Context)c); + await c.SaveChangesAsync(t); + }) .ConfigureWarnings( w => w.Ignore(CoreEventId.SaveChangesStarting, CoreEventId.SaveChangesCompleted)); @@ -248,7 +268,4 @@ private static void ConfigureConstructorBinding( parameterBindings ); } - - protected override Task SeedAsync(F1Context context) - => F1Context.SeedAsync(context); } diff --git a/test/EFCore.Specification.Tests/Scaffolding/CompiledModelTestBase.cs b/test/EFCore.Specification.Tests/Scaffolding/CompiledModelTestBase.cs index 34abe7ad7d3..758b05ac42b 100644 --- a/test/EFCore.Specification.Tests/Scaffolding/CompiledModelTestBase.cs +++ b/test/EFCore.Specification.Tests/Scaffolding/CompiledModelTestBase.cs @@ -1396,7 +1396,8 @@ protected virtual Task Test( if (useContext != null) { ListLoggerFactory.Clear(); - await TestStore.InitializeAsync(ServiceProvider, contextFactory.CreateContext); + var testStore = await TestStore.InitializeAsync(ServiceProvider, contextFactory.CreateContext); + await using var _ = testStore; using var compiledModelContext = (await CreateContextFactory( onConfiguring: options => diff --git a/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/F1Context.cs b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/F1Context.cs index c962b5998c0..a76a389719b 100644 --- a/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/F1Context.cs +++ b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/F1Context.cs @@ -15,17 +15,9 @@ public class F1Context(DbContextOptions options) : PoolableDbContext(options) public DbSet Fans { get; set; } public DbSet FanTpts { get; set; } public DbSet FanTpcs { get; set; } - public DbSet Circuits { get; set; } - public static Task SeedAsync(F1Context context) - { - AddEntities(context); - - return context.SaveChangesAsync(); - } - - private static void AddEntities(F1Context context) + public static void AddSeedData(F1Context context) { foreach (var engineSupplier in new List { diff --git a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsInfrastructureSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsInfrastructureSqlServerTest.cs index 1c3e933f398..0e7dc25e32a 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsInfrastructureSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsInfrastructureSqlServerTest.cs @@ -6,7 +6,6 @@ using Microsoft.EntityFrameworkCore.Diagnostics.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; using Microsoft.EntityFrameworkCore.TestModels.AspNetIdentity; -using static Microsoft.EntityFrameworkCore.DbLoggerCategory; #nullable disable @@ -975,7 +974,7 @@ public async Task Empty_Migration_Creates_Database() var creator = (SqlServerDatabaseCreator)context.GetService(); creator.RetryTimeout = TimeSpan.FromMinutes(10); - await context.Database.MigrateAsync(null, "Empty"); + await context.Database.MigrateAsync("Empty"); Assert.True(creator.Exists()); } @@ -1012,10 +1011,8 @@ IF SERVERPROPERTY('EngineEdition') <> 5 SELECT 1 -@LockTimeout='?' (DbType = Double) - DECLARE @result int; -EXEC @result = sp_getapplock @Resource = '__EFMigrationsLock', @LockOwner = 'Session', @LockMode = 'Exclusive', @LockTimeout = @LockTimeout; +EXEC @result = sp_getapplock @Resource = '__EFMigrationsLock', @LockOwner = 'Session', @LockMode = 'Exclusive'; SELECT @result SELECT OBJECT_ID(N'[__EFMigrationsHistory]'); @@ -1107,10 +1104,8 @@ IF SERVERPROPERTY('EngineEdition') <> 5 SELECT 1 -@LockTimeout='?' (DbType = Double) - DECLARE @result int; -EXEC @result = sp_getapplock @Resource = '__EFMigrationsLock', @LockOwner = 'Session', @LockMode = 'Exclusive', @LockTimeout = @LockTimeout; +EXEC @result = sp_getapplock @Resource = '__EFMigrationsLock', @LockOwner = 'Session', @LockMode = 'Exclusive'; SELECT @result SELECT OBJECT_ID(N'[__EFMigrationsHistory]'); diff --git a/test/EFCore.SqlServer.FunctionalTests/SqlServerDatabaseCreatorTest.cs b/test/EFCore.SqlServer.FunctionalTests/SqlServerDatabaseCreatorTest.cs index f67fe7903b8..85269729ae0 100644 --- a/test/EFCore.SqlServer.FunctionalTests/SqlServerDatabaseCreatorTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/SqlServerDatabaseCreatorTest.cs @@ -14,7 +14,7 @@ namespace Microsoft.EntityFrameworkCore; // Tests are split into classes to enable parallel execution // Some combinations are skipped to reduce run time [SqlServerCondition(SqlServerCondition.IsNotCI)] -public class SqlServerDatabaseCreatorExistsTest : SqlServerDatabaseCreatorTest +public class SqlServerDatabaseCreatorExistsTest : SqlServerDatabaseCreatorTestBase { [ConditionalTheory] [InlineData(true, true, false)] @@ -108,7 +108,7 @@ await context.Database.CreateExecutionStrategy().ExecuteAsync( } [SqlServerCondition(SqlServerCondition.IsNotCI)] -public class SqlServerDatabaseCreatorEnsureDeletedTest : SqlServerDatabaseCreatorTest +public class SqlServerDatabaseCreatorEnsureDeletedTest : SqlServerDatabaseCreatorTestBase { [ConditionalTheory] [InlineData(true, true, true)] @@ -202,7 +202,7 @@ private static async Task Noop_when_database_does_not_exist_test(bool async, boo } [SqlServerCondition(SqlServerCondition.IsNotCI)] -public class SqlServerDatabaseCreatorEnsureCreatedTest : SqlServerDatabaseCreatorTest +public class SqlServerDatabaseCreatorEnsureCreatedTest : SqlServerDatabaseCreatorTestBase { [ConditionalTheory] [InlineData(true, true)] @@ -342,10 +342,30 @@ private static async Task Noop_when_database_exists_and_has_schema_test(bool asy Assert.Equal(ConnectionState.Closed, context.Database.GetDbConnection().State); } + + [ConditionalFact] + public async Task Throws_for_missing_seed() + { + using var testDatabase = await SqlServerTestStore.CreateInitializedAsync("EnsureCreatedSeedTest"); + using var context = new BloggingContext(testDatabase.ConnectionString, asyncSeed: true); + + Assert.Equal(CoreStrings.MissingSeeder, + Assert.Throws(() => context.Database.EnsureCreated()).Message); + } + + [ConditionalFact] + public async Task Throws_for_missing_seed_async() + { + using var testDatabase = await SqlServerTestStore.CreateInitializedAsync("EnsureCreatedSeedTest"); + using var context = new BloggingContext(testDatabase.ConnectionString, seed: true); + + Assert.Equal(CoreStrings.MissingSeeder, + (await Assert.ThrowsAsync(() => context.Database.EnsureCreatedAsync())).Message); + } } [SqlServerCondition(SqlServerCondition.IsNotCI)] -public class SqlServerDatabaseCreatorHasTablesTest : SqlServerDatabaseCreatorTest +public class SqlServerDatabaseCreatorHasTablesTest : SqlServerDatabaseCreatorTestBase { [ConditionalTheory] [InlineData(true)] @@ -410,7 +430,7 @@ await GetExecutionStrategy(testDatabase).ExecuteAsync( } [SqlServerCondition(SqlServerCondition.IsNotCI)] -public class SqlServerDatabaseCreatorDeleteTest : SqlServerDatabaseCreatorTest +public class SqlServerDatabaseCreatorDeleteTest : SqlServerDatabaseCreatorTestBase { [ConditionalTheory] [InlineData(true, true)] @@ -453,7 +473,7 @@ public async Task Throws_when_database_does_not_exist(bool async) } else { - Assert.Throws(() => creator.Delete()); + Assert.Throws(creator.Delete); } } @@ -465,14 +485,14 @@ public void Throws_when_no_initial_catalog() var creator = GetDatabaseCreator(connectionStringBuilder.ToString()); - var ex = Assert.Throws(() => creator.Delete()); + var ex = Assert.Throws(creator.Delete); Assert.Equal(SqlServerStrings.NoInitialCatalog, ex.Message); } } [SqlServerCondition(SqlServerCondition.IsNotCI)] -public class SqlServerDatabaseCreatorCreateTablesTest : SqlServerDatabaseCreatorTest +public class SqlServerDatabaseCreatorCreateTablesTest : SqlServerDatabaseCreatorTestBase { [ConditionalTheory] [InlineData(true, true)] @@ -534,7 +554,7 @@ public async Task Throws_if_database_does_not_exist(bool async) var exception = async ? (await Assert.ThrowsAsync(() => creator.CreateTablesAsync())) - : Assert.Throws(() => creator.CreateTables()); + : Assert.Throws(creator.CreateTables); Assert.Equal(CoreStrings.RetryLimitExceeded(6, "TestSqlServerRetryingExecutionStrategy"), exception.Message); @@ -599,7 +619,7 @@ public void GenerateCreateScript_works() } [SqlServerCondition(SqlServerCondition.IsNotCI)] -public class SqlServerDatabaseCreatorCreateTest : SqlServerDatabaseCreatorTest +public class SqlServerDatabaseCreatorCreateTest : SqlServerDatabaseCreatorTestBase { [ConditionalTheory] [InlineData(true, false)] @@ -656,7 +676,7 @@ public async Task Throws_if_database_already_exists(bool async) var ex = async ? await Assert.ThrowsAsync(() => creator.CreateAsync()) - : Assert.Throws(() => creator.Create()); + : Assert.Throws(creator.Create); Assert.Equal( 1801, // Database with given name already exists ex.Number); @@ -665,7 +685,7 @@ public async Task Throws_if_database_already_exists(bool async) #pragma warning disable RCS1102 // Make class static. [SqlServerCondition(SqlServerCondition.IsNotSqlAzure | SqlServerCondition.IsNotCI)] -public class SqlServerDatabaseCreatorTest +public abstract class SqlServerDatabaseCreatorTestBase { protected static IDisposable CreateTransactionScope(bool useTransaction) => TestStore.CreateTransactionScope(useTransaction); @@ -697,7 +717,11 @@ private static IServiceProvider CreateServiceProvider() .AddScoped() .BuildServiceProvider(validateScopes: true); - protected class BloggingContext(string connectionString) : DbContext + protected class BloggingContext( + string connectionString, + bool seed = false, + bool asyncSeed = false) + : DbContext { private readonly string _connectionString = connectionString; @@ -707,9 +731,20 @@ public BloggingContext(SqlServerTestStore testStore) } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - => optionsBuilder + { + optionsBuilder .UseSqlServer(_connectionString, b => b.ApplyConfiguration()) .UseInternalServiceProvider(CreateServiceProvider()); + if (seed) + { + optionsBuilder.UseSeeding((_, __) => { }); + } + + if (asyncSeed) + { + optionsBuilder.UseAsyncSeeding((_, __, ___) => Task.CompletedTask); + } + } protected override void OnModelCreating(ModelBuilder modelBuilder) => modelBuilder.Entity( diff --git a/test/EFCore.Sqlite.FunctionalTests/TestUtilities/SqliteTestStore.cs b/test/EFCore.Sqlite.FunctionalTests/TestUtilities/SqliteTestStore.cs index 823452e857e..f53602725cf 100644 --- a/test/EFCore.Sqlite.FunctionalTests/TestUtilities/SqliteTestStore.cs +++ b/test/EFCore.Sqlite.FunctionalTests/TestUtilities/SqliteTestStore.cs @@ -72,7 +72,7 @@ protected override async Task InitializeAsync(Func createContext, Fun } using var context = createContext(); - if (!await context.Database.EnsureCreatedAsync()) + if (!await context.Database.EnsureCreatedResilientlyAsync()) { if (clean != null) { @@ -80,6 +80,9 @@ protected override async Task InitializeAsync(Func createContext, Fun } await CleanAsync(context); + + // Run context seeding + await context.Database.EnsureCreatedResilientlyAsync(); } if (seed != null)