diff --git a/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs b/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs index 7a02c8b87e8..10f632be330 100644 --- a/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs +++ b/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs @@ -338,7 +338,7 @@ public override IReadOnlyList GenerateFluentApiCalls( : null; // for the RevEng path, we avoid adding period properties to the entity - // because we don't want code for them to be generated - they need to be in shadow state + // because we don't want code for them to be generated - they are created as shadow properties // so if we don't find property on the entity, we know it's this scenario // and in that case period column name is actually the same as the period property name annotation // since in RevEng scenario there can't be custom column mapping diff --git a/src/EFCore.SqlServer/EFCore.SqlServer.baseline.json b/src/EFCore.SqlServer/EFCore.SqlServer.baseline.json index 8c014f27cad..7e1a54b632a 100644 --- a/src/EFCore.SqlServer/EFCore.SqlServer.baseline.json +++ b/src/EFCore.SqlServer/EFCore.SqlServer.baseline.json @@ -249,6 +249,12 @@ { "Type": "class Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationTemporalTableBuilder : Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationTemporalTableBuilder where TOwnerEntity : class where TDependentEntity : class", "Methods": [ + { + "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationTemporalPeriodPropertyBuilder Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationTemporalTableBuilder.HasPeriodEnd(System.Linq.Expressions.Expression> propertyExpression);" + }, + { + "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationTemporalPeriodPropertyBuilder Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationTemporalTableBuilder.HasPeriodStart(System.Linq.Expressions.Expression> propertyExpression);" + }, { "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationTemporalTableBuilder Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationTemporalTableBuilder.UseHistoryTable(string name);" }, @@ -2630,7 +2636,7 @@ "Member": "static System.Linq.IQueryable> Microsoft.EntityFrameworkCore.SqlServerQueryableExtensions.FreeTextTable(this Microsoft.EntityFrameworkCore.DbSet source, string freeText, System.Linq.Expressions.Expression>? columnSelector = null, string? languageTerm = null, int? topN = null);" }, { - "Member": "static System.Linq.IQueryable> Microsoft.EntityFrameworkCore.SqlServerQueryableExtensions.VectorSearch(this Microsoft.EntityFrameworkCore.DbSet source, System.Linq.Expressions.Expression> vectorPropertySelector, TVector similarTo, string metric, int topN);", + "Member": "static System.Linq.IQueryable> Microsoft.EntityFrameworkCore.SqlServerQueryableExtensions.VectorSearch(this Microsoft.EntityFrameworkCore.DbSet source, System.Linq.Expressions.Expression> vectorPropertySelector, TVector similarTo, string metric);", "Stage": "Experimental" } ] @@ -3030,6 +3036,12 @@ { "Type": "class Microsoft.EntityFrameworkCore.Metadata.Builders.TemporalTableBuilder : Microsoft.EntityFrameworkCore.Metadata.Builders.TemporalTableBuilder where TEntity : class", "Methods": [ + { + "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.Builders.TemporalPeriodPropertyBuilder Microsoft.EntityFrameworkCore.Metadata.Builders.TemporalTableBuilder.HasPeriodEnd(System.Linq.Expressions.Expression> propertyExpression);" + }, + { + "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.Builders.TemporalPeriodPropertyBuilder Microsoft.EntityFrameworkCore.Metadata.Builders.TemporalTableBuilder.HasPeriodStart(System.Linq.Expressions.Expression> propertyExpression);" + }, { "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.Builders.TemporalTableBuilder Microsoft.EntityFrameworkCore.Metadata.Builders.TemporalTableBuilder.UseHistoryTable(string name);" }, diff --git a/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerModelValidator.cs b/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerModelValidator.cs index 9396be1c3f0..fbc3ed070a6 100644 --- a/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerModelValidator.cs +++ b/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerModelValidator.cs @@ -449,13 +449,6 @@ private static void ValidateTemporalPeriodProperty(IEntityType temporalEntityTyp temporalEntityType.DisplayName(), annotationPropertyName)); } - if (!periodProperty.IsShadowProperty() && !temporalEntityType.IsPropertyBag) - { - throw new InvalidOperationException( - SqlServerStrings.TemporalPeriodPropertyMustBeInShadowState( - temporalEntityType.DisplayName(), periodProperty.Name)); - } - if (periodProperty.IsNullable || periodProperty.ClrType != typeof(DateTime)) { diff --git a/src/EFCore.SqlServer/Metadata/Builders/OwnedNavigationTemporalTableBuilder``.cs b/src/EFCore.SqlServer/Metadata/Builders/OwnedNavigationTemporalTableBuilder``.cs index 5569c0196e6..e2f33345c3e 100644 --- a/src/EFCore.SqlServer/Metadata/Builders/OwnedNavigationTemporalTableBuilder``.cs +++ b/src/EFCore.SqlServer/Metadata/Builders/OwnedNavigationTemporalTableBuilder``.cs @@ -49,4 +49,36 @@ public OwnedNavigationTemporalTableBuilder(OwnedNavigationBuilder referenceOwner /// The same builder instance so that multiple calls can be chained. public new virtual OwnedNavigationTemporalTableBuilder UseHistoryTable(string name, string? schema) => (OwnedNavigationTemporalTableBuilder)base.UseHistoryTable(name, schema); + + /// + /// Returns an object that can be used to configure a period start property of the entity type mapped to a temporal table. + /// + /// + /// See Using SQL Server temporal tables with EF Core + /// for more information. + /// + /// + /// A lambda expression representing the property to be configured as the period start property + /// (entity => entity.PeriodStart). + /// + /// An object that can be used to configure the period start property. + public virtual OwnedNavigationTemporalPeriodPropertyBuilder HasPeriodStart( + Expression> propertyExpression) + => HasPeriodStart(Check.NotNull(propertyExpression).GetMemberAccess().Name); + + /// + /// Returns an object that can be used to configure a period end property of the entity type mapped to a temporal table. + /// + /// + /// See Using SQL Server temporal tables with EF Core + /// for more information. + /// + /// + /// A lambda expression representing the property to be configured as the period end property + /// (entity => entity.PeriodEnd). + /// + /// An object that can be used to configure the period end property. + public virtual OwnedNavigationTemporalPeriodPropertyBuilder HasPeriodEnd( + Expression> propertyExpression) + => HasPeriodEnd(Check.NotNull(propertyExpression).GetMemberAccess().Name); } diff --git a/src/EFCore.SqlServer/Metadata/Builders/TemporalTableBuilder`.cs b/src/EFCore.SqlServer/Metadata/Builders/TemporalTableBuilder`.cs index cbaa5b76a66..99fc3d67506 100644 --- a/src/EFCore.SqlServer/Metadata/Builders/TemporalTableBuilder`.cs +++ b/src/EFCore.SqlServer/Metadata/Builders/TemporalTableBuilder`.cs @@ -47,4 +47,34 @@ public TemporalTableBuilder(EntityTypeBuilder entityTypeBuilder) /// The same builder instance so that multiple calls can be chained. public new virtual TemporalTableBuilder UseHistoryTable(string name, string? schema) => (TemporalTableBuilder)base.UseHistoryTable(name, schema); + + /// + /// Returns an object that can be used to configure a period start property of the entity type mapped to a temporal table. + /// + /// + /// See Using SQL Server temporal tables with EF Core + /// for more information and examples. + /// + /// + /// A lambda expression representing the property to be configured as the period start property + /// (blog => blog.PeriodStart). + /// + /// An object that can be used to configure the period start property. + public virtual TemporalPeriodPropertyBuilder HasPeriodStart(Expression> propertyExpression) + => HasPeriodStart(Check.NotNull(propertyExpression).GetMemberAccess().Name); + + /// + /// Returns an object that can be used to configure a period end property of the entity type mapped to a temporal table. + /// + /// + /// See Using SQL Server temporal tables with EF Core + /// for more information and examples. + /// + /// + /// A lambda expression representing the property to be configured as the period end property + /// (blog => blog.PeriodEnd). + /// + /// An object that can be used to configure the period end property. + public virtual TemporalPeriodPropertyBuilder HasPeriodEnd(Expression> propertyExpression) + => HasPeriodEnd(Check.NotNull(propertyExpression).GetMemberAccess().Name); } diff --git a/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs b/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs index f8065d8582f..333834cd4ae 100644 --- a/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs +++ b/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs @@ -431,14 +431,6 @@ public static string TemporalPeriodPropertyCantHaveDefaultValue(object? entityTy GetString("TemporalPeriodPropertyCantHaveDefaultValue", nameof(entityType), nameof(propertyName)), entityType, propertyName); - /// - /// Period property '{entityType}.{propertyName}' must be a shadow property. - /// - public static string TemporalPeriodPropertyMustBeInShadowState(object? entityType, object? propertyName) - => string.Format( - GetString("TemporalPeriodPropertyMustBeInShadowState", nameof(entityType), nameof(propertyName)), - entityType, propertyName); - /// /// Period property '{entityType}.{propertyName}' must be mapped to a column of type '{columnType}'. /// diff --git a/src/EFCore.SqlServer/Properties/SqlServerStrings.resx b/src/EFCore.SqlServer/Properties/SqlServerStrings.resx index 765b0926c0d..96fcf47d70d 100644 --- a/src/EFCore.SqlServer/Properties/SqlServerStrings.resx +++ b/src/EFCore.SqlServer/Properties/SqlServerStrings.resx @@ -375,9 +375,6 @@ Period property '{entityType}.{propertyName}' can't have a default value specified. - - Period property '{entityType}.{propertyName}' must be a shadow property. - Period property '{entityType}.{propertyName}' must be mapped to a column of type '{columnType}'. diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs index 0d3392faeee..b1c00826353 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs @@ -1093,25 +1093,21 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) skipBuild: true); [ConditionalFact] - public async Task Temporal_table_works() - // Shadow properties. Issue #26007. - => Assert.Equal( - SqlServerStrings.TemporalPeriodPropertyMustBeInShadowState("Customer", "PeriodStart"), - (await Assert.ThrowsAsync(() => - TestAsync( - modelBuilder => modelBuilder.Entity( - "Customer", e => - { - e.Property("Id"); - e.Property("Name"); - e.HasKey("Id"); - e.ToTable(tb => tb.IsTemporal()); - }), - new ModelCodeGenerationOptions { UseDataAnnotations = false }, - code => - { - AssertFileContents( - $$""" + public Task Temporal_table_works() + => TestAsync( + modelBuilder => modelBuilder.Entity( + "Customer", e => + { + e.Property("Id"); + e.Property("Name"); + e.HasKey("Id"); + e.ToTable(tb => tb.IsTemporal()); + }), + new ModelCodeGenerationOptions { UseDataAnnotations = false }, + code => + { + AssertFileContents( + $$""" using System; using System.Collections.Generic; using Microsoft.EntityFrameworkCore; @@ -1157,12 +1153,15 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) partial void OnModelCreatingPartial(ModelBuilder modelBuilder); } """, - code.ContextFile); - }, - model => - { - // TODO - }))).Message); + code.ContextFile); + }, + model => + { + var entityType = model.FindEntityType("TestNamespace.Customer")!; + Assert.True(entityType.IsTemporal()); + Assert.Equal("PeriodStart", entityType.GetPeriodStartPropertyName()); + Assert.Equal("PeriodEnd", entityType.GetPeriodEndPropertyName()); + }); [ConditionalFact] public Task Sequences_work() diff --git a/test/EFCore.SqlServer.FunctionalTests/ModelBuilding/SqlServerModelBuilderTestBase.cs b/test/EFCore.SqlServer.FunctionalTests/ModelBuilding/SqlServerModelBuilderTestBase.cs index 357b2555afa..e1b70082613 100644 --- a/test/EFCore.SqlServer.FunctionalTests/ModelBuilding/SqlServerModelBuilderTestBase.cs +++ b/test/EFCore.SqlServer.FunctionalTests/ModelBuilding/SqlServerModelBuilderTestBase.cs @@ -4,6 +4,7 @@ using System.Collections.ObjectModel; using System.ComponentModel.DataAnnotations.Schema; using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; using Microsoft.Data.SqlTypes; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata.Builders; @@ -1674,6 +1675,68 @@ public virtual void Implicit_many_to_many_converted_from_non_temporal_to_tempora Assert.True(joinEntity.IsTemporal()); } + [ConditionalFact] + public virtual void Temporal_table_with_period_mapped_to_CLR_property() + { + var modelBuilder = CreateModelBuilder(); + var model = modelBuilder.Model; + + modelBuilder.Entity().ToTable(tb => tb.IsTemporal(ttb => + { + ttb.HasPeriodStart("PeriodStart"); + ttb.HasPeriodEnd("PeriodEnd"); + })); + + modelBuilder.FinalizeModel(); + + var entity = model.FindEntityType(typeof(TemporalCustomer))!; + Assert.True(entity.IsTemporal()); + + var periodStart = entity.GetProperty(entity.GetPeriodStartPropertyName()!); + var periodEnd = entity.GetProperty(entity.GetPeriodEndPropertyName()!); + + Assert.Equal("PeriodStart", periodStart.Name); + Assert.False(periodStart.IsShadowProperty()); + Assert.Equal(typeof(DateTime), periodStart.ClrType); + Assert.Equal(ValueGenerated.OnAddOrUpdate, periodStart.ValueGenerated); + + Assert.Equal("PeriodEnd", periodEnd.Name); + Assert.False(periodEnd.IsShadowProperty()); + Assert.Equal(typeof(DateTime), periodEnd.ClrType); + Assert.Equal(ValueGenerated.OnAddOrUpdate, periodEnd.ValueGenerated); + } + + [ConditionalFact] + public virtual void Temporal_table_with_period_mapped_to_CLR_property_via_lambda() + { + var modelBuilder = CreateModelBuilder(); + var model = modelBuilder.Model; + + modelBuilder.Entity().ToTable(tb => tb.IsTemporal(ttb => + { + ttb.HasPeriodStart(e => e.PeriodStart); + ttb.HasPeriodEnd(e => e.PeriodEnd); + })); + + modelBuilder.FinalizeModel(); + + var entity = model.FindEntityType(typeof(TemporalCustomer))!; + Assert.True(entity.IsTemporal()); + + var periodStart = entity.GetProperty(entity.GetPeriodStartPropertyName()!); + var periodEnd = entity.GetProperty(entity.GetPeriodEndPropertyName()!); + + Assert.Equal("PeriodStart", periodStart.Name); + Assert.False(periodStart.IsShadowProperty()); + Assert.Equal(typeof(DateTime), periodStart.ClrType); + Assert.Equal(ValueGenerated.OnAddOrUpdate, periodStart.ValueGenerated); + + Assert.Equal("PeriodEnd", periodEnd.Name); + Assert.False(periodEnd.IsShadowProperty()); + Assert.Equal(typeof(DateTime), periodEnd.ClrType); + Assert.Equal(ValueGenerated.OnAddOrUpdate, periodEnd.ValueGenerated); + } + #pragma warning disable EF8001 // Owned JSON entities are obsolete [ConditionalFact] public virtual void Json_entity_and_normal_owned_can_exist_side_by_side_on_same_entity() @@ -2214,6 +2277,8 @@ public abstract class TestTemporalTableBuilder public abstract TestTemporalTableBuilder UseHistoryTable(string name, string? schema); public abstract TestTemporalPeriodPropertyBuilder HasPeriodStart(string propertyName); public abstract TestTemporalPeriodPropertyBuilder HasPeriodEnd(string propertyName); + public abstract TestTemporalPeriodPropertyBuilder HasPeriodStart(Expression> propertyExpression); + public abstract TestTemporalPeriodPropertyBuilder HasPeriodEnd(Expression> propertyExpression); } public class GenericTestTemporalTableBuilder(TemporalTableBuilder temporalTableBuilder) @@ -2237,6 +2302,12 @@ public override TestTemporalPeriodPropertyBuilder HasPeriodStart(string property public override TestTemporalPeriodPropertyBuilder HasPeriodEnd(string propertyName) => new(TemporalTableBuilder.HasPeriodEnd(propertyName)); + + public override TestTemporalPeriodPropertyBuilder HasPeriodStart(Expression> propertyExpression) + => new(TemporalTableBuilder.HasPeriodStart(propertyExpression)); + + public override TestTemporalPeriodPropertyBuilder HasPeriodEnd(Expression> propertyExpression) + => new(TemporalTableBuilder.HasPeriodEnd(propertyExpression)); } public class NonGenericTestTemporalTableBuilder(TemporalTableBuilder temporalTableBuilder) @@ -2259,6 +2330,12 @@ public override TestTemporalPeriodPropertyBuilder HasPeriodStart(string property public override TestTemporalPeriodPropertyBuilder HasPeriodEnd(string propertyName) => new(TemporalTableBuilder.HasPeriodEnd(propertyName)); + + public override TestTemporalPeriodPropertyBuilder HasPeriodStart(Expression> propertyExpression) + => HasPeriodStart(propertyExpression.GetMemberAccess().Name); + + public override TestTemporalPeriodPropertyBuilder HasPeriodEnd(Expression> propertyExpression) + => HasPeriodEnd(propertyExpression.GetMemberAccess().Name); } public abstract class TestOwnedNavigationTemporalTableBuilder @@ -2270,6 +2347,12 @@ public abstract TestOwnedNavigationTemporalTableBuilder> propertyExpression); + + public abstract TestOwnedNavigationTemporalPeriodPropertyBuilder HasPeriodEnd( + Expression> propertyExpression); } public class GenericTestOwnedNavigationTemporalTableBuilder( @@ -2297,6 +2380,14 @@ public override TestOwnedNavigationTemporalPeriodPropertyBuilder HasPeriodStart( public override TestOwnedNavigationTemporalPeriodPropertyBuilder HasPeriodEnd(string propertyName) => new(TemporalTableBuilder.HasPeriodEnd(propertyName)); + + public override TestOwnedNavigationTemporalPeriodPropertyBuilder HasPeriodStart( + Expression> propertyExpression) + => new(TemporalTableBuilder.HasPeriodStart(propertyExpression)); + + public override TestOwnedNavigationTemporalPeriodPropertyBuilder HasPeriodEnd( + Expression> propertyExpression) + => new(TemporalTableBuilder.HasPeriodEnd(propertyExpression)); } public class NonGenericTestOwnedNavigationTemporalTableBuilder( @@ -2323,6 +2414,14 @@ public override TestOwnedNavigationTemporalPeriodPropertyBuilder HasPeriodStart( public override TestOwnedNavigationTemporalPeriodPropertyBuilder HasPeriodEnd(string propertyName) => new(TemporalTableBuilder.HasPeriodEnd(propertyName)); + + public override TestOwnedNavigationTemporalPeriodPropertyBuilder HasPeriodStart( + Expression> propertyExpression) + => HasPeriodStart(propertyExpression.GetMemberAccess().Name); + + public override TestOwnedNavigationTemporalPeriodPropertyBuilder HasPeriodEnd( + Expression> propertyExpression) + => HasPeriodEnd(propertyExpression.GetMemberAccess().Name); } public class TestTemporalPeriodPropertyBuilder(TemporalPeriodPropertyBuilder temporalPeriodPropertyBuilder) @@ -2493,4 +2592,12 @@ public virtual TestFullTextCatalogBuilder IsAccentSensitive(bool accentSensitive private TestFullTextCatalogBuilder Wrap(SqlServerFullTextCatalogBuilder catalogBuilder) => new(catalogBuilder); } + + protected class TemporalCustomer + { + public int Id { get; set; } + public string? Name { get; set; } + public DateTime PeriodStart { get; set; } + public DateTime PeriodEnd { get; set; } + } } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalTableSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalTableSqlServerTest.cs index e42c1afb295..2e79dc2ad4e 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalTableSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalTableSqlServerTest.cs @@ -433,6 +433,110 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) } } + [ConditionalTheory, MemberData(nameof(IsAsyncData))] + public virtual async Task Temporal_with_CLR_period_properties(bool async) + { + var contextFactory = await InitializeNonSharedTest( + seed: async c => + { + c.Customers.Add(new TemporalCustomerWithClrPeriods { Name = "Customer1" }); + await c.SaveChangesAsync(); + }); + + using var context = contextFactory.CreateDbContext(); + + var query = context.Customers.OrderBy(c => c.Id); + var result = async ? await query.ToListAsync() : query.ToList(); + + Assert.Single(result); + Assert.Equal("Customer1", result[0].Name); + // Period properties should be populated with non-default values by SQL Server + Assert.NotEqual(default, result[0].PeriodStart); + Assert.NotEqual(default, result[0].PeriodEnd); + } + + [ConditionalTheory, MemberData(nameof(IsAsyncData))] + public virtual async Task Temporal_with_CLR_period_properties_and_TemporalAll(bool async) + { + var contextFactory = await InitializeNonSharedTest( + seed: async c => + { + c.Customers.Add(new TemporalCustomerWithClrPeriods { Name = "Customer1" }); + await c.SaveChangesAsync(); + }); + + using var context = contextFactory.CreateDbContext(); + + var query = context.Customers.TemporalAll().OrderBy(c => c.Id); + var result = async ? await query.ToListAsync() : query.ToList(); + + Assert.Single(result); + Assert.Equal("Customer1", result[0].Name); + Assert.NotEqual(default, result[0].PeriodStart); + Assert.NotEqual(default, result[0].PeriodEnd); + } + + [ConditionalTheory, MemberData(nameof(IsAsyncData))] + public virtual async Task Temporal_with_CLR_period_properties_configured_via_lambda(bool async) + { + var contextFactory = await InitializeNonSharedTest( + seed: async c => + { + c.Customers.Add(new TemporalCustomerWithClrPeriods { Name = "Customer1" }); + await c.SaveChangesAsync(); + }); + + using var context = contextFactory.CreateDbContext(); + + var query = context.Customers.OrderBy(c => c.Id); + var result = async ? await query.ToListAsync() : query.ToList(); + + Assert.Single(result); + Assert.Equal("Customer1", result[0].Name); + Assert.NotEqual(default, result[0].PeriodStart); + Assert.NotEqual(default, result[0].PeriodEnd); + } + + public class TemporalCustomerWithClrPeriods + { + public int Id { get; set; } + public string Name { get; set; } + public DateTime PeriodStart { get; set; } + public DateTime PeriodEnd { get; set; } + } + + public class ContextWithClrPeriodProperties(DbContextOptions options) : DbContext(options) + { + public DbSet Customers { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().ToTable( + "TemporalCustomerWithClrPeriods", tb => tb.IsTemporal(ttb => + { + ttb.HasPeriodStart("PeriodStart"); + ttb.HasPeriodEnd("PeriodEnd"); + })); + modelBuilder.Entity().Property(me => me.Id).UseIdentityColumn(); + } + } + + public class ContextWithClrPeriodPropertiesLambda(DbContextOptions options) : DbContext(options) + { + public DbSet Customers { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().ToTable( + "TemporalCustomerWithClrPeriods", tb => tb.IsTemporal(ttb => + { + ttb.HasPeriodStart(e => e.PeriodStart); + ttb.HasPeriodEnd(e => e.PeriodEnd); + })); + modelBuilder.Entity().Property(me => me.Id).UseIdentityColumn(); + } + } + [ConditionalTheory, InlineData(true), InlineData(false)] public virtual async Task Temporal_can_query_shared_derived_hierarchy(bool async) { diff --git a/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs b/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs index 2f1bf53f474..50fe67413e9 100644 --- a/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs +++ b/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs @@ -946,12 +946,12 @@ public void Temporal_enitty_without_expected_period_start_property() } [ConditionalFact] - public void Temporal_period_property_must_be_in_shadow_state() + public void Temporal_period_property_mapped_to_CLR_property_is_allowed() { var modelBuilder = CreateConventionModelBuilder(); modelBuilder.Entity().ToTable(tb => tb.IsTemporal(ttb => ttb.HasPeriodStart("DateOfBirth"))); - VerifyError(SqlServerStrings.TemporalPeriodPropertyMustBeInShadowState(nameof(Human), "DateOfBirth"), modelBuilder); + Validate(modelBuilder); } [ConditionalFact]