InMemory database throwing NullReferenceException when using a nullable value-converted property in a query
See original GitHub issueWhen upgrading our application to EF Core 5, we encountered problems when using the InMemory database on an entity class that contains a (non-required) property which is value-converted from string to a custom data class.
We managed to reduce our production code to a very simplified program that exhibits the problem.
Any help with this is greatly appreciated!
Reproduction code
The following program works with EF Core 3.1 but breaks with EF Core 5.0 and InMemoryDb:
using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
public static class Program
{
static void Main(string[] args)
{
// Use SQL server -- this works
var sqlContext = new FooDbContext(new DbContextOptionsBuilder<FooDbContext>()
.UseSqlServer("Server=(localdb)\\MSSQLLocalDB;Database=EfCoreIssueTestDb").Options);
Test(sqlContext);
// Use InMemoryDB -- this breaks
var inMemoryContext = new FooDbContext(new DbContextOptionsBuilder<FooDbContext>().UseInMemoryDatabase("EfCoreIssueTestDb").Options);
Test(inMemoryContext);
}
private static void Test(FooDbContext context)
{
Console.WriteLine($"Testing with database provider {context.Database.ProviderName}");
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
context.Device.Add(new Device { NetworkId = null });
context.Device.Add(new Device { NetworkId = new NetworkId("x") });
context.Device.Add(new Device { NetworkId = new NetworkId("y") });
context.Device.Add(new Device { NetworkId = null });
context.Device.Add(new Device { NetworkId = null });
context.SaveChanges();
Console.WriteLine($"We have {context.Device.Count(_ => _.NetworkId != null)} Devices with a NetworkId.");
Console.WriteLine($"We have {context.Device.Count(_ => _.NetworkId == null)} Devices without a NetworkId.");
Console.WriteLine();
}
}
public class FooDbContext : DbContext
{
public FooDbContext(DbContextOptions<FooDbContext> options) : base(options)
{
}
public DbSet<Device> Device { get; set; }
protected override void OnModelCreating(ModelBuilder model)
{
base.OnModelCreating(model);
model.Entity<Device>(entity =>
{
entity.HasKey(_ => _.Id);
entity.Property(_ => _.NetworkId)
.HasConversion(new ValueConverter<NetworkId, string>(
bar => bar != null ? bar.ToString() : null,
str => str != null ? new NetworkId(str) : null));
});
}
}
public class Device
{
public int Id { get; set; }
public NetworkId NetworkId { get; set; }
}
public class NetworkId
{
private readonly string _eui;
public NetworkId(string eui) => _eui = eui;
public override string ToString() => _eui;
private bool Equals(NetworkId other) => other != null && Equals(_eui, other._eui);
public override bool Equals(object obj) => ReferenceEquals(this, obj) || (obj is NetworkId other && Equals(other));
public override int GetHashCode() => _eui != null ? _eui.GetHashCode() : 0;
public static NetworkId FromString(string s) => s != null ? new NetworkId(s) : null;
public static bool operator ==(NetworkId left, NetworkId right)
{
return Equals(left, right);
}
public static bool operator !=(NetworkId left, NetworkId right)
{
return !Equals(left, right);
}
public static explicit operator string(NetworkId networkId)
{
return networkId?.ToString();
}
public static explicit operator NetworkId(string s)
{
return FromString(s);
}
}
Project file:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="5.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.9" />
</ItemGroup>
</Project>
Output with EF Core version 5.0.9
Testing with database provider Microsoft.EntityFrameworkCore.SqlServer
We have 2 Devices with a NetworkId.
We have 3 Devices without a NetworkId.
Testing with database provider Microsoft.EntityFrameworkCore.InMemory
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
at lambda_method(Closure , ValueBuffer )
at System.Linq.Enumerable.WhereListIterator`1.GetCount(Boolean onlyIfCheap)
at lambda_method(Closure )
at Microsoft.EntityFrameworkCore.InMemory.Query.Internal.InMemoryQueryExpression.ResultEnumerable.GetEnumerator()
at System.Linq.Enumerable.SelectEnumerableIterator`2.MoveNext()
at Microsoft.EntityFrameworkCore.InMemory.Query.Internal.InMemoryShapedQueryCompilingExpressionVisitor.QueryingEnumerable`1.Enumerator.MoveNextHelper()
at Microsoft.EntityFrameworkCore.InMemory.Query.Internal.InMemoryShapedQueryCompilingExpressionVisitor.QueryingEnumerable`1.Enumerator.MoveNext()
at System.Linq.Enumerable.Single[TSource](IEnumerable`1 source)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.Execute[TResult](Expression query)
at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.Execute[TResult](Expression expression)
at System.Linq.Queryable.Count[TSource](IQueryable`1 source, Expression`1 predicate)
at Program.Test(FooDbContext context) in C:\dev\hagleitner\scratch\EFCoreIssue\Program.cs:line 33
at Program.Main(String[] args) in C:\dev\hagleitner\scratch\EFCoreIssue\Program.cs:line 17
Program output with EF Core version 3.1.18
Testing with database provider Microsoft.EntityFrameworkCore.SqlServer
We have 2 Devices with a NetworkId.
We have 3 Devices without a NetworkId.
Testing with database provider Microsoft.EntityFrameworkCore.InMemory
We have 2 Devices with a NetworkId.
We have 3 Devices without a NetworkId.
Further observations
-
The problem goes away when removing the
public override bool Equals(object obj)
method from theNetworkId
class. -
However, stepping through with a debugger reveals that neither of the Equals method throw.
-
Interestingly, the problem still persists when changing the overridden Equals method to
public override bool Equals(object obj) => base.Equals(obj)
Issue Analytics
- State:
- Created 2 years ago
- Comments:6 (5 by maintainers)
https://github.com/dotnet/efcore/blob/eb9a2b8ac70dc1736e49302d19d76ad3186b4744/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs#L215-L221
My bet is that we shouldn’t pass
binaryExpression.Method
arg if there is a property on left/right which has value converter but no value comparer as it would be using wrong methodInfo to compare even after conversion.Thanks @ajcvickers for providing the workaround! This enabled us to finish our upgrade to EF Core 5.