-
Notifications
You must be signed in to change notification settings - Fork 3.2k
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
ValueConverter not applied to operands on ExecuteUpdateAsync #33330
Comments
I tried a local hack in our project where we luckily have a custom SqlServerSqlTranslatingExpressionVisitor registered. Its far from clean but applying the type mapping of the left expression to the right expression ensures the correct SQL: I wouldn't call this a feasable workaround but rather a proof that something with the type mappings is either missing or not supported well. internal class CustomSqlServerSqlTranslatingExpressionVisitor : SqlServerSqlTranslatingExpressionVisitor
{
public CustomSqlServerSqlTranslatingExpressionVisitor(
RelationalSqlTranslatingExpressionVisitorDependencies dependencies,
SqlServerQueryCompilationContext queryCompilationContext,
QueryableMethodTranslatingExpressionVisitor queryableMethodTranslatingExpressionVisitor)
: base(dependencies,
queryCompilationContext,
queryableMethodTranslatingExpressionVisitor)
{
}
protected override Expression VisitBinary(BinaryExpression binaryExpression)
{
// https://github.com/dotnet/efcore/issues/33330
// x => x.TimeSpanProp + timeSpanVariable
// x => x.TimeSpanProp + TimeSpan.FromSeconds(1)
// x => x.TimeSpanProp - timeSpanVariable
// x => x.TimeSpanProp - TimeSpan.FromSeconds(1)
if (binaryExpression.NodeType is ExpressionType.Add or ExpressionType.Subtract &&
binaryExpression.Left.Type == typeof(TimeSpan) &&
binaryExpression.Left.NodeType == ExpressionType.MemberAccess &&
binaryExpression.Right.Type == typeof(TimeSpan) &&
binaryExpression.Right.NodeType is ExpressionType.Parameter or ExpressionType.Constant)
{
var original = base.VisitBinary(binaryExpression);
if (original is SqlBinaryExpression { Left.TypeMapping.Converter: not null } sqlBinaryExpression
)
{
return sqlBinaryExpression.Right switch
{
SqlParameterExpression r => sqlBinaryExpression.Update(sqlBinaryExpression.Left,
r.ApplyTypeMapping(sqlBinaryExpression.Left.TypeMapping)),
SqlConstantExpression r => sqlBinaryExpression.Update(sqlBinaryExpression.Left,
r.ApplyTypeMapping(sqlBinaryExpression.Left.TypeMapping)),
_ => original
};
}
}
return base.VisitBinary(binaryExpression);
}
} |
In v8 the application of the type mapping has been cleaned up and no longer happens via a temporary equality node as before; this happens here: SqlExpression? TranslateSqlSetterValueSelector(
ShapedQueryExpression source,
Expression valueSelector,
ColumnExpression column)
{
if (TranslateSetterValueSelector(source, valueSelector, column.Type) is SqlExpression translatedSelector)
{
// Apply the type mapping of the column (translated from the property selector above) to the value
translatedSelector = _sqlExpressionFactory.ApplyTypeMapping(translatedSelector, column.TypeMapping);
return translatedSelector;
}
AddTranslationErrorDetails(RelationalStrings.InvalidValueInSetProperty(valueSelector.Print()));
return null;
} In any case, the bug is reproducible outside of ExecuteUpdate, wherever a value-converted column is concatenated (Add operator) with a parameter/constant: await context.WithTimeSpans.Where(w => w.TimeSpan + interval == TimeSpan.Zero).ToListAsync(); Full minimal reproawait using var context = new BlogContext();
await context.Database.EnsureDeletedAsync();
await context.Database.EnsureCreatedAsync();
var entity = new WithTimeSpan { TimeSpan = TimeSpan.FromTicks(100) };
await context.AddAsync(entity);
await context.SaveChangesAsync();
var interval = TimeSpan.FromTicks(10);
// Case 1:
// await context.WithTimeSpans.ExecuteUpdateAsync(x =>
// x.SetProperty(e => e.TimeSpan, e => e.TimeSpan + interval)
// );
// Case 2:
await context.WithTimeSpans.Where(w => w.TimeSpan + interval == TimeSpan.Zero).ToListAsync();
public class BlogContext : DbContext
{
public DbSet<WithTimeSpan> WithTimeSpans => Set<WithTimeSpan>();
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseSqlServer("Server=localhost;Database=test;User=SA;Password=Abcd5678;Connect Timeout=60;ConnectRetryCount=0;Encrypt=false")
.LogTo(Console.WriteLine, LogLevel.Information)
.EnableSensitiveDataLogging();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<WithTimeSpan>(e =>
{
e.HasKey(x => x.Id);
e.Property(x => x.Id).ValueGeneratedOnAdd();
e.Property(x => x.TimeSpan)
.HasColumnType("bigint")
.HasConversion(v => v.Ticks,
v => TimeSpan.FromTicks(v));
});
}
}
public class WithTimeSpan
{
public int Id { get; set; }
public TimeSpan TimeSpan { get; set; }
} @ajcvickers I strongly suspect the source of the bug is #32510 - that code was written with strings in mind, and when other things are concatenated, it seems to do the wrong thing... Assigning to you for investigation for now (but let me know if you'd rather not). |
I can confirm that this code seems to be the root cause. I enabled the hidden switch for enabling the old behavior and it generates the right SQL afterwards. efcore/src/EFCore.Relational/Query/SqlExpressionFactory.cs Lines 210 to 213 in c2f9969
|
Thanks for confirming @Danielku15! |
As opposed to when an `Add` note is for something other than concatenation. Fixes #33330
As opposed to when an `Add` note is for something other than concatenation. Fixes #33330
When upgrading from EFCore7 to EFCore8 the code below doesn't work anymore. In EFCore7 the ValueConverter configured on the property was applied to the TimeSpan operand making the update successful.
In EFCore8 the ValueConverter is not applied anymore to the expression and the SQL translates to a string which leads to an exception.
Include your code
Include stack traces
Include provider and version information
EF Core version: 8.0.3
Database provider: Microsoft.EntityFrameworkCore.SqlServer
Target framework: .NET 8.0
Operating system: Windows 11
IDE: Rider 2023.3.4
Entry point of difference
In v7 a special trick was used to translate a setter expression and unwrap it. The translated SQL expression has the ValueConverter set:
efcore/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
Line 1372 in c9d1c2d
In v8 this type mapping is not applied at the respective location leading to an invalid SQL:
efcore/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
Lines 1555 to 1564 in c2f9969
The text was updated successfully, but these errors were encountered: