diff --git a/entity-framework/core/providers/sql-server/temporal-tables.md b/entity-framework/core/providers/sql-server/temporal-tables.md index 118ccfe778..46ea79edd0 100644 --- a/entity-framework/core/providers/sql-server/temporal-tables.md +++ b/entity-framework/core/providers/sql-server/temporal-tables.md @@ -64,7 +64,7 @@ EXEC(N'CREATE TABLE [Employees] ( ) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + N'].[EmployeeHistory]))'); ``` -Notice that SQL Server creates two hidden `datetime2` columns called `PeriodEnd` and `PeriodStart`. These "period columns" represent the time range during which the data in the row existed. These columns are mapped to [shadow properties](xref:core/modeling/shadow-properties) in the EF Core model, allowing them to be used in queries as shown later. +Notice that SQL Server creates two hidden `datetime2` columns called `PeriodEnd` and `PeriodStart`. These "period columns" represent the time range during which the data in the row existed. By default, these columns are mapped to [shadow properties](xref:core/modeling/shadow-properties) in the EF Core model, allowing them to be used in queries as shown later. Starting with EF Core 11, period columns can also be [mapped to CLR properties](#mapping-period-columns-to-clr-properties) on your entity type. > [!IMPORTANT] > The times in these columns are always UTC time generated by SQL Server. UTC times are used for all operations involving temporal tables, such as in the queries shown below. @@ -148,7 +148,7 @@ context.SaveChanges(); --> [!code-csharp[NormalQuery](../../../../samples/core/Miscellaneous/NewInEFCore6/TemporalTablesSample.cs?name=NormalQuery)] -Also, after a normal [tracking query](xref:core/querying/tracking#no-tracking-queries), the values from the period columns of the current data can be [accessed from the tracked entities](xref:core/change-tracking/entity-entries). For example: +Also, after a normal [tracking query](xref:core/querying/tracking#no-tracking-queries), the values from the period columns of the current data can be [accessed from the tracked entities](xref:core/change-tracking/entity-entries). If the period columns are mapped to CLR properties, you can access them directly on the entity; otherwise, use `EF.Property` to access them as shadow properties. For example: [!code-csharp[TemporalAll](../../../../samples/core/Miscellaneous/NewInEFCore6/TemporalTablesSample.cs?name=TemporalAll)] -Notice how the [EF.Property method](xref:core/modeling/shadow-properties#accessing-shadow-properties) can be used to access values from the period columns. This is used in the `OrderBy` clause to sort the data, and then in a projection to include these values in the returned data. +Notice how the [EF.Property method](xref:core/modeling/shadow-properties#accessing-shadow-properties) can be used to access values from the period columns. This is used in the `OrderBy` clause to sort the data, and then in a projection to include these values in the returned data. If the period columns are [mapped to CLR properties](#mapping-period-columns-to-clr-properties), you can reference them directly in the query instead of using `EF.Property`. This query brings back the following data: @@ -280,3 +280,62 @@ Historical data for Rainbow Dash: Employee Rainbow Dash was 'Wonderbolt' from 8/26/2021 4:43:29 PM to 8/26/2021 4:44:59 PM Employee Rainbow Dash was 'Wonderbolt Trainee' from 8/26/2021 4:44:59 PM to 12/31/9999 11:59:59 PM ``` + +## Mapping period columns to CLR properties + +> [!NOTE] +> This feature is being introduced in EF Core 11, which is currently in preview. + +By default, the period columns in a temporal table are mapped to [shadow properties](xref:core/modeling/shadow-properties) in the EF Core model, meaning they don't need to exist on your .NET entity type. Starting with EF Core 11, you can instead map period columns to regular CLR properties on your entity type, which allows you to access their values directly. + +To do this, add `DateTime` properties for the period start and end to your entity type: + +```csharp +public class Employee +{ + public Guid EmployeeId { get; set; } + public string Name { get; set; } + public string Position { get; set; } + public string Department { get; set; } + public string Address { get; set; } + public decimal AnnualSalary { get; set; } + public DateTime PeriodStart { get; set; } + public DateTime PeriodEnd { get; set; } +} +``` + +Then configure the temporal table to use these properties via a lambda expression: + +```csharp +modelBuilder + .Entity() + .ToTable( + "Employees", + b => b.IsTemporal( + b => + { + b.HasPeriodStart(e => e.PeriodStart); + b.HasPeriodEnd(e => e.PeriodEnd); + })); +``` + +> [!NOTE] +> Period properties are automatically configured with `ValueGenerated.OnAddOrUpdate`, so their values are always generated by SQL Server. You don't need to — and should not — set their values when inserting or updating entities. + +When period columns are mapped to CLR properties, you can access their values directly on the entity instead of using `EF.Property`: + +```csharp +var history = context + .Employees + .TemporalAll() + .Where(e => e.Name == "Rainbow Dash") + .OrderBy(e => e.PeriodStart) + .Select( + e => new + { + Employee = e, + e.PeriodStart, + e.PeriodEnd + }) + .ToList(); +``` diff --git a/entity-framework/core/what-is-new/ef-core-11.0/whatsnew.md b/entity-framework/core/what-is-new/ef-core-11.0/whatsnew.md index 5a433de4d0..94e1cf12b3 100644 --- a/entity-framework/core/what-is-new/ef-core-11.0/whatsnew.md +++ b/entity-framework/core/what-is-new/ef-core-11.0/whatsnew.md @@ -394,6 +394,50 @@ WHERE JSON_CONTAINS([b].[JsonData], 8, N'$.Rating') = 1 For the full `JSON_CONTAINS` SQL Server documentation, see [`JSON_CONTAINS`](/sql/t-sql/functions/json-contains-transact-sql). + + +### Temporal period properties mapped to CLR properties + +Previously, the period properties (`PeriodStart`/`PeriodEnd`) on temporal tables were required to be [shadow properties](xref:core/modeling/shadow-properties), meaning they existed only in the EF model and not as CLR properties on your .NET entity types. Starting with EF Core 11, period properties can now be mapped to regular CLR properties on the entity type, making it easier to access their values directly without using `EF.Property`. + +To map period columns to CLR properties, add `DateTime` properties to your entity type and configure them with the lambda-based `HasPeriodStart`/`HasPeriodEnd` overloads: + +```csharp +public class Employee +{ + public Guid EmployeeId { get; set; } + public string Name { get; set; } + public string Position { get; set; } + public DateTime PeriodStart { get; set; } + public DateTime PeriodEnd { get; set; } +} + +modelBuilder + .Entity() + .ToTable( + "Employees", + b => b.IsTemporal( + b => + { + b.HasPeriodStart(e => e.PeriodStart); + b.HasPeriodEnd(e => e.PeriodEnd); + })); +``` + +Once period properties are mapped to CLR properties, you can reference them directly in LINQ queries — for example, in `OrderBy`, `Select`, or `Where` clauses — without needing `EF.Property`: + +```csharp +var history = context.Employees + .TemporalAll() + .OrderBy(e => e.PeriodStart) + .Select(e => new { e.Name, e.PeriodStart, e.PeriodEnd }) + .ToList(); +``` + +Period properties remain configured with `ValueGenerated.OnAddOrUpdate`, so their values are always generated by SQL Server and excluded from INSERT and UPDATE statements. + +For more information, see the [full documentation on temporal tables](xref:core/providers/sql-server/temporal-tables#mapping-period-columns-to-clr-properties). + ## Cosmos DB