From 69d3cd46c63e39e636513da1d6e9172b31d0bcc9 Mon Sep 17 00:00:00 2001 From: maumar Date: Thu, 18 Nov 2021 00:42:11 -0800 Subject: [PATCH] Fix to #26451 - Temporal Table: Owned Entities support Added extensibility point to SharedTypeEntityExpandingExpressionVisitor so that we can create temporal table expressions representing owned entities, if their owner is also a temporal table expression. Just like any other nav expansion, this only works for AsOf operation. Also added internal interface to TableExpression, so that we can extract table metadata information (table name and schema) from provider specific table expressions. --- ...ntityFrameworkRelationalServicesBuilder.cs | 5 +- ...lationalSharedTypeEntityExpansionHelper.cs | 32 ++ .../Query/Internal/ITableMetadata.cs | 30 ++ ...yableMethodTranslatingExpressionVisitor.cs | 112 ++++- ...ranslatingExpressionVisitorDependencies.cs | 11 +- ...lationalSharedTypeEntityExpansionHelper.cs | 41 ++ ...edTypeEntityExpansionHelperDependencies.cs | 58 +++ .../Query/SqlExpressionFactoryDependencies.cs | 13 +- .../Query/SqlExpressions/SelectExpression.cs | 6 +- .../Query/SqlExpressions/TableExpression.cs | 3 +- .../SqlServerServiceCollectionExtensions.cs | 4 + .../Properties/SqlServerStrings.Designer.cs | 8 + .../Properties/SqlServerStrings.resx | 3 + ...qlServerSharedTypeEntityExpansionHelper.cs | 57 +++ .../Query/Internal/TemporalTableExpression.cs | 5 +- .../Query/TemporalTableSqlServerTest.cs | 398 ++++++++++++++++++ 16 files changed, 766 insertions(+), 20 deletions(-) create mode 100644 src/EFCore.Relational/Query/Internal/IRelationalSharedTypeEntityExpansionHelper.cs create mode 100644 src/EFCore.Relational/Query/Internal/ITableMetadata.cs create mode 100644 src/EFCore.Relational/Query/RelationalSharedTypeEntityExpansionHelper.cs create mode 100644 src/EFCore.Relational/Query/RelationalSharedTypeEntityExpansionHelperDependencies.cs create mode 100644 src/EFCore.SqlServer/Query/Internal/SqlServerSharedTypeEntityExpansionHelper.cs create mode 100644 test/EFCore.SqlServer.FunctionalTests/Query/TemporalTableSqlServerTest.cs diff --git a/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs b/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs index c6b92dce01a..d8145062e3f 100644 --- a/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs +++ b/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs @@ -81,6 +81,7 @@ public static readonly IDictionary RelationalServi { typeof(ISqlExpressionFactory), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IRelationalQueryStringFactory), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IRelationalParameterBasedSqlProcessorFactory), new ServiceCharacteristics(ServiceLifetime.Scoped) }, + { typeof(IRelationalSharedTypeEntityExpansionHelper), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IMigrationsModelDiffer), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IMigrationsSqlGenerator), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IMigrator), new ServiceCharacteristics(ServiceLifetime.Scoped) }, @@ -192,6 +193,7 @@ public override EntityFrameworkServicesBuilder TryAddCoreServices() TryAdd(); TryAdd(); TryAdd(); + TryAdd(); ServiceCollectionMap.GetInfrastructure() .AddDependencySingleton() @@ -227,7 +229,8 @@ public override EntityFrameworkServicesBuilder TryAddCoreServices() .AddDependencyScoped() .AddDependencyScoped() .AddDependencyScoped() - .AddDependencyScoped(); + .AddDependencyScoped() + .AddDependencyScoped(); return base.TryAddCoreServices(); } diff --git a/src/EFCore.Relational/Query/Internal/IRelationalSharedTypeEntityExpansionHelper.cs b/src/EFCore.Relational/Query/Internal/IRelationalSharedTypeEntityExpansionHelper.cs new file mode 100644 index 00000000000..ffac581a2b9 --- /dev/null +++ b/src/EFCore.Relational/Query/Internal/IRelationalSharedTypeEntityExpansionHelper.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.EntityFrameworkCore.Query.Internal +{ + /// + /// Service which helps with various aspects of shared type entity expansion extensibility for relational providrers. + /// + /// + /// The service lifetime is . This means that each + /// instance will use its own instance of this service. + /// The implementation may depend on other services registered with any lifetime. + /// The implementation does not need to be thread-safe. + /// + /// + /// See Implementation of database providers and extensions + /// and How EF Core queries work for more information. + /// + public interface IRelationalSharedTypeEntityExpansionHelper + { + /// + /// Creates a related to the source table for a given entity type. + /// + public TableExpressionBase CreateRelatedTableExpression( + TableExpressionBase sourceTable, + IEntityType targetEntityType); + } +} diff --git a/src/EFCore.Relational/Query/Internal/ITableMetadata.cs b/src/EFCore.Relational/Query/Internal/ITableMetadata.cs new file mode 100644 index 00000000000..9602080faa4 --- /dev/null +++ b/src/EFCore.Relational/Query/Internal/ITableMetadata.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query.Internal +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public interface ITableMetadata + { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + string Name { get; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + string? Schema { get; } + } +} diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs index d41c2aa5d77..de21ba68e43 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -50,7 +50,10 @@ public RelationalQueryableMethodTranslatingExpressionVisitor( _queryCompilationContext = queryCompilationContext; _sqlTranslator = relationalDependencies.RelationalSqlTranslatingExpressionVisitorFactory.Create(queryCompilationContext, this); _sharedTypeEntityExpandingExpressionVisitor = - new SharedTypeEntityExpandingExpressionVisitor(_sqlTranslator, sqlExpressionFactory); + new SharedTypeEntityExpandingExpressionVisitor( + _sqlTranslator, + sqlExpressionFactory, + relationalDependencies.RelationalSharedTypeEntityExpansionHelper); _projectionBindingExpressionVisitor = new RelationalProjectionBindingExpressionVisitor(this, _sqlTranslator); _sqlExpressionFactory = sqlExpressionFactory; _subquery = false; @@ -74,7 +77,11 @@ protected RelationalQueryableMethodTranslatingExpressionVisitor( _sqlTranslator = RelationalDependencies.RelationalSqlTranslatingExpressionVisitorFactory.Create( parentVisitor._queryCompilationContext, parentVisitor); _sharedTypeEntityExpandingExpressionVisitor = - new SharedTypeEntityExpandingExpressionVisitor(_sqlTranslator, parentVisitor._sqlExpressionFactory); + new SharedTypeEntityExpandingExpressionVisitor( + _sqlTranslator, + parentVisitor._sqlExpressionFactory, + RelationalDependencies.RelationalSharedTypeEntityExpansionHelper); + _projectionBindingExpressionVisitor = new RelationalProjectionBindingExpressionVisitor(this, _sqlTranslator); _sqlExpressionFactory = parentVisitor._sqlExpressionFactory; _subquery = true; @@ -1157,15 +1164,18 @@ private static readonly MethodInfo _objectEqualsMethodInfo private readonly RelationalSqlTranslatingExpressionVisitor _sqlTranslator; private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly IRelationalSharedTypeEntityExpansionHelper _sharedTypeEntityExpansionHelper; private SelectExpression _selectExpression; public SharedTypeEntityExpandingExpressionVisitor( RelationalSqlTranslatingExpressionVisitor sqlTranslator, - ISqlExpressionFactory sqlExpressionFactory) + ISqlExpressionFactory sqlExpressionFactory, + IRelationalSharedTypeEntityExpansionHelper sharedTypeEntityExpansionHelper) { _sqlTranslator = sqlTranslator; _sqlExpressionFactory = sqlExpressionFactory; + _sharedTypeEntityExpansionHelper = sharedTypeEntityExpansionHelper; _selectExpression = null!; } @@ -1255,11 +1265,33 @@ protected override Expression VisitExtension(Expression extensionExpression) return null; } + var entityProjectionExpression = (EntityProjectionExpression) + (entityShaperExpression.ValueBufferExpression is ProjectionBindingExpression projectionBindingExpression + ? _selectExpression.GetProjection(projectionBindingExpression) + : entityShaperExpression.ValueBufferExpression); + + var useOldBehavior = AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue26469", out var enabled) + && enabled; + var foreignKey = navigation.ForeignKey; if (navigation.IsCollection) { - var innerShapedQuery = CreateShapedQueryExpression( - targetEntityType, _sqlExpressionFactory.Select(targetEntityType)); + var innerShapedQuery = default(ShapedQueryExpression); + if (!useOldBehavior) + { + var innerSelectExpression = BuildInnerSelectExpressionForOwnedTypeMappedToDifferentTable( + entityProjectionExpression, + navigation, + foreignKey, + targetEntityType); + + innerShapedQuery = CreateShapedQueryExpression(targetEntityType, innerSelectExpression); + } + else + { + innerShapedQuery = CreateShapedQueryExpression( + targetEntityType, _sqlExpressionFactory.Select(targetEntityType)); + } var makeNullable = foreignKey.PrincipalKey.Properties .Concat(foreignKey.Properties) @@ -1307,11 +1339,6 @@ outerKey is NewArrayExpression newArrayExpression Expression.Quote(correlationPredicate)); } - var entityProjectionExpression = (EntityProjectionExpression) - (entityShaperExpression.ValueBufferExpression is ProjectionBindingExpression projectionBindingExpression - ? _selectExpression.GetProjection(projectionBindingExpression) - : entityShaperExpression.ValueBufferExpression); - var innerShaper = entityProjectionExpression.BindNavigation(navigation); if (innerShaper == null) { @@ -1354,7 +1381,21 @@ outerKey is NewArrayExpression newArrayExpression // Owned types don't support inheritance See https://github.com/dotnet/efcore/issues/9630 // So there is no handling for dependent having TPT table = targetEntityType.GetViewOrTableMappings().Single().Table; - var innerSelectExpression = _sqlExpressionFactory.Select(targetEntityType); + + var innerSelectExpression = default(SelectExpression); + if (!useOldBehavior) + { + innerSelectExpression = BuildInnerSelectExpressionForOwnedTypeMappedToDifferentTable( + entityProjectionExpression, + navigation, + foreignKey, + targetEntityType); + } + else + { + innerSelectExpression = _sqlExpressionFactory.Select(targetEntityType); + } + var innerShapedQuery = CreateShapedQueryExpression(targetEntityType, innerSelectExpression); var makeNullable = foreignKey.PrincipalKey.Properties @@ -1380,7 +1421,11 @@ outerKey is NewArrayExpression newArrayExpression innerShaper = new RelationalEntityShaperExpression( targetEntityType, _selectExpression.GenerateWeakEntityProjectionExpression( - targetEntityType, table, null, leftJoinTable, nullable: true)!, + targetEntityType, + table, + null, + leftJoinTable, + nullable: true)!, nullable: true); } @@ -1388,6 +1433,49 @@ outerKey is NewArrayExpression newArrayExpression } return innerShaper; + + SelectExpression BuildInnerSelectExpressionForOwnedTypeMappedToDifferentTable( + EntityProjectionExpression entityProjectionExpression, + INavigation navigation, + IForeignKey foreignKey, + IEntityType targetEntityType) + { + // just need any column - we use it only to extract the table it originated from + var sourceColumn = entityProjectionExpression + .BindProperty( + navigation.IsOnDependent + ? foreignKey.Properties[0] + : foreignKey.PrincipalKey.Properties[0]); + + var sourceTable = FindRootTableExpressionForColumn(sourceColumn.Table, sourceColumn.Name); + + var ownedTable = _sharedTypeEntityExpansionHelper.CreateRelatedTableExpression( + sourceTable, + targetEntityType); + + return _sqlExpressionFactory.Select(targetEntityType, ownedTable); + } + + static TableExpressionBase FindRootTableExpressionForColumn(TableExpressionBase table, string columnName) + { + if (table is JoinExpressionBase joinExpressionBase) + { + table = joinExpressionBase.Table; + } + else if (table is SetOperationBase setOperationBase) + { + table = setOperationBase.Source1; + } + + if (table is SelectExpression selectExpression) + { + var matchingProjection = (ColumnExpression)selectExpression.Projection.Where(p => p.Alias == columnName).Single().Expression; + + return FindRootTableExpressionForColumn(matchingProjection.Table, matchingProjection.Name); + } + + return table; + } } private static Expression AddConvertToObject(Expression expression) diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitorDependencies.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitorDependencies.cs index 89f166d6976..38a6ef3d03d 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitorDependencies.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitorDependencies.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Query.Internal; using Microsoft.EntityFrameworkCore.Utilities; using Microsoft.Extensions.DependencyInjection; @@ -54,13 +55,16 @@ public sealed record RelationalQueryableMethodTranslatingExpressionVisitorDepend [EntityFrameworkInternal] public RelationalQueryableMethodTranslatingExpressionVisitorDependencies( IRelationalSqlTranslatingExpressionVisitorFactory relationalSqlTranslatingExpressionVisitorFactory, - ISqlExpressionFactory sqlExpressionFactory) + ISqlExpressionFactory sqlExpressionFactory, + IRelationalSharedTypeEntityExpansionHelper relationalSharedTypeEntityExpansionHelper) { Check.NotNull(relationalSqlTranslatingExpressionVisitorFactory, nameof(relationalSqlTranslatingExpressionVisitorFactory)); Check.NotNull(sqlExpressionFactory, nameof(sqlExpressionFactory)); + Check.NotNull(relationalSharedTypeEntityExpansionHelper, nameof(relationalSharedTypeEntityExpansionHelper)); RelationalSqlTranslatingExpressionVisitorFactory = relationalSqlTranslatingExpressionVisitorFactory; SqlExpressionFactory = sqlExpressionFactory; + RelationalSharedTypeEntityExpansionHelper = relationalSharedTypeEntityExpansionHelper; } /// @@ -72,5 +76,10 @@ public RelationalQueryableMethodTranslatingExpressionVisitorDependencies( /// The SQL expression factory. /// public ISqlExpressionFactory SqlExpressionFactory { get; init; } + + /// + /// Shared type entity expansion helper. + /// + public IRelationalSharedTypeEntityExpansionHelper RelationalSharedTypeEntityExpansionHelper { get; init; } } } diff --git a/src/EFCore.Relational/Query/RelationalSharedTypeEntityExpansionHelper.cs b/src/EFCore.Relational/Query/RelationalSharedTypeEntityExpansionHelper.cs new file mode 100644 index 00000000000..4e4d8d3b397 --- /dev/null +++ b/src/EFCore.Relational/Query/RelationalSharedTypeEntityExpansionHelper.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Query.Internal; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; + +namespace Microsoft.EntityFrameworkCore.Query +{ + /// + public class RelationalSharedTypeEntityExpansionHelper : IRelationalSharedTypeEntityExpansionHelper + { + /// + /// Creates a new instance of the class. + /// + /// Dependencies for this service. + [EntityFrameworkInternal] + public RelationalSharedTypeEntityExpansionHelper(RelationalSharedTypeEntityExpansionHelperDependencies dependencies) + { + Dependencies = dependencies; + } + + /// + /// Dependencies for this service. + /// + protected virtual RelationalSharedTypeEntityExpansionHelperDependencies Dependencies { get; } + + /// + [EntityFrameworkInternal] + public virtual TableExpressionBase CreateRelatedTableExpression( + TableExpressionBase sourceTable, + IEntityType targetEntityType) + { + var table = targetEntityType.GetTableMappings().Single().Table; + + return new TableExpression(table); + } + } +} diff --git a/src/EFCore.Relational/Query/RelationalSharedTypeEntityExpansionHelperDependencies.cs b/src/EFCore.Relational/Query/RelationalSharedTypeEntityExpansionHelperDependencies.cs new file mode 100644 index 00000000000..26c2af11d47 --- /dev/null +++ b/src/EFCore.Relational/Query/RelationalSharedTypeEntityExpansionHelperDependencies.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.EntityFrameworkCore.Query +{ + /// + /// + /// Service dependencies parameter class for + /// + /// + /// This type is typically used by database providers (and other extensions). It is generally + /// not used in application code. + /// + /// + /// Do not construct instances of this class directly from either provider or application code as the + /// constructor signature may change as new dependencies are added. Instead, use this type in + /// your constructor so that an instance will be created and injected automatically by the + /// dependency injection container. To create an instance with some dependent services replaced, + /// first resolve the object from the dependency injection container, then replace selected + /// services using the 'With...' methods. Do not call the constructor at any point in this process. + /// + /// + /// The service lifetime is . This means that each + /// instance will use its own instance of this service. + /// The implementation may depend on other services registered with any lifetime. + /// The implementation does not need to be thread-safe. + /// + /// + public sealed record RelationalSharedTypeEntityExpansionHelperDependencies + { + /// + /// + /// Creates the service dependencies parameter object for a . + /// + /// + /// Do not call this constructor directly from either provider or application code as it may change + /// as new dependencies are added. Instead, use this type in your constructor so that an instance + /// will be created and injected automatically by the dependency injection container. To create + /// an instance with some dependent services replaced, first resolve the object from the dependency + /// injection container, then replace selected services using the 'With...' methods. Do not call + /// the constructor at any point in this process. + /// + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + /// + [EntityFrameworkInternal] + public RelationalSharedTypeEntityExpansionHelperDependencies() + { + } + } +} diff --git a/src/EFCore.Relational/Query/SqlExpressionFactoryDependencies.cs b/src/EFCore.Relational/Query/SqlExpressionFactoryDependencies.cs index adf6e1eacc8..69185f46079 100644 --- a/src/EFCore.Relational/Query/SqlExpressionFactoryDependencies.cs +++ b/src/EFCore.Relational/Query/SqlExpressionFactoryDependencies.cs @@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Query.Internal; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Utilities; using Microsoft.Extensions.DependencyInjection; @@ -54,13 +55,18 @@ public sealed record SqlExpressionFactoryDependencies /// /// [EntityFrameworkInternal] - public SqlExpressionFactoryDependencies(IModel model, IRelationalTypeMappingSource typeMappingSource) + public SqlExpressionFactoryDependencies( + IModel model, + IRelationalTypeMappingSource typeMappingSource, + IRelationalSharedTypeEntityExpansionHelper relationalSharedTypeEntityExpansionHelper) { Check.NotNull(model, nameof(model)); Check.NotNull(typeMappingSource, nameof(typeMappingSource)); + Check.NotNull(relationalSharedTypeEntityExpansionHelper, nameof(relationalSharedTypeEntityExpansionHelper)); Model = model; TypeMappingSource = typeMappingSource; + RelationalSharedTypeEntityExpansionHelper = relationalSharedTypeEntityExpansionHelper; } /// @@ -68,6 +74,11 @@ public SqlExpressionFactoryDependencies(IModel model, IRelationalTypeMappingSour /// public IRelationalTypeMappingSource TypeMappingSource { get; init; } + /// + /// Shared type entity expansion helper. + /// + public IRelationalSharedTypeEntityExpansionHelper RelationalSharedTypeEntityExpansionHelper { get; init; } + /// /// The type mapping source. /// diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs index 91caabab0b4..7f5798c813f 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs @@ -1868,16 +1868,16 @@ static TableReferenceExpression FindTableReference(SelectExpression selectExpres bool nullable) { var unwrappedTable = UnwrapJoinExpression(tableExpressionBase); - if (unwrappedTable is TableExpression tableExpression) + if (unwrappedTable is ITableMetadata tableExpressionMetadata) { - if (!string.Equals(tableExpression.Name, table.Name, StringComparison.OrdinalIgnoreCase)) + if (!string.Equals(tableExpressionMetadata.Name, table.Name, StringComparison.OrdinalIgnoreCase)) { // Fetch the table for the type which is defining the navigation since dependent would be in that table tableExpressionBase = selectExpression.Tables .First( e => { - var t = (TableExpression)UnwrapJoinExpression(e); + var t = (ITableMetadata)UnwrapJoinExpression(e); return t.Name == table.Name && t.Schema == table.Schema; }); } diff --git a/src/EFCore.Relational/Query/SqlExpressions/TableExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/TableExpression.cs index 2c33e4c358a..d7fb4c8e47c 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/TableExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/TableExpression.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Query.Internal; using Microsoft.EntityFrameworkCore.Utilities; namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions @@ -18,7 +19,7 @@ namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions /// an issue at https://github.com/dotnet/efcore. /// /// - public sealed class TableExpression : TableExpressionBase, IClonableTableExpressionBase + public sealed class TableExpression : TableExpressionBase, IClonableTableExpressionBase, ITableMetadata { internal TableExpression(ITableBase table) : base(table.Name.Substring(0, 1).ToLowerInvariant()) diff --git a/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs index 1b36617aa96..9495ced6d2e 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs @@ -11,6 +11,7 @@ using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Query.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Diagnostics.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal; @@ -112,6 +113,7 @@ public static IServiceCollection AddEntityFrameworkSqlServer(this IServiceCollec { Check.NotNull(serviceCollection, nameof(serviceCollection)); +#pragma warning disable EF1001 // Internal EF Core API usage. new EntityFrameworkRelationalServicesBuilder(serviceCollection) .TryAdd() .TryAdd>() @@ -142,6 +144,8 @@ public static IServiceCollection AddEntityFrameworkSqlServer(this IServiceCollec .TryAdd() .TryAdd() .TryAdd() + .TryAdd() +#pragma warning restore EF1001 // Internal EF Core API usage. .TryAddProviderSpecificServices( b => b .TryAddSingleton() diff --git a/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs b/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs index 23fd122e7e9..65171e2c643 100644 --- a/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs +++ b/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs @@ -331,6 +331,14 @@ public static string TemporalSetOperationOnMismatchedSources(object? entityType) GetString("TemporalSetOperationOnMismatchedSources", nameof(entityType)), entityType); + /// + /// Only '{operationName}' temporal operation is supported for entity types that own an entity mapped to a different table. + /// + public static string TemporalOwnedTypeMappedToDifferentTableOnlySupportedForAsOf(object? operationName) + => string.Format( + GetString("TemporalOwnedTypeMappedToDifferentTableOnlySupportedForAsOf", nameof(operationName)), + operationName); + /// /// An exception has been raised that is likely due to a transient failure. Consider enabling transient error resiliency by adding 'EnableRetryOnFailure' to the 'UseSqlServer' call. /// diff --git a/src/EFCore.SqlServer/Properties/SqlServerStrings.resx b/src/EFCore.SqlServer/Properties/SqlServerStrings.resx index 5770c19ea28..e5f010182ee 100644 --- a/src/EFCore.SqlServer/Properties/SqlServerStrings.resx +++ b/src/EFCore.SqlServer/Properties/SqlServerStrings.resx @@ -325,6 +325,9 @@ Set operation can't be applied on entity '{entityType}' because temporal operations on both arguments don't match. + + Only '{operationName}' temporal operation is supported for entity types that own an entity mapped to a different table. + An exception has been raised that is likely due to a transient failure. Consider enabling transient error resiliency by adding 'EnableRetryOnFailure' to the 'UseSqlServer' call. diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerSharedTypeEntityExpansionHelper.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerSharedTypeEntityExpansionHelper.cs new file mode 100644 index 00000000000..7aea26124ff --- /dev/null +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerSharedTypeEntityExpansionHelper.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; +using Microsoft.EntityFrameworkCore.SqlServer.Internal; + +namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal +{ + /// + [EntityFrameworkInternal] +#pragma warning disable EF1001 // Internal EF Core API usage. + public class SqlServerSharedTypeEntityExpansionHelper : RelationalSharedTypeEntityExpansionHelper +#pragma warning restore EF1001 // Internal EF Core API usage. + { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + [EntityFrameworkInternal] + public SqlServerSharedTypeEntityExpansionHelper(RelationalSharedTypeEntityExpansionHelperDependencies dependencies) +#pragma warning disable EF1001 // Internal EF Core API usage. + : base(dependencies) +#pragma warning restore EF1001 // Internal EF Core API usage. + { + } + + /// + public override TableExpressionBase CreateRelatedTableExpression( + TableExpressionBase sourceTable, + IEntityType targetEntityType) + { + if (sourceTable is TemporalAsOfTableExpression temporalAsOf) + { + var table = targetEntityType.GetTableMappings().Single().Table; + + return new TemporalAsOfTableExpression(table, temporalAsOf.PointInTime); + } + + if (sourceTable is TemporalTableExpression) + { + throw new InvalidOperationException( + SqlServerStrings.TemporalOwnedTypeMappedToDifferentTableOnlySupportedForAsOf("AsOf")); + } + +#pragma warning disable EF1001 // Internal EF Core API usage. + return base.CreateRelatedTableExpression(sourceTable, targetEntityType); +#pragma warning restore EF1001 // Internal EF Core API usage. + } + } +} diff --git a/src/EFCore.SqlServer/Query/Internal/TemporalTableExpression.cs b/src/EFCore.SqlServer/Query/Internal/TemporalTableExpression.cs index 6668c77e25f..cd81bb03b6d 100644 --- a/src/EFCore.SqlServer/Query/Internal/TemporalTableExpression.cs +++ b/src/EFCore.SqlServer/Query/Internal/TemporalTableExpression.cs @@ -3,6 +3,7 @@ using System; using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Query.Internal; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal @@ -13,7 +14,9 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public abstract class TemporalTableExpression : TableExpressionBase, IClonableTableExpressionBase +#pragma warning disable EF1001 // Internal EF Core API usage. + public abstract class TemporalTableExpression : TableExpressionBase, IClonableTableExpressionBase, ITableMetadata +#pragma warning restore EF1001 // Internal EF Core API usage. { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalTableSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalTableSqlServerTest.cs new file mode 100644 index 00000000000..e422ed093b4 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalTableSqlServerTest.cs @@ -0,0 +1,398 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.SqlServer.Internal; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Xunit; + +namespace Microsoft.EntityFrameworkCore.Query +{ + [SqlServerCondition(SqlServerCondition.SupportsTemporalTablesCascadeDelete)] + public class TemporalTableSqlServerTest : NonSharedModelTestBase + { + protected override string StoreName => "TemporalTableSqlServerTest"; + + protected TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + protected override ITestStoreFactory TestStoreFactory => SqlServerTestStoreFactory.Instance; + + protected void AssertSql(params string[] expected) => TestSqlLoggerFactory.AssertBaseline(expected); + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public virtual async Task Temporal_owned_basic(bool async) + { + var contextFactory = await InitializeAsync(); + using (var context = contextFactory.CreateContext()) + { + var date = new DateTime(2000, 1, 1); + + var query = context.MainEntities.TemporalAsOf(date); + var _ = async ? await query.ToListAsync() : query.ToList(); + } + + AssertSql( + @"SELECT [m].[Id], [m].[Description], [m].[PeriodEnd], [m].[PeriodStart], [o].[MainEntityId], [o].[Description], [o].[Number], [o].[PeriodEnd], [o].[PeriodStart], [o0].[OwnedEntityMainEntityId], [o1].[OwnedEntityMainEntityId], [o1].[Id], [o1].[Name], [o1].[PeriodEnd], [o1].[PeriodStart], [o0].[Name], [o0].[PeriodEnd], [o0].[PeriodStart] +FROM [MainEntity] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [m] +LEFT JOIN [OwnedEntity] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [o] ON [m].[Id] = [o].[MainEntityId] +LEFT JOIN [OwnedEntityNestedOne] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [o0] ON [o].[MainEntityId] = [o0].[OwnedEntityMainEntityId] +LEFT JOIN [OwnedEntityNestedMany] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [o1] ON [o].[MainEntityId] = [o1].[OwnedEntityMainEntityId] +ORDER BY [m].[Id], [o].[MainEntityId], [o0].[OwnedEntityMainEntityId], [o1].[OwnedEntityMainEntityId]"); + } + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public virtual async Task Temporal_owned_join(bool async) + { + var contextFactory = await InitializeAsync(); + using (var context = contextFactory.CreateContext()) + { + var date = new DateTime(2000, 1, 1); + + var query = context.MainEntities + .TemporalAsOf(date) + .Join(context.MainEntities, o => o.Id, i => i.Id, (o, i) => new { o, i }); + + var _ = async ? await query.ToListAsync() : query.ToList(); + } + + AssertSql( + @"SELECT [m].[Id], [m].[Description], [m].[PeriodEnd], [m].[PeriodStart], [o].[MainEntityId], [o].[Description], [o].[Number], [o].[PeriodEnd], [o].[PeriodStart], [m0].[Id], [o0].[OwnedEntityMainEntityId], [o1].[MainEntityId], [o2].[OwnedEntityMainEntityId], [o3].[OwnedEntityMainEntityId], [o3].[Id], [o3].[Name], [o3].[PeriodEnd], [o3].[PeriodStart], [o0].[Name], [o0].[PeriodEnd], [o0].[PeriodStart], [m0].[Description], [m0].[PeriodEnd], [m0].[PeriodStart], [o1].[Description], [o1].[Number], [o1].[PeriodEnd], [o1].[PeriodStart], [o4].[OwnedEntityMainEntityId], [o4].[Id], [o4].[Name], [o4].[PeriodEnd], [o4].[PeriodStart], [o2].[Name], [o2].[PeriodEnd], [o2].[PeriodStart] +FROM [MainEntity] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [m] +INNER JOIN [MainEntity] AS [m0] ON [m].[Id] = [m0].[Id] +LEFT JOIN [OwnedEntity] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [o] ON [m].[Id] = [o].[MainEntityId] +LEFT JOIN [OwnedEntityNestedOne] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [o0] ON [o].[MainEntityId] = [o0].[OwnedEntityMainEntityId] +LEFT JOIN [OwnedEntity] AS [o1] ON [m0].[Id] = [o1].[MainEntityId] +LEFT JOIN [OwnedEntityNestedOne] AS [o2] ON [o1].[MainEntityId] = [o2].[OwnedEntityMainEntityId] +LEFT JOIN [OwnedEntityNestedMany] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [o3] ON [o].[MainEntityId] = [o3].[OwnedEntityMainEntityId] +LEFT JOIN [OwnedEntityNestedMany] AS [o4] ON [o1].[MainEntityId] = [o4].[OwnedEntityMainEntityId] +ORDER BY [m].[Id], [m0].[Id], [o].[MainEntityId], [o0].[OwnedEntityMainEntityId], [o1].[MainEntityId], [o2].[OwnedEntityMainEntityId], [o3].[OwnedEntityMainEntityId], [o3].[Id], [o4].[OwnedEntityMainEntityId]"); + } + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public virtual async Task Temporal_owned_set_operation(bool async) + { + var contextFactory = await InitializeAsync(); + using (var context = contextFactory.CreateContext()) + { + var date = new DateTime(2000, 1, 1); + + var query = context.MainEntities + .TemporalAsOf(date) + .Union(context.MainEntities.TemporalAsOf(date)); + + var _ = async ? await query.ToListAsync() : query.ToList(); + } + + AssertSql( + @"SELECT [t].[Id], [t].[Description], [t].[PeriodEnd], [t].[PeriodStart], [o].[MainEntityId], [o].[Description], [o].[Number], [o].[PeriodEnd], [o].[PeriodStart], [o0].[OwnedEntityMainEntityId], [o1].[OwnedEntityMainEntityId], [o1].[Id], [o1].[Name], [o1].[PeriodEnd], [o1].[PeriodStart], [o0].[Name], [o0].[PeriodEnd], [o0].[PeriodStart] +FROM ( + SELECT [m].[Id], [m].[Description], [m].[PeriodEnd], [m].[PeriodStart] + FROM [MainEntity] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [m] + UNION + SELECT [m0].[Id], [m0].[Description], [m0].[PeriodEnd], [m0].[PeriodStart] + FROM [MainEntity] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [m0] +) AS [t] +LEFT JOIN [OwnedEntity] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [o] ON [t].[Id] = [o].[MainEntityId] +LEFT JOIN [OwnedEntityNestedOne] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [o0] ON [o].[MainEntityId] = [o0].[OwnedEntityMainEntityId] +LEFT JOIN [OwnedEntityNestedMany] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [o1] ON [o].[MainEntityId] = [o1].[OwnedEntityMainEntityId] +ORDER BY [t].[Id], [o].[MainEntityId], [o0].[OwnedEntityMainEntityId], [o1].[OwnedEntityMainEntityId]"); + } + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public virtual async Task Temporal_owned_FromSql(bool async) + { + var contextFactory = await InitializeAsync(); + using (var context = contextFactory.CreateContext()) + { + var date = new DateTime(2000, 1, 1); + + var query = context.MainEntities.FromSqlRaw( + @"SELECT [m].[Id], [m].[Description], [m].[PeriodEnd], [m].[PeriodStart] +FROM [MainEntity] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [m]"); + + var _ = async ? await query.ToListAsync() : query.ToList(); + } + + // just making sure we don't do anything weird here - there is no way to extract temporal information + // from the FromSql so owned entity will always be treated as a regular query + AssertSql( + @"SELECT [m].[Id], [m].[Description], [m].[PeriodEnd], [m].[PeriodStart], [o].[MainEntityId], [o].[Description], [o].[Number], [o].[PeriodEnd], [o].[PeriodStart], [o0].[OwnedEntityMainEntityId], [o1].[OwnedEntityMainEntityId], [o1].[Id], [o1].[Name], [o1].[PeriodEnd], [o1].[PeriodStart], [o0].[Name], [o0].[PeriodEnd], [o0].[PeriodStart] +FROM ( + SELECT [m].[Id], [m].[Description], [m].[PeriodEnd], [m].[PeriodStart] + FROM [MainEntity] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [m] +) AS [m] +LEFT JOIN [OwnedEntity] AS [o] ON [m].[Id] = [o].[MainEntityId] +LEFT JOIN [OwnedEntityNestedOne] AS [o0] ON [o].[MainEntityId] = [o0].[OwnedEntityMainEntityId] +LEFT JOIN [OwnedEntityNestedMany] AS [o1] ON [o].[MainEntityId] = [o1].[OwnedEntityMainEntityId] +ORDER BY [m].[Id], [o].[MainEntityId], [o0].[OwnedEntityMainEntityId], [o1].[OwnedEntityMainEntityId]"); + } + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public virtual async Task Temporal_owned_subquery(bool async) + { + var contextFactory = await InitializeAsync(); + using (var context = contextFactory.CreateContext()) + { + var date = new DateTime(2000, 1, 1); + + var query = context.MainEntities + .TemporalAsOf(date) + .Distinct() + .OrderByDescending(x => x.Id) + .Take(3); + + var _ = async ? await query.ToListAsync() : query.ToList(); + } + + AssertSql( + @"@__p_0='3' + +SELECT [t0].[Id], [t0].[Description], [t0].[PeriodEnd], [t0].[PeriodStart], [o].[MainEntityId], [o].[Description], [o].[Number], [o].[PeriodEnd], [o].[PeriodStart], [o0].[MainEntityId], [o1].[OwnedEntityMainEntityId], [o2].[OwnedEntityMainEntityId], [o2].[Id], [o2].[Name], [o2].[PeriodEnd], [o2].[PeriodStart], [o1].[Name], [o1].[PeriodEnd], [o1].[PeriodStart] +FROM ( + SELECT TOP(@__p_0) [t].[Id], [t].[Description], [t].[PeriodEnd], [t].[PeriodStart] + FROM ( + SELECT DISTINCT [m].[Id], [m].[Description], [m].[PeriodEnd], [m].[PeriodStart] + FROM [MainEntity] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [m] + ) AS [t] + ORDER BY [t].[Id] DESC +) AS [t0] +LEFT JOIN [OwnedEntity] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [o] ON [t0].[Id] = [o].[MainEntityId] +LEFT JOIN [OwnedEntity] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [o0] ON [t0].[Id] = [o0].[MainEntityId] +LEFT JOIN [OwnedEntityNestedOne] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [o1] ON [o0].[MainEntityId] = [o1].[OwnedEntityMainEntityId] +LEFT JOIN [OwnedEntityNestedMany] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [o2] ON [o0].[MainEntityId] = [o2].[OwnedEntityMainEntityId] +ORDER BY [t0].[Id] DESC, [o].[MainEntityId], [o0].[MainEntityId], [o1].[OwnedEntityMainEntityId], [o2].[OwnedEntityMainEntityId]"); + } + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public virtual async Task Temporal_owned_complex(bool async) + { + var contextFactory = await InitializeAsync(); + using (var context = contextFactory.CreateContext()) + { + var date = new DateTime(2000, 1, 1); + + var query = context.MainEntities.TemporalAsOf(date) + .Join(context.MainEntities, x => x.Id, x => x.Id, (o, i) => new { o, i }) + .Distinct().OrderByDescending(x => x.o.Id).Take(3) + .Join(context.MainEntities, xx => xx.o.Id, x => x.Id, (o, i) => new { o, i }); + + var _ = async ? await query.ToListAsync() : query.ToList(); + } + + AssertSql( + @"@__p_0='3' + +SELECT [t0].[Id], [t0].[Description], [t0].[PeriodEnd], [t0].[PeriodStart], [o].[MainEntityId], [o].[Description], [o].[Number], [o].[PeriodEnd], [o].[PeriodStart], [t0].[Id0], [m1].[Id], [o0].[OwnedEntityMainEntityId], [o1].[MainEntityId], [o2].[OwnedEntityMainEntityId], [o3].[MainEntityId], [o4].[OwnedEntityMainEntityId], [o5].[OwnedEntityMainEntityId], [o5].[Id], [o5].[Name], [o5].[PeriodEnd], [o5].[PeriodStart], [o0].[Name], [o0].[PeriodEnd], [o0].[PeriodStart], [t0].[Description0], [t0].[PeriodEnd0], [t0].[PeriodStart0], [o1].[Description], [o1].[Number], [o1].[PeriodEnd], [o1].[PeriodStart], [o6].[OwnedEntityMainEntityId], [o6].[Id], [o6].[Name], [o6].[PeriodEnd], [o6].[PeriodStart], [o2].[Name], [o2].[PeriodEnd], [o2].[PeriodStart], [m1].[Description], [m1].[PeriodEnd], [m1].[PeriodStart], [o3].[Description], [o3].[Number], [o3].[PeriodEnd], [o3].[PeriodStart], [o7].[OwnedEntityMainEntityId], [o7].[Id], [o7].[Name], [o7].[PeriodEnd], [o7].[PeriodStart], [o4].[Name], [o4].[PeriodEnd], [o4].[PeriodStart] +FROM ( + SELECT TOP(@__p_0) [t].[Id], [t].[Description], [t].[PeriodEnd], [t].[PeriodStart], [t].[Id0], [t].[Description0], [t].[PeriodEnd0], [t].[PeriodStart0] + FROM ( + SELECT DISTINCT [m].[Id], [m].[Description], [m].[PeriodEnd], [m].[PeriodStart], [m0].[Id] AS [Id0], [m0].[Description] AS [Description0], [m0].[PeriodEnd] AS [PeriodEnd0], [m0].[PeriodStart] AS [PeriodStart0] + FROM [MainEntity] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [m] + INNER JOIN [MainEntity] AS [m0] ON [m].[Id] = [m0].[Id] + ) AS [t] + ORDER BY [t].[Id] DESC +) AS [t0] +INNER JOIN [MainEntity] AS [m1] ON [t0].[Id] = [m1].[Id] +LEFT JOIN [OwnedEntity] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [o] ON [t0].[Id] = [o].[MainEntityId] +LEFT JOIN [OwnedEntityNestedOne] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [o0] ON [o].[MainEntityId] = [o0].[OwnedEntityMainEntityId] +LEFT JOIN [OwnedEntity] AS [o1] ON [t0].[Id0] = [o1].[MainEntityId] +LEFT JOIN [OwnedEntityNestedOne] AS [o2] ON [o1].[MainEntityId] = [o2].[OwnedEntityMainEntityId] +LEFT JOIN [OwnedEntity] AS [o3] ON [m1].[Id] = [o3].[MainEntityId] +LEFT JOIN [OwnedEntityNestedOne] AS [o4] ON [o3].[MainEntityId] = [o4].[OwnedEntityMainEntityId] +LEFT JOIN [OwnedEntityNestedMany] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [o5] ON [o].[MainEntityId] = [o5].[OwnedEntityMainEntityId] +LEFT JOIN [OwnedEntityNestedMany] AS [o6] ON [o1].[MainEntityId] = [o6].[OwnedEntityMainEntityId] +LEFT JOIN [OwnedEntityNestedMany] AS [o7] ON [o3].[MainEntityId] = [o7].[OwnedEntityMainEntityId] +ORDER BY [t0].[Id] DESC, [t0].[Id0], [m1].[Id], [o].[MainEntityId], [o0].[OwnedEntityMainEntityId], [o1].[MainEntityId], [o2].[OwnedEntityMainEntityId], [o3].[MainEntityId], [o4].[OwnedEntityMainEntityId], [o5].[OwnedEntityMainEntityId], [o5].[Id], [o6].[OwnedEntityMainEntityId], [o6].[Id], [o7].[OwnedEntityMainEntityId]"); + } + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public virtual async Task Temporal_owned_complex_with_nontrivial_alias(bool async) + { + var contextFactory = await InitializeAsync(); + using (var context = contextFactory.CreateContext()) + { + var date = new DateTime(2000, 1, 1); + + var query = context.MainEntities + .Join(context.MainEntities.TemporalAsOf(date), x => x.Id, x => x.Id, (o, i) => new { o, i }) + .Distinct().OrderByDescending(x => x.o.Id).Take(3) + .Join(context.MainEntities, xx => xx.o.Id, x => x.Id, (o, i) => new { o, i }); + + var _ = async ? await query.ToListAsync() : query.ToList(); + } + + AssertSql( + @"@__p_0='3' + +SELECT [t0].[Id], [t0].[Description], [t0].[PeriodEnd], [t0].[PeriodStart], [o].[MainEntityId], [o].[Description], [o].[Number], [o].[PeriodEnd], [o].[PeriodStart], [t0].[Id0], [m1].[Id], [o0].[OwnedEntityMainEntityId], [o1].[MainEntityId], [o2].[OwnedEntityMainEntityId], [o3].[MainEntityId], [o4].[OwnedEntityMainEntityId], [o5].[OwnedEntityMainEntityId], [o5].[Id], [o5].[Name], [o5].[PeriodEnd], [o5].[PeriodStart], [o0].[Name], [o0].[PeriodEnd], [o0].[PeriodStart], [t0].[Description0], [t0].[PeriodEnd0], [t0].[PeriodStart0], [o1].[Description], [o1].[Number], [o1].[PeriodEnd], [o1].[PeriodStart], [o6].[OwnedEntityMainEntityId], [o6].[Id], [o6].[Name], [o6].[PeriodEnd], [o6].[PeriodStart], [o2].[Name], [o2].[PeriodEnd], [o2].[PeriodStart], [m1].[Description], [m1].[PeriodEnd], [m1].[PeriodStart], [o3].[Description], [o3].[Number], [o3].[PeriodEnd], [o3].[PeriodStart], [o7].[OwnedEntityMainEntityId], [o7].[Id], [o7].[Name], [o7].[PeriodEnd], [o7].[PeriodStart], [o4].[Name], [o4].[PeriodEnd], [o4].[PeriodStart] +FROM ( + SELECT TOP(@__p_0) [t].[Id], [t].[Description], [t].[PeriodEnd], [t].[PeriodStart], [t].[Id0], [t].[Description0], [t].[PeriodEnd0], [t].[PeriodStart0] + FROM ( + SELECT DISTINCT [m].[Id], [m].[Description], [m].[PeriodEnd], [m].[PeriodStart], [m0].[Id] AS [Id0], [m0].[Description] AS [Description0], [m0].[PeriodEnd] AS [PeriodEnd0], [m0].[PeriodStart] AS [PeriodStart0] + FROM [MainEntity] AS [m] + INNER JOIN [MainEntity] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [m0] ON [m].[Id] = [m0].[Id] + ) AS [t] + ORDER BY [t].[Id] DESC +) AS [t0] +INNER JOIN [MainEntity] AS [m1] ON [t0].[Id] = [m1].[Id] +LEFT JOIN [OwnedEntity] AS [o] ON [t0].[Id] = [o].[MainEntityId] +LEFT JOIN [OwnedEntityNestedOne] AS [o0] ON [o].[MainEntityId] = [o0].[OwnedEntityMainEntityId] +LEFT JOIN [OwnedEntity] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [o1] ON [t0].[Id0] = [o1].[MainEntityId] +LEFT JOIN [OwnedEntityNestedOne] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [o2] ON [o1].[MainEntityId] = [o2].[OwnedEntityMainEntityId] +LEFT JOIN [OwnedEntity] AS [o3] ON [m1].[Id] = [o3].[MainEntityId] +LEFT JOIN [OwnedEntityNestedOne] AS [o4] ON [o3].[MainEntityId] = [o4].[OwnedEntityMainEntityId] +LEFT JOIN [OwnedEntityNestedMany] AS [o5] ON [o].[MainEntityId] = [o5].[OwnedEntityMainEntityId] +LEFT JOIN [OwnedEntityNestedMany] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [o6] ON [o1].[MainEntityId] = [o6].[OwnedEntityMainEntityId] +LEFT JOIN [OwnedEntityNestedMany] AS [o7] ON [o3].[MainEntityId] = [o7].[OwnedEntityMainEntityId] +ORDER BY [t0].[Id] DESC, [t0].[Id0], [m1].[Id], [o].[MainEntityId], [o0].[OwnedEntityMainEntityId], [o1].[MainEntityId], [o2].[OwnedEntityMainEntityId], [o3].[MainEntityId], [o4].[OwnedEntityMainEntityId], [o5].[OwnedEntityMainEntityId], [o5].[Id], [o6].[OwnedEntityMainEntityId], [o6].[Id], [o7].[OwnedEntityMainEntityId]"); + } + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public virtual async Task Temporal_owned_range_operation_negative(bool async) + { + var contextFactory = await InitializeAsync(); + using (var context = contextFactory.CreateContext()) + { + var message = async + ? (await Assert.ThrowsAsync( + () => context.MainEntities.TemporalAll().ToListAsync())).Message + : Assert.Throws(() => context.MainEntities.TemporalAll().ToList()).Message; + + Assert.Equal( + SqlServerStrings.TemporalOwnedTypeMappedToDifferentTableOnlySupportedForAsOf("AsOf"), + message); + } + } + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public virtual async Task Temporal_owned_many(bool async) + { + var contextFactory = await InitializeAsync(); + using (var context = contextFactory.CreateContext()) + { + var date = new DateTime(2000, 1, 1); + var query = context.MainEntitiesMany.TemporalAsOf(date); + + var _ = async ? await query.ToListAsync() : query.ToList(); + } + + AssertSql( + @"SELECT [m].[Id], [m].[Name], [m].[PeriodEnd], [m].[PeriodStart], [t].[MainEntityManyId], [t].[Id], [t].[Name], [t].[Number], [t].[PeriodEnd], [t].[PeriodStart], [t].[OwnedEntityManyMainEntityManyId], [t].[OwnedEntityManyId], [t].[OwnedEntityManyMainEntityManyId0], [t].[OwnedEntityManyId0], [t].[Id0], [t].[Name0], [t].[PeriodEnd0], [t].[PeriodStart0], [t].[Name1], [t].[PeriodEnd1], [t].[PeriodStart1] +FROM [MainEntityMany] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [m] +LEFT JOIN ( + SELECT [o].[MainEntityManyId], [o].[Id], [o].[Name], [o].[Number], [o].[PeriodEnd], [o].[PeriodStart], [o0].[OwnedEntityManyMainEntityManyId], [o0].[OwnedEntityManyId], [o1].[OwnedEntityManyMainEntityManyId] AS [OwnedEntityManyMainEntityManyId0], [o1].[OwnedEntityManyId] AS [OwnedEntityManyId0], [o1].[Id] AS [Id0], [o1].[Name] AS [Name0], [o1].[PeriodEnd] AS [PeriodEnd0], [o1].[PeriodStart] AS [PeriodStart0], [o0].[Name] AS [Name1], [o0].[PeriodEnd] AS [PeriodEnd1], [o0].[PeriodStart] AS [PeriodStart1] + FROM [OwnedEntityMany] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [o] + LEFT JOIN [OwnedEntityManyNestedOne] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [o0] ON ([o].[MainEntityManyId] = [o0].[OwnedEntityManyMainEntityManyId]) AND ([o].[Id] = [o0].[OwnedEntityManyId]) + LEFT JOIN [OwnedEntityManyNestedMany] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [o1] ON ([o].[MainEntityManyId] = [o1].[OwnedEntityManyMainEntityManyId]) AND ([o].[Id] = [o1].[OwnedEntityManyId]) +) AS [t] ON [m].[Id] = [t].[MainEntityManyId] +ORDER BY [m].[Id], [t].[MainEntityManyId], [t].[Id], [t].[OwnedEntityManyMainEntityManyId], [t].[OwnedEntityManyId], [t].[OwnedEntityManyMainEntityManyId0], [t].[OwnedEntityManyId0]"); + } + + public class MainEntity + { + public int Id { get; set; } + public string Description { get; set; } + public OwnedEntity OwnedEntity { get; set; } + } + + public class OwnedEntity + { + public string Description { get; set; } + public int Number { get; set; } + public OwnedEntityNestedOne One { get; set; } + public List Many { get; set; } + } + + public class OwnedEntityNestedOne + { + public string Name { get; set; } + } + + public class OwnedEntityNestedMany + { + public string Name { get; set; } + } + + public class MainEntityMany + { + public int Id { get; set; } + public string Name { get; set; } + public List OwnedCollection { get; set; } + } + + public class OwnedEntityMany + { + public string Name { get; set; } + public int Number { get; set; } + + public OwnedEntityManyNestedOne One { get; set; } + public List Many { get; set; } + } + + public class OwnedEntityManyNestedOne + { + public string Name { get; set; } + } + + public class OwnedEntityManyNestedMany + { + public string Name { get; set; } + } + + public class MyContext26451 : DbContext + { + public MyContext26451(DbContextOptions options) + : base(options) + { + } + + public DbSet MainEntities { get; set; } + public DbSet MainEntitiesMany { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().ToTable("MainEntity", tb => tb.IsTemporal()); + modelBuilder.Entity().Property(me => me.Id); + modelBuilder.Entity().OwnsOne(me => me.OwnedEntity, oeb => + { + oeb.ToTable("OwnedEntity", tb => tb.IsTemporal()); + oeb.OwnsOne(x => x.One, nb => nb.ToTable("OwnedEntityNestedOne", tb => tb.IsTemporal())); + oeb.OwnsMany(x => x.Many, nb => nb.ToTable("OwnedEntityNestedMany", tb => tb.IsTemporal())); + }); + + modelBuilder.Entity(eb => + { + eb.ToTable("MainEntityMany", tb => tb.IsTemporal()); + eb.OwnsMany(x => x.OwnedCollection, oeb => + { + oeb.ToTable("OwnedEntityMany", tb => tb.IsTemporal()); + oeb.OwnsOne(x => x.One, nb => nb.ToTable("OwnedEntityManyNestedOne", tb => tb.IsTemporal())); + oeb.OwnsMany(x => x.Many, nb => nb.ToTable("OwnedEntityManyNestedMany", tb => tb.IsTemporal())); + }); + }); + } + } + } +}