dotnet / dotnet/efcore

Fully support overloaded operators with value-converted custom types

Open
#36,244 1 comment 1 reaction 0 assignees View on GitHub
area-query customer-reported
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.