Skip to content

NpgsqlArrayConverter.ArrayConversionExpression throws IndexOutOfRangeException when using a custom derived type for the collection #3092

Description

@PressXtoChris

When using a custom derived class for a collection, the ArrayConversionExpression method throws an IndexOutOfRangeException when it attempts to get the (missing) generic type argument.

var iListType = typeof(IList<>).MakeGenericType(typeof(TInput).GetGenericArguments()[0]);

Example use case:

public class TestEntity(Guid id, CustomCollection values)
{
    public Guid Id { get; } = id;
    public CustomCollection Values { get; } = values;
}

public class CustomCollection : ReadOnlyCollection<string>
{
    public CustomCollection(string[] strings) : base(strings)
    {
        if (strings.Length == 0)
            throw new ArgumentException("At least one string must be provided", nameof(strings));
    }

    public bool SomeBusinessLogic() => this.Any(s => s.Contains("foo"));
}

public class TestDbContext : DbContext
{
    public DbSet<TestEntity> TestEntities { get; set; } = default!;
    public TestDbContext(DbContextOptions<TestDbContext> options) : base(options) { }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<TestEntity>()
            .ToTable("test_entities");

        modelBuilder.Entity<TestEntity>()
            .Property(x => x.Id)
            .HasColumnName("id");

        modelBuilder.Entity<TestEntity>()
            .PrimitiveCollection(x => x.Values)
            .HasColumnName("values")
            .ElementType()
            .HasConversion(typeof(CustomCollectionConversion));
    }
}

I think this could be resolved by getting the generic type arguments of the base class if TInput doesn't have any:

typeof(TInput).BaseType.GetGenericArguments()[0]

