efcore icon indicating copy to clipboard operation
efcore copied to clipboard

NullReferenceException getting navigational property just set (using lazy loading)

Open codetuner opened this issue 1 year ago • 3 comments

In the following code a product is created, then an orderline for that product is created.

On line 39 (marked with /*39*/) line.Product is set with a non-null value. On line 40, the line.Product is dereferrenced and results in a NullReferenceException. This should not be the case. "line.Product" should not be null at this stage.

The project uses an SQLite in-memory database with lazy loading and change tracking proxies.

using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

public static class Program
{
    static OrderContext Context = new OrderContext();
    static int prod1Id;

    public static void Main(string[] args)
    {
        Context.Database.EnsureDeleted();
        Context.Database.EnsureCreated();

        CreateProduct();
        AddOrderline();
    }

    public static void CreateProduct()
    {
        // Create and store a product with Id=1:
        var prod = Context.Products.CreateProxy();
        prod.Name = "FooBar";
        prod.CatalogPrice = 12.99m;
        Context.Products.Add(prod);
        Context.SaveChanges();
        prod1Id = prod.Id;
        Console.WriteLine(prod1Id);
    }

    public static void AddOrderline()
    {
        // Get a product:
        var prod = Context.Products.Find(prod1Id)!;

        // Create orderline:
        var line = Context.OrderLines.CreateProxy();
/*39*/  line.Product = prod;
/*40*/  var cp = line.Product.CatalogPrice;
        line.OrderPrice = cp;
        Context.OrderLines.Add(line);

        Context.SaveChanges();
        Console.WriteLine(line.OrderPrice);
    }
}

public class OrderContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        var connection = new SqliteConnection("Filename=:memory:");
        connection.Open();
        optionsBuilder.UseSqlite(connection);
        optionsBuilder.UseChangeTrackingProxies();
        optionsBuilder.UseLazyLoadingProxies();
    }

    public virtual DbSet<OrderLine> OrderLines { get; set; }
    public virtual DbSet<Product> Products { get; set; }
}

public class OrderLine
{
    [Key]
    public virtual int Id { get; set; }

    public virtual int ProductId { get; set; }

    [ForeignKey(nameof(ProductId))]
    public virtual required Product Product { get; set; }

    public virtual decimal OrderPrice { get; set; }
}

public class Product
{
    [Key]
    public virtual int Id { get; set; }

    public virtual required string Name { get; set; }

    public virtual decimal CatalogPrice { get; set; }
}

Target framework: .NET 8.0 Included package references:

<ItemGroup>
	<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.1" />
	<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="8.0.1" />
	<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.1" />
	<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.1">
		<PrivateAssets>all</PrivateAssets>
		<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
	</PackageReference>
</ItemGroup>

Exception thrown:

Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
   at Program.AddOrderline3() in C:\Stuff\EfCoreLazyAddOrder\EfCoreLazyAddOrder\Program.cs:line 40
   at Program.Main(String[] args) in C:\Stuff\EfCoreLazyAddOrder\EfCoreLazyAddOrder\Program.cs:line 17

codetuner avatar Feb 11 '24 13:02 codetuner

Note for team: This is a consequence of lazy-loading for detached entities, which is kicking in even though the navigation is non null. Still repros on the latest daily.

ajcvickers avatar Feb 12 '24 10:02 ajcvickers

@ajcvickers You say it is a consequence of lazy-loading for detached entities. Would the suggestion a made in issue 33041 be a solution for this too ? My suggestion is that when a detached (but to a context associated entity) performs a lazy loading operation, that (lazy) loaded entity would be attached to (and tracked by) the loading context.

codetuner avatar Feb 14 '24 11:02 codetuner

My suggestion is that when a detached (but to a context associated entity) performs a lazy loading operation, that (lazy) loaded entity would be attached to (and tracked by) the loading context.

No, this would be wrong. The loaded entity instance should not be tracked just because it has been loaded. These are two separate behaviors.

ajcvickers avatar Feb 14 '24 11:02 ajcvickers