Skip to content

Commit

Permalink
Fix to #26451 - Temporal Table: Owned Entities support
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
maumar committed Nov 12, 2021
1 parent fa3cea4 commit 2c23d7d
Show file tree
Hide file tree
Showing 12 changed files with 556 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ public static readonly IDictionary<Type, ServiceCharacteristics> 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) },
Expand Down Expand Up @@ -190,6 +191,7 @@ public override EntityFrameworkServicesBuilder TryAddCoreServices()
TryAdd<IRelationalParameterBasedSqlProcessorFactory, RelationalParameterBasedSqlProcessorFactory>();
TryAdd<IRelationalQueryStringFactory, RelationalQueryStringFactory>();
TryAdd<IQueryCompilationContextFactory, RelationalQueryCompilationContextFactory>();
TryAdd<IRelationalSharedTypeEntityExpansionHelper, RelationalSharedTypeEntityExpansionHelper>();

ServiceCollectionMap.GetInfrastructure()
.AddDependencySingleton<RelationalSqlGenerationHelperDependencies>()
Expand Down Expand Up @@ -225,7 +227,8 @@ public override EntityFrameworkServicesBuilder TryAddCoreServices()
.AddDependencyScoped<RelationalConnectionDependencies>()
.AddDependencyScoped<RelationalDatabaseDependencies>()
.AddDependencyScoped<RelationalQueryContextDependencies>()
.AddDependencyScoped<RelationalQueryCompilationContextDependencies>();
.AddDependencyScoped<RelationalQueryCompilationContextDependencies>()
.AddDependencyScoped<RelationalSharedTypeEntityExpansionHelperDependencies>();

return base.TryAddCoreServices();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Service which helps with various aspects of shared type entity expansion extensibility for relational providrers.
/// </summary>
/// <para>
/// The service lifetime is <see cref="ServiceLifetime.Scoped" />. This means that each
/// <see cref="DbContext" /> 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.
/// </para>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-providers">Implementation of database providers and extensions</see>
/// and <see href="https://aka.ms/efcore-how-queries-work">How EF Core queries work</see> for more information.
/// </remarks>
public interface IRelationalSharedTypeEntityExpansionHelper
{
/// <summary>
/// Creates a SelectExpression representing owned type.
/// </summary>
public SelectExpression CreateInnerSelectExpression(
TableExpressionBase sourceTable,
IEntityType targetEntityType);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,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;
Expand All @@ -70,7 +73,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;
Expand Down Expand Up @@ -997,15 +1004,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!;
}

Expand Down Expand Up @@ -1186,7 +1196,20 @@ 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);

// 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 innerSelectExpression = _sharedTypeEntityExpansionHelper.CreateInnerSelectExpression(
sourceTable,
targetEntityType);

var innerShapedQuery = CreateShapedQueryExpression(targetEntityType, innerSelectExpression);

var makeNullable = foreignKey.PrincipalKey.Properties
Expand Down Expand Up @@ -1220,6 +1243,27 @@ outerKey is NewArrayExpression newArrayExpression
}

return innerShaper;

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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,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;
}

/// <summary>
Expand All @@ -65,5 +71,10 @@ public RelationalQueryableMethodTranslatingExpressionVisitorDependencies(
/// The SQL expression factory.
/// </summary>
public ISqlExpressionFactory SqlExpressionFactory { get; init; }

/// <summary>
/// Shared type entity expansion helper.
/// </summary>
public IRelationalSharedTypeEntityExpansionHelper RelationalSharedTypeEntityExpansionHelper { get; init; }
}
}
Original file line number Diff line number Diff line change
@@ -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;

namespace Microsoft.EntityFrameworkCore.Query
{
/// <inheritdoc/>
public class RelationalSharedTypeEntityExpansionHelper : IRelationalSharedTypeEntityExpansionHelper
{
/// <summary>
/// Creates a new instance of the <see cref="RelationalSharedTypeEntityExpansionHelper" /> class.
/// </summary>
/// <param name="dependencies">Dependencies for this service.</param>
public RelationalSharedTypeEntityExpansionHelper(RelationalSharedTypeEntityExpansionHelperDependencies dependencies)
{
Dependencies = dependencies;
}

/// <summary>
/// Dependencies for this service.
/// </summary>
protected virtual RelationalSharedTypeEntityExpansionHelperDependencies Dependencies { get; }

/// <inheritdoc/>
public virtual SelectExpression CreateInnerSelectExpression(
TableExpressionBase sourceTable,
IEntityType targetEntityType)
=> Dependencies.SqlExpressionFactory.Select(targetEntityType);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// 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.EntityFrameworkCore.Utilities;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.EntityFrameworkCore.Query
{
/// <summary>
/// <para>
/// Service dependencies parameter class for <see cref="RelationalSharedTypeEntityExpansionHelper" />
/// </para>
/// <para>
/// This type is typically used by database providers (and other extensions). It is generally
/// not used in application code.
/// </para>
/// <para>
/// 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.
/// </para>
/// <para>
/// The service lifetime is <see cref="ServiceLifetime.Scoped" />. This means that each
/// <see cref="DbContext" /> 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.
/// </para>
/// </summary>
public sealed record RelationalSharedTypeEntityExpansionHelperDependencies
{
/// <summary>
/// <para>
/// Creates the service dependencies parameter object for a <see cref="RelationalQueryableMethodTranslatingExpressionVisitor" />.
/// </para>
/// <para>
/// 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.
/// </para>
/// <para>
/// 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.
/// </para>
/// </summary>
[EntityFrameworkInternal]
public RelationalSharedTypeEntityExpansionHelperDependencies(ISqlExpressionFactory sqlExpressionFactory)
{
Check.NotNull(sqlExpressionFactory, nameof(sqlExpressionFactory));

SqlExpressionFactory = sqlExpressionFactory;
}

/// <summary>
/// The SQL expression factory.
/// </summary>
public ISqlExpressionFactory SqlExpressionFactory { get; init; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ public static IServiceCollection AddEntityFrameworkSqlServer(this IServiceCollec
.TryAdd<IRelationalParameterBasedSqlProcessorFactory, SqlServerParameterBasedSqlProcessorFactory>()
.TryAdd<INavigationExpansionExtensibilityHelper, SqlServerNavigationExpansionExtensibilityHelper>()
.TryAdd<IQueryableMethodTranslatingExpressionVisitorFactory, SqlServerQueryableMethodTranslatingExpressionVisitorFactory>()
.TryAdd<IRelationalSharedTypeEntityExpansionHelper, SqlServerSharedTypeEntityExpansionHelper>()
.TryAddProviderSpecificServices(
b => b
.TryAddSingleton<ISqlServerValueGeneratorCache, SqlServerValueGeneratorCache>()
Expand Down
8 changes: 8 additions & 0 deletions src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/EFCore.SqlServer/Properties/SqlServerStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,9 @@
<data name="TemporalSetOperationOnMismatchedSources" xml:space="preserve">
<value>Set operation can't be applied on entity '{entityType}' because temporal operations on both arguments don't match.</value>
</data>
<data name="TemporalOwnedTypeMappedToDifferentTableOnlySupportedForAsOf" xml:space="preserve">
<value>Only '{operationName}' temporal operation is supported for entitiy that owns another entity which is mapped different table.</value>
</data>
<data name="TransientExceptionDetected" xml:space="preserve">
<value>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.</value>
</data>
Expand Down
Loading

0 comments on commit 2c23d7d

Please sign in to comment.