-
Notifications
You must be signed in to change notification settings - Fork 228
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
NpgsqlArrayConverter.ArrayConversionExpression throws IndexOutOfRangeException when using a custom derived type for the collection #3092
Comments
Note to self: see also #3074 which may be related. |
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:
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 reproawait 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? |
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
Here's my full code: Full repro of aboveModel.csusing 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 caseModel.csusing 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 implementationModel.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:
|
When using a custom derived class for a collection, the
ArrayConversionExpression
method throws anIndexOutOfRangeException
when it attempts to get the (missing) generic type argument.efcore.pg/src/EFCore.PG/Storage/ValueConversion/NpgsqlArrayConverter.cs
Line 142 in 35f0fd5
Example use case:
I think this could be resolved by getting the generic type arguments of the base class if
TInput
doesn't have any:The text was updated successfully, but these errors were encountered: