Pattern required for adding/updating entities in EF5 including relationships - c#

I am using VS 2010 with Entity Framework 5 code first and C# and have a web application (hence disconnected entities). I am used to working with SQL queries directly but am very new to EF and code first.
I have two classes:
public class User
{
public int UserID {get; set;}
public string UserName { get; set; }
public bool IsSuspended { get; set; }
public int UnitID { get; set; }
public virtual MyTrust MyTrusts { get; set; }
}
public class MyTrust
{
public int MyTrustID { get; set; }
public string MyTrustName { get; set; }
public string Region { get; set; }
public bool DoNotUse { get; set; }
}
and my DbContext class contains:
public DbSet<MyTrust> MyTrust { get; set; }
public DbSet<User> Users { get; set; }
modelBuilder.Entity<User>()
.HasRequired(m => m.MyTrust);
The MyTrust entity will not be changed
There are three scenarios I am interested in:
Adding a user with an existing MyTrust
Updating a user with no change to the trust
Updating a user with a change to the trust
When the website returns the data the MyTrust object has only the MyTrustID set. When I update/add the user the MyTrust record is also updated.
CLARIFICATION The relationship in the User object is NOT updated; the actual MyTrust object is updated with the data returned from the website; as most fields are empty this is corrupting the object AND not achieving the required update of the User record.
In fact, the problem seems to boil down to the fact that the wrong end of the relationship is being updated.
I have looked at some many examples I cannot see a simple solution.
Can anyone please suggest a straightforward pattern for this (it was so easy in the SQL days).
UPDATE
I resolved this by adding specific keys to the User and MyTrust classes.
public int NHSTrustID { get; set; }
and a matching key in the MyTrust class.
In retrospect the question was wrong. I wasn't after patterns but the solution to a specific problem.

I've given some examples below - I've done them from memory but hopefully will give you a good starting point:
Adding a user with an existing MyTrust
using(var context = new MyDbContext()){
context.Entry(myUser).State = EntityState.Added
context.Entry(myUser.MyTrusts).State = EntityState.Modified;
context.Entry(myUser.MyTrusts).Property(x => x.MyTrustName).IsModified = false;
context.Entry(myUser.MyTrusts).Property(x => x.Region).IsModified = false;
context.Entry(myUser.MyTrusts).Property(x => x.DoNotUse).IsModified = false;
context.SaveChanges();
}
Updating a user with no change to trusts:
using(var context = new MyDbContext()){
context.Entry(myUser).State = EntityState.Modified
context.Entry(myUser.MyTrusts).State = EntityState.Unchanged;
context.SaveChanges();
}
Updating a user with a change to trusts:
using(var context = new MyDbContext()){
context.Entry(myUser).State = EntityState.Modified
context.Entry(myUser.MyTrusts).State = EntityState.Modified;
context.SaveChanges();
}

Related

How to add new users from a logged-in user?

I have this weird behaviour when it comes to creating a new users from scratch(registration) and creating new ones from a logged in user that doesn't depend on the DB underneath(in this case I'm using postgreSQL).
But first here's the relationship:
Now both at registration time and logged-in time I do use UserManager for the creation of the users.
First I create a new UserSetting, SaveChanges() and follow by assigning to the AppUser property UserSettingId the value of the Id of the new created entity.
Here's where it fails depending if you are already a registered and logged-in user or you are a new registering user, the code doesn't change:
var userSettings = await _userService.UserSettings.AddAsync(new UserSettings
{
LanguageId = langId,
ThemeId = themeId
});
//wrapper to the Context.SaveChangesAsync()
//failure point!
if (!await _userService.SaveChangesAsync<bool>())
And the exception error(at the creation of a new user from an already logged-in user) talks about a PK_LanguageId already existing as if I was creating a new Language entity but, as you can see, I'm just assigning just a default langId to the new UserSetting. Here's the exception screenshot:
(sorry for the italian, it does auto-translate)
Short translation: duplicated key for Languages, as if it was trying to insert also a new Language entity.
And at the Startup the registered service(_userService) is transient.
And here's the model classes and their configuration in OnModelCreating:
public class AppUser : IdentityUser
{
public long UserSettingsId { get; set; }
public virtual UserSettings UserSettings { get; set; }
//...other properties
}
public class UserSettings
{
public long Id { get; set; }
public int LanguageId { get; set; }
public virtual Language Language { get; set; }
public int ThemeId { get; set; }
public virtual Theme Theme { get; set; }
public virtual AppUser AppUser { get; set; }
}
public class Theme
{
[Key]
public int Id { get; set; }
[StringLength(10)]
public string Type { get; set; }
}
public class Language
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
[MaxLength(2)]
public string Code { get; set; }
}
builder.Entity<AppUser>().HasKey(u => u.Id);
builder.Entity<AppUser>().HasOne(u => u.UserSettings).WithOne(us => us.AppUser);
builder.Entity<UserSettings>().HasKey(us => us.Id);
builder.Entity<UserSettings>().Property(us => us.Id).ValueGeneratedOnAdd();
builder.Entity<UserSettings>().HasOne(us => us.Language);
builder.Entity<UserSettings>().HasOne(us => us.Theme);
What I'm doing wrong? Why it does behave so differently? Is there a different way to create users from an already logged-in user?
The problem might be that you are adding object to the database context which marks it as added. Because database context isn't aware of existence Language and Theme objects it is trying to insert all of them.
It may be issue with postgreSQL provider. You might want to check opened issues.
I'd suggest to try loading Language object from database first and then try to add new UserSettings.
Found the source of the behaviour, it's the damn UserManager.GetUserAsync(), which I user to get the currently logged-in user with added UserSettings later on to propagate the "Admin"'s settings to the new creating account.
GetUserAsync(User) does return an entity that does keep being tracked(instead of, as I thought, being like AsNoTracking()). So consequently when I did fill it's UserSetting I did in fact added a new UserSetting with new sub classes properties!
Sooo, never expect UserManager give you the user without tracking!
Thanks for everyone involved with the effort.

Entity Framework Code First Many to Many with Custom User and Mapping Table

I am working with WebAPI, EF Code First and have a problem concerning many-to-many relationships:
I am working with a custom user inherited from "IdentityUser" who can have many Projects.
These Projects can now have multiple users. In Addition to this I want to store additional fields in a mapping table.
public class MyCustomUser : IdentityUser
{
// blah blah...
public virtual ICollection<Project> Projects { get; set; }
}
public class Project
{
public int Id { get; set; }
public string Title { get; set; }
public virtual ICollection<MyCustomUser> Users { get; set; }
}
public class Users2Projects
{
public int UserId { get; set; }
public int ProjectId { get; set; }
public virtual MyCustomUser User { get; set; }
public virtual Project Project { get; set; }
public string AdditionalField1 {get;set;}
public string AdditionalField1 {get;set;}
}
The question is: How do I have to set the relations (FluentAPI) to insert a Project for a User which is already there? Because the user can use the application no matter if he has a Project or not.
So the User CAN have a Project, but a Project has to have at least one User. I am getting the User through the Owin "UserManager" "FindById" which returns "MyCustomUser". Creating a new Users2Project-Class, adding the new Project to it, adding the user to it fails because EF wants to store a new User. How can I solve this?
Kind Regards
You don't need to set anything with FluentAPI when you explicitly declare your many-to-many relationship entity. But you should remove the following lines from User and Project, respectively:
public virtual ICollection<Project> Projects { get; set; }
public virtual ICollection<MyCustomUser> Users { get; set; }
and add on both entities:
public virtual ICollection<Users2Projects> Users2Projects { get; set; }
To add a new Project for an existing User you can:
var relation = new Users2Projects() {
User = existingUser, // reference retrieved from Context.
Project = new Project() { Title = "Project Alpha" }
}
Context.Users2Projects.Add(relation);
Context.SaveChanges();
As far as I know, it is not possible to force the Project to have at least one User using a explicit relationship class on Entity Framework, so you would have to check that programmatically upon inserting a Project into the Context:
if (project.Users2Projects.Count() == 0)
throw new Exception("Project must have at least one user.")
Context.Projects.Add(project);
Context.SaveChanges();
and upon deleting an User you do the check before:
var projectsToDelete = new List<Project>();
foreach (var relationship in user.Users2Projects)
{
if (relationship.Project.Users2Projects.Count() <= 1)
projectsToDelete.Add(relationship.Project);
}
Context.MyCustomUsers.Remove(user);
Context.Projects.RemoveRange(projectsToDelete);
Context.SaveChanges();
You are clearly missing a conceptual entity here although you actually already modeled it a bit as your Users2Projects mapping table. You may want to untangle a few concepts around the bounded contexts that exist in your application, and look again at your requirements
User administration (i.e. administration of users that are allowed to access your system) seems to be a different concern than project administration:
Because the user can use the application no matter if he has a Project or not.
In project administration, for the assignment of a user to a project, it appears there is additional information specific to the project that needs to be captured as part of the ProjectMemberAssignment. Your Project appears to be the aggregate root for this bounded context, and should be made responsible for managing it's member assignments:
public class Project
{
public virtual int Id { get; private set; }
public string Title { get; set; }
public void AssignMember(int userId, string someField, string someOtherField)
{
if (MemberAssignments.Any(x => x.UserId == userId))
throw new InvalidOperationException("User already assigned");
MemberAssignments.Add(new ProjectMemberAssignment(Id, userId, someField, someOtherField));
}
// Other behavior methods.
public virtual ICollection<ProjectMemberAssignment> MemberAssignments
{
get;
set;
}
}
Note that you can get the projects for a user using a simple query, without the need to maintain a many-to-many relationship in your model:
var user = ...;
var projectsForUser =
from project in db.Projects
where project.MemberAssignments.Any(x => x.userId == user.UserId)
select project;

How to add a new entity with relationships to existing entities without getting duplicates? (EF 6.1)

Using Web API 2 and EF 6.1 code first.
I am trying to add a new Template (see model) which has relationships to already existing TimePeriods and Stations.
public class Template
{
public int TemplateID { get; set; }
public string Name { get; set; }
public List<TimePeriod> TimePeriods { get; set; }
public List<Station> Stations { get; set; }
}
public class Station
{
public int StationID { get; set; }
public string Name { get; set; }
public List<Template> Templates { get; set; }
}
public class TimePeriod
{
public int TimePeriodID { get; set; }
public TimeSpan From { get; set; }
public TimeSpan To { get; set; }
public List<Template> Templates { get; set; }
}
The new template object contains a list of Station and a list of TimePeriod with correct IDs/primary keys. I hoped that EF would recognize that the related entities were already existing by looking att their primary keys but it seems not. Instead, all the related entities are added again resulting in duplicates.
private SchedulingContext db = new SchedulingContext();
[ResponseType(typeof(Template))]
public IHttpActionResult PostTemplate(Template template)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
db.Templates.Add(template);
db.SaveChanges();
return CreatedAtRoute("DefaultApi", new { id = template.TemplateID }, template);
}
Does this have something to do with me using a new context? If so, how can I prevent this behavior?
Solution thanks to Evandro:
public void PostTemplate(Template template)
{
db.Templates.Add(template);
foreach (var item in template.Stations)
{
db.Entry<Station>(item).State = EntityState.Unchanged;
}
foreach (var item in template.TimePeriods)
{
db.Entry<TimePeriod>(item).State = EntityState.Unchanged;
}
db.SaveChanges();
}
Lorentz, this is the default behavior of the Entity Framework. You have to explicitly define your custom behavior based on what the system should do.
First, you can access the State of your entities within the context using hte following example:
EntityState state = db.Entry<Station>(station).State;
You can print the states and then see what EF is doing.
Now, when you first receive the instance of Template, its state on the context it will be Detached.
After you add it to the context, the state will change to Added. This will apply for Template(s), Station(s) and TimePeriod(s).
Even if you set the Id (Primary Key) correctly, EF will discard the ids, create new Ids and add new lines to the tables, which is what is happening with your program. That is what I managed to reproduce in my code.
You have to define the EntityState for each entity so the EF will know that it should not persist new items. Below are the possible values at EF 6.1:
// This is probably what you are looking for
db.Entry<Station>(station).State = EntityState.Unchanged;
// This one maybe, if you are receiving updated values for the State
db.Entry<Station>(station).State = EntityState.Modified;
// Others that may apply for other scenarios
db.Entry<Station>(station).State = EntityState.Detached;
db.Entry<Station>(station).State = EntityState.Added;
db.Entry<Station>(station).State = EntityState.Deleted;
Since Template have multiple itens for Station and TimePeriod you will have to iterate over them and set each one as "Unchanged" I assume, or "Modified".
Let me know if it works.

Why does my ASP.NET MVC 4 application create new entities instead of updating the old ones?

EDIT: The solution I selected probably wasn't the best, but it definitely worked. I'll be going through my code over the next week (once this project is done) and I'll update my question when I understand what went wrong.
I'm using the ASP.NET MVC 4 framework with Entity 5. Here's some code:
The class to be instantiated and saved (fresh) in the database:
public class ClassCancellation
{
[Key]
public int Id { get; set; }
public Faculty Professor { get; set; }
public DateTime CancelledOn { get; set; }
public Course Course { get; set; }
[Required]
public ClassDate ClassCancelled { get; set; }
public Message CancellationMessage { get; set; }
[Required]
public List<Student> Students { get; set; }
}
It's mapped from the viewmodel called CancellationFull (with AutoMapper):
public class CancellationForList
{
[Key]
public int Id { get; set; }
public CourseForList Course { get; set; }
public ClassDateForList ClassCancelled { get; set; }
}
public class CancellationFull : CancellationForList
{
public CancellationFull()
{
this.Students = new List<StudentForList>();
}
public FacultyForList Professor { get; set; }
public MessageForList CancellationMessage { get; set; }
public DateTime CancelledOn { get; set; }
public List<StudentForList> Students { get; set; }
}
This is the repo method that turns a CancellationFull into a ClassCancellation and then saves it to the database:
public CancellationFull createClassCancellation(CancellationFull c)
{
ClassCancellation newCancellation = Mapper.Map<ClassCancellation>(c);
dc.ClassCancellations.Add(newCancellation);
dc.SaveChanges();
return Mapper.Map<CancellationFull>(dc.ClassCancellations.FirstOrDefault(cc => cc.Id == newCancellation.Id));
}
Why, for the love of god why, does the database create new objects for Faculty and Course when the Id (primary key) of each's existing entity counterpart is provided? It might also be doing the same with Student objects but I haven't looked that closely.
Before the ClassCancellation instance is saved to the database the debugger shows that it's attributes Professor of type Faculty and Course of type Course have the correct primary key - that is, the primary key of the already existing entities of those types that I'm trying to update with a reference to the new ClassCancellation object.
Driving me nuts. Feel free to ask for clarification!
EDIT:
Here's the logic where the CancellationFull viewmodel is constructed from form data and viewmodels about existing objects retrieved from their respective repos:
newCancellation = new CancellationFull();
newCancellation.CancelledOn = DateTime.Now;
newCancellation.ClassCancelled = repoClass.getClassDateForListById(Int32.Parse(classIds[i]));
newCancellation.Course = repoCourse.getForList(newCancellation.ClassCancelled.Course.Id);
newCancellation.CancellationMessage = repoMessage.getMessageForList(newMessage.Id);
newCancellation.Professor = repoFac.getFacultyForList((int)Session["facId"]);
var students = repoStudent.getStudentsForListByCourse(newCancellation.Course.Id);
foreach ( var student in students )
{
newCancellation.Students.Add(student);
}
repoCancellation.createClassCancellation(newCancellation);
Here's an example of one of those repo methods (the rest are very similar):
public CourseForList getForList(int? id)
{
return Mapper.Map<CourseForList>(dc.Courses.FirstOrDefault(c => c.Id == id));
}
What I find the easiest solution is when updating a model, clear any related entities, then re add them.
ie:
newCancellation.Students.Clear();
foreach ( var student in students )
{
newCancellation.Students.Add(student);
}
Try using Attach() instead of Add()
dc.ClassCancellations.Attach(newCancellation);
dc.SaveChanges();
Add() is used for new objects that do not already exist in the database. Attach() is used for creating relationships to entities that already exist in the database.
EDIT
Without seeing your code, the best solution I can recommend to attach is to create a 'stub' instance and then attach that to your newCancellation:
var existingCourse = new Course{ Id = newCancellation.ClassCancelled.Course.Id };
db.Courses.Attach(existingCourse);
newCancellation.Course = existingCourse;
The problem is that you have multiple contexts, or units of work. When you add the newCancellation to the dc context, it also adds any related entity in the object graph that is not tracked in the dc context. I think your best option is:
dc.ClassCancellations.Add(newCancellation);
dc.Entry(newCancellation.Course).State = EntityState.Unchanged;
dc.Entry(newCancellation.Faculty).State = EntityState.Unchanged;
See Julie Lerman's article on this issue for an explanation and other options.
In my opinion, EF should recognize entities that have autonumbered keys and not insert them if the key is assigned.

Entity Framework ObjectSet detach method functionality

Well, first of all I'll explain the whole situation. I have simple POCO domain model which I'm persisting with EF 4.0. For the first time I used only navigation properties and no FK properties. But later due to some binding purposes I decided to add FK properties to my model (Company_ID in the code below). Here are two classes from that model:
public class Company:EntityObject<Int32>, {
public virtual string Name { get; set; }
public virtual string Phone { get; set; }
public virtual string Fax { get; set; }
public virtual IList<Customer> Customers { get; set; }
}
public class Customer:EntityObject<Int32> {
public virtual string FirstName { get; set; }
public virtual string LastName { get; set; }
public virtual Company Company { get; set; }
public virtual int Company_ID { get; set; }
}
I simplified this model a little bit just to highlight the main issue. After adding FK properties I regenerated the EDMX with FK inclusion. So now some of my test code doesn't work anymore. The problem is with detach method of ObjectSet (Repository Detach is a wrapper around it). Here is the test code:
using (IEntityModelContext context = new EFDataContext()) {
var compFact = context.GetFactory<Company>();
var custFact = context.GetFactory<Customer>();
Company comp = compFact.CreateObject();
comp.Fax = "111111";
comp.Name = "Testcomp";
comp.Phone = "222222";
context.CompanyRepository.Add(comp);
context.SaveChanges();
Customer cust = custFact.CreateObject();
cust.FirstName = "John";
cust.LastName = "Smith";
comp.Customers.Add(cust);
context.SaveChanges();
context.CompanyRepository.Detach(comp);
Company newComp = context.CompanyRepository.Load(com => com.Name == "Testcomp");
Assert.IsNotNull(newComp);
Assert.IsFalse(newComp.IsTransient);
Assert.AreEqual(comp.Fax, newComp.Fax);
Assert.AreEqual(industryList.Values[0], newComp.Industry);
Assert.AreEqual(comp.Name, newComp.Name);
Assert.AreEqual(comp.Phone, newComp.Phone);
Assert.AreEqual(sizeList.Values[0], newComp.Size);
Assert.AreEqual(1, newComp.Customers.Count);
The problem rises when newComp object is loaded: Customers property is empty, moreover it's null (I checked the DB - Customer was successfully saved). So the last assertion fails. This code worked quite well until I added FK property. So is there any explanation of this behavior?
Assuming it was successfully saved to the db (so the customer/company relationship exists in db)
The problem lies in the fact that you did not load the reference to Customers. So either:
A) Include Customers when you retrieve company from the context, or
B) Lazy Load it before you check the reference.

Categories

Resources