Allow replacing the dependent side of an identifying FK with a new instance
See original GitHub issueI have a One-to-ZeroOrOne relationship that uses a pattern that is different to the style given in EF Core Relationships documentation. EF Core picks up my pattern, i.e. it knows it is a one-to-one relationship, but fails on an update when there is already an entry.
I have successfully used this one-to-one relationship pattern in EF6 and, to me anyway, it seems like a good pattern as it combines the Primary Key and Foreign Key.
NOTE: This is not urgent as I can swap to your recommended style for a one-to-one relationship, but it would be useful to know if you plan to fix this, as I use this pattern in an example.
If you are seeing an exception, include the full exceptions details (message and stack trace).
The instance of entity type 'PriceOffer' cannot be tracked because another instance of this type with the same key is already being tracked. When adding new entities, for most key types a unique temporary key value will be created if no key is set (i.e. if the key property is assigned the default value for its type). If you are explicitly setting key values for new entities, ensure they do not collide with existing entities or temporary values generated for other new entities. When attaching existing entities, ensure that only one entity instance with a given key value is attached to the context.
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.Add(TKey key, InternalEntityEntry entry)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.StartTracking(InternalEntityEntry entry)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetEntityState(EntityState oldState, EntityState newState, Boolean acceptChanges)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityGraphAttacher.PaintAction(EntityEntryGraphNode node)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityEntryGraphIterator.TraverseGraph(EntityEntryGraphNode node, Func`2 handleNode)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntryNotifier.NavigationReferenceChanged(InternalEntityEntry entry, INavigation navigation, Object oldValue, Object newValue)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.ChangeDetector.DetectNavigationChange(InternalEntityEntry entry, INavigation navigation)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.ChangeDetector.DetectChanges(InternalEntityEntry entry)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.ChangeDetector.DetectChanges(IStateManager stateManager)
at Microsoft.EntityFrameworkCore.DbContext.SaveChanges(Boolean acceptAllChangesOnSuccess)
at test.UnitTests.DataLayer.TestOneToOneUpdate.TestConnectedUpdateExistingRelationship() in C:\Users\Jon\Documents\Visual Studio 2015\Projects\EfCoreInAction\test\UnitTests\DataLayer\TestOneToOneUpdate.cs:line 84
Steps to reproduce
Include a complete code listing (or project/solution) that we can run to reproduce the issue.
Partial code listings, or multiple fragments of code, will slow down our response or cause us to push the issue back to you to provide code to repoduce the issue.
The two main entity classes are:
public class Book
{
public int BookId { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public DateTime PublishedOn { get; set; }
public string Publisher { get; set; }
public decimal Price { get; set; }
public string ImageUrl { get; set; }
//----------------------------------------------
//relationships
public PriceOffer Promotion { get; set; }
public ICollection<Review> Reviews { get; set; }
public ICollection<BookAuthor> AuthorsLink { get; set; }
}
And the one-to-one entity class, PriceOffer
public class PriceOffer
{
public int BookId { get; set; }
public decimal NewPrice { get; set; }
public string PromotionalText { get; set; }
}
My DbContext contains the information to define the key for PriceOffer
public class EfCoreContext : DbContext
{
public DbSet<Book> Books { get; set; }
public DbSet<Author> Authors { get; set; }
public DbSet<PriceOffer> PriceOffers { get; set; }
public EfCoreContext( DbContextOptions<EfCoreContext> options)
: base(options) {}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<BookAuthor>()
.HasKey(x => new {x.BookId, x.AuthorId});
modelBuilder.Entity<PriceOffer>()
.HasKey(x => x.BookId);
}
}
The problem comes when I try to replace/remove a PriceOffer
from a Book
, i.e. it only fails if there is a PriceOffer
on the Book already. Here is the Unit Test that fails. It fails on the line context.SaveChanges()
.
[Fact]
public void TestConnectedUpdateExistingRelationship()
{
//SETUP
var inMemDb = new SqliteInMemory();
using (var context = inMemDb.GetContextWithSetup())
{
context.SeedDatabaseFourBooks();
var book = context.Books
.Include(p => p.Promotion)
.First(p => p.Promotion != null);
//ATTEMPT
book.Promotion = new PriceOffer
{
NewPrice = book.Price / 2,
PromotionalText = "Half price today!"
};
context.SaveChanges();
//VERIFY
var bookAgain = context.Books
.Include(p => p.Promotion)
.Single(p => p.BookId == book.BookId);
bookAgain.Promotion.ShouldNotBeNull();
bookAgain.Promotion.PromotionalText.ShouldEqual("Half price today!");
}
}
One extra piece of information. If I set the book.Promotion to null and call SaveChanges then it works, i.e. it deletes the existing PriceOffer
. See Unit Test below that does not fail
[Fact]
public void TestDeleteExistingRelationship()
{
//SETUP
var inMemDb = new SqliteInMemory();
using (var context = inMemDb.GetContextWithSetup())
{
context.SeedDatabaseFourBooks();
var book = context.Books
.Include(p => p.Promotion)
.First(p => p.Promotion != null);
//ATTEMPT
book.Promotion = null;
context.SaveChanges();
//VERIFY
var bookAgain = context.Books
.Include(p => p.Promotion)
.Single(p => p.BookId == book.BookId);
bookAgain.Promotion.ShouldBeNull();
context.PriceOffers.Count().ShouldEqual(0);
}
}
Further technical details
EF Core version: “version”: “1.1.0” Database Provider: “Microsoft.EntityFrameworkCore.SqlServer”: “1.1.0”, NOTE: It also fails on “Microsoft.EntityFrameworkCore.SqlServer”: “1.0.1”/NET Core “version”: “1.0.1”, so its not a regression. Operating system: Window 10 IDE: Visual Studio 2015, update 3.
Issue Analytics
- State:
- Created 7 years ago
- Reactions:1
- Comments:37 (19 by maintainers)
Hi @divega,
Clearly there is an alternative to the one-to-one PK+FK pattern I am using and I will swap to that. For that reason this issue isn’t a priority at all.
My only long-term concern is that the one-to-one PK+FK pattern looks and feels like a proper relationship until you try and change an existing relationship. All of my example code is a) simple and b) has a unit tests, so I caught this problem easily. Someone else might hit this problem and struggle to diagnose it.
Hi @AndriySvyryd, I would like to check this fix out, as my book will go to the publishers before 2.1.0 will be released. Is there a nightly NuGet build I can access?