Skip to content

Use of InMemoryDatabaseRoot breaks OwnsOne in InMemoryDatabase #20784

New issue

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

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

Already on GitHub? # to your account

Closed
alalloo opened this issue Apr 29, 2020 · 2 comments · Fixed by #21033
Closed

Use of InMemoryDatabaseRoot breaks OwnsOne in InMemoryDatabase #20784

alalloo opened this issue Apr 29, 2020 · 2 comments · Fixed by #21033

Comments

@alalloo
Copy link

alalloo commented Apr 29, 2020

Owning entity has two members of same type, both marked with OwnsOne. Without use of InMemoryDatabaseRoot entities can be saved, but with use - and no other changes two unexpected behaviors occur

  • unset (null) property A gets the value of B
  • if both are in fact set to different object, ef throws claiming duplicate key

Steps to reproduce

public class OwningEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
    public OwnedEntity OwnedA { get; set; }
    public OwnedEntity OwnedB { get; set; }
}

public class OwnedEntity
{
    public int A { get; set; }
    public string B { get; set; }
}

public class MyDbContext : DbContext
{
    public DbSet<OwningEntity> OwningEntities { get; set; }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        modelBuilder.Entity<OwningEntity>().OwnsOne(o => o.OwnedA);
        modelBuilder.Entity<OwningEntity>().OwnsOne(o => o.OwnedB);
    }
}

Test code includes an InMemoryDbContext with a parameter controlling whether a db root should be used or not:

public class InMemoryMyDbContext : MyDbContext
{
    private string dbName;
    private bool useRoot;

	public InMemoryMyDbContext(string dbName, bool useRoot)
	{
		this.dbName = dbName;
		this.useRoot = useRoot;
	}

	protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
	{
		if (useRoot)
		{
			optionsBuilder.UseInMemoryDatabase(dbName, InMemoryDbRoot.DbRoot);
		}
		else
		{
			optionsBuilder.UseInMemoryDatabase(dbName);
		}
		base.OnConfiguring(optionsBuilder);
	}
}

public class InMemoryDbRoot
{
	private static readonly object DbRootInitLock = new object();
	private static InMemoryDatabaseRoot root;
	public static InMemoryDatabaseRoot DbRoot
	{
		get
		{
			{
				lock (DbRootInitLock)
				{
					if (root == null)
					{
						root = new InMemoryDatabaseRoot();
					}
					return root;
				}
			}
		}
	}
}

Finally, two tests which fail or pass only depending on the useRoot boolean:

[TestClass]
public class UnitTest1
{
	// Not rooted - tests pass
	// const bool useRoot = false;
	// Rooted - tests fail
	const bool useRoot = true;

	[TestMethod]
	public async Task TestMethod1()
	{
		string dbName = Guid.NewGuid().ToString();

		using (var dbContext = new InMemoryMyDbContext(dbName, useRoot))
		{
			var newEntity = new OwningEntity
			{
				Name = "The entity",
				// OwnedA = new OwnedEntity { A = 1, B = "hello" },
				OwnedB = new OwnedEntity { A = 2, B = "goodbye" }
			};

			dbContext.OwningEntities.Add(newEntity);
			await dbContext.SaveChangesAsync();

			var fromDb = dbContext.OwningEntities.Single();
			fromDb.Name.ShouldBe("The entity");
			newEntity.OwnedA.ShouldBeNull();
	   }
	}

	[TestMethod]
	public async Task TestMethod2()
	{
		string dbName = Guid.NewGuid().ToString();

		using (var dbContext = new InMemoryMyDbContext(dbName, useRoot))
		{
			var newEntity = new OwningEntity
			{
				Name = "The entity",
				OwnedA = new OwnedEntity { A = 1, B = "hello" },
				OwnedB = new OwnedEntity { A = 2, B = "goodbye" }
			};

			dbContext.OwningEntities.Add(newEntity);

			await dbContext.SaveChangesAsync();
		}

		using (var dbContext2 = new InMemoryMyDbContext(dbName, useRoot))
		{
			var fromDb = dbContext2.OwningEntities.Single();
			fromDb.Name.ShouldBe("The entity");
		}
	}
}

The whole project is attached to this issue. ef_bug.zip

The first test fails because newEntity.OwnedA that is supposed to stay null has magically gotten the value of OwnedB.

Second test fails with an exception from SaveChangesAsync()

System.ArgumentException: An item with the same key has already been added. Key: 1

The change tracker shows both instances indeed have a key 1, but only when db root is not used the save will succeed:

image

Further technical details

EF Core version: 3.1.3
Database provider: Microsoft.EntityFrameworkCore.InMemory 3.1.3
Target framework: .NET Core 3.1
Operating system: Windows 10 1909 build 18363.815
IDE: Visual Studio 2019 16.5.4
XUnit 2.4.1, Shouldly 3.0.2

@macwier
Copy link

macwier commented May 2, 2023

Any chance for this to get merged into 3.x? Or are there any workarounds?

@ajcvickers
Copy link
Contributor

@macwier No. 3.1 is out-of-support.

@ajcvickers ajcvickers removed their assignment Sep 1, 2024
# for free to join this conversation on GitHub. Already have an account? # to comment
Projects
None yet
3 participants