Entity Framework 6 Email Checking causes 'Attaching an Entity failed' exception - c#

I have an issue that has been plaguing me for a few hours, I've been able to narrow it down to the exact code block, but I can't seem to make any further progress, I'm still fairly new to EF6 so I might not be 100% current on the best practices.
I have a User model;
public class User
{
public Guid ID { get; set; }
[DisplayName("Display Name")]
[Required]
public string DisplayName { get; set; }
[DisplayName("Email Address")]
[DataType(DataType.EmailAddress, ErrorMessage = "Please enter a valid email address")]
[Required]
public string EmailAddress { get; set; }
public string Company { get; set; }
[DisplayName("Internal ID")]
public string InternalId { get; set; }
[DisplayName("User Status")]
public UserStatus Status { get; set; }
[DisplayName("Access Request Notifications")]
public bool SendNotifications { get; set; }
[DisplayName("Admin")]
public bool IsAdmin { get; set; }
public virtual ICollection<Transaction> Submissions { get; set; }
}
Within my Views the users within the database will be enumerated and displayed to the user, this is all working wonderfully.
On my Create action on the User controller ([HTTP Post]), I am running the check below to see if the email address that is being passed already exists within the database and if it does, it returns a message back to the user informing them and prevents the user from being created.
public ActionResult Create([Bind(Include = "DisplayName,EmailAddress,Company,InternalId,Status,SendNotifications,IsAdmin")] User user)
{
try
{
user.ID = Guid.NewGuid();
if (ModelState.IsValid)
{
var existingUser = db.Users.FirstOrDefault(x => x.EmailAddress.Equals(user.EmailAddress, StringComparison.InvariantCultureIgnoreCase));
if(existingUser == null)
{
db.Users.Add(user);
db.SaveChanges();
return RedirectToAction("Index");
}
else
{
StaticConfig.Trace.Trace(SFTraceEvents.DbFailedAddingUser1, string.Format("User with email address '{0}' already exists in database", user.EmailAddress));
ViewData.Add("DbError", string.Format("Creation failed. User with email address '{0}' already exists", user.EmailAddress));
}
}
}
catch (Exception ex)
{
StaticConfig.Trace.Trace(SFTraceEvents.DbFailedAddingUser1, ex);
ViewData.Add("DbError", "Unable to create user, an internal error has occured. Please try again, if the problem persists, please contact your system administrator.");
}
return View(user);
}
This process works fine, I'm not using the built in 'Find()' method since this only seems to search on the Primary key on an entity and I want to find on something other than the PK.
When I try and implement the same logic on my Edit method, I'm encountering the error below.
Exception: [InvalidOperationException : System.InvalidOperationException: Attaching an entity of type Models.User failed because another entity of the same type already has the same primary key value. This can happen when using the 'Attach' method or setting the state of an entity to 'Unchanged' or 'Modified' if any entities in the graph have conflicting key values. This may be because some entities are new and have not yet received database-generated key values. In this case use the 'Add' method or the 'Added' entity state to track the graph and then set the state of non-new entities to 'Unchanged' or 'Modified' as appropriate.
My Edit method code is currently the following:
public ActionResult Edit([Bind(Include = "ID,DisplayName,EmailAddress,Company,InternalId,Status,SendNotifications,IsAdmin")] User user)
{
try
{
if (ModelState.IsValid)
{
var existingUser = db.Users.FirstOrDefault(x => x.EmailAddress.Equals(user.EmailAddress, StringComparison.InvariantCultureIgnoreCase));
if(existingUser == null || existingUser.ID.Equals(user.ID))
{
db.Entry(user).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
else
{
StaticConfig.Trace.Trace(SFTraceEvents.DbFailedUpdatingUser2, user.DisplayName, string.Format("Email address '{0}' already exists in database", user.EmailAddress));
ViewData.Add("DbError", string.Format("Unable to save changes, email address '{0}' already exists", user.EmailAddress));
}
}
}
catch(Exception ex)
{
StaticConfig.Trace.Trace(SFTraceEvents.DbFailedUpdatingUser2, user.DisplayName, ex);
ViewData.Add("DbError", "Unable to save changes, an internal error has occured. Please try again, if the problem persists, please contact your system administrator.");
}
return View(user);
}
So I am checking to see if there is an existing user with the email address that has been entered on the edit page, I am then checking to see if the ID of the user being edited, matches that of the user found within the database, if so, then of course the same email address should be allowed since we are editing the user that this address belongs to. If however the IDs are different, then there is another user in the database using the email address that has been entered on the edit page.
If I remove the 'var existingUser' and the subsequent if() statement that carries out the ID checks then the edit goes through fine, but then I run the risk of having several users with the same email address on the system. When I put the check back in, then I get the exception above.
Anyone got any suggestions on what I might be doing wrong....is there a more efficient way I can check for entities that might already contain certain data?
Edit
I have found that in EF6.1, it supports an 'Index' data annotation that seems to allow a unique property to be set within it as well. I need to have a look properly, but this might offer what I'm looking for.
EF6.1 Index Attribute
I also know I could do some raw SQL queries to resolve this, but if possible, I'd like to avoid this.

If you want to make sure that there are no duplicate email addresses in the database, including preventing a user from entering a new email address that is already in use, I would solve it this way:
try
{
// Make sure that there is an existing user, since this is an edit
var existingUser = db.Users.FirstOrDefault(x => x.ID.Equals(user.Id));
if (existingUser == null)
throw new ValidationException("User not found");
// Validate that the email address is unique, but only if it has changed
if (!existingUser.EmailAddress.Equals(user.EmailAddress) &&
db.Users.Any(x => x.EmailAddress.Equals(user.EmailAddress, StringComparison.InvariantCultureIgnoreCase)))
throw new Exception(string.Format("Email address '{0}' is already in use", user.EmailAddress));
// Move data to existing entity
db.Entry(existingUser).CurrentValues.SetValues(user);
db.SaveChanges();
}
catch (Exception ex)
{
StaticConfig.Trace.Trace(SFTraceEvents.DbFailedUpdatingUser2, user.DisplayName,
string.Format(ex.Message));
ViewData.Add("DbError", ex.Message);
}
This way of pulling the existing data and mapping properties over is the best way to solve updates, in my experience. You'll be able to do extensive validation involving both the new and the old data etc.
There are several different ways to handle errors. I prefer to throw exceptions and then act on them in the catch clause, but there are other ways too, of course.

The problem you have here is the expectation you have set. The ONLY time this function would run without error is if you are changing the e-mail address. If you are editing any other field, you have a consistency issue.
When you pass the user in to this function, Entity Framework isn't currently tracking that instance. You then go and do a search in the database, and find an existingUser from the database, which Entity Framework is now tracking. You then try to "attach" the first user instance to Entity Framework (via the .State change), but Entity Framework is already tracking the original, not modified version.
What you should do instead is merge the objects if there is an existing tracked item in the database, like so:
if(existingUser == null || existingUser.ID.Equals(user.ID))
{
attachedUser = db.Entry(existingUser);
attachedUser.CurrentValues.SetValues(user);
db.SaveChanges();
return RedirectToAction("Index");
}

Related

EF Core One to Many adding new object

I have tried many ways but I am still hitting different kinds of errors for each solution. I have tried to stop tracking, tried adding challenge itself, updating competition but all of them doesn't seem to work.
I basically have 1 competition to many challenges, and in this scenario, 1 competition and 1 challenge is already present, and I am adding another challenge which has a Foreign Key linking with the competition. I understand that I have asked a similar question before, but that was for bulk creation of 1 competition to many categories. I am thinking this is more like an update operation, which doesn't seem to be working. Appreciate your help a lot! :)
InvalidOperationException: The instance of entity type 'Challenge'
cannot be tracked because another instance with the same key value for
{'ID'} is already being tracked. When attaching existing entities,
ensure that only one entity instance with a given key value is
attached. Consider using
'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the
conflicting key values.
Competition Model Class:
public class Competition
{
[Key]
public int ID { get; set; }
public ICollection<CompetitionCategory> CompetitionCategories { get; set; }
public ICollection<Challenge> Challenges { get; set; }
}
Challenge Model Class:
public class Challenge
{
[Key]
public int ID { get; set; }
[ForeignKey("CompetitionID")]
public int CompetitionID { get; set; }
[Display(Name = "Competition Category")]
[ForeignKey("CompetitionCategoryID")]
public int CompetitionCategoryID { get; set; }
}
Controller:
public async Task<IActionResult> Create([Bind("ID,XXX,CompetitionID,CompetitionCategoryID")] Challenge challenge)
{
var competition = await _context.Competitions
.Include(c => c.CompetitionCategories)
.Include(c1 => c1.Challenges)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.ID == challenge.CompetitionID);
if (ModelState.IsValid)
{
//_context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
competition.Challenges.Add(challenge);
_context.Update(competition);
_context.Entry(competition).State = EntityState.Detached;
_context.Entry(competition.Challenges).State = EntityState.Detached;
await _context.SaveChangesAsync();
//_context.Add(challenge);
//await _context.SaveChangesAsync();
//return RedirectToAction(nameof(Index));
return RedirectToAction("Index", "Challenges", new { id = challenge.CompetitionID });
}
return View();
}
Update: I have actually tried to just add challenge itself but it also throws up another error. Really quite at a lost of what to do.
SqlException: Cannot insert explicit value for identity column in
table 'Challenges' when IDENTITY_INSERT is set to OFF.
System.Data.SqlClient.SqlCommand+<>c.b__122_0(Task
result)
DbUpdateException: An error occurred while updating the entries. See
the inner exception for details.
Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection
connection, CancellationToken cancellationToken)
Update 2: Removing the ID from the binding works as there was some unknown ID value being passed in and being tracked. Ivan's answer on adding a new object with Foreign Key is correct.
public async Task<IActionResult> Create([Bind("XXX,CompetitionID,CompetitionCategoryID")] Challenge challenge)
{
//Codes here
_context.Add(challenge);
await _context.SaveChangesAsync();
}
Working with disconnected entities is not easy and requires different techniques depending of the presense/absense of navigation / innverse navigation and FK properties in the entity model.
Your Challenge class has explicit FK properties and no navigation properties. Adding new object like this is the simplest operation - just call DbContext.Add or DbSet.Add:
_context.Add(challenge);
await _context.SaveChangesAsync();
However, the exception you are getting makes me think that the Challenge object received by the Create method has the PK property Id populated with a value of an existing Challenge. If you really want to add new Challenge and Id is auto-generated, exclude Id from the binding or make sure it is set to 0 (zero) before calling Add.
For more info, see Disconnected Entities and related links in the EF Core documentation.

An error occurred while saving entities that do not expose foreign key properties for their relationships [duplicate]

I have a simple code in Entity Framework (EF) v4.1 code first:
PasmISOContext db = new PasmISOContext();
var user = new User();
user.CreationDate = DateTime.Now;
user.LastActivityDate = DateTime.Now;
user.LastLoginDate = DateTime.Now;
db.Users.Add(user);
db.SaveChanges();
user.Avatar = new Avatar() { Link = new Uri("http://myUrl/%2E%2E/%2E%2E") };
db.SaveChanges();
db.Users.Add(new User() { Avatar = new Avatar() { Link = new Uri("http://myUrl/%2E%2E/%2E%2E") } });
db.SaveChanges();
The problem is that I get an error
An error occurred while saving entities that do not expose foreign key
properties for their relationships. The EntityEntries property will
return null because a single entity cannot be identified as the source
of the exception. Handling of exceptions while saving can be made
easier by exposing foreign key properties in your entity types. See
the InnerException for details.
at
db.Users.Add(new User() { Avatar = new Avatar() { Link = new Uri("http://myUrl/%2E%2E/%2E%2E") } });
db.SaveChanges();
I don't understand why the similar operation works. Is there something wrong with my model, or with ef-code-first?
public class Avatar
{
[Key]
public int Id { get; set; }
[Required]
public string LinkInString { get; set; }
[NotMapped]
public Uri Link
{
get { return new Uri(LinkInString); }
set { LinkInString = value.AbsoluteUri; }
}
}
public class User
{
[Key]
public int Id { get; set; }
public string UserName { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public Avatar Avatar { get; set; }
public virtual ICollection<Question> Questions { get; set; }
public virtual ICollection<Achievement> Achievements { get; set; }
public DateTime CreationDate { get; set; }
public DateTime LastLoginDate { get; set; }
public DateTime LastActivityDate { get; set; }
}
For those of you who would still have this error with all keys properly defined, have a look at your entities and make sure you don't leave a datetime field with a null value.
This error message can be thrown for any kind of reason. The 'InnerException' property (or its InnerException, or the InnerException of that, etc) contains the actual primary cause of the problem.
It would of course be useful to know something about where the problem occurred - which object(s) in the unit of work is causing the problem? The exception message would normally tell you in the 'EntityEntries' property, but in this case, for some reason, that can't be done. This diagnostic complication - of the 'EntityEntries' property being empty - is apparently because some Entities 'do not expose foreign key properties for their relationships.'
Even if the OP gets the error because of failing to initialize DateTimes for the second instance of User, they get the diagnostic complication - 'EntityEntries' being empty, and a confusing top-level message ... because one of their Entity's doesn't 'expose foreign key properties'. To fix this, Avatar should have a public virtual ICollection<User> Users { get; set; } property defined.
The issue was resolved by adding an FK property.
In my case the following situation was giving me the same Exception:
Imagine a code first EF model where you have a Garage entity that has a collection of Car entities. I needed to remove a car from the garage so I ended up with code that looked like this:
garageEntity.Cars.Remove(carEntity);
Instead, it should've been looked like this:
context.Cars.Remove(carEntity);
Just for others who might have similar problems. I had the same error, but for a different reason. In one of the child objects I defined the [Key] as being a value which was the same for different saves. A stupid mistake on my part, but the error message does not instantly lead you to the problem.
In my case the exeception was thrown because EF had created a migration incorrectly.
It missed setting the identity: true on the second table. So go into the migrations which created the relevant tables and check if it missed to add identity.
CreateTable(
"dbo.LogEmailAddressStats",
c => new
{
Id = c.Int(nullable: false, identity: true),
EmailAddress = c.String(),
})
.PrimaryKey(t => t.Id);
CreateTable(
"dbo.LogEmailAddressStatsFails",
c => new
{
Id = c.Int(nullable: false), // EF missed to set identity: true!!
Timestamp = c.DateTime(nullable: false),
})
.PrimaryKey(t => t.Id)
.ForeignKey("dbo.LogEmailAddressStats", t => t.Id)
.Index(t => t.Id);
An Id column should have identity (i.e. auto-incrementing!) so this must be a EF bug.
You could add identity manually with SQL directly to the database but I prefer using Entity Framework.
If you run in to the same problem I see two easy solutions:
Alt 1
reverse the incorrectly created migration with
update-database -target:{insert the name of the previous migration}
Then add the identity: true manually to the migration code and then update-database again.
Alt 2
you create a new migration that adds identity. If you have no changes in the models and you run
add-migration identity_fix
it will create an empty migration. Then just add this
public partial class identity_fix : DbMigration
{
public override void Up()
{
AlterColumn("dbo.LogEmailAddressStatsFails", "Id", c => c.Int(nullable: false, identity: true));
}
public override void Down()
{
AlterColumn("dbo.LogEmailAddressStatsFails", "Id", c => c.Int(nullable: false));
}
}
This problem can also arise from reversed key declarations. If you're using fluent to configure the relationship, make sure the left and right keys are mapped to the correct entity.
I hade same probleme. in my case, it was due to datetime field with a null value. I had to passe a value to datetime and evrythings went fine
Another answer:
I used this:
public List<EdiSegment> EdiSegments { get; set; }
instead of this:
public virtual ICollection<EdiSegment> EdiSegments { get; set; }
and got the error message noted above.
I had the same error and in my case the problem was that I added a relationship object which had already been loaded "AsNoTracking". I had to reload the relation property.
BTW, Some suggest using "Attach" for relations that already exist in db, I haven't tried that option though.
In my case, the problem was that I renamed a column improperly, so the migration made two columns, one called "TeamId" and one called "TeamID". C# cares, SQL doesn't.
Yet another different case here.
A query was cast to a list and while doing that, it created entities by their constructor for comparison in the linq expression right after the ToList(). This created entities that gotten into the deleted state after the linq expression finished.
However! There was a small adjustment that created another entity in the constructor, so that this new entity got linked to an entity that was marked as Deleted.
Some code to illustrate:
query.Except(_context.MyEntitySetSet()
.Include(b => b.SomeEntity)
.Where(p => Condition)
.ToList() // This right here calls the constructor for the remaining entities after the where
.Where(p => p.Collection.First(b => Condition).Value == 0)
.ToList();
The constructor of MyEntity:
public partial class MyEntity
{
protected MyEntity()
{
// This makes the entities connected though, this instance of MyEntity will be deleted afterwards, the instance of MyEntityResult will not.
MyEntityResult = new MyEntityResult(this);
}
}
My solution was to make sure the entire expression was done inside the IQueryable so that there won't be any objects created.
I'm not entirely sure that it's going to help in your case because I'm setting up my tables using Fluent API, however, as far I can tell, the issue arises regardless whether the schema is set up using data annotations (attributes) or Fluent API (configuration).
There seems to be a bug in EF (v. 6.1.3) as it omits certain changes to the schema when updating the DB to the next migration. The quickest route around it is (during the development stage) to remove all the tables from the DB and runt migrations from init stage again.
If you're already in production, the quickest solution I've found was to manually change the schema in the DB or, if you want to have version control of the changes, manually manipulate the methods Up() and Down() in your migration.
Today I faced this issue and tried the possible solutions posted above but none of them helped me. I had UnitOfWork pattern implemented and system was committing the data in last after adding all the records.
In my case system was combining the two models and querying the DB
Invalid object name 'dbo.RoleModelUserModel'.
where these were two different models actually.
I fixed this by reordering the insert statements and adding the parent entity first. In this case added the user first and issue resolved.
After a bit of investigation I found that whilst .Net supports a minimum date (DateTime.MinValue) of 01/01/0001 00:00:00 and a maximum (DateTime.MaxValue) of 31/12/9999 23:59:59 in SQL Server Compact Edition minimum date is 01/01/1753 00:00:00.
When I entered a date greater than 01/01/1753 00:00:00, this error disappeared.
Is your application or website being accessed from some third party application when this error is coming? If yes, then please check the access rights of the account which is sending the request to your application.
In our case, it was ServiceNow MID server service which was the culprit. It is a Windows service. If you want to know more about it then please read this link. So basically, you need to check two things:
Under the context of which account the calling service should run to access your application?
What all access rights are needed for the service's log on account to do all allowed operations in your application?
As per this article of ServiceNow we had to give Log on as a service right to the MID Server service's log on account. You can do it via in Local Security Policies console (Refer screenshot).
After we gave the proper access rights to the logon account, the Entity Framework issue went away. Please remember that the access rights and the log on account to be used will be specific to your application.

inner exception when using UserId as FK

this may be a simple problem for some of you, but I am have a difficult time trying to resolve it.
I have a Message.cs class:
public class Message : Audit
{
public int MessageId { get; set; }
public string TextBody { get; set; }
public string UserId { get; set; }
[ForeignKey("UserId")]
public virtual ApplicationUser User { get; set; }
}
I am using entity framework to have secure log ins and etc.
When I post a message, I have set break points to see that the message object that I am sending does contain the UserId, but when db.SaveChanges(); is called, there is an error:
"The INSERT statement conflicted with the FOREIGN KEY constraint \"FK_dbo.Messages_dbo.AspNetUsers_ApplicationUser_Id\". The conflict occurred in database \"aspnet-ChatApp-20140708035313\", table \"dbo.AspNetUsers\", column 'Id'.\r\nThe statement has been terminated."
Also, I am using data adapters and interfaces to work with my APIs.
my MessageDataAdapter for POST looks like this: (EDITTED LOOK BELOW)
public Models.Message PostMessage(Models.Message newMessage)
{
ApplicationUser user = new ApplicationUser();
Message message = new Message();
newMessage.TextBody = message.TextBody;
newMessage.DateSent = DateTime.Now;
newMessage.Hidden = message.Hidden;
newMessage.UserId = user.Id;
db.Messages.Add(newMessage);
db.SaveChanges();
return newMessage;
}
When I set a breakpoint, newMessage contains the necessary data plus the UserId when I am logged in, for I have it authroized so only "Users" can POST to the DATABASE. However, when it reaches db.SaveChanges that error occurs.
Any ideas on how to resolve this issue?
Thank you!
EDIT
this is what I have(I guess what I really want to be able to do is to be able to POST Message containing the UserId of whoever is logged in):
public Models.Message PostMessage(Models.Message newMessage)
{
ApplicationUser user = new ApplicationUser();
Message message = new Message();
message.TextBody = newMessage.TextBody;
message.DateSent = DateTime.Now;
message.Hidden = newMessage.Hidden;
message.UserId = newMessage.UserId;
db.Messages.Add(message);
db.SaveChanges();
return message;
}
My front end HTML has a input type hidden with the value being "User.Identity.GetUserId()" which I want to be POSTed with the Message.
Why are you creating new user when posting message?
Retrive the old-one from db.AspNetUsers(or db.Users ?). Or if creating new user is really needed, then you must also add user to db.AspNetUsers repository - id will be generated only when you add user to repository. If not, then user.Id property will be default - i.e. 0 for integer type. But there is no user with id=0 in AspNetUsers table. This error says about that.

Updating related data using MVC 4 and Entity Framework?

So, I have a problem in save data which contains related entities, when I save it a new relation blank is created.
Exemple:
Entities:
public class Project
{
public int Id { get; set; }
public int Code{ get; set; }
public string Description{ get; set; }
public virtual Client Client { get; set; }
}
public class Client
{
public int Id { get; set; }
public int Code { get; set; }
public string Name { get; set; }
}
The Controller GET:
public ActionResult Create()
{
PopulateDropDownClienteList(String.Empty); //Returns to ViewBag to create a combobox .in view
return View();
}
The View:
#Html.DropDownListFor(m => m.Client.Id, new SelectList(ViewBag.Client_Id, "Id", "Name"), new { Name = "Client.Id" });
The Controller POST:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(string command, Project project)
{
try
{
if (ModelState.IsValid)
{
projectRepository = new ProjeRepository();
Project pro = projectRepository.ReturnByCode(project.Code);
if (pro == null)
projectRepository.Save(project);
else
projectRepository.Update(project);
PopulateDropDownClienteList(String.Empty);
Return View();
}
else
{
return View(project);
}
}
catch (Exception ex)
{
return View();
}
}
So when I save the data, the client is not associated with the project. just creating a new blank Client.
You Project Save code is not updating the entity, it is ADDING a new one all the time.
You should have update logic similar to following grounds -
To Add new FK Entry and associate it with parent record -
var entity = entities.Students.Where(p => p.Id == "2").First();
entity.StudentContact = new StudentContact() { Contact = "xyz", Id = "2" };
entities.Students.Attach(entity);
var entry = entities.Entry(entity);
// other changed properties
entities.SaveChanges();
To update a FK record with new details -
var entity = entities.Students.FirstOrDefault();
entity.StudentContact.Contact = "ABC";
entities.Students.Attach(entity);
var entry = entities.Entry(entity);
entry.Property(e => e.StudentContact.Contact).IsModified = true;
// other changed properties
entities.SaveChanges();
The above code, I have a Student records which has FK relationship with StudentContacts. I updated Contact information of a student and then updated it to database using ATTACH.
You've got a number of issues here, so let me break them down.
First and foremost, do not ever catch Exception (at least without throwing it again). There's two very important things about using try...catch blocks: you should only wrap the code where you're expecting an exception (not nearly your entire method as you've done here), and you should only catch the specific exception you're expecting (not the base type Exception). When you catch Exception, any and every exception that could possibly be generated from your code will be caught, and in this case, simply discarded, which means you really will never know if this code works at all.
Second, you have a fine method that generates a dropdown list of choices, but never store the user's selection anywhere meaningful. To understand why, you need to stop and think about what's happening here. An HTML select element has a string value and a string text or label component. It does not support passing full objects back and forth. I can't see what your PopulateDropDownClienteList method does, but what it should be doing is creating an IEnumerable<SelectListItem>, where each item gets its Text property set to whatever you want displayed and its Value property to the PK of the Client. However, once you have that, you need some property on Project to post back to. Your virtual Client won't work as that needs a full Client instance, which your form will never have. So, you have two choices:
Implement a view model to feed to the view (and accept in the post). In that view model, in addition to all other editable fields, you'll include something like ClientId which will be an int type, and you'll bind this to your drop down list. Once you're in your post method, you map all the posted values to your project instance, and then use the ClientId to look up a client from the database. You then set the resulting client as the value for your Client property and save as usual.
You alter your database a bit. When you just specify a virtual, Entity Framework smartly creates a foreign key and a column to hold that relationship for you behind the scenes. That's great, but in situations like this, where you actually need to access that foreign key column, you're screwed. That way around that is to explicitly define a property to hold that relationship on your model and tell Entity Framework to use that instead of creating its own.
[ForeignKey("Client")]
public int ClientId { get; set; }
public virtual Client Client { get; set; }
With that, you can now directly use ClientId without worrying about filling in Client. You again bind your drop down list to ClientId, but now, you do not need to look up the client explicitly from the database. Entity Framework will just save the ClientId as it should to the database, and then restore the Client based on that when you look up the project again in the future.

Adding detached entities to a 1-many relationship in EF 4.1 Code First

I am attempting to use EF 4.1 Code First to model a simple relationship of a User having a single Role. When I attempt to save an existing User with a new Role to a different context (using a different context to simulate a client-server round-trip), I get the following exception:
System.Data.Entity.Infrastructure.DbUpdateException: An error occurred while saving entities that do not expose foreign key properties for their relationships. The EntityEntries property will return null because a single entity cannot be identified as the source of the exception. Handling of exceptions while saving can be made easier by exposing foreign key properties in your entity types. See the InnerException for details. ---> System.Data.UpdateException: A relationship from the 'User_CurrentRole' AssociationSet is in the 'Added' state. Given multiplicity constraints, a corresponding 'User_CurrentRole_Source' must also in the 'Added' state.
What I expect is that a new Role is created and associated with the exising User.
What am I doing wrong, is this possible to achieve in EF 4.1 code first? The error message seems to suggest that it needs both the User and the Role to be in the added state, but I'm modifying an exising User, so how can that be?
Things to note: I'd like to avoid modifying the structure of the entities (eg by introducing foreign key properties visible on the entities), and in the database I'd like the User to have a foreign key pointing to Role (not the other way around). I'm also not prepared to move to Self Tracking Entities (unless there's no other way).
Here are the entities:
public class User
{
public int UserId { get; set; }
public string Name { get; set; }
public Role CurrentRole { get; set; }
}
public class Role
{
public int RoleId { get; set; }
public string Description { get; set; }
}
And here's the mapping I'm using:
public class UserRolesContext : DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<Role> Roles { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<User>().HasKey(u => u.UserId);
modelBuilder.Entity<Role>().HasKey(r => r.RoleId);
modelBuilder.Entity<User>().HasRequired(u => u.CurrentRole);
}
}
I pre-populate the database with this:
public class UserInitializer : DropCreateDatabaseAlways<UserRolesContext>
{
protected override void Seed(UserRolesContext context)
{
context.Users.Add(new User() {Name = "Bob",
CurrentRole = new Role() {Description = "Builder"}});
context.SaveChanges();
}
}
And finally, here's the failing test:
[TestMethod]
public void CanModifyDetachedUserWithRoleAndReattach()
{
Database.SetInitializer<UserRolesContext>(new UserInitializer());
var context = new UserRolesContext();
// get the existing user
var user = context.Users.AsNoTracking().Include(c => c.CurrentRole).First(u => u.UserId == 1);
//modify user, and attach to a new role
user.Name = "MODIFIED_USERNAME";
user.CurrentRole = new Role() {Description = "NEW_ROLE"};
var newContext = new UserRolesContext();
newContext.Users.Attach(user);
// attachment doesn't mark it as modified, so mark it as modified manually
newContext.Entry(user).State = EntityState.Modified;
newContext.Entry(user.CurrentRole).State = EntityState.Added;
newContext.SaveChanges();
var verificationContext = new UserRolesContext();
var afterSaveUser = verificationContext.Users.Include(c => c.CurrentRole).First(u => u.UserId == 1);
Assert.AreEqual("MODIFIED_USERNAME", afterSaveUser.Name, "User should have been modified");
Assert.IsTrue(afterSaveUser.CurrentRole != null, "User used to have a role, and should have retained it");
Assert.AreEqual("NEW_ROLE", afterSaveUser.CurrentRole.Description, "User's role's description should have changed.");
}
}
}
Surely this is a scenario that's covered, I would guess it's something I'm missing in the way I've defined the model mapping?
You have broken EF state model. You mapped your entity with mandatory CurrentRole so EF knows that you cannot have existing User without the Role. You have also used independent associations (no FK property exposed on your entity). It means that relation between role and user is another tracked entry which has its state. When you assign the role to existing user the relation entry has state set to Added but it is not possible for existing User (because it must have already role assigned) unless you mark the old relation as Deleted (or unless you are working with a new user). Solving this in detached scenario is very hard and it leads to the code where you must pass information about old role during the roundtrip and manually play with state manager or with entity graph itself. Something like:
Role newRole = user.CurrentRole; // Store the new role to temp variable
user.CurrentRole = new Role { Id = oldRoleId }; // Simulate old role from passed Id
newContext.Users.Attach(user);
newCotnext.Entry(user).State = EntityState.Modified;
newContext.Roles.Add(newRole);
user.CurrentRole = newRole; // Reestablish the role so that context correctly set the state of the relation with the old role
newContext.SaveChanges();
The simplest solution is load the old state from the database and merge changes from the new state to the loaded (attached) one. This can be also avoided by exposing FK properties.
Btw. your model is not one to one but one to many where the role can be assigned to multiple users - in case of one-to-one it would be even more complicated because you will have to delete the old role prior to creating a new one.

Categories