DefaultTypeMapping().HasConversion() ignores mapping hints
- Dominant language
- C#
- Stars
- 14.8k
- Forks
- 3.4k
- PR merge metrics
- PR metrics pending
Description
Where `ModelConfigurationBuilder.Properties().HaveConversion()` honors the converter _including its mapping hints_, `ModelConfigurationBuilder.DefaultTypeMapping().HasConversion()` will use the converter but _not_ its mapping hints.
This can be an issue where the type matters outside the context of a property. For example, when custom struct is used in place of a decimal, EF writes a cast to `decimal(precision, scale)` into the query, such as when using `Sum()`. A cast to decimal is there, indicating that EF is aware of the conversion, but the precision and scale default back to `18,2` instead of what the mapping hint has configured.
### Minimal Repro
```cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace ComplexTypeConstructorBindingDemo;
internal class Program
{
public static void Main()
{
var builder = Host.CreateApplicationBuilder();
builder.Services.AddPooledDbContextFactory(dbContext => dbContext
.UseSqlServer(@"Data Source=(LocalDB)\MSSQLLocalDB;Integrated Security=True;Initial Catalog=ComplexTypeConstructorBindingDemo;", sqlServer => sqlServer.EnableRetryOnFailure()));
using var host = builder.Build();
host.Start();
using var dbContext = host.Services.GetRequiredService>().CreateDbContext();
dbContext.Database.EnsureCreated();
// Extremely simple example to demonstrate the ignored MappingHints on the DefaultTypeMapping:
// The default type mapping applies to "property-less" scenarios, such as type casts
// EF is aware of the conversion, using "decimal" in place of "FruitId"
// However, it ignores the mapping hints, using "decimal(18,2)" instead of the requested "decimal(28,0)"
var query = dbContext.Set()
.Select(x => (FruitId)x.Id);
var queryString = query.ToQueryString();
Console.WriteLine(queryString);
var result = query.ToListAsync();
host.StopAsync().GetAwaiter().GetResult();
}
}
#region DbContext Configuration
internal class TestDbContext(
DbContextOptions options)
: DbContext(options)
{
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
base.ConfigureConventions(configurationBuilder);
// This one is just to show that it is not decimal's configured default being used either, but the regular "18, 2" default
configurationBuilder.DefaultTypeMapping()
.HasPrecision(20, 5);
configurationBuilder.Properties()
.HaveConversion>();
configurationBuilder.DefaultTypeMapping()
.HasConversion>();
configurationBuilder.Properties()
.HaveConversion>();
configurationBuilder.DefaultTypeMapping()
.HasConversion>();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity(item =>
{
item.Property(x => x.Id);
item.HasKey(x => x.Id);
});
}
private class IdConverter()
: CastingConverter(
new ConverterMappingHints(precision: 28, scale: 0)) // Here is the mapping hint
{
}
}
#endregion
#region Model
internal class Banana // Entity
{
public BananaId Id { get; }
public Banana(BananaId id)
{
this.Id = id;
}
private Banana() // Reconstitution only
{
}
}
internal readonly struct BananaId(decimal value)
{
public decimal Value { get; } = value;
public static implicit operator decimal(BananaId instance) => instance.Value;
public static implicit operator BananaId(decimal value) => new BananaId(value);
public static implicit operator FruitId(BananaId instance) => new FruitId(instance.Value);
public static implicit operator BananaId(FruitId value) => new BananaId(value);
}
internal readonly struct FruitId(decimal value)
{
public decimal Value { get; } = value;
public static implicit operator decimal(FruitId instance) => instance.Value;
public static implicit operator FruitId(decimal value) => new FruitId(value);
}
#endregion
```
### Query String
```sql
SELECT CAST([b].[Id] AS decimal(18,2))
FROM [Banana] AS [b]
```
### Expected Query String
```sql
SELECT CAST([b].[Id] AS decimal(28,0))
FROM [Banana] AS [b]
```
### Include provider and version information
EF Core version:
Database provider: Microsoft.EntityFrameworkCore.SqlServer
Target framework: .NET 8.0
Operating system: Windows 10
IDE: Visual Studio 2022 17.8.0
Contributor guide
Assessment
This issue has not been assessed yet.