Skip to content

Ef core 10 regression: owned JSON combined with DbFunction #37842

@PaulVrugt

Description

@PaulVrugt

Bug description

In ef core 10 there is a regression preventing us from updating. When using any entity which has an OwnsMany or OwnsOne which is selected via a DbFunction an object reference error occurs while compiling the query

Your code

using Microsoft.EntityFrameworkCore;

namespace EFCore10ReproductionTest;

// ============================================================
// Minimal self-contained reproduction of EF Core 10 bug:
//   NullReferenceException in AddJsonNavigationBindings
//   when combining [DbFunction] with OwnsOne/OwnsMany().ToJson()
//
// Bug report: https://github.com/dotnet/efcore/issues/37842
// ============================================================

// --- Minimal entity with an owned JSON type ---

public class MinimalEntity
{
    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public MinimalOwnedType OwnedData { get; set; } = new();
}

public class MinimalOwnedType
{
    public string? Value { get; set; }
}

// --- Minimal DbContext ---

public class MinimalDbContext : DbContext
{
    public DbSet<MinimalEntity> Entities { get; set; } = null!;

    // [DbFunction] returning IQueryable<MinimalEntity> - this is the trigger
    [DbFunction(Name = "GetEntities", Schema = "dbo")]
    public IQueryable<MinimalEntity> GetEntities() => FromExpression(() => GetEntities());

    protected override void OnConfiguring(DbContextOptionsBuilder builder)
    {
        // Must use SQL Server provider - the InMemory provider does NOT trigger the bug
        builder.UseSqlServer("Server=fake;Database=fake;Trusted_Connection=yes;Connect Timeout=1");
        builder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<MinimalEntity>(b =>
        {
            b.HasKey(x => x.Id);
            b.ToTable("MinimalEntities");

            // OwnsOne with ToJson() - this is the second ingredient of the bug
            b.OwnsOne(x => x.OwnedData, o => o.ToJson("owned_data"));
        });
    }
}

// --- Reproduction tests ---

[TestClass]
public class MinimalReproductionTests
{
    private static async Task<Exception?> CatchAnyExceptionAsync(Func<Task> action)
    {
        try { await action(); return null; }
        catch (Exception ex) { return ex; }
    }

    /// <summary>
    /// BUG: Querying via a [DbFunction] method on an entity that has OwnsOne().ToJson()
    /// throws NullReferenceException in EF Core 10 during query compilation.
    ///
    /// Root location: RelationalQueryableMethodTranslatingExpressionVisitor.AddJsonNavigationBindings
    ///
    /// Works fine in EF Core 9. Works fine in EF Core 10 when using DbSet directly (without DbFunction).
    /// </summary>
    [TestMethod]
    public async Task DbFunction_Plus_OwnsOneToJson_Throws_NullReferenceException()
    {
        using var ctx = new MinimalDbContext();

        // Using the [DbFunction] method (not ctx.Entities directly)
        var query = ctx.GetEntities().Select(x => new { x.Id, x.Name, x.OwnedData });

        var ex = await CatchAnyExceptionAsync(() => query.ToListAsync());

        Assert.IsNotNull(ex, "Expected NullReferenceException but no exception was thrown");
        Assert.IsInstanceOfType<NullReferenceException>(ex,
            $"Expected NullReferenceException in AddJsonNavigationBindings but got: {ex.GetType().Name}: {ex.Message}\n{ex.StackTrace}");
    }

    /// <summary>
    /// Confirms the bug does NOT occur when using DbSet directly (without DbFunction).
    /// Same entity, same query shape - only difference is DbSet vs DbFunction.
    /// This test should fail with SqlException (can't connect to 'fake' server) - NOT NullReferenceException.
    /// </summary>
    [TestMethod]
    public async Task DbSet_Plus_OwnsOneToJson_Does_Not_Throw_NullReferenceException()
    {
        using var ctx = new MinimalDbContext();

        // Same query but using DbSet directly - should NOT crash in query compilation
        var query = ctx.Entities.Select(x => new { x.Id, x.Name, x.OwnedData });

        var ex = await CatchAnyExceptionAsync(() => query.ToListAsync());

        Assert.IsNotNull(ex, "Expected SqlException (connection refused) but no exception was thrown");
        Assert.IsNotInstanceOfType<NullReferenceException>(ex,
            "Bug is also present in DbSet path - expected only in DbFunction path");
        // Expected: Microsoft.Data.SqlClient.SqlException - cannot connect to 'fake'
        Console.WriteLine($"Got expected non-null exception: {ex.GetType().Name}: {ex.Message}");
    }

    /// <summary>
    /// Confirms OwnsMany().ToJson() also triggers the bug (not just OwnsOne).
    /// </summary>
    [TestMethod]
    public async Task DbFunction_Plus_OwnsManyToJson_Also_Throws_NullReferenceException()
    {
        using var ctx = new MinimalDbContextWithOwnsMany();

        var query = ctx.GetEntities().Select(x => new { x.Id, x.Items });

        var ex = await CatchAnyExceptionAsync(() => query.ToListAsync());

        Assert.IsNotNull(ex, "Expected NullReferenceException but no exception was thrown");
        Assert.IsInstanceOfType<NullReferenceException>(ex,
            $"Expected NullReferenceException in AddJsonNavigationBindings but got: {ex.GetType().Name}: {ex.Message}");
    }
}

// --- Variant with OwnsMany ---

public class MinimalEntityWithMany
{
    public int Id { get; set; }
    public ICollection<MinimalOwnedType> Items { get; set; } = null!;
}

public class MinimalDbContextWithOwnsMany : DbContext
{
    public DbSet<MinimalEntityWithMany> Entities { get; set; } = null!;

    [DbFunction(Name = "GetEntities", Schema = "dbo")]
    public IQueryable<MinimalEntityWithMany> GetEntities() => FromExpression(() => GetEntities());

    protected override void OnConfiguring(DbContextOptionsBuilder builder)
    {
        builder.UseSqlServer("Server=fake;Database=fake;Trusted_Connection=yes;");
        builder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<MinimalEntityWithMany>(b =>
        {
            b.HasKey(x => x.Id);
            b.ToTable("MinimalEntitiesWithMany");
            b.OwnsMany(x => x.Items, o => o.ToJson("items"));
        });
    }
}

Stack traces

System.NullReferenceException: Object reference not set to an instance of an object.
   at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.AddJsonNavigationBindings(IEntityType entityType, StructuralTypeProjectionExpression projection, Dictionary`2 propertyExpressions, Dictionary`2 tableMap)
   at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.CreateSelect(IEntityType entityType, TableExpressionBase tableExpressionBase)
   at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.VisitExtension(Expression extensionExpression)
   at Microsoft.EntityFrameworkCore.SqlServer.Query.Internal.SqlServerQueryableMethodTranslatingExpressionVisitor.VisitExtension(Expression extensionExpression)
   at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.Translate(Expression expression)
   at Microsoft.EntityFrameworkCore.Query.QueryCompilationContext.CreateQueryExecutorExpression[TResult](Expression query)
   at Microsoft.EntityFrameworkCore.Query.QueryCompilationContext.CreateQueryExecutor[TResult](Expression query)
   at Microsoft.EntityFrameworkCore.Storage.Database.CompileQuery[TResult](Expression query, Boolean async)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.CompileQueryCore[TResult](IDatabase database, Expression query, IModel model, Boolean async)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.<>c__DisplayClass11_0`1.<ExecuteCore>b__0()
   at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func`1 compiler)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.ExecuteCore[TResult](Expression query, Boolean async, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.ExecuteAsync[TResult](Expression query, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.ExecuteAsync[TResult](Expression expression, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1.GetAsyncEnumerator(CancellationToken cancellationToken)
   at System.Runtime.CompilerServices.ConfiguredCancelableAsyncEnumerable`1.GetAsyncEnumerator()
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()

Verbose output


EF Core version

10.0.3

Database provider

Microsoft.EntityFrameworkCore.SqlServer

Target framework

.Net 10

Operating system

Windows 11

IDE

Visual studio 2026 18.2.1

Metadata

Metadata

Assignees

Type

No fields configured for Bug.

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions