Start Debugging

EF Core 11 транслирует Contains в JSON_CONTAINS на SQL Server 2025

EF Core 11 автоматически транслирует LINQ Contains по JSON-коллекциям в новую функцию JSON_CONTAINS из SQL Server 2025 и добавляет EF.Functions.JsonContains для запросов с путём и режимом, способных задействовать JSON-индекс.

SQL Server 2025 получил нативную функцию JSON_CONTAINS, а EF Core 11 - тот релиз, который к ней подключается. Меняются две вещи для всех, кто хранит коллекции как JSON-колонки: Contains по JSON-коллекциям теперь получает прямую трансляцию вместо старого join через OPENJSON, и появился новый EF.Functions.JsonContains() для случаев, где нужен JSON-путь или конкретный режим поиска. Работа входит в EF Core 11 Preview 3.

Включение уровня совместимости SQL Server 2025

Новая трансляция включается, только когда провайдер знает, что общается с SQL Server 2025. Делаете это через UseCompatibilityLevel(170) на опциях провайдера:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.UseSqlServer(
        connectionString,
        o => o.UseCompatibilityLevel(170));

Уровень совместимости 170 - это то, что отчитывает SQL Server 2025; более низкие уровни продолжат использовать старую трансляцию, поэтому безопасно не указывать его, пока вы реально не обновили базу.

Как теперь выглядит Contains

Возьмём классическую форму «теги как JSON-массив»:

public class Blog
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public List<string> Tags { get; set; } = new();
}

modelBuilder.Entity<Blog>()
    .Property(b => b.Tags)
    .HasColumnType("json"); // SQL Server 2025 native JSON type

На EF Core 10 или против более старого SQL Server этот запрос:

var posts = await context.Blogs
    .Where(b => b.Tags.Contains("ef-core"))
    .ToListAsync();

даст трансляцию через OPENJSON, читающуюся как коррелированный подзапрос:

WHERE N'ef-core' IN (
    SELECT [t].[value]
    FROM OPENJSON([b].[Tags]) WITH ([value] nvarchar(max) '$') AS [t]
)

EF Core 11 на уровне совместимости 170 эмитит вместо этого:

WHERE JSON_CONTAINS([b].[Tags], 'ef-core') = 1

Причина важности не только в красоте SQL. JSON_CONTAINS - единственный предикат в SQL Server 2025, способный использовать JSON-индекс. Если у вас есть CREATE JSON INDEX IX_Tags ON Blogs(Tags), путь через OPENJSON его никогда не затронет, а трансляция EF 11 - затронет.

Есть подвох, отмеченный в release notes: JSON_CONTAINS обрабатывает NULL не так, как LINQ-овский Contains, поэтому EF выбирает новую трансляцию только когда хотя бы одна сторона доказуемо не-nullable (не-null константа или не-nullable колонка). Если обе стороны могут быть null, EF откатывается на OPENJSON, сохраняя прежнее поведение.

Когда нужен путь или режим поиска

Contains покрывает случай «есть ли этот скаляр в массиве». Для всего остального EF Core 11 выставляет EF.Functions.JsonContains(container, value, path?, mode?). Классический пример - поиск значения по конкретному пути внутри структурированного JSON-документа:

public class Blog
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public string JsonData { get; set; } = "{}"; // { "Rating": 8, ... }
}

var ratedEights = await context.Blogs
    .Where(b => EF.Functions.JsonContains(b.JsonData, 8, "$.Rating") == 1)
    .ToListAsync();

Транслируется в:

WHERE JSON_CONTAINS([b].[JsonData], 8, N'$.Rating') = 1

Можно использовать со скалярными string-колонками, с комплексными типами, замапленными в JSON, и с owned-типами, замапленными через OwnsOne(... b.ToJson()). Сравнение с = 1 принципиально: JSON_CONTAINS возвращает bit, и EF это сохраняет, чтобы составные предикаты вида WHERE ... AND JSON_CONTAINS(...) = 1 оставались SARGable против JSON-индекса.

Сочетайте это с EF.Functions.JsonPathExists для проверок «а свойство вообще есть?», и вы покроете большую часть поверхности запросов по JSON-колонкам без скатывания к сырым SQL. Полный список изменений транслятора EF Core 11 - в документе What’s New.

< Назад