dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.77k stars 3.18k forks source link

NullReferenceException when a hierarchy has OwnedEntity #30629

Open vflame opened 1 year ago

vflame commented 1 year ago

File a bug

NullReferenceException thrown when entity hierarchy has owned entity when performing select projection. Not sure if related to: https://github.com/dotnet/efcore/issues/30107 and https://github.com/dotnet/efcore/issues/30373

Can provide repro project if needed

Include your code

Entities/Configs

 public record OwnedEntity
    {
        public string Mode { get; private set; }
    }
    public class ConcreteWithOwned : BaseEntity
    {
        private class ConcreteWithOwnedConfig : IEntityTypeConfiguration<ConcreteWithOwned>
        {
            public void Configure(EntityTypeBuilder<ConcreteWithOwned> builder)
            {
                builder.OwnsOne(p => p.Owned, c =>
                {
                });
            }
        }

        public OwnedEntity Owned { get; set; }
    }
    public abstract class BaseEntity
    {
        private class BaseEntityEFConfiguration : IEntityTypeConfiguration<BaseEntity>
        {
            public void Configure(EntityTypeBuilder<BaseEntity> builder)
            {
            }
        }

        public string Id { get; init; } = Guid.NewGuid().ToString();

        public ComputedResult ComputeReturnNull()
        {
            return null;
        }

        public ComputedResult ComputeReturnValue()
        {
            return new();
        }
    }
    public class ComputedResult
    {
        public string Value { get; set; } = "VALUE";
    }

    public class ConcreteEntity : BaseEntity
    {
        private class ConcreteEntityEFConfiguration : IEntityTypeConfiguration<ConcreteEntity>
        {
            public void Configure(EntityTypeBuilder<ConcreteEntity> builder)
            {

            }
        }
    }
var entity1 = new ConcreteWithOwned();
var entity2 = new ConcreteEntity();

dbcontext.Add(entity1);
dbcontext.Add(entity2);

dbcontext.SaveChanges();
dbcontext.ChangeTracker.Clear();

//concrete without owned projection
var x3 = dbcontext.Set<ConcreteEntity>()
           .Select(p => new
           {
               Computed1 = p.ComputeReturnValue() == null ? null : p.ComputeReturnValue().Value, //does not throw
               Computed2 = p.ComputeReturnNull() == null ? null : p.ComputeReturnNull().Value, //does not throw
           }).ToList();

//concrete with owned projection throws NRE
var x1 = dbcontext.Set<ConcreteWithOwned>()
           .Select(p => new
           {
               Computed1 = p.ComputeReturnValue() == null ? null : p.ComputeReturnValue().Value, //does not throw with non-null return
               Computed2 = p.ComputeReturnNull() == null ? null : p.ComputeReturnNull().Value,  //NRE thrown when return is null
           }).ToList();

//base projection throws NRE
var x2 = dbcontext.Set<BaseEntity>()
           .Select(p => new
           {
               Computed1 = p.ComputeReturnValue() == null ? null : p.ComputeReturnValue().Value, //does not throw with non-null return
               Computed2 = p.ComputeReturnNull() == null ? null : p.ComputeReturnNull().Value, //NRE thrown when return is null
           }).ToList();

Include provider and version information

EF Core version: 7.0.4 Database provider: Microsoft.EntityFrameworkCore.SqlServer & Npgsql Target framework: .NET 7.0 Operating system: WIN10 IDE: Visual Studio 2022

ajcvickers commented 1 year ago

Note for triage: I am able to reproduce this. Looks like a potential issue with client evaluation in the final projection.

tuggernuts commented 1 year ago

Is this the same issue?

public class DateRange 
{
     public DateTime Start {get;set;}
     public DateTime End {get;set;}
     // various datetime helper methods, properties
}

public class ContractPeriod
{
    public int Id {get;set;}
    public int ContractId {get;set;}    
    public DateRange Duration {get;set;} // etc...
   public DateTime StartDate => Duration.Start;
}

// in dbContext
public override void Configure(EntityTypeBuilder<ContractPeriod> builder)
{
     builder.OwnsOne(e => e.Duration);
}

public class PeriodCollection : ObservableCollection<ContractPeriod>
{
    protected override void InsertItem(int index, ContractPeriod item)
    {
        if (item == null) return;
        if (Items.Count != 0)
        {
            index = _binarySearch(item.StartDate);
        }
        base.InsertItem(index, item);
    }

    private int _binarySearch(DateTime date)
    {
        var lower = 0;
        var upper = Items.Count - 1;
        while (lower <= upper)
        {
            var middle = lower + (upper - lower) / 2;
            var result = DateTime.Compare(date, Items[middle].StartDate);
            if (result == 0)
                return middle;
            if (result < 0)
                upper = middle - 1;
            else
                lower = middle + 1;
        }
        return lower;
    }
}

public class Contract 
{
    public int Id {get;set;}
    public string Buyer {get;set;}
    public string Terms {get;set;} // etc...

    private PeriodCollection _periods = new PeriodCollection();
    public IEnumerable<ContractPeriod> Periods => _periods;
}
await AppData.Contracts
            .Include(c => c.Periods)
            .FirstAsync();

I will receive a System.NullReferenceException: 'Object reference not set to an instance of an object.' at the InsertItem() of the custom collection. The ContractPeriod.Duration property is null. If I use a plain ObservableCollection or List, and do my sorting later, I get no such exception. It seems the Owned Entity is added to owner after the owner entity is added to the collection.

Please advise. I can open a separate issue if warranted. Thanks.

Jerry