Start Debugging

EF Core 11 Preview 4: столбцы периода во временных таблицах наконец могут быть настоящими свойствами

EF Core 11 Preview 4 снимает многолетнее ограничение shadow-свойств для временных таблиц SQL Server. PeriodStart и PeriodEnd теперь могут быть обычными CLR-свойствами, настраиваемыми строго типизированными лямбдами HasPeriodStart и HasPeriodEnd.

EF Core поддерживает временные таблицы SQL Server с версии 6.0, но с одним досадным ограничением: столбцы PeriodStart и PeriodEnd приходилось моделировать как shadow-свойства. Их можно было запрашивать через EF.Property<DateTime>(entity, "PeriodStart"), но нельзя было разместить в классе сущности и читать как любой другой столбец. Это ограничение снято в EF Core 11 Preview 4, выпущенном 12 мая 2026 года в составе .NET 11 Preview 4.

Исправление приходит как efcore#38110 и закрывает issue, открытое в 2021 году. Это небольшое изменение по строкам кода и большое улучшение эргономики.

Как выглядела старая модель с shadow-свойствами

До Preview 4 временная сущность могла объявлять только прикладные столбцы. Столбцы периода жили в модели, но не в классе:

public class Order
{
    public int Id { get; set; }
    public string Status { get; set; } = "";
}

modelBuilder.Entity<Order>().ToTable(tb => tb.IsTemporal());

Чтобы прочитать значения периода, приходилось обращаться к change tracker:

var start = context.Entry(order).Property<DateTime>("PeriodStart").CurrentValue;
var end   = context.Entry(order).Property<DateTime>("PeriodEnd").CurrentValue;

Две проблемы. Во-первых, теряются IntelliSense и безопасность рефакторинга: имя столбца представляет собой магическую строку. Во-вторых, проецировать значения периода в LINQ-запросе неудобно, поскольку свойство отсутствует на CLR-типе. Маппинг DTO с окном валидности означал либо сырой SQL-запрос, либо EF.Property<> внутри Select, и оба варианта конфликтуют с системой типов.

Модель в Preview 4

В Preview 4 столбцы периода можно просто поместить на сущность:

public class Order
{
    public int Id { get; set; }
    public string Status { get; set; } = "";
    public DateTime PeriodStart { get; set; }
    public DateTime PeriodEnd { get; set; }
}

Настройте их новыми строго типизированными перегрузками HasPeriodStart и HasPeriodEnd, принимающими лямбду:

modelBuilder.Entity<Order>().ToTable(tb => tb.IsTemporal(ttb =>
{
    ttb.HasPeriodStart(o => o.PeriodStart);
    ttb.HasPeriodEnd(o => o.PeriodEnd);
}));

Теперь PeriodStart и PeriodEnd ведут себя как любой другой смаппированный столбец. Их можно читать на материализованной сущности, проецировать в Select, сортировать по ним и фильтровать в обычном LINQ:

var openOrders = await context.Orders
    .Where(o => o.PeriodEnd == DateTime.MaxValue)
    .Select(o => new { o.Id, o.Status, o.PeriodStart })
    .ToListAsync();

Предикат PeriodEnd == DateTime.MaxValue представляет собой канонический фильтр “текущей строки” для временных таблиц SQL Server. Раньше его приходилось писать против shadow-свойства.

Что не меняется

Семантика временной таблицы на стороне базы данных идентична. SQL Server по-прежнему ведёт таблицу истории, по-прежнему обновляет PeriodStart и PeriodEnd при каждой записи и по-прежнему отклоняет прямые записи в столбцы периода. EF Core продолжает помечать их как ValueGeneratedOnAddOrUpdate и игнорировать при insert и update. Их можно читать, но нельзя задавать.

Запросы путешествия во времени также работают одинаково:

var snapshot = await context.Orders
    .TemporalAsOf(new DateTime(2026, 5, 1))
    .ToListAsync();

Единственное отличие в том, что возвращаемые сущности теперь несут значения периода прямо на CLR-объекте, а не прячут их за EF.Property<>.

Миграция необязательна

Если у вас уже есть модель с shadow-столбцами периода, менять ничего не нужно. Старый API продолжает работать. Для любой новой временной сущности размещение PeriodStart и PeriodEnd в классе теперь является путём наименьшего сопротивления.

Полные заметки по EF Core в Preview 4: release-notes/11.0/preview/preview4/efcore.md.

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Назад