EF 4.1 Code First - duplicate entities in object graph causes exception - entity-framework

I am getting the following exception when attempting to save my entity:
"AcceptChanges cannot continue because the object's key values conflict with another object in the ObjectStateManager. Make sure that the key values are unique before calling AcceptChanges."
I'm creating a 3 tiered application where the data access layer is using EF Code First, and where the client calls the middle tier using WCF. I am therefore unable able to let the context track the entity state when building up an entity on the client.
In some situations I am finding that the same entity is contained twice in the object graph. In this situation it fails when I try and set the entity state of the duplicate.
For example, I have the following entities:
Customer
Country
Curreny
From the client I create a new
instance of a Customer. I then make
a service call to get Country
instance and assign it to the Customer. The Country instance has
an associated Currency.
The user can then associate a
Currency with the customer. They
may well choose the same Currency
that's associated with the Country.
I make another service call to get
this. Thus at this stage we may
have two separate instances of the
same currency.
So what I end up with are two instance of the same entity in the object graph.
When then saving the entity (in my service) I need to tell EF that both Currency entities are not modified (if I don't do this I get duplicates). Problem is that I get the exception above.
On saving if I set the Currency instance on Country instance to null, it resolves the problem, but I feel like the code is becoming increasingly messy (due to this and other WCF related EF workarounds I'm having to put in place).
Are there any suggestions on how to resolve this in a nicer way?
Many thanks for any help in advance. Here's the code:
using System;
using System.Collections.Generic;
using System.Data.Entity.ModelConfiguration;
using System.ComponentModel.DataAnnotations;
using System.Data.Entity;
using System.Linq;
namespace OneToManyWithDefault
{
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public Country Country { get; set; }
public Currency Currency { get; set; }
public byte[] TimeStamp { get; set; }
}
public class Country
{
public int Id { get; set; }
public string Name { get; set; }
public Currency Currency { get; set; }
public byte[] TimeStamp { get; set; }
}
public class Currency
{
public int Id { get; set; }
public string Symbol { get; set; }
public byte[] TimeStamp { get; set; }
}
public class MyContext
: DbContext
{
public DbSet<Customer> Customers { get; set; }
public DbSet<Currency> Currency { get; set; }
public DbSet<Country> Country { get; set; }
public MyContext(string connectionString)
: base(connectionString)
{
Configuration.LazyLoadingEnabled = false;
Configuration.ProxyCreationEnabled = false;
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new CustomerConfiguration());
modelBuilder.Configurations.Add(new CountryConfiguration());
modelBuilder.Configurations.Add(new CurrencyConfiguration());
base.OnModelCreating(modelBuilder);
}
}
public class CustomerConfiguration
: EntityTypeConfiguration<Customer>
{
public CustomerConfiguration()
: base()
{
HasKey(p => p.Id);
Property(p => p.Id)
.HasColumnName("Id")
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity)
.IsRequired();
Property(p => p.TimeStamp)
.HasColumnName("TimeStamp")
.IsRowVersion();
ToTable("Customers");
}
}
public class CountryConfiguration
: EntityTypeConfiguration<Country>
{
public CountryConfiguration()
: base()
{
HasKey(p => p.Id);
Property(p => p.Id)
.HasColumnName("Id")
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity)
.IsRequired();
Property(p => p.TimeStamp)
.HasColumnName("TimeStamp")
.IsRowVersion();
ToTable("Countries");
}
}
public class CurrencyConfiguration
: EntityTypeConfiguration<Currency>
{
public CurrencyConfiguration()
: base()
{
HasKey(p => p.Id);
Property(p => p.Id)
.HasColumnName("Id")
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity)
.IsRequired();
Property(p => p.TimeStamp)
.HasColumnName("TimeStamp")
.IsRowVersion();
ToTable("Currencies");
}
}
class Program
{
private const string ConnectionString =
#"Server=.\sql2005;Database=DuplicateEntities;integrated security=SSPI;";
static void Main(string[] args)
{
// Seed the database
MyContext context1 = new MyContext(ConnectionString);
Currency currency = new Currency();
currency.Symbol = "GBP";
context1.Currency.Add(currency);
Currency currency2 = new Currency();
currency2.Symbol = "USD";
context1.Currency.Add(currency2);
Country country = new Country();
country.Name = "UK";
country.Currency = currency;
context1.Country.Add(country);
context1.SaveChanges();
// Now add a new customer
Customer customer = new Customer();
customer.Name = "Customer1";
// Assign a country to the customer
// Create a new context (to simulate making service calls over WCF)
MyContext context2 = new MyContext(ConnectionString);
var countries = from c in context2.Country.Include(c => c.Currency) where c.Name == "UK" select c;
customer.Country = countries.First();
// Assign a currency to the customer
// Again create a new context (to simulate making service calls over WCF)
MyContext context3 = new MyContext(ConnectionString);
customer.Currency = context3.Currency.First(e => e.Symbol == "GBP");
// Again create a new context (to simulate making service calls over WCF)
MyContext context4 = new MyContext(ConnectionString);
context4.Customers.Add(customer);
// Uncommenting the following line prevents the exception raised below
//customer.Country.Currency = null;
context4.Entry(customer.Country).State = System.Data.EntityState.Unchanged;
context4.Entry(customer.Currency).State = System.Data.EntityState.Unchanged;
// The following line will result in this exception:
// AcceptChanges cannot continue because the object's key values conflict with another
// object in the ObjectStateManager. Make sure that the key values are unique before
// calling AcceptChanges.
context4.Entry(customer.Country.Currency).State = System.Data.EntityState.Unchanged;
context4.SaveChanges();
Console.WriteLine("Done.");
Console.ReadLine();
}
}
}

I guess you get the exception only if customer.Currency and customer.Country.Currency refer to the same currency, i.e. have the same identity key. The problem is that those two currency objects come from different object contexts, therefore they are different objects (ReferenceEquals(customer.Currency, customer.Country.Currency) is false). When you attach both to your last context (by setting the State) the exception occurs because they are two different objects with the same key.
Looking at your code, perhaps the easiest option would be to check if the currency you want to assign to the customer is the same as the country's currency before you even load the currency, something like:
if (customer.Country.Currency.Symbol == "GBP")
customer.Currency = customer.Country.Currency;
// currencies refer now to same object, avoiding the exception
else
{
MyContext context3 = new MyContext(ConnectionString);
customer.Currency = context3.Currency.First(e => e.Symbol == "GBP");
}
(I assume here that Symbol is the key for currency or a least unique in the DB.) You would also avoid one service/DB call if the currencies are the same.
Other options would be: Don't include the currency in the country query, if you can. Your solution to set customer.Country.Currency to null (not bad at all). Make the references to the two currencies equal in the last context before you add the customer (if (customer.Country.Currency.Symbol == customer.Currency.Symbol) customer.Currency = customer.Country.Currency;). Reload the currencies in your last context and assign them to the customer.
But that's all not really a "nicer way" to solve the problem, only another way - in my opinion.

I think the issue is because you are setting the EntityState to Unchanged. The exception you are seeing only happens if the entity keys always exist AND the entity state is not Added.
See http://msdn.microsoft.com/en-us/library/bb896271.aspx
The last paragraph of Considerations for Attaching Objects is:
"An InvalidOperationException occurs when an object being attached has the same EntityKey as a different object already present in the object context. This error does not occur if an object in the context with same key but is in the Added state."
So the question is, why are you forcing the state to Unchanged instead of leaving it as added?
EDIT:
Edited after looking at your post again and your comment. Ultimately the problem is you are telling EF "Hey, add these Currency and Country objects with this Customer" but two of those objects already exist.
You can use the Attach instead of Add method, but the customer doesn't exist yet.
I suggest wrapping these calls in a transactionscope, calling SaveChanges right after creating the Customer, than using Attach rather then Add. If you get errors, you can roll back the transaction if necessary. I don't have a code sample handy, but does what I am saying make sense?
Something like:
using (TransactionScope scope = new TransactionScope())
{
// Now add a new customer
Customer customer = new Customer();
customer.Name = "Customer1";
context1.SaveChange();
// Assign a country to the customer
// Create a new context (to simulate making service calls over WCF)
MyContext context2 = new MyContext(ConnectionString);
var countries = from c in context2.Country.Include(c => c.Currency) where c.Name == "UK" select c;
customer.Country = countries.First();
// Assign a currency to the customer
// Again create a new context (to simulate making service calls over WCF)
MyContext context3 = new MyContext(ConnectionString);
customer.Currency = context3.Currency.First(e => e.Symbol == "GBP");
// Again create a new context (to simulate making service calls over WCF)
MyContext context4 = new MyContext(ConnectionString);
context4.Customers.Attach(customer);
// The following line will result in this exception:
// AcceptChanges cannot continue because the object's key values conflict with another
// object in the ObjectStateManager. Make sure that the key values are unique before
// calling AcceptChanges.
context4.SaveChanges();
scope.Complete();
}

I had this same problem in a Windows Service and solved it by creating and disposing the DBContext in every insert/update/get call. I was previously keeping the dbContext as a private variable in my repos and reusing it.
So far so good. YMMV. I can't say I understand exactly why it works - I haven't gone deep enough into Code First yet. The magic unicorn features are nice but I'm bordlerine about to throw it out and hand code the TSQL as the magic makes it hard to actually understand what is going on.

Related

EF6:How to include subproperty with Select so that single instance is created. Avoid "same primary key" error

I'm trying to fetch (in disconnected way) an entity with its all related entities and then trying to update the entity. But I'm getting the following error:
Attaching an entity of type 'Feature' failed because another entity of the same type already has the same primary key value.
public class Person
{
public int PersonId { get; set; }
public string Personname { get; set }
public ICollection Addresses { get; set; }
}
public class Address
{
public int AddressId { get; set; }
public int PersonId { get; set; }
public string Line1 { get; set; }
public string City { get; set; }
public string State { get; set; }
public Person Person { get; set; }
public ICollection<Feature> Features { get; set; }
}
// Many to Many: Represented in database as AddressFeature (e.g Air Conditioning, Central Heating; User could select multiple features of a single address)
public class Feature
{
public int FeatureId { get; set; }
public string Featurename { get; set; }
public ICollection<Address> Addresses { get; set; } // Many-To-Many with Addresses
}
public Person GetCandidate(int id)
{
using (MyDbContext dbContext = new MyDbContext())
{
var person = dbContext.People.AsNoTracking().Where(x => x.PersonId == id);
person = person.Include(prop => prop.Addresses.Select(x => x.Country)).Include(prop => prop.Addresses.Select(x => x.Features));
return person.FirstOrDefault();
}
}
public void UpdateCandidate(Person newPerson)
{
Person existingPerson = GetPerson(person.Id); // Loading the existing candidate from database with ASNOTRACKING
dbContext.People.Attach(existingPerson); // This line is giving error
.....
.....
.....
}
Error:
Additional information: Attaching an entity of type 'Feature' failed because another entity of the same type already has the same primary key value.
It seems like (I may be wrong) GetCandidate is assigning every Feature within Person.Addresses a new instance. So, how could I modify the GetCandidate to make sure that the same instance (for same values) is bing assisgned to Person.Addresses --> Features.
Kindly suggest.
It seems like (I may be wrong) GetCandidate is assigning every Feature within Person.Addresses a new instance. So, how could I modify the GetCandidate to make sure that the same instance (for same values) is bing assisgned to Person.Addresses --> Features.
Since you are using a short lived DbContext for retrieving the data, all you need is to remove AsNoTracking(), thus allowing EF to use the context cache and consolidate the Feature entities. EF tracking serves different purposes. One is to allow consolidating the entity instances with the same PK which you are interested in this case, and the second is to detect the modifications in case you modify the entities and call SaveChanges(), which apparently you are not interested when using the context simply to retrieve the data. When you disable the tracking for a query, EF cannot use the cache, thus generates separate object instances.
What you really not want is to let EF create proxies which hold reference to the context used to obtain them and will cause issues when trying to attach to another context. I don't see virtual navigation properties in your models, so most likely EF will not create proxies, but in order to be absolutely sure, I would turn ProxyCreationEnabled off:
public Person GetCandidate(int id)
{
using (MyDbContext dbContext = new MyDbContext())
{
dbContext.Configuration.ProxyCreationEnabled = false;
var person = dbContext.People.Where(x => x.PersonId == id);
person = person.Include(prop => prop.Addresses.Select(x => x.Country)).Include(prop => prop.Addresses.Select(x => x.Features));
return person.FirstOrDefault();
}
}

"A dependent property in a ReferentialConstraint is mapped to a store-generated column" with Id change

Situation
I have searched for the answer to this extensively (on SO and elsewhere) and I am aware that there are many questions on SO by this same title.
I had a table mapping and model that were working. Then the schema was changed (I do not have direct control of the DB) such that a new Primary Key was introduced and the old Primary Key became the Foreign Key to another table. I believe this is the heart of the problem as no other entities seem to have issues
Mapping
Here is the method that maps my entity (called from OnModelCreating)
private static void MapThing(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Thing>().ToTable("ThingTable");
modelBuilder.Entity<Thing>().HasKey(p => p.Id);
modelBuilder.Entity<Thing>().Property(p => p.Id).HasColumnName("NewId");
modelBuilder.Entity<Thing>().Property(p => p.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
modelBuilder.Entity<Thing>().Property(p => p.FileName).HasColumnName("ColumnWhosNameChanged");
modelBuilder.Entity<Thing>().HasRequired(p => p.MetaDataOnThing);
}
The old PK of the table is now defined as a property on the model and it is the same name as the column (the reason it is not defined in the mapping above).
Model
Here is the Model (I have applied names that I hope will make it more clear what has changed):
public class Thing
{
[DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
//This used to be the PK, its names (Property AND Column) have not changed
public int OldId { get; set; }
//The column name for FileName changed to something else
public string FileName { get; set; }
//Unchanged
public byte[] Document { get; set; }
public string ContentType { get; set; }
//Navigation Property
public ThingMetaData MetaDataOnThing { get; set; }
}
Integration test
I removed a lot of structure to, hopefully, make it clear..the test is pretty straight forward
[TestMethod]
public void ThenThingWillBePersisted()
{
var thing = new Thing()
{
OldId = metaDataObject.Id,
Document = new byte[] { 42 },
FileName = "foo.jpg",
ContentType = "image/jpeg"
};
context.Things.Add(thing);
context.SaveChanges();
}
This test produces the error "A dependent property in a ReferentialConstraint is mapped to a store-generated column. Column:'NewId'" and the inner exception points to the NewId as being the issue. It does so on the SaveChanges() call.
Admittedly, I have a lot more experience with nHibernate than I do with Entity Framework but I am pretty sure my mappings and model are setup properly.
Has anyone seen this issue and how did you solve it?

Handling dependent entities when deleting the principal with Entity Framework 5

Here's the situation in its most simplified form using the EF5 Code-First approach:
public abstract class EntityBase<PK>
{
public PK ID { get; set; }
}
public class Country : EntityBase<string>
{
public string Name { get; set; }
}
public class Address : EntityBase<int>
{
[Required]
public string CountryID { get; set; }
public Country Country { get; set; }
// ... other address properties ...
}
The one-to-many relationship between Address and Country is set up with no cascade-delete like so:
modelBuilder.Entity<Address>()
.HasRequired(a => a.Country)
.WithMany()
.HasForeignKey(a => a.CountryID)
.WillCascadeOnDelete(false);
Finally, I have a generic base repository class with CRUD methods that call SaveChanges on the underlying DbContext to commit data changes atomically. E.g.:
public class EFRepository<T, PK> : IRepository<T, PK> where T : EntityBase<PK>
{
//
// ... other methods ...
//
public virtual void Delete(T instance)
{
// ... trigger validations, write to log, etc...
_dbContext.Set<T>().Remove(instance);
try
{
_dbContext.SaveChanges();
}
catch(Exception ex)
{
// ... handle the error ...
}
}
}
Part 1:
Scenario:
var countryRepo = new EFRepository<Country>();
var country = countryRepo.Save(new Country() { ID="??", Name="Test Country" });
var addressRepo = new EFRepository<Address>();
var address = addressRepo.Save(new Address() { Country=country });
countryRepo.Delete(country);
This should fail due to the existence of a dependent Address. However, afterwards the address ends up with a null in CountryID, which is invalid because Address.CountryID is required, so subsequent SaveChanges calls throw a validation exception unless the address is detached.
I expected that when an object is deleted, EF5 will be smart enough to first check for any cascade-delete constraints like the one above and, failing to find any, then proceed to delete the data. But exactly the opposite seems to be the case.
Is this a normal behaviour or am I doing something wrong?
Part 2:
Following a failed SaveChanges call, some Addresses are now in an invalid state in my DbContext and need to be restored to their original values. Of course, I can always do so explicitly for each entity type (Country, State, Order, etc.) by creating specialized repository classes and overriding Delete, but it smells big time. I'd much rather write some general purpose code to gracefully recover related entities after a failed SaveChanges call.
It would require interrogating DbContext to get all relationships in which an entity (e.g. Country) is the principal, regardless of whether or not its class defines navigational properties to dependent entities.
E.g. Country has no Addresses property, so I need to somehow find in DbContext the definition of the one-to-many relationship between Country and Address and use it to restore all related Addresses to their original values.
Is this possible?
Answering my own question in Part 2:
Here is my approach to checking for related dependents when deleting an entity on the principal end of a many-to-one relationship and where dependents are NOT exposed as a navigation collection in the principal (e.g. class Address has a Country property, but class Country doesn't have an Addresses collection).
DbContext
Add the following method to the context class:
/// <summary>
/// Returns an array of entities tracked by the
/// context that satisfy the filter criteria.
/// </summary>
public DbEntityEntry[] GetTrackedEntities<T>(
Expression<Func<DbEntityEntry<T>, bool>> filterCriteria)
where T : class
{
var result = new List<DbEntityEntry>();
var doesItMatch = filterCriteria.Compile();
foreach (var entry in this.ChangeTracker.Entries<T>())
{
if (doesItMatch(entry))
result.Add(entry);
}
return result.ToArray();
}
Repositories
Create a repository for each class that has some dependencies, override the Delete method and use the new GetTrackedEntities<T> method to get all related dependents and either:
explicitly delete them if they are cascade-deletable in code
detach them from the context if they are cascade-deletable in the DB itself
throw an exception if they are NOT cascade-deletable.
Example of the latter case:
public class EFCountryRepository :
EFReadWriteRepository<Country, string>,
ICountryRepository
{
public override void Delete(Country instance)
{
// Allow the Country to be deleted only if there are no dependent entities
// currently in the context that are NOT cascade-deletable.
if (
// are there any Regions in the context that belong to this Country?
_dbContext.GetTrackedEntities<Region>(e =>
e.Entity.CountryID == instance.ID ||
e.Entity.Country == instance).Length > 0
||
// are there any Addresses in the context that belong to this Country?
_dbContext.GetTrackedEntities<Address>(e =>
e.Entity.CountryID == instance.ID ||
e.Entity.Country == instance).Length > 0
)
throw new Exception(String.Format(
"Country '{0}' is in use and cannot be deleted.", instance.ID));
base.Delete(instance);
}
// ... other methods ...
}
Example of a case where cascade-deleting will be done by the DB itself, so all we need to do is detach the dependents from the context:
public class EFOrderRepository :
EFReadWriteRepository<Order, string>,
IOrderRepository
{
public override void Delete(Order instance)
{
foreach (var orderItem in _dbContext.GetTrackedEntities<OrderItem>(e =>
e.Entity.OrderID == instance.ID ||
e.Entity.Order == instance))
{
_dbContext.Entry(orderItem).State = System.Data.EntityState.Detached;
}
base.Delete(instance);
}
// ... other methods ...
}
Hope someone will find this solution helpful.

EF Code First Detached Entity not updating object reference

I'm posting the exact entity:
public class Person : ContactableEntity
{
public Plan Plan { get; set; }
public int Record { get; set; }
public int PersonTypeValue { get; set; }
}
I'm using the following code to update in a disconected context fashion:
public void Update(DbSet MySet, object Obj)
{
MySet.Attach(Obj);
var Entry = this.Entry(Obj);
Entry.State = EntityState.Modified;
this.SaveChanges();
}
This is a method exposed by my dbContext
Called this way:
PersistentManager.Update(PersistentManager.Personas,UpdatedPersona);
The problem is, EF will update any property but the referenced Plan object.
Can someone tell me where is the mistake?
In advance : the entity reaches the point of update with all the properties correctly set.
EF just fails to update the FK in the Database (no exception though)
Update:
tried solving the problem like this but it didn't work:
PersistentMgr.Contacts.Attach(Obj);
PersistentMgr.Entry(Obj).State = EntityState.Modified;
PersistentMgr.Entry(Obj.Plan).State = EntityState.Modified;
PersistentMgr.SaveChanges();
You need...
this.Entry(person).State = EntityState.Modified;
this.Entry(person.Plan).State = EntityState.Modified;
...because when you set the state of the person to Modified the person gets attached to the context in state Modified but related entities like person.Plan are attached in state Unchanged.
If the relationship between Person and Plan has been changed while the entities were detached it is more difficult (especially, like in your model, when no foreign key is exposed as property ("independent association")) to update the entities correctly. You basically need to load the original object graph from the database, compare it with detached graph if relationships have been changed and merge the changes into the loaded graph. An example is here (see the second code snippet in that answer).
Edit
Example to show that it works (with EF 5.0):
using System.Data;
using System.Data.Entity;
using System.Linq;
namespace EFModifyTest
{
public class Person
{
public int Id { get; set; }
public Plan Plan { get; set; }
public int Record { get; set; }
public int PersonTypeValue { get; set; }
}
public class Plan
{
public int Id { get; set; }
public string SomeText { get; set; }
}
public class MyContext : DbContext
{
public DbSet<Person> Contacts { get; set; }
public DbSet<Plan> Plans { get; set; }
}
class Program
{
static void Main(string[] args)
{
Database.SetInitializer(new DropCreateDatabaseAlways<MyContext>());
// Create a person with plan
using (var ctx = new MyContext())
{
ctx.Database.Initialize(true);
var plan = new Plan { SomeText = "Old Text" };
var person = new Person { Plan = plan, Record = 1, PersonTypeValue = 11 };
ctx.Contacts.Add(person);
ctx.SaveChanges();
}
// see screenshot 1 from SQL Server Management Studio
Person detachedPerson = null;
// Load the person with plan
using (var ctx = new MyContext())
{
detachedPerson = ctx.Contacts.Include(c => c.Plan).First();
}
// Modify person and plan while they are detached
detachedPerson.Record = 2;
detachedPerson.PersonTypeValue = 12;
detachedPerson.Plan.SomeText = "New Text";
// Attach person and plan to new context and set their states to Modified
using (var ctx = new MyContext())
{
ctx.Entry(detachedPerson).State = EntityState.Modified;
ctx.Entry(detachedPerson.Plan).State = EntityState.Modified;
ctx.SaveChanges();
}
// see screenshot 2 from SQL Server Management Studio
}
}
}
Screenshot 1 from SQL Server Management Studio (before the modification, Person table is left, Plan table is right):
Screenshot 2 from SQL Server Management Studio (after the modification, Person table is left, Plan table is right):
If it doesn't work for you there must be an important difference to my test model and code. I don't know which one, you must provide more details.
Edit 2
If you change the relationship from Person to another (existing) Plan you must load the original and then update the relationship. With independent associations (no FK property in model) you can update relationships only by using change tracking (aside from more advanced modifications of relationship entries in the ObjectContext change tracker):
var originalPerson = this.Contacts.Include(c => c.Plan)
.Single(c => c.Id == person.Id);
this.Plans.Attach(person.Plan);
this.Entry(originalPerson).CurrentValues.SetValues(person);
originalPerson.Plan = person.Plan;
this.SaveChanges();

Problem Saving with Entity Framework (Need conceptual help)

Problem Summary: I have a Master and Detail entities. When I initialize a Master (myMaster), it creates an instance of Details (myMaster.Detail) and both appear to persist in the database when myMaster is added. However, when I reload the context and access myMasterReloaded.detail its properties are not initialized. However, if I pull the detail from the context directly, then this magically seems to initialize myMasterReloaded.detail. I've distilled this down with a minimal unit test example below. Is this a "feature" or am I missing some important conceptual detail?
//DECLARE CLASSES
public class Master
{
[Key, DatabaseGenerated(DatabaseGenerationOption.Identity)]
public Guid MasterId { get; set; }
public Detail Detail { get; set; }
public Master() { Detail = new Detail(); }
}
public class Detail
{
[Key, DatabaseGenerated(DatabaseGenerationOption.Identity)]
public Guid DetailId { get; set; }
public Master MyMaster{ get; set; }
}
public class MyDbContext : DbContext
{
public DbSet<Master> Masters { get; set; }
public DbSet<Detail> Details { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Master>()
.HasOptional(x => x.Detail)
.WithOptionalPrincipal(x => x.MyMaster)
.WillCascadeOnDelete(true);
}
}
//PERFORM UNIT TEST
[TestMethod]
public void UnitTestMethod()
{
//Start with fresh DB
var context = new MyDbContext();
context.Database.Delete();
context.Database.CreateIfNotExists();
//Create and save entities
var master = context.Masters.Create();
context.Masters.Add(master);
context.SaveChanges();
//Reload entity
var contextReloaded = new MyDbContext();
var masterReloaded = contextReloaded.Masters.First();
//This should NOT Pass but it does..
Assert.AreNotEqual(master.Detail.DetailId, masterReloaded.Detail.DetailId);
//Let's say 'hi' to the instance of details in the db without using it.
contextReloaded.Details.First();
//By simply referencing the instance above, THIS now passes, contracting the first Assert....WTF??
Assert.AreEqual(master.Detail.DetailId, masterReloaded.Detail.DetailId);
}
(This is the sticking point for a more sophisticated entity set. I've simply distilled this down to its simplest case I can't simply replace details with a complex type).
Cheers,
Rob
I think it's because when you first reload the Master, you have not eager-loaded the Detail, so the Detail entity will not be in the Entity Framework "graph" (internal memory). The only thing in the graph will be a single Master entity.
But when you query the Detail ("Let's say hi"), it is loaded into the graph and the reference was resolved based on the FK association, therefore your final test passes as the Master is now related to the Detail.
I could be wrong though - but that's what it sounds like.
Do you have lazy-loading enabled? If not, you need to eager-load the relationships you need.
Instead of this:
var masterReloaded = contextReloaded.Masters.First();
Try this:
var masterReloaded = contextReloaded.Masters.Include(x => x.Detail).First();
Matt Hamilton was right (See above). The problem was:
The Detail property should not be instantiated within the constructor, nor through the getters/setters via a backing member. If it's convenient to instantiate a Entity containing Properties in your new instance, then it may be helpful to have a separate initialize method which will not be automatically executed by the Entity Framework as it reconstructs objects from the database.
The Detail property needs to be declared virtual in the Master class for this to work properly.
The following WILL PASS (As expected/hope)
public class Master
{
[Key, DatabaseGenerated(DatabaseGenerationOption.Identity)]
public Guid MasterId { get; set; }
//Key new step: Detail MUST be declared VIRTUAL
public virtual Detail Detail { get; set; }
}
public class Detail
{
[Key, DatabaseGenerated(DatabaseGenerationOption.Identity)]
public Guid DetailId { get; set; }
//Set this to be VIRTUAL as well
public virtual Master MyMaster { get; set; }
}
public class MyDbContext : DbContext
{
public DbSet<Master> Masters { get; set; }
public DbSet<Detail> Details { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
//This sets up a BI-DIRECTIONAL relationship
modelBuilder.Entity<Master>()
.HasOptional(x => x.Detail)
.WithOptionalPrincipal(x => x.MyMaster)
.WillCascadeOnDelete(true);
}
}
[TestMethod]
public void UnitTestMethod()
{
var context = new MyDbContext();
context.Database.Delete();
context.Database.CreateIfNotExists();
//Create and save entities
var master = context.Masters.Create();
//Key new step: Detail must be instantiated and set OUTSIDE of the constructor
master.Detail = new Detail();
context.Masters.Add(master);
context.SaveChanges();
//Reload entity
var contextReloaded = new MyDbContext();
var masterReloaded = contextReloaded.Masters.First();
//This NOW Passes, as it should
Assert.AreEqual(master.Detail.DetailId, masterReloaded.Detail.DetailId);
//This line is NO LONGER necessary
contextReloaded.Details.First();
//This shows that there is no change from accessing the Detail from the context
Assert.AreEqual(master.Detail.DetailId, masterReloaded.Detail.DetailId);
}
Finally, it is also not necessary to have a bidirectional relationship. The reference to "MyMaster" can be safely removed from the Detail class and the following mapping can be used instead:
modelBuilder.Entity<Master>()
.HasOptional(x => x.Detail)
.WithMany()
.IsIndependent();
With the above, performing context.Details.Remove(master.Detail), resulted in master.Detail == null being true (as you would expect/hope).
I think some of the confusion emerged from the X-to-many mapping where you can initialize a virtual list of entities in the constructor (For instance, calling myDetails = new List(); ), because you are not instantiating the entities themselves.
Incidentally, in case anyone is having some difficulties with a one-to-many unidirectional map from Master to a LIST of Details, the following worked for me:
modelBuilder.Entity<Master>()
.HasMany(x => x.Details)
.WithMany()
.Map((x) =>
{
x.MapLeftKey(m => m.MasterId, "MasterId");
x.MapRightKey(d => d.DetailId, "DetailId");
});
Cheers, Rob