From fb8ee9775fbf64fe435e02e1bac1eee36529436c Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sat, 4 Apr 2026 19:07:57 +0200 Subject: [PATCH 1/2] Filter out full-text catalog operations from history table creation When the migration history table is created, the model differ generates AlterDatabaseOperation with full-text catalog annotations. The SQL generator then emits CREATE FULLTEXT CATALOG too early, causing the later actual migration to fail. Filter out SqlServerAnnotationNames.FullTextCatalogs from AlterDatabaseOperation in SqlServerHistoryRepository.GetCreateCommands(), following the same approach as npgsql/efcore.pg#3713. This is a workaround for #34991. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Migrations/HistoryRepository.cs | 9 +++++- .../Internal/SqlServerHistoryRepository.cs | 31 +++++++++++++++++++ .../SqlServerHistoryRepositoryTest.cs | 24 ++++++++++++-- 3 files changed, 60 insertions(+), 4 deletions(-) diff --git a/src/EFCore.Relational/Migrations/HistoryRepository.cs b/src/EFCore.Relational/Migrations/HistoryRepository.cs index 48934123be0..45f374ace26 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 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..274f0124467 100644 --- a/test/EFCore.SqlServer.Tests/Migrations/SqlServerHistoryRepositoryTest.cs +++ b/test/EFCore.SqlServer.Tests/Migrations/SqlServerHistoryRepositoryTest.cs @@ -21,6 +21,22 @@ [ProductVersion] nvarchar(32) NOT NULL, CONSTRAINT [PK___EFMigrationsHistory] PRIMARY KEY ([MigrationId]) ); +""", sql, ignoreLineEndingDifferences: true); + } + + [ConditionalFact] + public void GetCreateScript_works_with_full_text_catalog() + { + var sql = CreateHistoryRepository(configureModel: b => b.HasFullTextCatalog("MyCatalog")).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 +165,18 @@ public void GetEndIfScript_works() """, sql, ignoreLineEndingDifferences: true); } - private static IHistoryRepository CreateHistoryRepository(string schema = null) + private static IHistoryRepository CreateHistoryRepository(string schema = null, Action configureModel = null) => new TestDbContext( new DbContextOptionsBuilder() .UseInternalServiceProvider(SqlServerTestHelpers.Instance.CreateServiceProvider()) .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,6 +186,7 @@ public IQueryable TableFunction() protected override void OnModelCreating(ModelBuilder modelBuilder) { + configureModel?.Invoke(modelBuilder); } } From d17103a66d6b8a9fb872fbd8c04986f51fbe5c66 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sat, 4 Apr 2026 20:08:02 +0200 Subject: [PATCH 2/2] Make EnsureModel virtual, improve test with convention plugin - Make EnsureModel() protected virtual to fix API consistency test - Replace configureModel approach with IConventionSetPlugin injection to properly test that the full-text catalog filtering override works when conventions add annotations to the history table's mini-model Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Migrations/HistoryRepository.cs | 2 +- .../SqlServerHistoryRepositoryTest.cs | 47 +++++++++++++++++-- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/EFCore.Relational/Migrations/HistoryRepository.cs b/src/EFCore.Relational/Migrations/HistoryRepository.cs index 45f374ace26..714797d9fea 100644 --- a/src/EFCore.Relational/Migrations/HistoryRepository.cs +++ b/src/EFCore.Relational/Migrations/HistoryRepository.cs @@ -92,7 +92,7 @@ protected virtual string MigrationIdColumnName /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [EntityFrameworkInternal] - protected IModel EnsureModel() + protected virtual IModel EnsureModel() { if (_model == null) { diff --git a/test/EFCore.SqlServer.Tests/Migrations/SqlServerHistoryRepositoryTest.cs b/test/EFCore.SqlServer.Tests/Migrations/SqlServerHistoryRepositoryTest.cs index 274f0124467..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; @@ -27,7 +28,10 @@ CONSTRAINT [PK___EFMigrationsHistory] PRIMARY KEY ([MigrationId]) [ConditionalFact] public void GetCreateScript_works_with_full_text_catalog() { - var sql = CreateHistoryRepository(configureModel: b => b.HasFullTextCatalog("MyCatalog")).GetCreateScript(); + // 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( """ @@ -165,16 +169,26 @@ public void GetEndIfScript_works() """, sql, ignoreLineEndingDifferences: true); } - private static IHistoryRepository CreateHistoryRepository(string schema = null, Action configureModel = 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, configureModel) .GetService(); + } private class TestDbContext(DbContextOptions options, Action configureModel = null) : DbContext(options) { @@ -190,6 +204,31 @@ protected override void OnModelCreating(ModelBuilder 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; }