Fully support overloaded operators with value-converted custom types
- Dominant language
- C#
- Stars
- 14.8k
- Forks
- 3.4k
- PR merge metrics
- PR metrics pending
Description
### Bug description
When using custom value objects (`record struct`) with global value converters in EF Core, projecting a computed value (e.g., `Mark1 - Mark2` returning a `Difference` struct) into a model property causes the following exception during query translation:
```
System.InvalidOperationException: No coercion operator is defined between types 'EfInvalidSql.MarkValue' and 'System.Nullable<1>[EfInvalidSql.Difference]'
```
This happens even when `Difference` has a global `ValueConverter` configured in `ConfigureConventions`.
### Your code
```csharp
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Microsoft.Extensions.Logging;
namespace EfInvalidSql;
public class Program
{
public static async Task Main(params string[] args)
{
MyContext context = new();
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
var marks = await context.Set()
.Select(x => new Model
{
Id = x.Id,
Difference = x.Mark1 - x.Mark2
})
.ToListAsync();
}
}
public readonly record struct MarkValue
{
public MarkValue(int value)
{
Value = value;
}
public int Value { get; }
public static Difference operator -(MarkValue left, MarkValue right)
{
return new Difference(left.Value - right.Value);
}
}
public readonly record struct Difference
{
public Difference(int value)
{
Value = value;
}
public int Value { get; }
}
public class MarkValueConverter : ValueConverter
{
public MarkValueConverter()
: base(
id => id.Value,
value => new MarkValue(value))
{
}
}
public class DifferenceConverter : ValueConverter
{
public DifferenceConverter()
: base(
id => id.Value,
value => new Difference(value))
{
}
}
public class User
{
public int Id { get; set; }
public MarkValue Mark1 { get; set; }
public MarkValue Mark2 { get; set; }
}
public class Model
{
public int Id { get; set; }
public Difference Difference { get; set; }
}
public class MyContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseNpgsql("Host=localhost;Port=7435;Database=testdb;Username=admin;Password=testpass")
.LogTo(Console.WriteLine, LogLevel.Information)
.EnableSensitiveDataLogging();
}
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
base.ConfigureConventions(configurationBuilder);
configurationBuilder
.Properties()
.HaveConversion();
configurationBuilder
.Properties()
.HaveConversion();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity();
modelBuilder.Entity().HasData(
new User
{
Id = 1,
Mark1 = new MarkValue(10),
Mark2 = new MarkValue(5)
},
new User
{
Id = 2,
Mark1 = new MarkValue(7),
Mark2 = new MarkValue(3)
}
);
}
}
```
### Stack traces
```text
Unhandled exception. System.InvalidOperationException: No coercion operator is defined between types 'EfInvalidSql.MarkValue' and 'System.Nullable`1[EfInvalidSql.Difference]'.
at System.Linq.Expressions.Expression.GetUserDefinedCoercionOrThrow(ExpressionType coercionType, Expression expression, Type convertToType)
at Microsoft.EntityFrameworkCore.Query.RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.CreateGetValueExpression(ParameterExpression dbDataReader, Int32 index, Boolean nullable, RelationalTypeMapping typeMapping, Type type, IPropertyBase property)
at Microsoft.EntityFrameworkCore.Query.RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.VisitExtension(Expression extensionExpression)
at System.Linq.Expressions.ExpressionVisitor.VisitUnary(UnaryExpression node)
at System.Linq.Expressions.ExpressionVisitor.VisitMemberAssignment(MemberAssignment node)
at System.Linq.Expressions.ExpressionVisitor.VisitMemberBinding(MemberBinding node)
at System.Linq.Expressions.ExpressionVisitor.Visit[T](ReadOnlyCollection`1 nodes, Func`2 elementVisitor)
at System.Linq.Expressions.ExpressionVisitor.VisitMemberInit(MemberInitExpression node)
at Microsoft.EntityFrameworkCore.Query.RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.ProcessShaper(Expression shaperExpression, Expression& relationalCommandResolver, IReadOnlyList`1& readerColumns, LambdaExpression& relatedDataLoaders, Int32& collectionId)
at Microsoft.EntityFrameworkCore.Query.RelationalShapedQueryCompilingExpressionVisitor.VisitShapedQuery(ShapedQueryExpression shapedQueryExpression)
at Microsoft.EntityFrameworkCore.Query.ShapedQueryCompilingExpressionVisitor.VisitExtension(Expression extensionExpression)
at Microsoft.EntityFrameworkCore.Query.RelationalShapedQueryCompilingExpressionVisitor.VisitExtension(Expression extensionExpression)
at Microsoft.EntityFrameworkCore.Query.QueryCompilationContext.CreateQueryExecutorExpression[TResult](Expression query)
at Microsoft.EntityFrameworkCore.Query.QueryCompilationContext.CreateQueryExecutor[TResult](Expression query)
at Microsoft.EntityFrameworkCore.Storage.Database.CompileQuery[TResult](Expression query, Boolean async)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.CompileQueryCore[TResult](IDatabase database, Expression query, IModel model, Boolean async)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.<>c__DisplayClass11_0`1.b__0()
at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func`1 compiler)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.ExecuteCore[TResult](Expression query, Boolean async, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.ExecuteAsync[TResult](Expression query, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.ExecuteAsync[TResult](Expression expression, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1.GetAsyncEnumerator(CancellationToken cancellationToken)
at System.Runtime.CompilerServices.ConfiguredCancelableAsyncEnumerable`1.GetAsyncEnumerator()
at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken)
at EfInvalidSql.Program.Main(String[] args) in D:\Sync\repos\EfCoreTpc10\EfCoreTpc\Program.cs:line 17
at EfInvalidSql.Program.(String[] args)
```
### Verbose output
```text
dbug: 13.06.2025 17:50:06.584 CoreEventId.QueryCompilationStarting[10111] (Microsoft.EntityFrameworkCore.Query)
Compiling query expression:
'DbSet()
.Select(x => new Model{
Id = x.Id,
Difference = x.Mark1 - x.Mark2
}
)'
Unhandled exception. System.InvalidOperationException: No coercion operator is defined between types 'EfInvalidSql.MarkValue' and 'System.Nullable`1[EfInvalidSql.Difference]'.
at System.Linq.Expressions.Expression.GetUserDefinedCoercionOrThrow(ExpressionType coercionType, Expression expression, Type convertToType)
... the rest part of the error
```
### EF Core version
9.0.6
### Database provider
Npgsql Entity Framework Core provider for PostgreSQL
### Target framework
.NET 9.0
### Operating system
Windows 11
### IDE
Visual Studio 17.4.5
Contributor guide
Assessment
This issue has not been assessed yet.