Skip to content

NullReferenceException in shaper for tuple query when a query-filtered entity is reached via both a scalar and collection navigation #3933

Description

@stevendarby

We found this while migrating a production application from SQL Server to PostgreSQL -- a query that had worked correctly on SQL Server started throwing this exception against efcore.pg.

Include your code

using Microsoft.EntityFrameworkCore;
using Npgsql;
using Testcontainers.MsSql;
using Testcontainers.PostgreSql;

var sqlServerContainer = new MsSqlBuilder().Build();
var postgresContainer = new PostgreSqlBuilder().Build();

await sqlServerContainer.StartAsync();
await postgresContainer.StartAsync();

await Run("sqlserver", new SqlServerReproContext(sqlServerContainer.GetConnectionString()));
await Run("postgres", new PostgresReproContext(postgresContainer.GetConnectionString()));

await sqlServerContainer.DisposeAsync();
await postgresContainer.DisposeAsync();

static async Task Run(string label, ReproContext context)
{
    await using (context)
    {
        await context.Database.EnsureCreatedAsync();

        const short OrgId = 1;
        var orderIdentifier = Guid.NewGuid();

        var representative = new Representative { Id = 1, OrgId = OrgId, IsActive = true };
        var vendor = new Vendor { Id = 1, OrgId = OrgId, IsActive = true, Representatives = [representative] };
        var vendorLink = new VendorLink { Id = 1, OrgId = OrgId, Vendor = vendor };
        var order = new Order { Id = 1, OrgId = OrgId, OrderIdentifier = orderIdentifier, VendorLinks = [vendorLink] };

        context.Orders.Add(order);
        await context.SaveChangesAsync();

        Console.WriteLine($"--- {label} ---");

        try
        {
            var result = await context.Orders
                .Where(x => x.OrderIdentifier == orderIdentifier)
                .Select(x => ValueTuple.Create(
                    x.Id,
                    x.VendorLinks.Any(vl => vl.Vendor.IsActive && vl.Vendor.Representatives.Any(r => r.IsActive))))
                .FirstOrDefaultAsync();

            Console.WriteLine($"SUCCESS: ({result.Item1}, {result.Item2})");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"FAILED: {ex.GetType().Name}: {ex.Message}");
            Console.WriteLine(ex.StackTrace);
        }

        Console.WriteLine();
    }
}

public class Representative
{
    public int Id { get; set; }
    public short OrgId { get; set; }
    public Guid RepresentativeIdentifier { get; set; }
    public int VendorId { get; set; }
    public bool IsActive { get; set; }
}

public class Vendor
{
    public int Id { get; set; }
    public short OrgId { get; set; }
    public Guid VendorIdentifier { get; set; }
    public bool IsActive { get; set; }
    public List<Representative> Representatives { get; set; } = [];
}

public class VendorLink
{
    public int Id { get; set; }
    public short OrgId { get; set; }
    public Guid VendorLinkIdentifier { get; set; }
    public int OrderId { get; set; }
    public int? VendorId { get; set; }
    public Vendor? Vendor { get; set; }
}

public class Order
{
    public int Id { get; set; }
    public short OrgId { get; set; }
    public Guid OrderIdentifier { get; set; }
    public List<VendorLink> VendorLinks { get; set; } = [];
}

public abstract class ReproContext : DbContext
{
    public short OrgId { get; set; } = 1;

    public DbSet<Order> Orders => Set<Order>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Representative>().HasKey(e => new { e.OrgId, e.Id });

        modelBuilder.Entity<Vendor>(builder =>
        {
            builder.HasKey(e => new { e.OrgId, e.Id });
            builder
                .HasMany(v => v.Representatives)
                .WithOne()
                .HasForeignKey(r => new { r.OrgId, r.VendorId })
                .IsRequired();

            modelBuilder.Entity<Vendor>().HasQueryFilter(x => x.OrgId == OrgId);
        });

        modelBuilder.Entity<VendorLink>(builder =>
        {
            builder.HasKey(e => new { e.OrgId, e.Id });
            builder
                .HasOne(vl => vl.Vendor)
                .WithMany()
                .HasForeignKey(vl => new { vl.OrgId, vl.VendorId })
                .IsRequired(false);
        });

        modelBuilder.Entity<Order>(builder =>
        {
            builder.HasKey(e => new { e.OrgId, e.Id });
            builder
                .HasMany(o => o.VendorLinks)
                .WithOne()
                .HasForeignKey(vl => new { vl.OrgId, vl.OrderId })
                .IsRequired();
        });
    }
}

public sealed class SqlServerReproContext(string connectionString) : ReproContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder.UseSqlServer(connectionString);
}

public sealed class PostgresReproContext(string connectionString) : ReproContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString);
        dataSourceBuilder.EnableRecordsAsTuples();
        optionsBuilder.UseNpgsql(dataSourceBuilder.Build());
    }
}

Include the output of your code

Against SQL Server, the query returns (1, True) as expected.

Against Npgsql, it throws:

System.NullReferenceException: Object reference not set to an instance of an object.
   at Microsoft.EntityFrameworkCore.Query.RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.CreateGetValueExpression(ParameterExpression dbDataReader, Int32 index, Boolean nullable, RelationalTypeMapping typeMapping, Type type, IPropertyBase property)
   at Microsoft.EntityFrameworkCore.Query.RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.VisitExtension(Expression extensionExpression)
   at System.Linq.Expressions.ExpressionVisitor.VisitUnary(UnaryExpression node)
   at Microsoft.EntityFrameworkCore.Query.RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.ProcessShaper(Expression shaperExpression, Expression& relationalCommandResolver, IReadOnlyList`1& readerColumns, LambdaExpression& relatedDataLoaders, Int32& collectionId)
   at Microsoft.EntityFrameworkCore.Query.RelationalShapedQueryCompilingExpressionVisitor.VisitShapedQuery(ShapedQueryExpression shapedQueryExpression)
   ...

Include provider and version information

EF Core version: 10.0.12
Npgsql.EntityFrameworkCore.PostgreSQL version: 10.0.3
Database server version: PostgreSQL (tested via Testcontainers' default image); also reproduces against Aurora PostgreSQL
Operating system: Windows (also seen in Linux-hosted production)
IDE: n/a (dotnet run)

Further technical details

Removing the query filter on Vendor, or splitting the tuple-projecting query into two separate non-tuple queries, both make it work again.

Activity

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions