diff --git a/src/EFCore.Relational/Migrations/HistoryRepository.cs b/src/EFCore.Relational/Migrations/HistoryRepository.cs index 48934123be0..714797d9fea 100644 --- a/src/EFCore.Relational/Migrations/HistoryRepository.cs +++ b/src/EFCore.Relational/Migrations/HistoryRepository.cs @@ -85,7 +85,14 @@ protected virtual string MigrationIdColumnName .FindProperty(nameof(HistoryRow.MigrationId))! .GetColumnName(); - private IModel EnsureModel() + /// + /// 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] + protected virtual IModel EnsureModel() { if (_model == null) { diff --git a/src/EFCore.SqlServer/Migrations/Internal/SqlServerHistoryRepository.cs b/src/EFCore.SqlServer/Migrations/Internal/SqlServerHistoryRepository.cs index e8128cd0ef5..6189774c621 100644 --- a/src/EFCore.SqlServer/Migrations/Internal/SqlServerHistoryRepository.cs +++ b/src/EFCore.SqlServer/Migrations/Internal/SqlServerHistoryRepository.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Text; +using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal; namespace Microsoft.EntityFrameworkCore.SqlServer.Migrations.Internal; @@ -24,6 +25,36 @@ public SqlServerHistoryRepository(HistoryRepositoryDependencies dependencies) { } + /// + /// 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. + /// + protected override IReadOnlyList GetCreateCommands() + { + // TODO: This is a hack around https://github.com/dotnet/efcore/issues/34991: provider-specific conventions may add + // database-level annotations (e.g. full-text catalogs) to the model, and the default EF logic causes them to be created + // at this point, when the history table is being created. This is too early, and causes the later actual migration to fail. + // So we filter out full-text catalog annotations from AlterDatabaseOperation. + // This follows the same approach as the Npgsql provider (npgsql/efcore.pg#3713). +#pragma warning disable EF1001 // Internal EF Core API usage. + var model = EnsureModel(); +#pragma warning restore EF1001 // Internal EF Core API usage. + + var operations = Dependencies.ModelDiffer.GetDifferences(null, model.GetRelationalModel()); + + foreach (var operation in operations) + { + if (operation is AlterDatabaseOperation alterDatabaseOperation) + { + alterDatabaseOperation.RemoveAnnotation(SqlServerAnnotationNames.FullTextCatalogs); + } + } + + return Dependencies.MigrationsSqlGenerator.Generate(operations, model); + } + /// /// 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/test/EFCore.SqlServer.Tests/Migrations/SqlServerHistoryRepositoryTest.cs b/test/EFCore.SqlServer.Tests/Migrations/SqlServerHistoryRepositoryTest.cs index a19a9c536f1..273953819b5 100644 --- a/test/EFCore.SqlServer.Tests/Migrations/SqlServerHistoryRepositoryTest.cs +++ b/test/EFCore.SqlServer.Tests/Migrations/SqlServerHistoryRepositoryTest.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal; // ReSharper disable InconsistentNaming namespace Microsoft.EntityFrameworkCore.Migrations; @@ -21,6 +22,25 @@ [ProductVersion] nvarchar(32) NOT NULL, CONSTRAINT [PK___EFMigrationsHistory] PRIMARY KEY ([MigrationId]) ); +""", sql, ignoreLineEndingDifferences: true); + } + + [ConditionalFact] + public void GetCreateScript_works_with_full_text_catalog() + { + // Inject a model finalizing convention that adds a full-text catalog to the model, simulating a scenario where + // provider conventions add database-level annotations. Without filtering, the history table creation script would + // include CREATE FULLTEXT CATALOG. + var sql = CreateHistoryRepository(addFullTextCatalogConvention: true).GetCreateScript(); + + Assert.Equal( + """ +CREATE TABLE [__EFMigrationsHistory] ( + [MigrationId] nvarchar(150) NOT NULL, + [ProductVersion] nvarchar(32) NOT NULL, + CONSTRAINT [PK___EFMigrationsHistory] PRIMARY KEY ([MigrationId]) +); + """, sql, ignoreLineEndingDifferences: true); } @@ -149,17 +169,28 @@ public void GetEndIfScript_works() """, sql, ignoreLineEndingDifferences: true); } - private static IHistoryRepository CreateHistoryRepository(string schema = null) - => new TestDbContext( + private static IHistoryRepository CreateHistoryRepository( + string schema = null, + Action configureModel = null, + bool addFullTextCatalogConvention = false) + { + var serviceProvider = addFullTextCatalogConvention + ? SqlServerTestHelpers.Instance.CreateServiceProvider( + new ServiceCollection().AddSingleton()) + : SqlServerTestHelpers.Instance.CreateServiceProvider(); + + return new TestDbContext( new DbContextOptionsBuilder() - .UseInternalServiceProvider(SqlServerTestHelpers.Instance.CreateServiceProvider()) + .UseInternalServiceProvider(serviceProvider) .UseSqlServer( new SqlConnection("Database=DummyDatabase"), b => b.MigrationsHistoryTable(HistoryRepository.DefaultTableName, schema)) - .Options) + .Options, + configureModel) .GetService(); + } - private class TestDbContext(DbContextOptions options) : DbContext(options) + private class TestDbContext(DbContextOptions options, Action configureModel = null) : DbContext(options) { public DbSet Blogs { get; set; } @@ -169,9 +200,35 @@ public IQueryable TableFunction() protected override void OnModelCreating(ModelBuilder modelBuilder) { + configureModel?.Invoke(modelBuilder); + } + } + + /// + /// A convention plugin that adds a full-text catalog annotation to the model, simulating what a provider convention + /// might do. This allows testing that properly filters + /// out the full-text catalog from the history table creation script. + /// + private class FullTextCatalogConventionPlugin : IConventionSetPlugin + { + public ConventionSet ModifyConventions(ConventionSet conventionSet) + { + conventionSet.ModelFinalizingConventions.Add(new FullTextCatalogAddingConvention()); + return conventionSet; } } + private class FullTextCatalogAddingConvention : IModelFinalizingConvention + { +#pragma warning disable EF1001 // Internal EF Core API usage. + public void ProcessModelFinalizing( + IConventionModelBuilder modelBuilder, + IConventionContext context) + => SqlServerFullTextCatalog.AddFullTextCatalog( + (IMutableModel)modelBuilder.Metadata, "TestCatalog", ConfigurationSource.Convention); +#pragma warning restore EF1001 // Internal EF Core API usage. + } + private class Blog { public int Id { get; set; }