Activity

  1. added this to the 8.0.1 milestone on Feb 14, 2024
  2. roji commented on Feb 14, 2024

    @roji
    Member

    Note to self: see also #3074 which may be related.

  3. roji commented on Feb 17, 2024

    @roji
    Member

    I can't reproduce the precise error, probably because the code above is partial (e.g. CustomCollectionConversion is missing); when reporting issues, please always include a fully runnable, minimal code sample so that nothing is missing.

    What I do get is the following exception:

    /Users/roji/projects/test/bin/Debug/net8.0/Test
    Unhandled exception. System.InvalidOperationException: Converter for element type 'CustomCollection' cannot be used for 'TestEntity.Values' because its element type is 'string'.
       at Microsoft.EntityFrameworkCore.Metadata.Internal.ElementType.SetValueConverter(ValueConverter converter, ConfigurationSource configurationSource)
       at Microsoft.EntityFrameworkCore.Metadata.Internal.ElementType.SetValueConverter(Type converterType, ConfigurationSource configurationSource)
       at Microsoft.EntityFrameworkCore.Metadata.Internal.InternalElementTypeBuilder.HasConverter(Type converterType, ConfigurationSource configurationSource)
       at Microsoft.EntityFrameworkCore.Metadata.Builders.ElementTypeBuilder.HasConversion(Type conversionType)
       at BlogContext.OnModelCreating(ModelBuilder modelBuilder) in /Users/roji/projects/test/Program.cs:line 42
    

    This error makes sense: you're doing HasConversion on the element type, rather than on the collection as a whole. changing the configuration to the following makes the program run without errors:

    modelBuilder.Entity<TestEntity>()
        .PrimitiveCollection(x => x.Values)
        .HasColumnName("values")
        .Metadata.SetValueConverter(typeof(CustomCollectionConversion));
    Full repro
    await using var ctx = new BlogContext();
    await ctx.Database.EnsureDeletedAsync();
    await ctx.Database.EnsureCreatedAsync();
    
    public class BlogContext : DbContext
    {
        public DbSet<TestEntity> TestEntities { get; set; } = default!;
    
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
            => optionsBuilder
                .UseNpgsql("Host=localhost;Username=test;Password=test")
                .LogTo(Console.WriteLine, LogLevel.Information)
                .EnableSensitiveDataLogging();
    
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<TestEntity>()
                .ToTable("test_entities");
    
            modelBuilder.Entity<TestEntity>()
                .Property(x => x.Id)
                .HasColumnName("id");
    
            modelBuilder.Entity<TestEntity>()
                .PrimitiveCollection(x => x.Values)
                .HasColumnName("values")
                .Metadata.SetValueConverter(typeof(CustomCollectionConversion));
    
            // modelBuilder.Entity<TestEntity>()
            //     .PrimitiveCollection(x => x.Values)
            //     .HasColumnName("values")
            //     .ElementType()
            //     .HasConversion(typeof(CustomCollectionConversion));
        }
    
        public class CustomCollectionConversion()
            : ValueConverter<CustomCollection, string[]>(
                c => c.ToArray(),
                a => new CustomCollection(a));
    }
    
    public class Blog
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
    
    public class TestEntity(Guid id, CustomCollection values)
    {
        public Guid Id { get; } = id;
        public CustomCollection Values { get; } = values;
    }
    
    public class CustomCollection : ReadOnlyCollection<string>
    {
        public CustomCollection(string[] strings) : base(strings)
        {
            if (strings.Length == 0)
                throw new ArgumentException("At least one string must be provided", nameof(strings));
        }
    
        public bool SomeBusinessLogic() => this.Any(s => s.Contains("foo"));
    }

    If this doesn't fix your problem in some way, can you please share a full, runnable repro (like the code just above) that shows the problem?

  4. removed this from the 8.0.1 milestone on Feb 17, 2024
  5. PressXtoChris commented on Feb 18, 2024

    @PressXtoChris
    Author

    Apologies, didn't realise I'd left it off. My CustomCollection converter simply converted from string to string.

    public class CustomCollectionConversion() : ValueConverter<string, string>(
        v => v,
        v => v);

    Since using .HasConversion<string>() caused the following exception:

    Unhandled exception. System.InvalidCastException: Unable to cast object of type 'System.String[]' to type 'NpgsqlArrayConversion.CustomCollection'.
       at lambda_method13(Closure, InternalEntityEntry)
       at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.OriginalValues..ctor(InternalEntityEntry entry)
       at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.EnsureOriginalValues()
       at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntrySubscriber.SnapshotAndSubscribe(InternalEntityEntry entry)
       at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.StartTracking(InternalEntityEntry entry)
       at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetEntityState(EntityState oldState, EntityState newState, Boolean acceptChanges, Boolean modifyProper
    ties)
       at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetEntityState(EntityState entityState, Boolean acceptChanges, Boolean modifyProperties, Nullable`1 fo
    rceStateWhenUnknownKey, Nullable`1 fallbackState)
       at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityGraphAttacher.PaintAction(EntityEntryGraphNode`1 node)
       at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityEntryGraphIterator.TraverseGraph[TState](EntityEntryGraphNode`1 node, Func`2 handleNode)
       at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityGraphAttacher.AttachGraph(InternalEntityEntry rootEntry, EntityState targetState, EntityState storeGeneratedWithKeyS
    etTargetState, Boolean forceStateWhenUnknownKey)
       at Microsoft.EntityFrameworkCore.Internal.InternalDbSet`1.SetEntityState(InternalEntityEntry entry, EntityState entityState)
       at Microsoft.EntityFrameworkCore.Internal.InternalDbSet`1.Add(TEntity entity)
    
    

    Here's my full code:

    Full repro of above Model.cs
    using System.Collections.ObjectModel;
    using Microsoft.EntityFrameworkCore;
    using Microsoft.EntityFrameworkCore.Design;
    using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
    
    namespace NpgsqlArrayConversion;
    
    public class TestEntity(Guid id, CustomCollection values)
    {
        public Guid Id { get; } = id;
        public CustomCollection Values { get; } = values;
    
        private TestEntity() : this(default, default!) { }
    }
    
    public class CustomCollection : ReadOnlyCollection<string>
    {
        public CustomCollection(string[] strings) : base(strings)
        {
            if (strings.Length == 0)
                throw new ArgumentException("At least one string must be provided", nameof(strings));
        }
    
        public bool SomeBusinessLogic() => this.Any(s => s.Contains("foo"));
    }
    
    public class TestDbContext : DbContext
    {
        public DbSet<TestEntity> TestEntities { get; set; } = default!;
        public TestDbContext(DbContextOptions<TestDbContext> options) : base(options) { }
    
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<TestEntity>()
                .ToTable("test_entities");
    
            modelBuilder.Entity<TestEntity>()
                .Property(x => x.Id)
                .HasColumnName("id");
    
            modelBuilder.Entity<TestEntity>()
                .PrimitiveCollection(x => x.Values)
                .HasColumnName("values")
                .ElementType()
                .HasConversion(typeof(CustomCollectionConversion));
        }
    }
    
    public class CustomCollectionConversion() : ValueConverter<string, string>(
        v => v,
        v => v);
    
    public class TestDbContextFactory : IDesignTimeDbContextFactory<TestDbContext>
    {
        public TestDbContext CreateDbContext(string[] args)
        {
            var optionsBuilder = new DbContextOptionsBuilder<TestDbContext>();
            optionsBuilder.UseNpgsql("Host=localhost;Database=postgres;Username=postgres;Password=password");
            return new TestDbContext(optionsBuilder.Options);
        }
    }

    Program.cs

    using NpgsqlArrayConversion;
    
    using var db = new TestDbContextFactory().CreateDbContext(Array.Empty<string>());
    
    Console.WriteLine("Inserting a new item");
    
    var strings = new[] { "One", "Two" };
    db.TestEntities.Add(new TestEntity(Guid.NewGuid(), new CustomCollection(strings)));
    
    db.SaveChanges();
    Console.WriteLine("Saved");

    Also worth noting that in my real-world use case, I've got a wrapper class for my strings:

    Repro of real-world case Model.cs
    using System.Collections.ObjectModel;
    using Microsoft.EntityFrameworkCore;
    using Microsoft.EntityFrameworkCore.Design;
    using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
    
    namespace NpgsqlArrayConversion;
    
    public class TestEntity(Guid id, CustomCollection values)
    {
        public Guid Id { get; } = id;
        public CustomCollection Values { get; } = values;
    
        private TestEntity() : this(default, default!) { }
    }
    
    public class CustomCollection : ReadOnlyCollection<StringWrapper>
    {
        public CustomCollection(string[] strings) : base(strings.Select(StringWrapper.FromString).ToList())
        {
            if (strings.Length == 0)
                throw new ArgumentException("At least one string must be provided", nameof(strings));
        }
    
        public bool SomeBusinessLogic() => this.Any(s => s.Value.Contains("foo"));
    }
    
    public class StringWrapper
    {
        public string Value { get; }
        private StringWrapper(string value) => Value = value;
        public static StringWrapper FromString(string value) => new(value);
    }
    
    public class TestDbContext : DbContext
    {
        public DbSet<TestEntity> TestEntities { get; set; } = default!;
        public TestDbContext(DbContextOptions<TestDbContext> options) : base(options) { }
    
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<TestEntity>()
                .ToTable("test_entities");
    
            modelBuilder.Entity<TestEntity>()
                .Property(x => x.Id)
                .HasColumnName("id");
    
            modelBuilder.Entity<TestEntity>()
                .PrimitiveCollection(x => x.Values)
                .HasColumnName("values")
                .ElementType()
                .HasConversion(typeof(StringWrapperConverter));
        }
    }
    
    public class StringWrapperConverter() : ValueConverter<StringWrapper, string>(
        v => v.Value,
        v => StringWrapper.FromString(v));
    
    public class TestDbContextFactory : IDesignTimeDbContextFactory<TestDbContext>
    {
        public TestDbContext CreateDbContext(string[] args)
        {
            var optionsBuilder = new DbContextOptionsBuilder<TestDbContext>();
            optionsBuilder.UseNpgsql("Host=localhost;Database=postgres;Username=postgres;Password=password");
            return new TestDbContext(optionsBuilder.Options);
        }
    }

    Program.cs

    using NpgsqlArrayConversion;
    
    using var db = new TestDbContextFactory().CreateDbContext(Array.Empty<string>());
    
    Console.WriteLine("Inserting a new item");
    
    var strings = new[] { "One", "Two" };
    db.TestEntities.Add(new TestEntity(Guid.NewGuid(), new CustomCollection(strings)));
    
    db.SaveChanges();
    Console.WriteLine("Saved");

    Using Microsoft.EntityFrameworkCore 8.0.2, and Ngpsql.EntityFrameworkCore.PostgreSQL 8.0.2

    When I try the SetValueConverter implementation:

    SetValueConverter implementation

    Model.cs

    using System.Collections.ObjectModel;
    using Microsoft.EntityFrameworkCore;
    using Microsoft.EntityFrameworkCore.Design;
    using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
    
    namespace NpgsqlArrayConversion;
    
    public class TestEntity(Guid id, CustomCollection values)
    {
        public Guid Id { get; } = id;
        public CustomCollection Values { get; } = values;
    
        private TestEntity() : this(default, default!) { }
    }
    
    public class CustomCollection : ReadOnlyCollection<StringWrapper>
    {
        public CustomCollection(string[] strings) : base(strings.Select(StringWrapper.FromString).ToList())
        {
            if (strings.Length == 0)
                throw new ArgumentException("At least one string must be provided", nameof(strings));
        }
    
        public bool SomeBusinessLogic() => this.Any(s => s.Value.Contains("foo"));
    }
    
    public class StringWrapper
    {
        public string Value { get; }
    
        private StringWrapper(string value) => Value = value;
    
        public static StringWrapper FromString(string value) => new(value);
    }
    
    public class TestDbContext : DbContext
    {
        public DbSet<TestEntity> TestEntities { get; set; } = default!;
        public TestDbContext(DbContextOptions<TestDbContext> options) : base(options) { }
    
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<TestEntity>()
                .ToTable("test_entities");
    
            modelBuilder.Entity<TestEntity>()
                .Property(x => x.Id)
                .HasColumnName("id");
    
            modelBuilder.Entity<TestEntity>()
                .PrimitiveCollection(x => x.Values)
                .HasColumnName("values")
                .Metadata.SetValueConverter(typeof(CustomCollectionConverter));
        }
    }
    
    public class CustomCollectionConverter() : ValueConverter<CustomCollection, string[]>(
        c => c.Select(x => x.Value).ToArray(),
        a => new CustomCollection(a));
    
    public class TestDbContextFactory : IDesignTimeDbContextFactory<TestDbContext>
    {
        public TestDbContext CreateDbContext(string[] args)
        {
            var optionsBuilder = new DbContextOptionsBuilder<TestDbContext>();
            optionsBuilder.UseNpgsql("Host=localhost;Database=postgres;Username=postgres;Password=password");
            return new TestDbContext(optionsBuilder.Options);
        }
    }

    Program.cs is the same as above

    I get the following exception:

    Unhandled exception. System.InvalidOperationException: The 'CustomCollection' property 'TestEntity.Values' could not be mapped because the database provider does not support this typ
    e. Consider converting the property value to a type supported by the database using a value converter. See https://aka.ms/efcore-docs-value-converters for more information. Alternate
    ly, exclude the property from the model using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.
       at Microsoft.EntityFrameworkCore.Infrastructure.ModelValidator.ThrowPropertyNotMappedException(String propertyType, IConventionTypeBase typeBase, IConventionProperty unmappedPrope
    rty)
       at Microsoft.EntityFrameworkCore.Infrastructure.RelationalModelValidator.ThrowPropertyNotMappedException(String propertyType, IConventionTypeBase typeBase, IConventionProperty unm
    appedProperty)
       at Microsoft.EntityFrameworkCore.Infrastructure.ModelValidator.<ValidatePropertyMapping>g__Validate|7_0(IConventionTypeBase typeBase, <>c__DisplayClass7_0&)
       at Microsoft.EntityFrameworkCore.Infrastructure.ModelValidator.ValidatePropertyMapping(IModel model, IDiagnosticsLogger`1 logger)
       at Microsoft.EntityFrameworkCore.Infrastructure.ModelValidator.Validate(IModel model, IDiagnosticsLogger`1 logger)
       at Microsoft.EntityFrameworkCore.Infrastructure.RelationalModelValidator.Validate(IModel model, IDiagnosticsLogger`1 logger)
       at Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal.NpgsqlModelValidator.Validate(IModel model, IDiagnosticsLogger`1 logger)
       at Microsoft.EntityFrameworkCore.Infrastructure.ModelRuntimeInitializer.Initialize(IModel model, Boolean designTime, IDiagnosticsLogger`1 validationLogger)
       at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.GetModel(DbContext context, ModelCreationDependencies modelCreationDependencies, Boolean designTime)
       at Microsoft.EntityFrameworkCore.Internal.DbContextServices.CreateModel(Boolean designTime)
       at Microsoft.EntityFrameworkCore.Internal.DbContextServices.get_Model()
       at Microsoft.EntityFrameworkCore.Infrastructure.EntityFrameworkServicesBuilder.<>c.<TryAddCoreServices>b__8_4(IServiceProvider p)
       at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitFactory(FactoryCallSite factoryCallSite, RuntimeResolverContext context)
       at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
       at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope s
    erviceProviderEngine, RuntimeResolverLock lockType)
       at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
       at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
       at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)        
       at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
       at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope s
    erviceProviderEngine, RuntimeResolverLock lockType)
       at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
       at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
       at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)        
       at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
       at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope s
    erviceProviderEngine, RuntimeResolverLock lockType)
       at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
       at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
       at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)        
       at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
       at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope s
    erviceProviderEngine, RuntimeResolverLock lockType)
       at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
       at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
       at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)        
       at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
       at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope s
    erviceProviderEngine, RuntimeResolverLock lockType)
       at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
       at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
       at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)        
       at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
       at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope s
    erviceProviderEngine, RuntimeResolverLock lockType)
       at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
       at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
       at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope)
       at Microsoft.Extensions.DependencyInjection.ServiceLookup.DynamicServiceProviderEngine.<>c__DisplayClass2_0.<RealizeService>b__0(ServiceProviderEngineScope scope)
       at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(ServiceIdentifier serviceIdentifier, ServiceProviderEngineScope serviceProviderEngineScope)
       at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType)
       at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
       at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
       at Microsoft.EntityFrameworkCore.DbContext.get_DbContextDependencies()
       at Microsoft.EntityFrameworkCore.DbContext.get_ContextServices()
       at Microsoft.EntityFrameworkCore.DbContext.get_InternalServiceProvider()
       at Microsoft.EntityFrameworkCore.DbContext.get_DbContextDependencies()
       at Microsoft.EntityFrameworkCore.DbContext.Microsoft.EntityFrameworkCore.Internal.IDbContextDependencies.get_StateManager()
       at Microsoft.EntityFrameworkCore.Internal.InternalDbSet`1.EntryWithoutDetectChanges(TEntity entity)
       at Microsoft.EntityFrameworkCore.Internal.InternalDbSet`1.Add(TEntity entity)
    at Program.<Main>$(String[] args) in \NpgsqlArrayConversion\Program.cs:line 10
    
  6. added theissue type on Oct 18, 2026
  7. roji commented on Oct 18, 2026

    @roji
    Member

    AI Triage

    The below is an AI-generated analysis and may contain inaccuracies.

    Confirmed; classified as a bug. The IndexOutOfRangeException reproduces with the reporter's identity element converter. It occurs during model initialization, before connecting to PostgreSQL, so neither seed data nor a database is needed. The StringWrapper variant also reproduces.

    Versions and regression

    Npgsql EF provider EF Core Model initialization
    8.0.0 8.0.0 IndexOutOfRangeException
    8.0.2 8.0.2 IndexOutOfRangeException
    8.0.11 8.0.32 IndexOutOfRangeException
    9.0.4 9.0.21 NotSupportedException: “Array value converter input type must be an IEnumerable, but is CustomCollection”
    10.0.3 10.0.13 Same NotSupportedException

    The minimized 8.0.2 repro was also verified on .NET 8. The cross-version comparisons used .NET 10.

    This was not introduced in 8.0.2, since 8.0.0 already fails. No previously working version was established, so a regression from an earlier major version is not confirmed. The same PrimitiveCollection configuration cannot be tested unchanged on EF 7 because that API was introduced in EF 8.

    Cause and provider comparison

    CustomCollection implements IEnumerable<string> through its base class, but is not itself a generic type. In 8.0.2's converter, it falls through to typeof(TInput).GetGenericArguments()[0], indexing an empty array. In 10.0.3, the collection-handling branches still require the input type itself to be generic, so they reject it instead.

    SQLite 8.0.2 initializes this same model successfully. Therefore, the reported converter/model-initialization failure is PostgreSQL-provider-specific; this issue should not simply be moved upstream.

    There is also a separate, shared EF limitation in the follow-up: adding an entity with this read-only collection on SQLite 8.0.2 reproduces the InvalidCastException from string[] to CustomCollection during original-value snapshotting. SQLite 10.0.13 instead throws MissingMethodException because the collection has no parameterless constructor. Merely correcting element-type discovery will not make this read-only collection constructible and usable throughout tracking/materialization.

    As a control, replacing the collection with List<string> successfully inserts and reloads the values on PostgreSQL with both 8.0.2 and 10.0.3. I could not validate the earlier whole-property CustomCollection → string[] conversion suggestion as a workaround for the StringWrapper variant: saving/tracking still fails, so successful model initialization alone is insufficient.

    Related issues

    • Closest overlap: npgsql/efcore.pg#3082, supporting IList<T> implementations beyond array/List<T>. npgsql/efcore.pg#3413, already closed as its duplicate, reports the same 9.x converter rejection for a non-generic List<T> subclass. The read-only construction/snapshotting aspect here needs to be distinguished from supporting mutable custom lists.
    • Upstream: dotnet/efcore#32055 is a closed, related non-generic primitive-collection validation bug, but has a different failing component and exception. dotnet/efcore#31374 tracks comparer behavior for immutable/read-only primitive collections. Neither is an exact duplicate of the Npgsql converter failure.
    • npgsql/efcore.pg#3074 reaches the same old converter exception through migration defaults, rather than a custom collection property.
    minimal repro

    Repro.csproj:

    <Project Sdk="Microsoft.NET.Sdk">
      <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net8.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
      </PropertyGroup>
      <ItemGroup>
        <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.2" />
      </ItemGroup>
    </Project>

    Program.cs:

    using System.Collections.ObjectModel;
    using Microsoft.EntityFrameworkCore;
    using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
    using Microsoft.Extensions.Logging;
    
    await using var context = new TestDbContext();
    _ = context.Model;
    
    public class TestDbContext : DbContext
    {
        public DbSet<TestEntity> TestEntities { get; set; } = null!;
    
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
            => optionsBuilder
                .UseNpgsql("Host=localhost;Database=triage_3092")
                .LogTo(Console.WriteLine, LogLevel.Information)
                .EnableSensitiveDataLogging();
    
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<TestEntity>().Property(x => x.Id);
            modelBuilder.Entity<TestEntity>()
                .PrimitiveCollection(x => x.Values)
                .ElementType()
                .HasConversion<CustomCollectionConversion>();
        }
    }
    
    public class TestEntity(Guid id, CustomCollection values)
    {
        public Guid Id { get; } = id;
        public CustomCollection Values { get; } = values;
    
        private TestEntity() : this(default, null!) { }
    }
    
    public class CustomCollection(string[] strings) : ReadOnlyCollection<string>(strings);
    
    public class CustomCollectionConversion() : ValueConverter<string, string>(v => v, v => v);

    Run dotnet run. No database is contacted. The business-logic method and non-empty validation from the original collection are unnecessary to trigger the failure.

  8. changed the issue type fromtoon Oct 18, 2026
  9. added this to the Backlog milestone on Oct 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions