Skip to content
Open
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
4 changes: 2 additions & 2 deletions src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2101,7 +2101,7 @@ bool PreserveConvertNode(Expression expression)
// In many databases, parameter names must start with a letter or underscore.
// The same is true for C# variable names, from which we derive the parameter name, so in principle we shouldn't see an issue;
// but just in case, prepend an underscore if the parameter name doesn't start with a letter or underscore.
if (!char.IsLetter(parameterName[0]) && parameterName[0] != '_')
if (parameterName.Length > 0 && !char.IsLetter(parameterName[0]) && parameterName[0] != '_')
{
parameterName = "_" + parameterName;
}
Expand Down Expand Up @@ -2215,7 +2215,7 @@ static string SanitizeCompilerGeneratedName(string s)
{
// Compiler-generated field names intentionally contain illegal characters, specifically angle brackets <>.
// In cases where there's something within the angle brackets, that tends to be the original user-provided variable name
// (e.g. <PropertyName>k__BackingField). If we see angle brackets, extract that out, or it the angle brackets contain no
// (e.g. <PropertyName>k__BackingField). If we see angle brackets, extract that out, or if the angle brackets contain no
// content, strip them out entirely and take what comes after.
var closingBracket = s.IndexOf('>');
if (closingBracket == -1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ private static readonly PropertyInfo QueryContextContextPropertyInfo

private readonly Dictionary<string, object?> _parameters = new();


/// <summary>
/// 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
Expand Down Expand Up @@ -1082,11 +1083,18 @@ private Expression ProcessExecuteUpdate(NavigationExpansionExpression source, Me

// Apply any pending selector before processing the ExecuteUpdate setters; this adds a Select() (if necessary) before
// ExecuteUpdate, to avoid the pending selector flowing into each setter lambda and making it more complicated.
// However, only do this when the pending selector produces entity/structural type references (i.e. the snapshot is not just
// a DefaultExpression). When the pending selector projects only scalar values (e.g. select new { p.Used, n.Qty }),
// applying it would lose the connection between the projected scalar and the original entity property, breaking
// ExecuteUpdate's property selector recognition (#37771).
var newStructure = SnapshotExpression(source.PendingSelector);
var queryable = Reduce(source);
var navigationTree = new NavigationTreeExpression(newStructure);
var parameterName = source.CurrentParameter.Name ?? GetParameterName("e");
source = new NavigationExpansionExpression(queryable, navigationTree, navigationTree, parameterName);
if (newStructure is not DefaultExpression)
{
var queryable = Reduce(source);
var navigationTree = new NavigationTreeExpression(newStructure);
var parameterName = source.CurrentParameter.Name ?? GetParameterName("e");
source = new NavigationExpansionExpression(queryable, navigationTree, navigationTree, parameterName);
}

NewArrayExpression settersArray;
switch (setters)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,44 @@ FROM [Customers]
}
});

[ConditionalTheory, MemberData(nameof(IsAsyncData))] // #37771
public virtual Task Update_with_select_mixed_entity_scalar_anonymous_projection(bool async)
=> TestHelpers.ExecuteWithStrategyInTransactionAsync(
() => Fixture.CreateContext(),
(facade, transaction) => Fixture.UseTransaction(facade, transaction),
async context =>
{
var queryable = context.Set<Customer>().Select(c => new { Entity = c, c.ContactName });

if (async)
{
await queryable.ExecuteUpdateAsync(s => s.SetProperty(c => c.Entity.ContactName, "Updated"));
}
else
{
queryable.ExecuteUpdate(s => s.SetProperty(c => c.Entity.ContactName, "Updated"));
}
});

[ConditionalTheory, MemberData(nameof(IsAsyncData))] // #37771
public virtual Task Update_with_select_scalar_anonymous_projection(bool async)
=> TestHelpers.ExecuteWithStrategyInTransactionAsync(
() => Fixture.CreateContext(),
(facade, transaction) => Fixture.UseTransaction(facade, transaction),
async context =>
{
var queryable = context.Set<Customer>().Select(c => new { c.ContactName, c.City });

if (async)
{
await queryable.ExecuteUpdateAsync(s => s.SetProperty(c => c.ContactName, "Updated"));
}
else
{
queryable.ExecuteUpdate(s => s.SetProperty(c => c.ContactName, "Updated"));
}
});

protected static async Task AssertTranslationFailed(string details, Func<Task> query)
{
var exception = await Assert.ThrowsAsync<InvalidOperationException>(query);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1665,6 +1665,34 @@ OFFSET @p ROWS
""");
}

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

AssertSql(
"""
@p='Updated' (Size = 30)
UPDATE [c]
SET [c].[ContactName] = @p
FROM [Customers] AS [c]
""");
}

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

AssertSql(
"""
@p='Updated' (Size = 30)
UPDATE [c]
SET [c].[ContactName] = @p
FROM [Customers] AS [c]
""");
}

private void AssertSql(params string[] expected)
=> Fixture.TestSqlLoggerFactory.AssertBaseline(expected);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1571,6 +1571,32 @@ ORDER BY "o"."OrderID"
""");
}

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

AssertSql(
"""
@p='Updated' (Size = 7)
UPDATE "Customers" AS "c"
SET "ContactName" = @p
""");
}

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

AssertSql(
"""
@p='Updated' (Size = 7)
UPDATE "Customers" AS "c"
SET "ContactName" = @p
""");
}

private void AssertSql(params string[] expected)
=> Fixture.TestSqlLoggerFactory.AssertBaseline(expected);

Expand Down
Loading