Repository navigation
System.InvalidCastException: Object must implement IConvertible. when passing List to Intersect method on primitive collection of non-List type with element converter type #3805
Description
Activity
Curiosly enough
.Where(user => user.RelatedIds.Any(x => toFindAsList.Contains(x)))still works. So the maybe the problem is not intrinsic in the collection translation, but specifically in the.Intersect(...).Any()translation.AI Triage
The below is an AI-generated analysis and may contain inaccuracies.
Summary
Confirmed bug. When a primitive collection property has a non-matching CLR type from the LINQ parameter (e.g. property is
MyId[]but the parameter isList<MyId>) and the element type has a value converter,Intersect(...).Any()throwsInvalidCastException: Object must implement IConvertible.Root Cause
The issue is in
NpgsqlArrayTypeMapping.CreateParameter(). When theIntersect().Any()pattern is matched, it's translated to the PostgreSQL&&(overlap) operator, and the type mapping from the column (NpgsqlArrayTypeMapping<MyId[], MyId[], long>) is applied to theList<MyId>parameter.In
CreateParameter, when there is a value converter (Converter is not null), the code skips materialization of the enumerable entirely. TheList<MyId>is then passed tobase.CreateParameter(), which invokesValueConverter.Sanitize<MyId[]>(). Sanitize callsConvert.ChangeType(List<MyId>, typeof(MyId[])), which fails becauseList<MyId>doesn't implementIConvertible.Without a value converter, the code at line 228 proceeds to materialize the enumerable into a
List<TElement>, which Npgsql ADO.NET handles natively. With a converter, this materialization is intentionally skipped (to preserve types likeHashSet<T>that converters expect), but this causes the mismatch when the parameter's collection type differs from the property's.Reproduction
- Confirmed with Npgsql.EntityFrameworkCore.PostgreSQL 9.0.4 on .NET 9
- Also reproduced on 8.0. not a regression between 8.x and 9.x (the bug has existed since element-level value converters on primitive collections were introduced)11
- Does not reproduce on SQL this is PostgreSQL-specific (SQL Server uses JSON-based primitive collections and doesn't have the same parameterization path)Server
- Does not reproduce without value
List<long>againstlong[]works fineconverter
Workaround: use an array parameter (
MyId[]) instead ofList<MyId>, as the reporter noted.Minimal repro
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.Extensions.Logging; await using var context = new TestContext(); await context.Database.EnsureDeletedAsync(); await context.Database.EnsureCreatedAsync(); var toFindAsList = new List<MyId> { new MyId(42) }; _ = await context.Users .Where(user => user.RelatedIds.Intersect(toFindAsList).Any()) .ToListAsync(); public class TestContext : DbContext { public DbSet<ReproUser> Users => Set<ReproUser>(); protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder .UseNpgsql(Environment.GetEnvironmentVariable("Test__Npgsql__DefaultConnection")) .LogTo(Console.WriteLine, LogLevel.Information) .EnableSensitiveDataLogging(); protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<ReproUser>().PrimitiveCollection(u => u.RelatedIds).ElementType().HasConversion<MyIdValueConverter>(); } } public class ReproUser { public int Id { get; set; } public MyId[] RelatedIds { get; set; } = []; } public readonly record struct MyId(long Value); public class MyIdValueConverter : ValueConverter<MyId, long> { public MyIdValueConverter() : base(v => v.Value, v => new MyId(v)) { } }
Possible Duplicates
- Properly support non-array/list collections/enumerables in value-converted arrays #3286 ( same
IConvertibleerror withHashSeton value-converted arrays; fixed by Support arbitrary enumerables in NpgsqlArrayConverter #3290, but that fix specifically handled non-array/list enumerables inNpgsqlArrayConverter, not this type-mismatch scenario inCreateParameterclosed) - Npgsql8: Contains() doesn't work with HashSet anymore #3420 (
Contains()withHashSet<string>, same errorclosed)
There is still a regression from 8.x to 9.x: with 8.x this doesn't fail if both the primitive collection and the list passed to
IntersectimplementIList<>.For example this worked with 8.x, but not with 9.x:
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.Extensions.Logging; using System.Collections.Immutable; await using var context = new ReproContext(); await context.Database.EnsureDeletedAsync(); await context.Database.EnsureCreatedAsync(); var toFindAsImmutableList = ImmutableList<MyId>.Empty.Add(new MyId(42)); _ = await context.Users .Where(user => user.RelatedIds.Intersect(toFindAsImmutableList).Any()) .ToListAsync(); public class ReproContext : DbContext { public DbSet<ReproUser> Users => Set<ReproUser>(); protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder .UseNpgsql("Host=localhost;Username=postgres;Password=passw0rd;database=repro") .LogTo(Console.WriteLine, LogLevel.Information) .EnableSensitiveDataLogging(); protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity<ReproUser>().PrimitiveCollection(u => u.RelatedIds).ElementType().HasConversion<MyIdValueConverter>(); } } public class ReproUser { public ulong Id { get; set; } public List<MyId> RelatedIds { get; } = []; } public readonly record struct MyId(long Value); public class MyIdValueConverter : ValueConverter<MyId, long> { public MyIdValueConverter() : base(v => v.Value, v => new MyId(v)){} }
Repro:
Repro
(Reproduction is based on the reproduction here #3286 (comment))
This gives the following output:
It seems to be a cast from the
List<MyId>toMyId[]that fails, based on what i traced with my debugger.Switching the
ToFindAsListtype to be an array, like this:Works without any exceptions, however I don't expect to remember to trace all of that everywhere.
For some background context:
I'm working on switching all our keys to be "strongly typed", and i recall this working fine before, when everything was just
intrather thanMyId.