Updating a slightly complex entity in EntityFrameworkCore - c#

I am trying out EntityFrameworkCore. I looked at the documentation, but couldn't find a way to easily update a complex entity that is related to another entity.
Here is a simple example. I have 2 classes - Company & Employee.
public class Company
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
public Company Company { get; set; }
}
Company is a simple class, and Employee is only slightly complex, as it contains a property with reference to the Company class.
In my action method, which takes in the updated entity, I could first look up the existing entity by id, and then set each property on it before I call SaveChanges.
[HttpPut]
public IActionResult Update(int id, [FromBody]Employee updatedEmployee)
{
if (updatedEmployee == null || updatedEmployee.Id != id)
return BadRequest();
var existingEmployee = _dbContext.Employees
.FirstOrDefault(m => m.Id == id);
if (existingEmployee == null)
return NotFound();
existingEmployee.Name = updatedEmployee.Name;
if (updatedEmployee.Company == null)
existingEmployee.Company = null; //as this is not a PATCH
else
{
var existingCompany = _dbContext.Companies.FirstOrDefault(m =>
m.Id == updatedEmployee.Company.Id);
existingEmployee.Company = existingCompany;
}
_dbContext.SaveChanges();
return NoContent();
}
With this sample data, I make an HTTP PUT call on Employees/3.
{
"id": 3,
"name": "Road Runner",
"company":
{
"id": 1
}
}
And that works.
But, I hope to avoid having to set each property this way. Is there a way I could replace the existing entity with the new one, with a simple call such as this?
_dbContext.Entry(existingEmployee).Context.Update(updatedEmployee);
When I try this, it gives this error:
System.InvalidOperationException: The instance of entity type
'Employee' cannot be tracked because another instance of this type
with the same key is already being tracked. When adding new entities,
for most key types a unique tem porary key value will be created if no
key is set (i.e. if the key property is assigned the default value for
its t ype). If you are explicitly setting key values for new entities,
ensure they do not collide with existing entities or temporary values
generated for other new entities. When attaching existing entities,
ensure that only one entity instance with a given key value is
attached to the context.
I can avoid this error if I retrieve the existing entity without tracking it.
var existingEmployee = _dbContext.Employees.AsNoTracking()
.FirstOrDefault(m => m.Id == id);
And this works for simple entities, but if this entity has references to other entities, this causes an UPDATE statement for each of those referenced entities as well, which is not within the scope of the current entity update. The documentation for the Update method says that as well:
// Begins tracking the given entity, and any other reachable entities that are not already being tracked, in the Microsoft.EntityFrameworkCore.EntityState.Modified state such that they will be updated in the database when Microsoft.EntityFrameworkCore.DbContext.SaveChanges is called.
In this case, when I update the Employee entity, my Company entity changes from
{
"id": 1,
"name": "Acme Products"
}
to
{
"id": 1,
"name": null
}
How can I avoid the updates on the related entities?
UPDATE:
Based on the inputs in the comments and the accepted answer, this is what I ended up with:
Updated Employee class to include a property for CompanyId in addition to having a navigational property for Company. I don't like doing this as there are 2 ways in which the company id is contained within Employee, but this is what works best with EF.
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
public int CompanyId { get; set; }
public Company Company { get; set; }
}
And now my Update simply becomes:
[HttpPut]
public IActionResult Update(int id, [FromBody]Employee updatedEmployee)
{
if (updatedEmployee == null || updatedEmployee.Id != id)
return BadRequest();
var existingEmployeeCount = _dbContext.Employees.Count(m => m.Id == id);
if (existingEmployeeCount != 1)
return NotFound();
_dbContext.Update(updatedEmployee);
_dbContext.SaveChanges();
return NoContent();
}

Based on documentation of Update
Ref:Update
Begins tracking the given entity in the Modified state such that it will be updated in the database when SaveChanges() is called.
All properties of the entity will be marked as modified. To mark only some properties as modified, use Attach(Object) to begin tracking the entity in the Unchanged state and then use the returned EntityEntry to mark the desired properties as modified.
A recursive search of the navigation properties will be performed to find reachable entities that are not already being tracked by the context. These entities will also begin to be tracked by the context. If a reachable entity has its primary key value set then it will be tracked in the Modified state. If the primary key value is not set then it will be tracked in the Added state. An entity is considered to have its primary key value set if the primary key property is set to anything other than the CLR default for the property type.
In your case, you have updatedEmployee.Company navigation property filled in. So when you call context.Update(updatedEmployee) it will recursively search through all navigations. Since the entity represented by updatedEmployee.Company has PK property set, EF will add it as modified entity. A point to notice here is Company entity has only PK property filled in not others. (i.e. Name is null). Therefore while EF determines that Company with id=1 has been modified to have Name=null and issues appropriate update statement.
When you are updating navigation by yourself, then you are actually finding the company from server (with all properties populated) and attaching that to existingEmployee.Company Therefore it works since there are no changes in Company, only changes in existingEmployee.
In summary, if you want to use Update while having a navigation property filled in then you need to make sure that entity represented by navigation has all data and not just PK property value.
Now if you have only Company.Id available to you and cannot get other properties filled in updatedEmployee then for relationship fixup you should use foreign key property (which needs PK(or AK) values only) instead of navigation (which requires a full entity).
As said in question comments:
You should add CompanyId property to Employee class. Employee is still non-poco (complex) entity due to navigation present.
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
public int? CompanyId {get; set; }
public Company Company { get; set; }
}
During update action pass updatedEmployee in following structure. (See this is the same amount of data, just structured bit differently.)
{
"Id": 3,
"Name": "Road Runner",
"CompanyId": 1,
"Company": null //optional
}
Then in your action, you can just call context.Update(updatedEmployee) and it will save employee but not modify the company.
Due to Employee being complex class, you can still use the navigation. If you have loaded employee with eager loading (Include) then employee.Company will have relevant Company entity value.
Notes:
_dbContext.Entry(<any entity>).Context gives you _dbContext only so you can write just _dbContext.Update(updatedEmployee) directly.
As you figured out with AsNoTracking, if you load the entity in the context, then you cannot call Update with updatedEmployee. At that point you need to modify each property manually because you need to apply changes to the entity being tracked by EF. Update function gives EF telling, this is modified entity, start tracking it and do necessary things at SaveChanges. So AsNoTracking is right to use in this case. Further, if the purpose of retrieving entity from server for you is to check existence of employee only, then you can query _dbContext.Employees.Count(m => m.Id == id); and compare return value to 1. This fetches lesser data from the server and avoids materializing the entity.
There is no harm in putting property CompanyId if you don't add it to CLR class then EF creates one for you in background as shadow property. There will be database column to store value of FK property. Either you define property for it or EF will.

Related

Entity framework 6 Concept clear regarding Remove, RemoveRange, EntityState.Deleted

I use Entity framework 6 in my projects and I always have doubts regarding some of the concepts which are used to delete objects using EF.
I still don't know which one works in which scenario. I just try all and if one works I leave it until the code is working. But no wi need to understand this concept once and for all. I did my research my unable to understand the concept clearly.
I have a domain class in EF which have multiple referencing entities. For example. I have a domain class called Course and It has multiple referencing objects mentioned below in the code.
public class Course
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public int CompanyId { get; set; }
public virtual Company Company { get; set; }
public virtual PricingSchedule PricingSchedule { get; set; }
public virtual ICollection<CustomerCourse> AssignedCustomers { get; set; }
public virtual ICollection<License> Licenses { get; set; }
public virtual ICollection<GroupLicense> GroupLicenses { get; set; }
public virtual ICollection<GroupCourse> GroupCourses { get; set; }
public virtual ICollection<Learner> Learners { get; set; }
}
Now I have to delete the course from the DB with all of its referencing entities. For example, If the course is deleting then its properties like AssignedCustomers, Licenses etc all must be deleted.
But I don't understand one thing using Entity framework.
For deleting an entity from DB we have multiple options like.
Remove
RemoveRange
EntityState.Deleted
Sometimes Remove works but sometime RemoveRange Works and sometime Entitystate.Deleted works. Why?
My code is for deleting a Course
var courses = _context.Courses
.Include("AssignedCustomers")
.Include("PricingSchedule")
.Include("Licenses")
.Include("GroupCourses")
.Include("GroupLicenses")
.Where(e => courseIds.Contains(e.Id)).ToList();
if (courses != null && courses.Count > 0)
{
courses.ForEach(currentCourse =>
{
_context.Entry(currentCourse.PricingSchedule).State = EntityState.Deleted;
Sometime remove range works and code run successfully
_context.CustomerCourses.RemoveRange(currentCourse.AssignedCustomers);
Below line of code gives me error but in other scenario it works why?
//currentCourse.AssignedCustomers.ToList().ForEach(ac =>
//{
// //currentCourse.AssignedCustomers.Remove(ac);
// _context.Entry(ac).State = EntityState.Deleted;
//});
_context.Entry(currentCourse).State = EntityState.Deleted;
});
}
_context.SaveChanges();
Can anyone explain to me the difference in which situation I should use what?
The error I receive most of the time is
The operation failed: The relationship could not be changed because one or more of the foreign-key properties is non-nullable. When a change is made to a relationship, the related foreign-key property is set to a null value. If the foreign-key does not support null values, a new relationship must be defined, the foreign-key property must be assigned another non-null value, or the unrelated object must be deleted.
This error comes up when I use this piece of code
currentCourse.AssignedCustomers.ToList().ForEach(ac =>
{
_context.Entry(ac).State = EntityState.Deleted;
});
OR
currentCourse.AssignedCustomers.ToList().ForEach(ac =>
{
currentCourse.AssignedCustomers.Remove(ac):
});
after that when I hit SaveChanges The error comes up.
You need to set up the cascade rules in your schema and within Entity Framework so that it knows which related entities will be deleted when you go to delete a course. For instance you will want to cascade delete while others like Learner would likely have a null-able key which can be cleared if a course is removed.
Provided it is set up correctly, you should just need to use: context.Courses.Remove(course); and the related entities will be removed or disassociated automatically. Start with a simpler example of your parent-child relationships, one child to cascade delete, another to disassociate with a nullable FK. Your current example looks to also have many-to-many associations (GroupCourses) so depending on the mapping/relationships the approach will vary.

.NET - Attaching an entity of type failed because another entity of the same type already has the same primary key value

I know there are several questions posed about this very same thing but none of which seems to help me. I'm trying to do a .RemoveRange() and every question I've been seeing has to do with edits and adds.
Here's the relevant bits of the method in which the exception is getting thrown:
public bool UpdateFileboundApplications(IList<IFileboundApplicationDm> fileboundApplications)
{
// get all mappings in the DB that match the incoming fileboundApplications
var incomingFbAppsAlreadyExistingInDb =
fileboundApplications.Where(app => app.Id == Db.inf_DMS_FBApplicationProjectMapping.SingleOrDefault(a => a.ApplicationId == app.Id)?.ApplicationId
&& app.FileboundProject != null).ToList();
// in the case that application/project mappings include filebound applications with no project mapping,
// pass the collection to a method which will handle removal of these records.
var fbAppMappingsWithoutNulls = RemoveNullFileboundApplicationMappings(incomingFbAppsAlreadyExistingInDb, fileboundApplications);
var fbAppMappingsAppIdsAndProjectIds = fbAppMappingsWithoutNulls.Select(x => new { appId = x.Id, projectId = x.FileboundProject.Id}).ToList();
var dbRecords = Db.inf_DMS_FBApplicationProjectMapping.Select(y => new { appId = y.ApplicationId, projectId = y.ProjectID}).ToList();
var fbApplicationDifferences =
dbRecords.FindDifferences(fbAppMappingsAppIdsAndProjectIds,
s => new Tuple<int, int>(s.appId, s.projectId),
d => new Tuple<int, int>(d.appId, d.projectId));
if (fbApplicationDifferences.ExistOnlyInSource.Any())
{
// items to remove from the table, as these apps are now assigned to a different project.
var allAppsToRemove = fbApplicationDifferences.ExistOnlyInSource.Select(x => new inf_DMS_FBApplicationProjectMapping
{
ApplicationId = x.appId,
ProjectID = x.projectId,
MapId = Db.inf_DMS_FBApplicationProjectMapping.Single(m => m.ApplicationId == x.appId).MapId
}).ToList();
Db.inf_DMS_FBApplicationProjectMapping.RemoveRange(allAppsToRemove);
}
Db.SaveChanges();
return true;
}
FWIW, I'll include the code for the RemoveNullFileboundApplicationMappings as well:
private IEnumerable<IFileboundApplicationDm> RemoveNullFileboundApplicationMappings(IEnumerable<IFileboundApplicationDm> incomingFbAppsAlreadyExistingInDb,
IEnumerable<IFileboundApplicationDm> fileboundApplications)
{
// hold a collection of incoming fileboundApplication IDs for apps that have no associated fileboundProject
var appIdsWithNoFbProject = fileboundApplications.Except(incomingFbAppsAlreadyExistingInDb)
.Select(app => app.Id);
// get records in the table that now need to be removed
var dbRecordsWithMatchingIds = Db.inf_DMS_FBApplicationProjectMapping.Where(mapping => appIdsWithNoFbProject.Contains(mapping.ApplicationId));
if (dbRecordsWithMatchingIds.Any())
{
// remove records for apps that no will no longer have an associated Filebound project
Db.inf_DMS_FBApplicationProjectMapping.RemoveRange(dbRecordsWithMatchingIds);
Db.SaveChanges();
}
return fileboundApplications.Where(app => app.FileboundProject != null);
}
Finally, here's the inf_DMS_FBApplicationProjectMapping class:
public partial class inf_DMS_FBApplicationProjectMapping
{
public int MapId { get; set; } // <-- this is the PK
public int ApplicationId { get; set; }
public int ProjectID { get; set; }
public Nullable<int> Modified_By { get; set; }
public Nullable<System.DateTime> Modified_On { get; set; }
public virtual glb_Applications glb_Applications { get; set; }
}
}
Exception reads as follows:
{"Attaching an entity of type 'xxxx' 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."}
I don't quite understand how I need to be using Db.inf_.....Add(), as I'm not intending to add records to the table; I need to be removing records.
I don't understand what this "attaching to context" is all about and what that really means.
I really appreciate any insight the community may have on this. It's been a struggle trying to find a way to solve this. Thanks!
I guess the problem is in the new that you use to compose the list you pass as parameter to RemoveRange. As the entities in that list have not been queried directly from your DbSet they have never been attached to your local context and so EF gets confused.
You need to understand the concept of entities attached to the context. Entity Framework keeps track of the changes done to entities you are working with, in order to be able to decide what to do when you do SaveChanges: insert, update, delete. EF is only able to do that if the entities are attached to the context. That means they have a property State with the value Added, Deleted, Modified, Unchanged, etc.
In simple scenarios this is transparent to you, because entities get automatically attached when you do DbSet.Add(entity), DbSet.Find(entityId), or when you get an entity instance as a result of a query, like DbSet.Where(...), DbSet.FirstOrDefault(...), etc. That is why you probably never had to worry about attached entities before in your EF code.
In more complex scenarios like your current one, the entities you are trying to delete have not been instantiated from one of those operations, so they have not been automatically attached to your context. You have to do it explicitly, if you instantiate them with new.
So you should do something like this before the SaveChanges:
foreach(var item in allAppsToRemove)
{
Db.Entry(item).State = EntityState.Deleted;
}
By using the method Entry the entities get attached to the context, and then you explicity set their state as Deleted, to have them deleted when SaveChanges is executed later.
Take a look at this page. Even if it deals mostly with Add and Update cases it contains information relevant to your problem with the Delete. Understanding the concept of entities attached to the local DbContext will help you a lot when programming with EF. There are some cases like this one where you will have trouble if you don't know how attached entities work (you will eventually get to some 'orphaned children' errors also).
Note: in Entity Framework Core (EF7) there is an AttachRange method that can be used before RemoveRange.
With Diana's help, I was able to solve this issue.
The problem was that I was manually flipping the entity state AND calling .RemoveRange(). I only needed to be flipping the entity state. Here's the relevant bits that solved the issue:
...
...
...
if (fbApplicationDifferences.ExistOnlyInSource.Any())
{
// items to remove from the table, as these apps are now assigned to a different project.
var allAppsToRemove = fbApplicationDifferences.ExistOnlyInSource.Select(x => new inf_DMS_FBApplicationProjectMapping
{
ApplicationId = x.appId,
ProjectID = x.projectId,
MapId = Db.inf_DMS_FBApplicationProjectMapping.Single(m => m.ApplicationId == x.appId).MapId
}).ToList();
foreach (var app in allAppsToRemove)
{
var item = Db.inf_DMS_FBApplicationProjectMapping.Find(app.MapId);
Db.Entry(item).State = EntityState.Deleted;
}
//Db.inf_DMS_FBApplicationProjectMapping.RemoveRange(allAppsToRemove); <-- these items are already "flagged for deletion" with .State property change a few lines above.
}
Just change your code after SaveChanges methot change EntityState Detached

Entity Framework inheritance

SQL Layer:
I have a table
Entity Framwork Layer:
I have the following rule: all Offers, which have State is null, are Outstanding offers, State is true are Accepted offers, State is false are Declined offers. Also, part of fields used only for Outstanding, part - only for Accepted etc... I use Database first approach, so, I updated EF model from DB and renamed Offer entity to OfferBase and created 3 child classes:
it works fine for add/select entities/records. Right now I want to "move" offer from outstanding to accept offer, so, I need to set Status=true (from Status is null) for appropriate record. But how to do it by Entity Framework? If I try to select outstanding offer as Accept offer I get an null reference (and clearly why)
// record with ID=1 exists, but State is null, so, EF can not find this record and offer will be null after the following string
var offer = (from i in _db.OfferBases.OfType<EFModels.OfferAccepted>() where i.ID == 1 select i).FirstOrDefault();
if I try to select as OfferBase entity I get the following error:
Unable to cast object of type
'System.Data.Entity.DynamicProxies.OfferOutstanding_9DD3E4A5D716F158C6875FA0EDF5D0E52150A406416D4D641148F9AFE2B5A16A'
to type 'VTS.EFModels.OfferAccepted'.
var offerB = (from i in _db.OfferBases where i.ID == 1 select i).FirstOrDefault();
var offer = (EFModels.OfferAccepted)offerB;
ADDED NOTES ABOUT ARCHITECTURE:
I have 3 types of Offer entity. There are: AcceptOffer, DeclineOffer and OutstandingOffer.
AcceptOffer:
UserID
ContactID
Notes
FirstContactDate
LastContactDate
[... and 5-10 the unique fields...]
DeclineOffer:
UserID
ContactID
Notes
[... and 5-10 the unique fields...]
OutstandingOffer:
UserID
ContactID
FirstContactDate
LastContactDate
[... and 5-10 the unique fields...]
How to do it correctly? Of course, I can select a record, remove from DB and add new with appropriate state value, but how to do it normally?
You can't change the type of an object once it's created. Your object model seems wrong.
Either you delete the outstanding offer and create an accepted offer from it (looks like what you are currently doing) but you may lose relations as you created a new object with a new identity (you can also copy them before removing the old object). Or you want to keep the same object and change its state.
If you want to keep the same identity then preffer composition over inheritance.
Your code could look like this :
public class Offer
{
public int Id { get; set; }
public virtual OfferState State { get; set }
}
public class OfferState
{
public int OfferId { get; set; }
public string Notes { get; set; }
}
public class AcceptedOfferState : OfferState
{
public DateTimeOffset AcceptDate { get; set; }
}
public class DeclinedOfferState : OfferState
{
public DateTimeOffset DeclinedDate { get; set; }
}
If you still want to change the type of the object and keep its identity then you may use stored procedures ; as stated by Noam Ben-Ami (PM owner for EF) : Changing the type of an entity.
Rather than trying to add these custom classes to your entity framework model, just create them as normal c# classes and then use a projection to convert from the entity framework generated class to your own class e.g.
var accepteOffers= from i in _db.Offers
where i.ID == 1 && i.Status == true
select new OfferAccepted { AcceptDate = i.AcceptDate, StartTime = i.StartTime /* map all releaveant fields here */};

Entity Framework 6 Lazy Loading returns null

I have a class with the following properties
public class Booking
{
public long BookingId {get;set;}
public string RoomNumber {get;set;}
[ForeignKey("BookingCustomer")]
public long? BookingCustomerId {get;set;}
public virtual Customer BookingCustomer {get;set;}
}
public class Customer
{
public long CustomerId {get;set;}
public string FirstName {get;set;}
}
if in a method I reference properties of the customer class am getting object null reference exception while BookingCustomerId is populated.i.e.,
hotel.BookingCustomerId=2
For instance,
string customerFirstName = hotel.BookingCustomer.FirstName;
if I peek at the hotel.BookingCustomer i get null
How do I go about this Lazy Loading?
If the related entity is coming back as null this means the relationship as understood by entity framework can't find any related entities.
It appears you are using data annotations to flag properties with properties such as foreign keys. You may also need to flag primary keys with the [key] attribute.
You will also need to add a related booking entity to your customer data.
Alternatively you can you the fluent api to do the following in your context.
// Configure the primary key for the Booking
modelBuilder.Entity<Booking>()
.HasKey(t => t.BookingID);
modelBuilder.Entity<Booking>()
.HasRequired(t => t.customer)
.WithRequiredPrincipal(t => t.booking);
More on the fluent a picture here:
https://msdn.microsoft.com/en-us/data/jj591620.aspx#RequiredToRequired
Lazy loading implies that the related objects are retreived when the getter of that object is used for the first time.
At just that time a query to the database is executed to retreive for that object ,for example Hotel.BookingCustomer.
Try to see if the query is indeed executed e.g. with Sql Server profiler.
Examine the query to makes sure everything is correct
If you can't see the query triggered, try to it without the virtual keyword (eager loading) and see if it's working then.

Lots of problems (foreign key or no rows were updated errors) when trying to save complex objects in EF code first

