I have an Entity Framework Project with several linked entities. Since it is utilized by multiple users at once I've set up a RowVersion-Field for entities which are likely to be edited by several users at once. Unfortunately I now get an OptimisticConecurrencyException every time I try to save a new entity, which is linked to an already existing entity.
Store update, insert, or delete statement affected an unexpected number of rows (0). Entities may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=472540 for information on understanding and handling optimistic concurrency exceptions.
The problem is now that this error doesn't really give any pointers as to where the error really lies. It could either be the underlying model that is modified in the meantime, there could be a validation error on the new model or something else.
The code I use to add the new entity is as follows:
using (ctx = new DbContext())
{
try
{
ctx.Samples.Add(model);
ctx.SaveChanges();
}
catch (DbUpdateConcurrencyException ex)
{
LogManager.HandleException(ex.InnerException);
}
}
model is the model i want to add to the database
Edit: As seen above i modified the code to ignore the update of an underlying model. Furthermore i have verified through:
ctx.Database.log = s => Debug.Write(s);
That only an insert statement is sent to the database and not an additional update statement.
INSERT [dbo].[Samples]([IDSample], [ModificationDate], [IDUser])
VALUES (#0, #1, #2)
SELECT [RowVersion]
FROM [dbo].[Samples]
WHERE ##ROWCOUNT > 0 AND [IDSample] = #0 AND [ModificationDate] = #1
I would understand the exception if i would update an entity and the rowversion column wouldn't match, but in this case it's a completely new entity. Is there a way to see if one of the properties is malformed?
Edit2:
Instead of just trimming the milliseconds i now used DateTime.Today instead of DateTime.Now which works. Seemingly there is some problem with datetime2(4) on ModificationDate. I already made sure that ModificationDate is truncated to 4 milliseconds so there should be no parse error.
Edit3:
After switching back to DateTime.Now and trimming the milliseconds it stopped working and the entities are not longer inserted into the database. Could this be caused by the fact that the sql server has problems matching the entities based on millisecond values. I executed the EF generated SQL as seen above with some fictional values and it went through although on some occasions the query didn't return a rowversion-value. In terms of the entity framework, the client would interpret this as a return value of 0 lines and therefore call an concurrency-exception. (It should also be of note that the ModificationDate together with the IDSample is the primary key of the entity.)
Edit4:
I'm now using DateTime.Today and then add the needed precision, which works for me. This can be flagged as solved. (Altough i would have expected that EF can take care of datetime-format-conversion by itself :/)
The question I have is where are/were you adding the DateTime? You are creating too many steps to hammer out this problem. Creating a datetime, modifying it, etc.
If you're entity is inheriting from a base class with mapped properties do your concurrency add/update in the DbContext override of SaveChanges().
Here's an example: (written without optimized syntax)
public abstract class EntityBase
{
public int Id {get; set;}
public DateTime CreationDate {get; set;}
public DateTime? ModifyDate {get; set;}
public string VersionHash {get; set;}
}
public static class EntityBaseExtensions
{
public static void MyBaseEntityMapping<T>(this EntityTypeConfiguration<T> configuration) where T : EntityBase
{
configuration.HasKey(x => x.Id);
configuration.Property(x => x.CreationDate)
.IsRequired();
configuration.Property(x => x.ModifyDate)
.IsOptional();
configuration.Property(x => x.VersionHash).IsConcurrencyToken();
}
}
public class MyEntity : EntityBase
{
public string MyProperty {get; set;}
}
public class MyEntityMapping : EntityTypeConfiguration<MyEntity>
{
public MyEntityMapping()
{
this.MyBaseEntityMapping();
Property(x=>x.MyProperty).IsRequired();
}
}
public class MyContext : DbContext
{
....
public override int SaveChanges()
{
this.ChangeTracker.DetectChanges(); //this forces EF to compare changes to originals including references and one to many relationships, I'm in the habit of doing this.
var context = ((IObjectContextAdapter)this).ObjectContext; //grab the underlying context
var ostateEntries = context.ObjectStateManager.GetObjectStateEntries(EntityState.Modified | EntityState.Added); // grab the entity entries (add/remove, queried) in the current context
var stateEntries = ostateEntries.Where(x => x.IsRelationship == false && x.Entity is EntityBase); // don't care about relationships, but has to inherit from EntityBase
var time = DateTime.Now; //getting a date for our auditing dates
foreach (var entry in stateEntries)
{
var entity = entry.Entity as EntityBase;
if (entity != null) //redundant, but resharper still yells at you :)
{
if (entry.State == EntityState.Added) //could also look at Id field > 0, but this is safe enough
{
entity.CreationDate = time;
}
entity.ModifyDate = time;
entity.VersionHash = Guid.NewGuid().ToString().Replace("-", "").Substring(0, 10); //this an example of a simple random configuration of letters/numbers.. since the query on sql server is primarily using the primary key index, you can use whatever you want without worrying about query execution.. just don't query on the version itself!
}
}
return base.SaveChanges();
}
....
}
Related
I am trying to set up property for an entity which uses entity's objects (other entities loaded from database based on foreign key). My code looks like this:
[Table("Notes")]
public class Note : FullAuditedEntity
{
public virtual int EntityAId { get; set; }
public EntityA EntityA { get; set; }
public virtual int EntityBId { get; set; }
public EntityB EntityB { get; set; }
public List<NoteXHashtag> AdditionalHashtags { get; set; } = new List<CardXHashtag>();
[NotMapped]
public List<Hashtag> AllHashtags
{
get
{
List<Hashtag> allHashtags = new List<Hashtag>();
allHashtags.AddRange(AdditionalHashtags.Select(x => x.Hashtag));
allHashtags.Add(EntityA.Hashtag);
allHashtags.Add(EntityB.Hashtag);
return allHashtags;
}
}
}
When I try to use property AllHashtags from outside, EntityA and EntityB are null. How to tell Entity Framework to load them from database when I need to work with them?
Note: When I am calling AllHashtags from outside, I have both EntityA and EntityB Included:
noteRepository
.GetAll()
.Include(x => x.AdditionalHashtags).ThenInclude(x => x.Hashtag)
.Include(x => x.EntityA).ThenInclude(x => x.Hashtag)
.Include(x => x.EntityB).ThenInclude(x => x.Hashtag)
.Select(x => new NoteDetailDto()
{
Id = x.Id,
AllHashtags = x.AllHashtags
});
If you are using projection (.Select()) then you do not need to use .Include() to have access to related entities.
I would start by looking at what your repository .GetAll() method is returning. .Include() only works against IQueryable so if the repository method is effectively doing something like this:
return _context.Notes.AsQueryable();
or this:
return _context.Notes.Where(x => x.SomeCondition);
Then you can leverage Include() outside of the repository method. However, if you were returning something like this:
return _context.Notes.Where(x => x.SomeCondition).ToList().AsQueryable();
Then the type would grant access the Include() method, but Include would not actually include the related tables. You can observe this by using a profiler on the database to inspect the queries. With Include, the query would join the EntityA and EntityB tables into the SELECT statement.
With regards to your example statement, it's obviously a simplified example you've supplied, but from what I can see it should not execute if the GetAll() method was returning an EF IQueryable. If it did return an EF IQueryable then accessing the AllHashtags property in a Select would result in an error because AllHashtags is not a mapped property of Note. This means that if your real code looks something like that, then GetAll() is not returning an EF IQueryable, or you've possibly created extension methods for Include/ThenInclude for IEnumerable that do an AsQueryable() to satisfy the EF include operations. (These will not work)
To get the results you want, I would recommend avoiding the use of an unmapped property on the entity, but rather take a 2-step approach with an anonymous type and keep the business logic transformation in the business layer:
Step 1. Ensure that the repository method is just returning an EF IQueryable, so context.[DbSet<TEntity>].AsQueryable() or context.[DbSet<TEntity>].Where([condition]) is fine:
return _context.Notes.AsQueryable();
Step 2. Define a DTO for your Hashtag. (avoid send/receive entities)
[Serializable]
public class HashtagDto
{
public int Id { get; set;}
public string Text { get; set; }
// etc.
}
Step 3. Select the appropriate fields you care about into an anonymous type and materialize it:
var noteData = repository.GetAll()
.Select( x= > new {
x.Id,
AdditionalHashtags = x.AdditionalHashtags.Select(h => new HashtagDto { Id = h.Id, Text = h.Text }).ToList(),
EntityAHashtag = new HashTagDto { Id = x.EntityA.Hashtag.Id, Text = x.EntityA.Hashtag.Text },
EntityBHashtag = new HashTagDto { Id = x.EntityB.Hashtag.Id, Text = x.EntityB.Hashtag.Text },
).ToList();
Step 4. Compose your view model / DTO:
var noteDetails = noteData.Select(x => new NoteDetailDto
{
Id = x.Id,
AllHashTags = condenseHashtags(x.AdditionalHashtags, EntityAHashtag, EntityBHashtag);
}).ToList();
Where the condenseHashtags is just a simple utility method:
private static ICollection<HashTagDto> condenseHashtags(IEnumerable<HashtagDto> source1, HashtagDto source2, HashtagDto source3)
{
var condensedHashtags = new List<HashtagDto>(source1);
if (source2 != null)
condensedHashtags.Add(source2);
if (source3 != null)
condensedHashtags.Add(source3);
return condensedHashtags;
}
The above example is synchronous, it can be transformed into async code without too much trouble if load performance is a concern for server responsiveness.
Steps 3 & 4 can be combined in a single statement, but there needs to be a .ToList() between them as Step 4 needs to run against Linq2Object to condense the hashtags. Step 3 ensures that the Linq2EF expression composes an efficient query to just return the information about the Note, and the associated hashtags that we care about, nothing more. Step 4 condenses those individual details into a DTO structure we intend to return.
Step 2 is important as you should avoid sending Entities back to the client/consumers. Sending entities can lead to performance issues and possible errors if lazy loading is enabled, or leads to incomplete renditions of data being passed around if lazy loading isn't enabled and you neglect to eager-load related info. (Is a related detail really #null, or did you forget to include it?) It can also reveal more about your data structure than consumers should necessarily know, and reflects larger data transfer packets than are needed. Accepting entities back from a client is highly inadvisable since it opens the door for unexpected data tampering, stale data overwrites, and your typical gambit of bugs around dealing with reattaching detached entities that a context may know about. If your code simply attaches entities to a DbContext, sets the modified state, and saves changes. Even if you don't do that today, it opens the door for later modifications to start doing it given an entity is already present in the call. Receive DTOs, load the entity, validate a row version, validate the snot out of the DTO against the entity, and only update the fields that are expected to change.
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
So I'm new to EF (I'm using EF6) and I have problem understanding the concept, I'm trying to update entity with child collection.
Here's my entity class :
public class TimeSheet
{
public int TimeSheetID { get; set; }
public virtual ICollection<TimeSheetDetail> Details { get; set; }
}
public class TimeSheetDetail
{
public int TimeSheetDetailID { get; set; }
public int TimeSheetID { get; set; }
public virtual TimeSheet TimeSheet { get; set; }
}
My update method :
public void Update(TimeSheet obj)
{
var objFromDB = Get(obj.TimeSheetID);
var deletedDetails = objFromDB.Details.Except(obj.Details).ToList();
_dbContext.Entry(obj).State = EntityState.Modified;
//track if details exist
foreach (var details in obj.Details)
{
_dbContext.Entry(details).State = details.TimeSheetDetailID == 0 ? EntityState.Added : EntityState.Modified;
}
//track deleted item
foreach (var deleted in deletedDetails)
{
_dbContext.Entry(deleted).State = EntityState.Deleted;
}
}
public TimeSheet Get(object id)
{
//return _timeSheet.Find(id); //Without AsNoTracking I got error
int x = Convert.ToInt32(id);
return _timeSheet.AsNoTracking().SingleOrDefault(a => a.TimeSheetID == x);
}
Above code give me Attaching an entity of type 'ClassName' failed because another entity of the same type already has the same primary key value. So my quesstion is :
How do you update child collection with EF? Means that I need to Add new if it doesn't exist in DB, update otherwise, or delete from DB if it is removed in the POST.
If I don't use AsNoTracking(), it will throw Saving or accepting changes failed because more than one entity of type 'ClassName' have the same primary key value. I notice that the error was cause by my DbSet add the data from DB to its Local property if I don't use AsNoTracking() which cause the EF framework to throw the error because it thinks I have a duplicate data. How does this work actually?
As you can see I'm trying to compare objFromDb against obj to check if user remove one of the details so I can remove it from the database. Instead I got bunch of DynamicProxies from the collection result. What is DynamicProxies and how does it work?
Is there any good article or 101 tutorial on EF? So far I've only see a simple one which doesn't help my case and I've looking around and find a mixed answer how to do stuff. To be honest, at this point I wish I would just go with classic ADO.Net instead of EF.
For a better understanding of the entity framework, think of the DbContext as proxy between your application and the database.
The DbContext will cache everything and will use every bit of data from the cached values unless you tell it not to do so.
For 1.: This depends on your environment if your DbContext is not disposed between selecting and updating the entites you can simply just call SaveChanges and your data will be saved. If your DbContext is disposed you can detach the entites from the context, change the data, reattach them and set the EntityState to modified.
I can't give you a 100% sure answer, because I stopped using the entity framework about half a year ago. But I know it is a pain to update complex relations.
For 2.: The command AsNoTracking tells the EF not to track the changes made to the entities inside this query. For example your select 5 TimeSheets from your Database, change some values in the first entity and delete the last one. The DbContext knows that the first entity is changed and the last one is deleted, if you call SaveChanges the DbContext will automatically update the first entity , delete the last one and leave the other ones untouched. Now you try to update an entity by yourself and attach the first entity again to the DbContext.
The DbContext will now have two entites with the same key and this leads to your exception.
For 3.: The DynamicProxies is the object that the entity framework uses to track the changes of these entities.
For 4.: Check this link, also there is a good book about entity framework 6 (Title: "Programming Entity Framework")
Have two fields with data type datetime.
Added
Modified
When inserting new record values for both fields must be System.DateTime.Now;
but when updating only Modified needs to be changed.
I can set StoreGeneratedPattern to Computed and handle Modified field with GETDATE() in database but problem is field Added.
My guess is that I have to override SavingChanges() or something similar but don't know how.
EDIT : What I have try so far
Added another class in my project with fallowing code
namespace Winpro
{
public partial class Customer
{
public Customer()
{
this.Added = DateTime.UtcNow;
}
}
}
but then cannot build solution
Type 'Winpro.Customer' already defines a member called 'Customer' with the same parameter types
One option is to define a constructor for the type that sets the field.
Big important note: unless you know exactly what you're doing, always store dates and times in a database in UTC. DateTime.Now is the computer's local time which can vary according to daylight savings, timezone changes (brought about by political/legislative reasons), and can end up rendering date information useless. Use DateTime.UtcNow.
public partial class MyEntity {
public MyEntity() {
this.Added = DateTime.UtcNow;
}
}
We did something quite similar in the past.
There was the need to store both Date and Time and the responsible for creating the record. Also, on every change, dispite if there's an audit record or not, the base record should also get a Date and Time and the user responsible for the changes.
Here's what we have done:
Interfaces
To add some standard behavior and make things more extensible, we've created two interfaces, as follows:
public interface IAuditCreated
{
DateTime CreatedDateTime { get; set; }
string CreationUser { get; set; }
}
public interface IAuditChanged
{
DateTime LastChangeDateTime { get; set; }
string LastChangeUser { get; set; }
}
Override SaveChanges() to add some automatic control
public class WhateverContext : DbContext
{
// Some behavior and all...
public override int SaveChanges()
{
// Added ones...
var _entitiesAdded = ChangeTracker.Entries()
.Where(_e => _e.State == EntityState.Added)
.Where(_e => _e.Entity.GetType().GetInterfaces().Any(_i => _i == typeof(IAuditCreated)))
.Select(_e => _e.Entity);
foreach(var _entity in _entitiesAdded) { /* Set date and user */ }
// Changed ones...
var _entitiesChanged = ChangeTracker.Entries()
.Where(_e => _e.State == EntityState.Modified)
.Where(_e => _e.Entity.GetType().GetInterfaces().Any(_i => _i == typeof(IAuditChanged)))
.Select(_e => _e.Entity);
foreach(var _entity in _entitiesChanged) { /* Set date and user */ }
// Save...
return base.SaveChanges();
}
}
Do not simply copy and paste!
This code was written a few years ago, on the age of EntityFramework v4. It assumes that you have already detected changes (ChangeTracker available) and some other.
Also, we have absolutely no idea of how this code impacts performance on any way. That's because the usage of this system is much or related to viewing than updating and also because it's a desktop application, so we have plenty available memory and processing time to waste.
You should take that into account and you might find a better way to implement this. But the whole idea is the same: filter which entities are being updated and which are being added to properly handle that.
Another approach
There are many approaches to this. One other that might be better for performance on some cases (but also more complex) is to have some sort of proxy, similar to an EF proxy, handling that.
Again, even with an empty interface, it's good to have one to clearly distinguish between auditable records and regular ones.
If possible to force all of them having the same property name and type, do it.
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.