Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[release/7.x] Fixes around IncludeExpression for ExecuteUpdate/Delete (#30571) #30579

Merged
merged 1 commit into from
Apr 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ public class RelationalQueryableMethodTranslatingExpressionVisitor : QueryableMe

private static readonly bool QuirkEnabled28727
= AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue28727", out var enabled) && enabled;
private static readonly bool QuirkEnabled30528
= AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue30528", out var enabled) && enabled;
private static readonly bool QuirkEnabled30572
= AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue30572", out var enabled) && enabled;

/// <summary>
/// Creates a new instance of the <see cref="QueryableMethodTranslatingExpressionVisitor" /> class.
Expand Down Expand Up @@ -1029,7 +1033,14 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
{
if (source.ShaperExpression is IncludeExpression includeExpression)
{
source = source.UpdateShaperExpression(PruneOwnedIncludes(includeExpression));
if (QuirkEnabled30572)
{
source = source.UpdateShaperExpression(PruneOwnedIncludes(includeExpression));
}
else
{
source = source.UpdateShaperExpression(PruneIncludes(includeExpression));
}
}

if (source.ShaperExpression is not EntityShaperExpression entityShaperExpression)
Expand Down Expand Up @@ -1129,6 +1140,7 @@ static bool AreOtherNonOwnedEntityTypesInTheTable(IEntityType rootType, ITableBa

return TranslateExecuteDelete((ShapedQueryExpression)Visit(newSource));

// Old quirked implementation for #30572
static Expression PruneOwnedIncludes(IncludeExpression includeExpression)
{
if (includeExpression.Navigation is ISkipNavigation
Expand Down Expand Up @@ -1163,6 +1175,16 @@ static Expression PruneOwnedIncludes(IncludeExpression includeExpression)
ShapedQueryExpression source,
LambdaExpression setPropertyCalls)
{
if (!QuirkEnabled30528)
{
// Our source may have IncludeExpressions because of owned entities or auto-include; unwrap these, as they're meaningless for
// ExecuteUpdate's lambdas. Note that we don't currently support updates across tables.
if (source.ShaperExpression is IncludeExpression includeExpression)
{
source = source.UpdateShaperExpression(PruneIncludes(includeExpression));
}
}

var propertyValueLambdaExpressions = new List<(LambdaExpression, Expression)>();
PopulateSetPropertyCalls(setPropertyCalls.Body, propertyValueLambdaExpressions, setPropertyCalls.Parameters[0]);
if (TranslationErrorDetails != null)
Expand Down Expand Up @@ -1386,7 +1408,6 @@ when methodCallExpression.Method.IsGenericMethod
&& methodCallExpression.Method.Name == nameof(SetPropertyCalls<int>.SetProperty)
&& methodCallExpression.Method.DeclaringType!.IsGenericType
&& methodCallExpression.Method.DeclaringType.GetGenericTypeDefinition() == typeof(SetPropertyCalls<>):

list.Add(((LambdaExpression)methodCallExpression.Arguments[0], methodCallExpression.Arguments[1]));

PopulateSetPropertyCalls(methodCallExpression.Object!, list, parameter);
Expand All @@ -1400,8 +1421,8 @@ when methodCallExpression.Method.IsGenericMethod
}

// For property setter selectors in ExecuteUpdate, we support only simple member access, EF.Function, etc.
// We also unwrap casts to interface/base class (#29618), as well as IncludeExpressions (which occur when the target entity has
// owned entities, #28727).
// We also unwrap casts to interface/base class (#29618). Note that owned IncludeExpressions have already been pruned from the
// source before remapping the lambda (#28727).
static bool TryProcessPropertyAccess(
IModel model,
ref Expression expression,
Expand Down Expand Up @@ -1449,9 +1470,12 @@ static Expression Unwrap(Expression expression)
{
expression = expression.UnwrapTypeConversion(out _);

while (expression is IncludeExpression includeExpression)
if (QuirkEnabled30528)
{
expression = includeExpression.EntityExpression;
while (expression is IncludeExpression includeExpression)
{
expression = includeExpression.EntityExpression;
}
}

return expression;
Expand Down Expand Up @@ -1660,6 +1684,18 @@ private Expression RemapLambdaBody(ShapedQueryExpression shapedQueryExpression,
private Expression ExpandSharedTypeEntities(SelectExpression selectExpression, Expression lambdaBody)
=> _sharedTypeEntityExpandingExpressionVisitor.Expand(selectExpression, lambdaBody);

private static Expression PruneIncludes(IncludeExpression includeExpression)
{
if (includeExpression.Navigation is ISkipNavigation or not INavigation)
{
return includeExpression;
}

return includeExpression.EntityExpression is IncludeExpression innerIncludeExpression
? PruneIncludes(innerIncludeExpression)
: includeExpression.EntityExpression;
}

private sealed class SharedTypeEntityExpandingExpressionVisitor : ExpressionVisitor
{
private readonly RelationalSqlTranslatingExpressionVisitor _sqlTranslator;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,58 @@ await AssertUpdate(
rowsAffectedCount: 0);
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task Update_non_owned_property_on_entity_with_owned2(bool async)
{
var contextFactory = await InitializeAsync<Context28671>(
onModelCreating: mb =>
{
mb.Entity<Owner>().OwnsOne(o => o.OwnedReference);
});

await AssertUpdate(
async,
contextFactory.CreateContext,
ss => ss.Set<Owner>(),
s => s.SetProperty(o => o.Title, o => o.Title + "_Suffix"),
rowsAffectedCount: 0);
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task Delete_entity_with_auto_include(bool async)
{
var contextFactory = await InitializeAsync<Context30572>();
await AssertDelete(async, contextFactory.CreateContext, ss => ss.Set<Context30572_Principal>(), rowsAffectedCount: 0);
}

protected class Context30572 : DbContext
{
public Context30572(DbContextOptions options)
: base(options)
{
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
=> modelBuilder.Entity<Context30572_Principal>().Navigation(o => o.Dependent).AutoInclude();
}

public class Context30572_Principal
{
public int Id { get; set; }
public string? Title { get; set; }

public Context30572_Dependent? Dependent { get; set; }
}

public class Context30572_Dependent
{
public int Id { get; set; }

public int Number { get; set; }
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task Delete_predicate_based_on_optional_navigation(bool async)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,30 @@ FROM [Owner] AS [o]
""");
}

public override async Task Update_non_owned_property_on_entity_with_owned2(bool async)
{
await base.Update_non_owned_property_on_entity_with_owned2(async);

AssertSql(
"""
UPDATE [o]
SET [o].[Title] = COALESCE([o].[Title], N'') + N'_Suffix'
FROM [Owner] AS [o]
""");
}

public override async Task Delete_entity_with_auto_include(bool async)
{
await base.Delete_entity_with_auto_include(async);

AssertSql(
"""
DELETE FROM [c]
FROM [Context30572_Principal] AS [c]
LEFT JOIN [Context30572_Dependent] AS [c0] ON [c].[DependentId] = [c0].[Id]
""");
}

public override async Task Delete_predicate_based_on_optional_navigation(bool async)
{
await base.Delete_predicate_based_on_optional_navigation(async);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,20 @@ public override async Task Update_non_owned_property_on_entity_with_owned(bool a
""");
}

public override async Task Update_non_owned_property_on_entity_with_owned2(bool async)
{
await base.Update_non_owned_property_on_entity_with_owned2(async);

AssertSql(
"""
UPDATE "Owner" AS "o"
SET "Title" = COALESCE("o"."Title", '') || '_Suffix'
""");
}

public override Task Delete_entity_with_auto_include(bool async)
=> Assert.ThrowsAsync<ArgumentOutOfRangeException>(() => base.Delete_entity_with_auto_include(async));

public override async Task Delete_predicate_based_on_optional_navigation(bool async)
{
await base.Delete_predicate_based_on_optional_navigation(async);
Expand Down