-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Description
Bug description
We have found that query performance degrades dramatically when using PrimitiveCollections with a complex ValueConverter. What takes 10 seconds under EF8 now takes over a minute in EF10.
This is a minimal reproduction that mimics our production code - we're seeing this same performance degradation in our real application, making it impossible to upgrade EF versions.
The Scenario
We were able to reproduce the issue in a minimal project: https://github.com/desruc/ef-valueconverter-performance-issue
Here's an overview of the reproduction and test results:
The Pattern
We use value objects in our domain, like this:
public class TagId
{
public string Value { get; }
public TagId(string value) => Value = value;
}
These value objects get stored in primitive collections on owned entities:
public class Child // Owned entity
{
public string ChildId { get; set; }
public List<TagId> TagIds { get; set; }
}
public class Parent
{
public string Id { get; set; }
public IReadOnlyList<Child> Children { get; set; } // Owns many Children
}
To make EF Core work, we use a generic ValueConverter:
public class ReflectionValueConverter<T> : ValueConverter<T, string>
{
public ReflectionValueConverter() : base(
convertToProviderExpression: v => ((TagId)(object)v).Value,
convertFromProviderExpression: v => (T)typeof(T)
.GetConstructor(new[] { typeof(string) })!
.Invoke(new object[] { v })
)
{
}
}
The EF configuration that triggers the issue:
builder.OwnsMany<Child>(x => x.Children, b =>
{
// The performance killer is this line:
b.PrimitiveCollection(p => p.TagIds)
.ElementType()
.HasConversion<ReflectionValueConverter<TagId>>();
});
Test Data
Our reproduction creates:
- 1,000 parent records
- Each parent has 100 children
- Each child has 100 TagId objects
- Total: 10 million primitive collection items
The Query
We run a simple query that loads everything:
await context.Parents.AsSplitQuery().ToListAsync();
Performance Results
EF Version | Time |
---|---|
EF8 (8.0.20) | 10.8 seconds |
EF9 (9.0.9) | 22.8 seconds |
EF10 (10.0.0-rc.1.25451.107) | 1 minute 20.3 seconds |
Note: When we tested the same scenario using List<string>
instead of List<TagId>
(no ValueConverter), all EF versions performed consistently fast. This confirms the regression is specifically related to ValueConverter processing, not PrimitiveCollections in general.
Additional Observation
The performance problem largely goes away when the expression is extracted to a static method:
public class ReflectionValueConverter<T> : ValueConverter<T, string>
{
public ReflectionValueConverter() : base(
convertToProviderExpression: v => ((TagId)(object)v).Value,
convertFromProviderExpression: v => ConvertFromString(v)
)
{
}
static T ConvertFromString(string v) => (T)typeof(T)
.GetConstructor(new[] { typeof(string) })!
.Invoke(new object[] { v });
}
EF Version | Time |
---|---|
EF8 (8.0.20) | 5.7 seconds |
EF9 (9.0.9) | 9.8 seconds |
EF10 (10.0.0-rc.1.25451.107) | 9 seconds |
And as List<string>
with no converter:
EF Version | Time |
---|---|
EF8 (8.0.20) | 5.1 seconds |
EF9 (9.0.9) | 5.4 seconds |
EF10 (10.0.0-rc.1.25451.107) | 3.1 seconds |
EF Core version
9.0.9, 10.0.0-rc.1.25451.107
Database provider
Microsoft.EntityFrameworkCore.SqlServer
Target framework
.NET9.0, .NET10.0
Operating system
MacOS
IDE
Rider 2025.2.0.1