Why can't EF handle two properties with same foreign key, but separate references/instances? - entity-framework

Apparently, EF6 doesn't like objects that have multiple foreign key properties that use the same key value, but do not share the same reference. For example:
var user1 = new AppUser { Id = 1 };
var user2 = new AppUser { Id = 1 };
var address = new Address
{
CreatedBy = user1, //different reference
ModifiedBy = user2 //different reference
};
When I attempt to insert this record, EF throws this exception:
Saving or accepting changes failed because more than one entity of type
'AppUser' have the same primary key value. [blah blah blah]
I've discovered that doing this resolves the issue:
var user1 = new AppUser { Id = 1 };
var user2 = user1; //same reference
I could write some helper code to normalize the references, but I'd rather EF just know they're the same object based on the ID alone.
As for why EF does this, one explanation could be that its trying to avoid doing multipe CRUD operations on the same object since separate instances of the same entity could contain different data. I'd like to be able to tell EF not to worry about that.
Update
So it's as I suspected per my last paragraph above. In absense of a means to tell EF not to do CRUD on either instance, I will just do this for now:
if (address.ModifiedBy.Id == address.CreatedBy.Id)
{
address.ModifiedBy = address.CreatedBy;
}
Works well enough so long as I am not trying to do CRUD on either.
Update2
I've previously resorted to doing this to prevent EF from validating otherwise-required null properties when all I need is the child entity's ID. However, it doesn't keep EF from going into a tizzy over separate instances with the same ID. If it's not going to do CRUD on either AppUser object, why does it care if the instances are different?
foreach (var o in new object[] { address.ModifiedBy, address.CreatedBy })
{
db.Entry(o).State = EntityState.Unchanged;
}

If you get AppUser from context, then you will not need to do anything, because Entity Framework will track entities:
var user1 = context.AppUsers.Find(1);
var user2 = context.AppUsers.Find(1);
var address = new Address
{
CreatedBy = user1, //different reference
ModifiedBy = user2 //different reference
};
Now, they both will point to same objects and will not cause to conflict.

You can add two extra properties to have the Id for the main objects which is the AppUser, then you can use only one AppUser object and reference it for both the created and modified by properties.
CreatedById = user1.Id,
ModifiedById = user1.Id
Otherwise, your code will end up by saving two instances of AppUser with the same primary key.
Another approach is to set both the foreign key properties to only one AppUserobject

The explanation is that EF's change tracker is an identity map. I.e. a record in the database is mapped to one, and only one, CLR object.
This can be demonstrated easily by trying to attach two objects with the same key:
context.AppUsers.Attach(new AppUser { Id = 1 });
context.AppUsers.Attach(new AppUser { Id = 1 });
The second line will throw an exception:
Attaching an entity of type 'AppUser' failed because another entity of the same type already has the same primary key value.
This also happens if you assign
CreatedBy = user1, //different reference
ModifiedBy = user2 //different reference
Somewhere in the process, user1 and user2 must be attached to the context, giving rise to the exception you get.
Apparently, you have a function that receives two Id values that can be different or identical. Admittedly, it would be very convenient if you could simply create two AppUser instances from these Ids, not having to worry about identical keys. Unfortunately, your solution ...
if (address.ModifiedBy.Id == address.CreatedBy.Id)
... is necessary. Solid enough, though.

Related

Making a temporary ID for entities in EF before saving

If I have two tables, which have an Id, whish is an autogenerated int (seed), anyway I have a many to many relationship between these two tables which requires another table.
Now, I do a "dry run" to generate the items for the first two table before saving them, this works perfect. The problem is when I try to generate the items for the (many-many relationship) in the third table. Before saving the items all Ids in the first two tables will be set to 0, when adding items to the relation table I have no problems, the problems comes when saving the tables because the relationship table will have the Ids of 0.
Is there a way to overcome this problem? like assigning a temp value which will be automatically changed to the real Id in the relationship table before saving it ?
For the same reason, I've chosen not to use default Seed methods (AddOrUpdate) provided by EF, but I'm rather writing my own seed methods.
Now if I want to set up relationships, I'm not explicitly using ID's, but rather use navigational properties.
Imagine the scenario:
public class User
{
public int Id {get;set;}
public string Name {get;set;}
public virtual IList<Roles> Roles { get;set;}
}
public class Role
}
public int Id {get;set;}
public string Name {get;set;}
public virtual IList<User> Users {get;set;}
}
Doing this will seed both values for user and roles and relationships once you hit the save changes:
Role admin = new Role
{
Name = "Administrator"
};
Role basic = new Role
{
Name = "Basic"
};
User user = new User
{
Name = "John",
Roles = new List<Role>()
{
basic,
admin
}
}
db.Users.Add(user);
db.SaveChanges();

