Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Loading entity from DB does not properly load owned entities (In memory provider) #23687

Closed
stefangrasboeck opened this issue Dec 15, 2020 · 9 comments
Assignees
Labels
closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported regression Servicing-approved type-bug
Milestone

Comments

@stefangrasboeck
Copy link

Description

When loading a entity from the DB, some owned entities are not loaded when using the InMemory provider. This does only occur with the InMemory provider. In the code sample below you can find that line: device.DistributorAssignments.First().CustomerId results in null

Expected Behaviour:
device.DistributorAssignments.First().CustomerId should not be null but be loaded correctly.

Code to reproduce

using (var context = new SampleContext())
{
	context.ChangeTracker.Tracked += (sender, args) => {	};
	context.Database.EnsureDeleted();
	context.Database.EnsureCreated();
	context.SaveChanges();
}

int id = -1;
using (var context = new SampleContext())
{
	var entity = await context.AddAsync(new Product());
	entity.Entity.DistributorAssignments.Add(new ProductDistributorAssignment(new CustomerId(1, 1)));
	context.SaveChanges();
	id = entity.Entity.Id;
}

using(var context = new SampleContext())
{
	var device = await context.Products.FindAsync(id);
	Console.WriteLine($"Customer ID is null here: {device.DistributorAssignments.First().CustomerId}");
}

class SampleContext : DbContext
{
	public DbSet<Product> Products { get; set; }

	public SampleContext() { }

	protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
	{
		base.OnConfiguring(optionsBuilder);
		optionsBuilder.LogTo(LINQPad.Util.SqlOutputWriter.WriteLine, new[] { RelationalEventId.CommandExecuting });
		optionsBuilder.UseInMemoryDatabase("databaseName");
	}

	protected override void OnModelCreating(ModelBuilder modelBuilder)
	{
		base.OnModelCreating(modelBuilder);
		var builder = modelBuilder.Entity<Product>();

		builder.OwnsMany(x => x.DistributorAssignments, assignment =>
		{
			assignment.ToTable("ProductDistributors");

			assignment.Property(x => x.Assigned);
			assignment.OwnsOne(x => x.CustomerId!, customerId =>
			{
				customerId.Property(x => x.TenantId);
				customerId.Property(x => x.CustomerNumber);
			});
		});
	}
}

public class ProductDistributorAssignment
{
	private ProductDistributorAssignment() { }
	
	public ProductDistributorAssignment(CustomerId distributor)
	{
		CustomerId = distributor;
		Assigned = DateTime.Now;
	}

	public CustomerId CustomerId { get; }
	public DateTime Assigned { get; }
}

public class Product
{
	public int Id {get; set;}
	public List<ProductDistributorAssignment> DistributorAssignments = new List<ProductDistributorAssignment>();
}

public class CustomerId
{
	public CustomerId(int tenantId, int customerNumber)
	{
		TenantId = tenantId;
		CustomerNumber = customerNumber;
	}

	public int TenantId { get; }
	public int CustomerNumber { get; }
}

Provider and version information

EF Core version: 5.0.1
Database provider: Microsoft.EntityFrameworkCore.InMemory 5.0.1
Target framework: NET 5.0
Operating system: Win 10
IDE: Visual Studio 2019 16.8.3

@ajcvickers
Copy link
Contributor

Note for triage: I am able to reproduce this; it looks like Includes for owned collections are not working here with in the in-memory database. Adding the Includes explicitly doesn't help, so it doesn't look like it's an issue with auto-includes specifically. Verified that the same thing works with SQL Server.

@smitpatel
Copy link
Contributor

Adding the Includes explicitly doesn't help, so it doesn't look like it's an issue with auto-includes specifically.

Makes me question if data is getting saved correctly in the InMemoryTables.

@ajcvickers
Copy link
Contributor

@smitpatel Could be--I didn't check that.

@J-Yen
Copy link

J-Yen commented Dec 16, 2020

I have a similar issue with my use case reproduced in the sample below. Querying on the owned type isn't returning the expected result. I only have the issue with InMemoryDatabase and not with SQL provider. When removing the composite primary key it's working as expected.

using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;

namespace EFCoreTrackedIssue
{
    class Program
    {
        static void Main(string[] args)
        {
            const int id2 = 1;
            using (var dbContext = new ExampleContext())
            {
                var aggregateRoot = new AnAggregateRoot
                {
                    Id2 = id2,
                    AProp = "Test",
                    OwnedDependent = new OwnedDependent
                    {
                        HasEnded = false
                    }
                };
                dbContext.AggregateRoots.Add(aggregateRoot);
                dbContext.SaveChanges();
            }
            

            using (var dbContext = new ExampleContext())
            {
                var test1 = dbContext.AggregateRoots.Include(x => x.OwnedDependent).Where(x => x.Id2 == id2).ToArray();
                Console.WriteLine("Test1 Count: {0}", test1.Length); // Output: Test1 Count: 1
                Console.WriteLine("Value OwnedDependent: {0}", test1.Single().OwnedDependent); // Output: Value OwnedDependent: 'null' => expects instance
                
                var test2 = dbContext.AggregateRoots.Include(x => x.OwnedDependent).Where(x => x.Id2 == id2 && x.OwnedDependent.HasEnded == false).ToArray();
                Console.Write("Test2 Count: {0}", test2.Length); // Output: Test2 Count: 0 => expects 1
            }

            Console.ReadLine();
        }

