From 3142eb09978333f5dbf671a696cb82f3d2d3c8fe Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 25 Sep 2026 18:20:03 -0700 Subject: [PATCH] [release/11.0] Validate unsupported SQL Server JSON index options (#39090) Fixes #39065 Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Internal/SqlServerModelValidator.cs | 30 +++++++ .../Properties/SqlServerStrings.Designer.cs | 8 ++ .../Properties/SqlServerStrings.resx | 3 + .../SqlServerModelValidatorTest.cs | 82 +++++++++++++++++++ 4 files changed, 123 insertions(+) diff --git a/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerModelValidator.cs b/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerModelValidator.cs index f795a3b0b35..3cf54b4123f 100644 --- a/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerModelValidator.cs +++ b/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerModelValidator.cs @@ -229,6 +229,13 @@ protected override void ValidateIndex( ValidateIndexIncludeProperties(index); ValidateFullTextIndex(index); ValidateVectorIndex(index); + +#pragma warning disable EF1001 // Internal EF Core API usage. + if (index.IsJsonIndex()) +#pragma warning restore EF1001 // Internal EF Core API usage. + { + ValidateUnsupportedJsonIndexOptions(index); + } } /// @@ -425,6 +432,29 @@ protected virtual void ValidateVectorIndex(IIndex index) } } + private static void ValidateUnsupportedJsonIndexOptions(IIndex index) + { + var option = index switch + { + { IsUnique: true } => nameof(index.IsUnique), + { IsDescending: not null } => nameof(index.IsDescending), + _ when index.GetFilter() is not null => "Filter", + _ when index.IsClustered() is true => "IsClustered", + _ when index.GetIncludeProperties()?.Count > 0 => "IncludeProperties", + _ when index.IsCreatedOnline() is true => "IsCreatedOnline", + _ when index.GetSortInTempDb() is true => "SortInTempDb", + _ when index.GetDataCompression() is not null => "DataCompression", + _ => null + }; + + if (option is not null) + { + throw new InvalidOperationException( + SqlServerStrings.JsonIndexUnsupportedOption( + index.DisplayName(), index.DeclaringEntityType.DisplayName(), option)); + } + } + private static void ValidateUnsupportedIndexOptions(IIndex index, Func errorFactory) { var option = index switch diff --git a/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs b/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs index ccf102d7c12..81f05e297b6 100644 --- a/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs +++ b/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs @@ -313,6 +313,14 @@ public static string InvalidTableToIncludeInScaffolding(object? table) GetString("InvalidTableToIncludeInScaffolding", nameof(table)), table); + /// + /// JSON index '{index}' on entity type '{entityType}' was configured with the '{option}' option, which is not supported on JSON indexes. + /// + public static string JsonIndexUnsupportedOption(object? index, object? entityType, object? option) + => string.Format( + GetString("JsonIndexUnsupportedOption", nameof(index), nameof(entityType), nameof(option)), + index, entityType, option); + /// /// A non-constant array index or property name was used when navigating inside a JSON document, but EF Core's SQL Server compatibility level is set to {compatibilityLevel}; this is only supported with compatibility level 140 (SQL Server 2017) or higher. /// diff --git a/src/EFCore.SqlServer/Properties/SqlServerStrings.resx b/src/EFCore.SqlServer/Properties/SqlServerStrings.resx index b7df6626971..81a6b6f495d 100644 --- a/src/EFCore.SqlServer/Properties/SqlServerStrings.resx +++ b/src/EFCore.SqlServer/Properties/SqlServerStrings.resx @@ -231,6 +231,9 @@ The specified table '{table}' is not in a valid format. Specify tables using the format '[schema].[table]'. + + JSON index '{index}' on entity type '{entityType}' was configured with the '{option}' option, which is not supported on JSON indexes. + A non-constant array index or property name was used when navigating inside a JSON document, but EF Core's SQL Server compatibility level is set to {compatibilityLevel}; this is only supported with compatibility level 140 (SQL Server 2017) or higher. diff --git a/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs b/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs index 4c2b2c123c4..0ab2130736a 100644 --- a/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs +++ b/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs @@ -711,6 +711,88 @@ protected class EntityWithIncludedComplexAddress public required string Street { get; set; } } + [Theory] + [InlineData("IsUnique")] + [InlineData("IsDescending")] + [InlineData("Filter")] + [InlineData("IsClustered")] + [InlineData("IncludeProperties")] + [InlineData("IsCreatedOnline")] + [InlineData("SortInTempDb")] + [InlineData("DataCompression")] + public void Json_index_with_unsupported_option_throws(string option) + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity(b => + { + b.ComplexProperty(e => e.Address, cb => cb.ToJson()); + var indexBuilder = b.HasIndex("Address.City"); + + switch (option) + { + case "IsUnique": + indexBuilder.IsUnique(); + break; + case "IsDescending": + indexBuilder.IsDescending(); + break; + case "Filter": + indexBuilder.HasFilter("[Id] > 0"); + break; + case "IsClustered": + indexBuilder.IsClustered(); + break; + case "IncludeProperties": + indexBuilder.IncludeProperties("Id"); + break; + case "IsCreatedOnline": + indexBuilder.IsCreatedOnline(); + break; + case "SortInTempDb": + indexBuilder.SortInTempDb(); + break; + case "DataCompression": + indexBuilder.Metadata.SetDataCompression(DataCompressionType.Page); + break; + default: + throw new InvalidOperationException(); + } + }); + + VerifyError( + SqlServerStrings.JsonIndexUnsupportedOption( + "{'City'}", nameof(EntityWithIncludedComplexJson), option), + modelBuilder); + } + + [Theory] + [InlineData("FillFactor")] + [InlineData("NonClustered")] + [InlineData("NoIncludeProperties")] + [InlineData("CreatedOffline")] + [InlineData("SortInTempDbDisabled")] + public void Json_index_with_supported_option_passes(string option) + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity(b => + { + b.ComplexProperty(e => e.Address, cb => cb.ToJson()); + var indexBuilder = b.HasIndex("Address.City"); + + _ = option switch + { + "FillFactor" => indexBuilder.HasFillFactor(80), + "NonClustered" => indexBuilder.IsClustered(false), + "NoIncludeProperties" => indexBuilder.IncludeProperties(), + "CreatedOffline" => indexBuilder.IsCreatedOnline(false), + "SortInTempDbDisabled" => indexBuilder.SortInTempDb(false), + _ => throw new InvalidOperationException() + }; + }); + + Validate(modelBuilder); + } + [Fact] public virtual void Detects_incompatible_memory_optimized_shared_table() {