EF delete entity with unloaded navigation property

I have an Entity. Mandate. Every mandate has a required:many relation to a Person (NavigationProperty). I use the DbContext API with (LazyLoadingEnabled, AutoDetectChangesEnabled, ValidateOnSaveEnabled, ProxyCreationEnabled)
Now I like to delete a Mandate entity. The mandate entities are loaded by another context with AsNoTracking().
message.Result.
ObserveOn(On<DataComposition>.Scheduler).
Where(r => r).
Subscribe(_ =>
{
using (var unit = UnitOfWork.Begin())
{
var mandate = this.SelectedItem.OriginalEntity;
this.mandateRepository.Attach(mandate);
// mandate.Person.ToString();
this.mandateRepository.Delete(mandate);
unit.Commit();
}
this.List.RemoveOnUi(this.SelectedItem);
});
Now during committing I get the following exception: Entities in 'CodeFirstContainer.Mandates' participate in the 'Mandate_Person' relationship. 0 related 'Mandate_Person_Target' were found. 1 'Mandate_Person_Target' is expected.
The delete works if I include the Person Property during the population/selection or if I visit the Property (lazyloading), but I DONT LIKE to materialize/hold many entities only for the deletion case and I DONT LIKE to trigger more than a single DELETE query to db!
The fact that, if you have the navigation property mandate.Person populated, the following SQL statement ...
delete [dbo].[Mandates]
where (([Id] = #0) and ([PersonId] = #1))
... is sent to the database, lets me think that the navigation property indeed must be populated with a person with the correct PersonId to delete the parent.
I have no idea why Entity Framework just doesn't send a delete statement with the primary key ...
delete [dbo].[Mandates]
where ([Id] = #0)
... as I had expected.
Edit
If the Mandate entity has a foreign key property PersonId for the Person navigation property, the expected SQL (the second above) is sent to the database. In this case the Person navigation property can be null and the value of the FK property PersonId doesn't matter.
Edit 2
If you don't want to introduce a FK property the way with the least DB-roundtrip-costs would probably be to fetch the person's Id and then create a dummy person with that key in memory:
// ...
var personId = context.Mandates
.Where(m => m.Id == mandate.Id)
.Select(m => m.Person.Id)
.Single();
mandate.Person = new Person { Id = personId };
this.mandateRepository.Attach(mandate);
this.mandateRepository.Delete(mandate);
// ...

Many-to-Many Inserts with Entity Framework

Say I have two entities with about 20 properties per entity and a Many-to-Many relationship like so:
User (Id int,Name string, .......)
Issue (Id int,Name string, .......)
IssueAssignment (UserId,RoleId)
I want to create a new Issue and assign it to a number of existing Users. If I have code like so:
foreach(var userId in existingUserIds)
{
int id = userId
var user = _db.Users.First(r => r.Id == id);
issue.AssignedUsers.add(user);
}
_db.Users.AddObject(user);
_db.SaveChanges();
I noticed it seems terrribly inefficient when I run it against my SQL Database. If I look at
the SQL Profiler it's doing the following:
SELECT TOP(1) * FROM User WHERE UserId = userId
SELECT * FROM IssueAssignment ON User.Id = userId
INSERT INTO User ....
INSERT INTO IssueAssignment
My questions are:
(a) why do (1) and (2) have to happen at all?
(b) Both (1) and (2) bring back all fields do I need to do a object projection to limit the
fields, seems like unnecessary work too.
Thanks for the help
I have some possible clues for you:
This is how EF behaves. _db.Users is actaully a query and calling First on the query means executing the query in database.
I guess you are using EFv4 with T4 template and lazy loading is turned on. T4 templates create 'clever' objects which are able to fixup their navigation properties so once you add a User to an Issue it internally triggers fixup and tries to add the Issue to the User as well. This in turns triggers lazy loading of all issues related to the user.
So the trick is using dummy objects instead of real user. You know the id and you only want to create realtion between new issue and existing user. Try this (works with EFv4+ and POCOs):
foreach(var userId in existingUserIds)
{
var user = new User { Id = userId };
var _db.Users.Attach(user); // User with this Id mustn't be already loaded
issue.AssignedUsers.Add(user);
}
context.Issues.AddObject(issue);
context.SaveChanges();

How to create and store a (self-tracking) entity object on the server side?

I am trying to achieve the following using Entity framework 4.0 and self-tracking entities:
1) The client application request a book form the server by providing an ISBN number
2) The server performs a query on its database to see if the book is already present
3a) If the book is in the database, it returns it.
3b) If the book is not in the database, it will query Amazon for info, extract the required attributes, create a new book, store it in the database, and return it to the client
Now, 3b) is where the problems are... I can't find any information on how I can create an entity object (a book) on the server side, add it to the context and store it in the database. I have tried all sorts of things:
public class BookBrowserService : IBookBrowserService {
public Book GetBook(string ISBN) {
using (var ctx = new BookBrowserModelContainer()) {
Book book = ctx.Books.Where(b => b.ISBN == ISBN).SingleOrDefault();
if (book == null) {
book = new Book();
book.ISBN = ISBN; // This is the key
book.Title = "This title would be retrieved from Amazon";
Author author = new Author();
author.Name = "The author's name would be retrieved from Amazon";
book.Authors.Add(author);
ctx.Books.AddObject(book);
ctx.SaveChanges(); // This one always throws an exception...
}
return book;
}
}
}
Could anyone tell me what I am doing wrong?
It looks like the problem is related to the EDMX model.
I have a Book entity and an Author entity, with a many-to-many relationship.
The Book entity's Key is ISBN, which is a string of Max length 13.
StoreGeneratedPattern is set to None.
The Author entity's Key is Id, which is a Guid.
StoreGeneratedPattern is Identity.
The exception message is:
"Cannot insert the value NULL into column 'Id', table 'BookBrowser.dbo.Authors'; column does not allow nulls. INSERT fails. The statement has been terminated. "
But since StoreGeneratedPattern is set to Identity, shouldn't an Id value be created automatically?
Thanks,
Peter
It looks that the problem was that I used a Guid as Key in combination with StoreGeneratedPattern = Identity.
When I set StoreGeneratedPattern to None and create my own Guid using Id = Guid.NewGuid(), the problem is gone.
Apparently, the SQL server cannot generate Guids...
you can use StoreGeneratedPattern=Identity, but generated sql script based on your edmx doesn`t contain newid() in describing primary key(GUID). you can do this manually in generated sql script. 'BookId uniqueidentifier NOT NULL
DEFAULT newid()'. So id value will create GUID automatically.

Why is this Exception?- The relationship between the two objects cannot be defined because they are attached to different ObjectContext objects

I m getting this Exception-"The relationship between the two objects cannot be defined because they are attached to different ObjectContext objects."
I ve user table and country table. The countryid is referred in user table.
I am getting the above Exception when I am trying to add entry in user table.
This is my code-
using (MyContext _db = new MyContext ())
{
User user = User .CreateUser(0, Name, address, city, 0, 0, email, zip);
Country country = _db.Country.Where("it.Id=#Id", new ObjectParameter("Id",countryId)).First();
user.Country = country;
State state = _db.State.Where("it.Id=#Id", new ObjectParameter("Id", stateId)).First();
user.State = state;
_db.AddToUser(user );//Here I am getting that Exception
_db.SaveChanges();
}
Try adding the user first, then adding the relationships.
See http://www.code-magazine.com/article.aspx?quickid=0907071&page=4
Or, don't use User.CreateUser where you are explicitly setting an Id = 0, instead use User user = new User() {Name = Name, Address = ...}
BTW, with Entity Framework 4 you can set the foreign key IDs directly removing the need to load the related object if you know its ID.