        public class ExampleContext : DbContext
        {
            protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
                => optionsBuilder.UseInMemoryDatabase("MyTest");
            public DbSet<AnAggregateRoot> AggregateRoots { get; set; }

            protected override void OnModelCreating(ModelBuilder mb)
            {
                base.OnModelCreating(mb);

                mb.Entity<AnAggregateRoot>().HasKey(e => new{e.Id1, e.Id2});
                mb.Entity<AnAggregateRoot>().Property(e => e.Id1).ValueGeneratedOnAdd();
                mb.Entity<AnAggregateRoot>().Property(e => e.Id2).ValueGeneratedNever();
                mb.Entity<AnAggregateRoot>().OwnsOne(e => e.OwnedDependent);
            }
        }
    }

    public class AnAggregateRoot
    {
        public int Id1 { get; set; }
        public int Id2 { get; set; }
        public string AProp { get; set; }
        public OwnedDependent OwnedDependent{ get; set; }
    }

    public class OwnedDependent
    {
        public bool HasEnded { get; set; }
    }
}

@almostchristian
Copy link

almostchristian commented Dec 18, 2020

I am also facing a similar issue with EF SqlServer 5.0.1. I have a Parent type with a child owned type, and the child owned type has a collection of owned types Grandchildren. If all the other properties in the ChildType aside from the grandchildren collection are null, the grandchildren will fail to load and the Child property is null. I have confirmed in the logs that the grandchildren are queried, they are just not loaded into the object. Below is a simple repro code:

public class ResourceType
{
    public int Id { get; set; }
    public ChildType Child { get; set; }
}

[Owned]
public class ChildType
{
    public string Text { get; set; }
    public ICollection<GrandchildType> Grandchildren { get; set; }
}

[Owned]
public class GrandchildType
{
    public string Value { get; set; }
}

int id1, id2;
using (var dbContext = new TestDbContext())
{
    var resource1 = new ResourceType
    {
        Child = new ChildType
        {
            Text = "hello",
            Grandchildren = new List<GrandchildType>
            {
                new GrandchildType{ Value= "hello" }
            }
        }
    };
    var resource2 = new ResourceType
    {
        Child = new ChildType
        {
            Text = null,
            Grandchildren = new List<GrandchildType>
            {
                new GrandchildType{ Value= "hello" }
            }
        }
    };
    dbContext.Resources.AddRange(resource1, resource2);
    await dbContext.SaveChangesAsync();
    id1 = resource1.Id;
    id2 = resource2.Id;
}

using (var dbContext = new TestDbContext())
{
    var resource1 = await dbContext.Resources.SingleOrDefaultAsync(x => x.Id == id1);
    var resource2 = await dbContext.Resources.SingleOrDefaultAsync(x => x.Id == id2);
    if (resource1.Child == null)
    {
        Console.WriteLine("Child missing from resource 1");
    }
    else if (resource2.Child == null)
    {
        Console.WriteLine("Child missing from resource 2");
    }
    else
    {
        Console.WriteLine("All ok");
    }
}

public class TestDbContext : DbContext
{
    public TestDbContext()
        : base(new DbContextOptionsBuilder<TestDbContext>()
              .UseSqlServer("Server=(localdb)\\MSSQLLocalDB;Database=TestOwnedType;Trusted_Connection=True;MultipleActiveResultSets=true")
              .Options) { Database.EnsureCreated(); }

    public DbSet<ResourceType> Resources { get; set; }
}

@ajcvickers
Copy link
Contributor

/cc @smitpatel

@smitpatel
Copy link
Contributor

Regression introduced in 58a07b6

CreateKeyValueReadExpression used to inject anonymous object struct. Which made comparison happen correctly between owned reference navigation. We removed it in 5.0 so now it is 2 object[] instances which never matches. This code path is only hit for composite key scenario.

@almostchristian - Your issue is different. This issue only happens for InMemory provider. You are using SqlServer provider. See #23198 (comment)

@smitpatel smitpatel added Servicing-consider closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. labels Dec 18, 2020
smitpatel added a commit that referenced this issue Dec 18, 2020
Resolves #23687

Composite key selector needs wrapper so that we compare them component-wise
@ajcvickers ajcvickers added this to the 5.0.3 milestone Dec 18, 2020
@Kolky
Copy link

Kolky commented Dec 21, 2020

Ran into the same issue when upgrading to 5.0.1 from 3.1.6; some of my unit-tests failed. Any ETA on 5.0.3?

@ajcvickers ajcvickers added Servicing-approved and removed closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. Servicing-consider labels Jan 17, 2021
@J-Yen
Copy link

J-Yen commented Feb 16, 2021

Similar issue but with SQL Server Provider: #24165

This was referenced Mar 15, 2021
@ajcvickers ajcvickers added the closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. label Apr 28, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported regression Servicing-approved type-bug
Projects
None yet
Development

No branches or pull requests

6 participants