I have a pretty deep object hierarchy in my application, and I am having trouble saving the entities. Depending on the order I do things, I either one of two errors:
[OptimisticConcurrencyException: Store update, insert, or delete statement affected an unexpected number of rows (0). Entities may have been modified or deleted since entities were loaded. Refresh ObjectStateManager entries.]
or
[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.]
Here is the classes I am working with:
public class SpecialEquipment : Entity
{
public Equipment Equipment { get; set; }
public virtual ICollection<AutoclaveValidation> Validations { get; set; }
}
public class Equipment : Entity
{
public string Model { get; set; }
public string SerialNumber { get; set; }
public Location Location { get; set; }
public EquipmentType EquipmentType { get; set; }
public ICollection<Identifier> Identifiers { get; set; }
}
public class Identifier : Entity
{
public IdentifierType Type { get; set; }
public string Value { get; set; }
}
public class Location : Entity
{
public Building Building { get; set; }
public string Room { get; set; }
}
What I was trying to do was populate one SpecialEquipment object based on form inputs and already existing objects in the database and then save the special equipment to push all changes through, it looks like this:
Building building = buildingService.GetExistingOrNew(viewModel.BuildingCode)); //checks to see if building exists already, if not, create it, save it, and re-query
Location location = locationService.GetExistingOrNew(viewModel.Room, building); //checks to see if location exists already, if not, create it, save it, and re-query
EquipmentType equipmentType = equipmentTypeService.GetOne(x => x.Name == EquipmentTypeConstants.Names.Special);
Equipment equipment = new Equipment{ EquipmentType = equipmentType, Location = location };
equipment.Identifiers = new Collection<Identifier>();
foreach (FormIdentifier formIdentifier in identifiers)
{
FormIdentifier fIdentifier = formIdentifier;
IdentifierType identifierType = identifierTypeService.GetOne(x => x.Id == fIdentifier.Key);
equipment.Identifiers.Add(new Identifier { Type = identifierType, Value = fIdentifier.Value });
}
EntityServiceFactory.GetService<EquipmentService>().Save(equipment);
SpecialEquipment specialEquipment = new SpecialEquipment();
specialEquipment.Equipment = equipment;
specialEquipmentService.Save(specialEquipment);
This code returns Store update, insert, or delete statement affected an unexpected number of rows (0). If I comment out the foreach identifiers OR put the foreach identifiers after the equipment save and then call equipment save after the loop the code works. If I comment out the foreach identifiers and the save equipment line, I get : The INSERT statement conflicted with the FOREIGN KEY constraint "SpeicalEquipment_Equipment". The conflict occurred in database "xxx", table "dbo.Equipments", column 'Id'.
So how can I make these errors not occur but still save my object? Is there a better way to do this? Also I don't like saving my equipment object, then associating/saving my identifiers and/or then my special equipment object because if there is an error occurring between those steps I will have orphaned data. Can someone help?
I should mention a few things that aren't inheritly clear from code, but were some answers I saw for similar questions:
My framework stores the context in the HttpContext, so all the service methods I am using in my API are using the same context in this block of code. So all objects are coming from/being stored in one context.
My Entity constructor populates ID anytime a new object is created, no entities have a blank primary key.
Edit: At the request of comments:
My .Save method calls Insert or Update depending on if the entity exists or not (in this example insert is called since the specialEquipment is new):
public void Insert(TClass entity)
{
if (Context.Entry(entity).State == EntityState.Detached)
{
Context.Set<TClass>().Attach(entity);
}
Context.Set<TClass>().Add(entity);
Context.SaveChanges();
}
public void Update(TClass entity)
{
DbEntityEntry<TClass> oldEntry = Context.Entry(entity);
if (oldEntry.State == EntityState.Detached)
{
Context.Set<TClass>().Attach(oldEntry.Entity);
}
oldEntry.CurrentValues.SetValues(entity);
//oldEntry.State = EntityState.Modified;
Context.SaveChanges();
}
GetExistingOrNew for Building and location both are identical in logic:
public Location GetExistingOrNew(string room, Building building)
{
Location location = GetOne(x => x.Building.Code == building.Code && x.Room == room);
if(location == null)
{
location = new Location {Building = building, Room = room};
Save(location);
location = GetOne(x => x.Building.Code == building.Code && x.Room == room);
}
return location;
}
Get one just passes that where predicate to the context in my repository with singleOrDefault. I am using a Service Layer/Repository Layer/Object Layer format for my framework.
Your Insert method does not seem to be correct:
public void Insert(TClass entity)
{
if (Context.Entry(entity).State == EntityState.Detached)
Context.Set<TClass>().Attach(entity);
Context.Set<TClass>().Add(entity);
Context.SaveChanges();
}
specialEquipment is a new entity and the related specialEquipment.Equipment as well (you are creating both with new)
Look what happens if you pass in the specialEquipment into the Insert method:
specialEquipment is detached because it is new
So, you attach it to the context
Attach attaches specialEquipment and the related specialEquipment.Equipment as well because both were detached from the context
Both are in state Unchanged now.
Now you add specialEquipment: This changes the state of specialEquipment to Added but not the state of specialEquipment.Equipment, it is still Unchanged.
Now you call SaveChanges: EF creates an INSERT for the added entity specialEquipment. But because specialEquipment.Equipment is in state Unchanged, it doesn't INSERT this entity, it just sets the foreign key in specialEquipment
But this FK value doesn't exist (because specialEquipment.Equipment is actually new as well)
Result: You get the FK constraint violation.
You are trying to fix the problem with calling Save for the equipment but you have the same problem with the new identifiers which will finally throw an exception.
I think your code should work if you add the specialEquipment (as the root of the object graph) at the end once to the context - without attaching it, so that the whole graph of new objects gets added, basically just:
context.Set<SpecialEquipment>().Add(specialEquipment);
context.SaveChanges();
(BTW: Your Update also doesn't look correct, you are just copying every property of entity to itself. The context won't detect any change and SaveChanges won't write any UPDATE statement to the database.)
My guess? It can't have an ID if you haven't saved it and that's the root of the problem (since it works if you save first).
Pop everything in a transaction, so if anything goes wrong all is rolled back. Then you don't have orphans.
See http://msdn.microsoft.com/en-us/library/bb738523.aspx for how to use transactions with EF.

Categories