Entity framework attach: object with same key exists - c#

I am building a windows form application, and I use multiple DBContext instances (mostly one per Business layer call).
After literally days of dealing with an issue (while inserting new entities, the ones they referred to were added as new ones, instead of using the existing entities), I found out that the problem was I had to attach the existing entities to the context.
All was good for about 2 hours, when I then got errors while attaching: the entity with the same key exists in the context.
I tried testing before attaching (similar method for every entity type):
private void attachIfNeeded(POCO.Program myObject, myContext context)
{
if (!context.Set<POCO.Program>().Local.Any(e => e.ID == myObject.ID))
{
context.programs.Attach(myObject);
return true;
}
else
{
myObject = context.Set<POCO.Program>().Local.Single(e => e.ID == myObject.ID);
return false;
}
}
But the tests return false, but it still fails when attaching.
So basically, if I don't attach, it will add a new entity instead of using the existing (and intended) one. If I do attach, there's an error I can't figure out.
I have looked around (doing this the whole day now) and I actually (think I) know what the problem is:
The entity I am trying to add has multiple relationships, and other entities can be reached by multiple paths. Could that cause the problem?
Please help with this, solutions out there really make no sense to me and haven't worked.
I am really close to the point where I will try-catch around the attach statement and be done with it. But I will hate doing it.
Here are my entities (not all of them, but this should be enough):
public class Word
{
[Key]
public int ID {get;set;}
[Required]
public string word { get; set; }
public WordCategories category { get; set; }
public Word parent {get;set;}
public List<Unit> units { get; set; }
public Program program { get; set; }
public List<Lesson> lessons { get; set; }
public Word()
{
units = new List<Unit>();
lessons = new List<Lesson>();
}
}
public class Unit
{
[Key ]
public int ID { get; set; }
[Required]
public string name { get; set; }
public string description { get; set; }
public List<Lesson> lessons { get; set; }
public Program program {get;set;}
public List<Word> words { get; set; }
public Unit()
{
lessons=new List<Lesson>();
words = new List<Word>();
}
}
And here is where I am calling the attach method. The error is thrown on the first attach:
public int addWords(List<POCO.Word > words,int programID, int unitID,int lessonID)
{
CourseHelperDBContext context = getcontext();
int result;
foreach(POCO.Word a in words)
{
foreach (POCO.Unit b in a.units)
attachIfNeeded(b, context);
foreach(POCO.Lesson c in a.lessons )
attachIfNeeded(c,context);
attachIfNeeded(a.program,context);
if (a.parent != null)
attachIfNeeded(a.parent,context);
}
context.words.AddRange(words);
result = context.SaveChanges();
return result;
}
I cannot believe I'm having so many issues with this. I just want to store those entities, add some (I haven't gotten to the point where I would change them) and save it.
So far I've figured:
Some words are new, some exist and some are changed (mostly parent property);
All units exist, as do programs and lessons (so I need to attach them);
The object graph contains multiple paths to entities, some of which exist, some of which are new;
I am using a new context for every request. I run into other issues when I was using the same all the time. I found solutions that pointed to this pattern, and I think it's OK since that's what you'd do on an ASP MVC project.
All these could be causing problems, but I don't know which and how to work around them.
I think I can make this work by adding one word at a time, and pulling programs, lessons and units every time... but that means many many round trips to the DB. This can't be the way.

Back to this after quite some time, the problem in this case was that I needed to retrieve the entities that were present on my relationships.
The solution was neither attach (because it would fail if the entity is already attached) nor add (because it already existed on the DB).
What I should have done was to retrieve every entity related to the one I was adding.
This helped:
Entity Framework creating new entity with relationship to existing entity, results in attempt to create new copy of the existing entity

After attaching the entity, try setting the entity state to modified.
context.programs.Attach(myObject);
context.Entry(myObject).State = EntityState.Modified;
I think there's a mistake in your test logic.
If entity does not exist in database, you should be adding instead of attaching. Your code is attaching if it can't find an entity when it should really be adding.
Code to add a new entity (Create/Insert)
context.Set<T>.Add(entity);
Code to attach an entity (Update)
context.Set<T>.Attach(entity);
context.Entry(entity).State = EntityState.Modified;
If your code is failing on the first attach, that would be attachIfNeeded(b,context); ? I don't think you have shown us the code for this.

I share my experience with the same exception.
First, here is my code:
public void UpdateBulk(IEnumerable<Position> pDokumentPosition, DbDal pCtx)
{
foreach (Position vPos in pDokumentPosition)
{
vPos.LastDateChanged = DateTime.Now;
pCtx.Entry(vPos).State = EntityState.Modified;
}
pCtx.SaveChanges();
}
I got the same exception on the EntityState.Modified line.
In my case the problem was that, when set the vPos state to modified, then all the related objects (vPos.Document and vPos.Produkt) loaded in the context with unchanged state.
In the foreach first step it not makes any exception, just on the second step, because eg. the related Dokument entity has already been loaded in the memory/context (so the key property of the Dokument too).
And how i solve it? (maybe not the best solution):
I detach the related entites in every step with this lines:
if (vPos.Dokument != null)
{
pCtx.Entry(vPos.Dokument).State = EntityState.Detached;
}
if (vPos.Produkt!=null)
{
pCtx.Entry(vPos.Produkt).State = EntityState.Detached;
}
If you have better solution, I'm looking forward to it...

You can try this
context.words.Add(words);
result=context.SaveChanges();

Related

EF Core Attach/AttachRange methods not working

I 'm using EF Core 3.1.10. I have the following entities:
public class Request {
public int Id { get; set; }
public string Title { get; set; }
public string ProjectId { get; set; }
public List<RequestAttachment> Attachments { get; set; } = new List<RequestAttachment> ();
}
public class RequestAttachment {
public int Id { get; set; }
public int RequestId { get; set; }
public Request Request { get; set; }
public byte[] FileStream { get; set; }
public string Filename { get; set; }
public RequestAttachmentType RequestAttachmentType { get; set; }
public int RequestAttachmentTypeId { get; set; }
}
public class RequestAttachmentType {
public int Id { get; set; }
public string Name { get; set; }
}
In my repository, I have a simple Update method:
public async Task UpdateRequest (Request aRequest) {
// I'm attaching aRequest.Attachments because they already exist in the database and I don 't want to update them here
// Option 1 Not working
// aRequest.Attachments.ForEach (a => theContext.RequestAttachments.Attach (a));
// Option 2 Not working
// theContext.RequestAttachments.AttachRange (aRequest.Attachments);
// Option 3 Working
aRequest.Attachments.ForEach (a => theContext.Entry (a).State = EntityState.Unchanged);
theContext.Requests.Update(aRequest);
await theContext.SaveChangesAsync ();
}
Note that I'm attaching "aRequest.Attachments" because I don 't want to update Attachments. I only want to update aRequest. "aRequest.Attachments" already exist in the database that's why I 'm using Attach so they don't get re-added. But Attach and AttachRange do not work when a request has more than one attachment. It throws the following error:
The instance of entity type 'RequestAttachmentType' cannot be tracked
because another instance with the key value '{Id: 1}' is already being
tracked. When attaching existing entities, ensure that only one entity
instance with a given key value is attached.
I don 't understand this error because I did not explicitly attach "RequestAttachmentType". The only thing I did was attaching its parent "aRequest.Attachments".
When I set the state manually like I did in Option 3, no error was thrown. I thought Attach is equivalent to theContext.Entry (a).State = EntityState.Unchanged. Why option 3 works but option 1 and 2 do not?
Working with detached entity graphs is going to continue to cause all kinds of headaches like this. Not only do you need to handle the scenario that you don't want to update/duplicate related entities, but you have to also handle cases where the DbContext is already tracking the entity you want to update. Sergey was on the right track there.
The problem is that you have a complete graph:
Request
Atachment
AttachmentType
Attachment
AttachmentType
where you want to update details in Request and the Attachments...
One issue with "Update" is that it will dive the graph to look for entities that might need to be added/updated. On its own with a detached graph this will usually result in duplicate items being created. Hence "attaching" them first. The trouble here is where the DbContext is already tracking one or more entities in the graph. One key detail to remember about EF is that References are everything. Deserializing entity graphs is a painful exercise.
For example lets say we deserialize a Request Id 1, with 2 attachments, #1, and #2, where both have an AttachmentType of "Document" (AttachmentType ID = 14)
What you will end up is something that looks like:
Document
{
ID:1
...
Attachments
{
Attachment
{
ID:1
...
AttachmentType
{
ID: 14
}
}
Attachment
{
ID:2
...
AttachmentType
{
ID: 14
}
}
}
}
Without considering what the DbContext may or may not already be tracking prior to looking at these entities, there is already a problem. Attachment ID 1 and 2 are distinct objects, however they both reference an AttachmentType ID 14. When de-serialized, these will be 2 completely distinct references to objects that have an ID of 14.
A common surprise is where test code appears to work fine because the two attachments had different attachment types, but then fails unexpectedly when they happen to have the same type. The first attachment would have the DbContext tracking the first attachment's "Type". If the second attachment's Type was a different ID, then attaching that 2nd type would succeed so long as the Context wasn't tracking it. However, when set to the same ID the "already tracking entity with the same ID" pops up.
When dealing with disconnected entities you need to be very deliberate about references and explicitly handle whenever the DbContext is tracking a reference. This means consulting the DbSet Local caches:
public async Task UpdateRequest (Request aRequest)
{
var existingRequest = theContext.Requests.Local.SingleOrDefault(x => x.Id = aRequest.Id);
if (existingRequest != null)
{
// copy values from aRequest -> existingRequest or Leverage something like automapper.Map(aRequest, existingRequest)
}
else
{
theContext.Requests.Attach(aRequest);
theContext.Entity(aRequest).State = EntityState.Modified; // Danger Will Robinson, make 100% sure your entity from client is validated!! This overwrites everything.
}
foreach(var attachment in aRequest)
{
var existingAttachment = theContext.Attachments.Local.SingleOrDefault(x => x.Id == attachment.Id);
// Look for a reference to the attachment type. If found, use it, if not attach and use that...
var existingAttachmentType = theContext.AttachmentTypes.Local.SingleOrDefault(x => x.Id == attachment.AttachmentType.Id);
if (existingAttachmentType == null)
{
theContext.AttachmentTypes.Attach(attachment.AttachmentType);
existingAttachmentType = attachment.AttachmentType;
}
if(existingAttachment != null)
{
// copy values across.
AttachmentType = existingAttachmentType; // in case we change the attachment type for this attachment.
}
else
{
theContext.Attachments.Attach(attachment);
theContext.Entity(attachment).State = EntityState.Modified;
attachment.AttachmentType = existingAttachmentType;
}
}
await theContext.SaveChangesAsync ();
}
Needless to say this is a lot of messing around to check and replace references to either get the DbContext to track detached entities or replace the references with tracked entities.
A simpler option is to leverage Automapper to establish a configuration for what fields can be updated from a source (ideally a ViewModel, but you can use an entity graph as a source) to a destination. (Entities tracked by the DbContext)
Step 1: Configure Automapper with the rules about what to update for a Request -> Attachments graph.. (Not shown)
Step 2: Load tracked entity graph, and the applicable AttachmentTypes:
var existingRequest = theContext.Requests
.Include(x => x.Attachments)
.ThenInclude(x => x.AttachmentType)
.Single(x => x.Id == aRequest.Id);
var referencedAttachmentTypeIds = aRequest.Attachments.Select(x => x.AttachmentTypeId)
.Distinct().ToList();
var referencedAttachmentTypes = theContext.AttachmentTypes
.Where(x => referencedAttachmentTypeIds.Contains(x.Id))
.ToList();
Getting the list of attachment types only applies if we can change an attachment's type, or are adding attachments.
Step 3: Leverage Automapper to copy across values
mapper.Map(aRequest, existingRequest);
If Attachments can be updated, added, and/or removed you will need to handle those scenarios against the existingRequest. Here we reference the loaded set of AttachmentTypes.
Step 4: Save Changes.
The primary benefits of this approach is that you do away with the constant checking for existing references and the consequences of missing a check. You also configure the rules about what values can legally be overwritten when calling the Automapper Map call so only values you expect are copied from the source to the existing data record. This also results in faster Update queries as EF will only build statements for the values that actually changed, where using Update or EntityState.Modified result in SQL UPDATE statements that update every column.
Try this:
var itemExist = await theContext.Requests.FirstOrDefaultAsync ( i=>i.Id == aRequest.Id);
if (itemExist !=null)
{
var attachments=aRequest.Attachments;
aRequest.Attachments=null;
theContext.Entry(itemExist ).CurrentValues.SetValues(aRequest);
await theContext.SaveChangesAsync();
aRequest.Attachments=attachments;
}

Entity Framework Core: Adding an object with a List to a DbContext results in an empty List

We are attempting to save an Entity Framework Model to the database.
In the model a Track always contains a Message and a Message MAY have any number of Messages in its List of Retransmits.
When saving the model, we check if the Track model already exists in the database; If not, we simply add the Track to the DbContext.
public class Track
{
[Key]
public Guid TrackId { get; set; }
public virtual Message Message { get; set; }
}
public class Message
{
[Key]
public Guid MessageId { get; set; }
public Guid TrackId { get; set; }
public virtual List<Message> Retransmits { get; set; }
}
public void Save (Track track)
{
using (var context = new DatabaseContext())
{
Track foundTrack = Read(track.TrackId); // Returns a Track if it already exists in the database
if (foundTrack == null)
{
context.Add(track);
}
else
{
// Update the Track. Not relevant to this question
}
context.SaveChanges();
}
}
// Returns a Track if it exists in the database or null
public Track Read(Guid trackId)
{
using (var context = new DatabaseContext())
{
return context.Tracks
.Include(t => t.Message)
.ThenInclude(m => m.Retransmits)
.FirstOrDefault(t => t.TrackId == trackId);
}
}
When adding Track to the DbContext Entity Framework will always clear the List of Retransmits. This means that a Message can contain one Message in its List of Retransmits before calling DbContext.Add, but after calling DbContext.Add it will suddenly have 0. Entity Framework is effectively clearing the list and not adding it to the database.
We expected that the List of Retransmits would be added to the DbContext and NOT deleted from the model without warning.
During debugging we discovered that changing the List of Retransmits from List<Message> to List<AnyOtherObject> returned the expected results and rows in the database.
It seems that because the List of Retransmits is of the same type as the class it is contained within (I.e. A Message can contain many Messages), Entity Framework treats it differently.
We have also tried to configure a One-to-Many relationship between the entities where modelBuilder.Entity<Message>().HasMany(m => m.Retransmits) makes no difference. And even
modelBuilder.Entity<Message>().HasMany(m => m.Retransmits).WithOne(m => m.Parent)
where Parent is a reference back to the containing object of a retransmitted Message. However, this resulted in a StackOverflow exception.
What could be the reason for Entity Framework refusing to add the list to the database? Is there any way to configure Entity Framework to allow a Message to contain a List of Retransmits(being of type Message) and have it added to the database as expected? Or are we doing something wrong entirely?
I have attached an image of the behavior in Visual Studio's Debugger.
EDIT: We have tried the latest stable Entity Framework Core version 2.2.6, as well as Entity Framework Core version 3.0.0 - Preview 9. The same behavior exists across both versions.
EDIT EDIT: The following code initializes the model and should reproduce the issue.
public void ReproduceIssue ()
{
// Create a Track and Message
Track track = new Track()
{
Message = new Message()
};
track.Message.TrackId = track.TrackId;
// Create a Retransmitted Message
Message retransmit = new Message()
{
TrackId = track.TrackId
};
// Add the Retransmitted Message to the List
track.Message.Retransmits.Add(retransmit);
Save(track);
}
We made some changes to our model. While I believe Entity Framework should never throw any information away without issuing a warning / exception, we did circumvented the issue.
I would like to post the code, but it is a bit more complex as the original question left out a lot of code details. However, the solution was basically to configure a many-to-many relationship between Messages using a join table.
After that, it behaved as expected.

A 'Required' Collection field could not be updated via `AddOrUpdate`

I'm using Asp.net web api 2 + entity framework 6.
Basically I have 2 models:
public class MyOrderModel
{
public int Id { get; set; }
public string OrderNumber { get; set;}
public string AuthCode { get; set; }
[Required]
public List<MyOrderDetailModel> Details { get; set; }
}
public class MyOrderDetailModel
{
public int Id { get; set; }
public decimal Amount{ get; set;}
}
After ran the Package Manager Console command Enable-Migration, in Configuration.Seed(WaynyCloudTest.Models.ApplicationDbContext context), I was trying to add some pre-loaded data:
context.MyOrderModels.AddOrUpdate(
s => s.OrderNumber,
new MyOrderModel
{
OrderNumber = "0001",
AuthCode = "ABCDE",
Details = new List<MyOrderDetailModel>()
{
new MyOrderDetailModel()
{
Amount = 5.67M
}
}
};
After the first(initial) Update-database command, everything is fine and I can see above data persisted to database 2 tables.
Later, I want to update the AuthCode property value from ABCDE to ABCDEXXX,
the only change is the value assignment:
context.MyOrderModels.AddOrUpdate(
s => s.OrderNumber,
new MyOrderModel
{
OrderNumber = "0001",
// THE ONLY CHANGE!
AuthCode = "ABCDEXXX",
Details = new List<MyOrderDetailModel>()
{
new MyOrderDetailModel()
{
Amount = 5.67M
}
}
};
I would expect the EntityFramework will find the target data row in database by searching condition on OrderNumber and then update the AuthCode, but now I always got this exception in Seed function:
Entity Validation Failed - errors follow:
MyTest.Models.MyOrderModel failed validation
Details : The Details field is required.
Obviously the value was supplied for field Details, so what I've missed?
The problem is with the Id field of PostPayQRCodeFuelOrderModel. In your environment, the database uses this field as an identity (primary key) field and wants to generate the value itself.
In your case, there is an easy workaround:
context.MyOrderModel.AddOrUpdate(
p => p.OrderNumber,
new PostPayQRCodeFuelOrderModel
{
OrderNumber = "00001",
Details = new List<MyOrderDetailModel>()
{
new MyOrderDetailModel()
{
Amount = 5.67M
}
}
}
);
Assumably, OrderNumber is unique, so this should work fine. In addition, running the seed again will not duplicate this data.
UPDATE:
It is possible to keep the original MyOrderModel.AddOrUpdate(), i.e., give the Id explicitly:
First, you need to prevent the auto-generation of the primary key value for MyOrderModel:
public class MyOrderModel
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int Id { get; set; }
Then, you need to re-create the MyOrderModel table. NOTE: the usual approach of adding a migration just to modify the Id field will not work, you need to re-create the table.
Running the seed multiple times works, i.e., there are no duplicates (just checked it myself).
UPDATE 2:
I don't have a full explanation, why your code is not working, but with the code below it should be possible for you to construct the creation and the update of the database objects as you wish.
This code can be run in Seed(), multiple times without duplication. The update of AuthCode is, of course, artificial, but my point was to separate the creation and the update (just in case these need to be separated in the final implementation).
The whole project is available in https://github.com/masa67/AspNetNg, branch SO34252506-1.
Here's the code:
var mom = context.MyOrderModel.Where(m => m.OrderNumber == "00001").FirstOrDefault();
if (mom == null)
{
mom = new PostPayQRCodeFuelOrderModel
{
OrderNumber = "00001",
AuthCode = "ABCDE",
Details = new List<MyOrderDetailModel>() {
new MyOrderDetailModel
{
Amount = 5.67M
}
}
};
context.MyOrderModel.AddOrUpdate(p => p.OrderNumber, mom);
context.SaveChanges();
}
mom.AuthCode = "ABCDEXXX";
context.SaveChanges();
UPDATE 3:
A couple of suggestions, if this is still not working:
Consider dropping the Required constraint for Details and handle the consistency programmatically. I would not use this constraint on navigation properties anyways (but I am only familiar with EF to the extent of how we are using it in our current project at work, so there might be different views on this).
Test your code by re-creating the database first.
Test my Solution (link above). It is working for me, so there might be a difference in configuration somewhere.
Since Details is not virtual, EF is not using lazy loading. I was expecting this to cause problems, as Details becomes null when the object is read from the database, but that was not the case in my environment. You might try eager loading, but I doubt if this has any impact:
Eager loading:
var mom = context.MyOrderModel.Where(m => m.OrderNumber == "00001").Include(m => m.Details).FirstOrDefault();
UPDATE 4:
If this is still not working, then delete the database, but re-create the migrations in addition:
Delete the existing migrations.
Do not let EF make the assumption that it knows the state of your database, but force it to create the migrations from scratch (see other SO questions for advice). However, BEFORE doing that, please do notice that this operation will most probably overwrite the Seed() function as well, so take a copy of that file before the operation.

Editing a child entity doesn't remove old rows from table, it always adds new ones in entity framework ASP.Net MVC

I have a teacher entity that has a child entity of ICollection called YogaClasses. When I modify the list of YogaClasses for a teacher and save, entity framework adds the new rows of YogaClasses to the table 'YogaClass' but it doesn't remove or edit the old rows. So I'm left with double the data. Shouldn't "context.SaveChanges()" in my repo be smart enough to know to delete removed classes and add the new ones (edit), not just add new ones?
In my controller I have something like this for a Teacher edit.
string userId = User.Identity.GetUserId();
Teacher teacher = teacherRepository.Find(userId);
//other code left out here
teacher.YogaClasses = GetSelectedClasses(Request.Form[2]);
// other stuff here
teacherRepository.Save();
In my reposity I have this;
public void Save()
{
context.SaveChanges();
}
Here is my teacher and YogaClass entity
public class Teacher
{
public int TeacherId { get; set; }
public virtual ICollection<YogaClass> YogaClasses { get; set; }
}
public class YogaClass
{
public int YogaClassId { get; set; }
[Index]
[Required]
public int TeacherRefId { get; set; }
[ForeignKey("TeacherRefId")]
public virtual Teacher Teacher { get; set; }
}
The problem here is the relationship has not been loaded at the time you set teacher.YogaClasses. So when you set teacher.YogaClasses, it assumes the new relationship will be added. (lazyloading is still a bit late in this case). The solution is somehow you ensure to load all the relationships first (using either Include or some fake access to trigger lazyloading first), then you set the new value normally:
//fake access to trigger lazy loading (of course this works only if
//lazy loading is enabled)
var someClasses = teacher.YogaClasses;
teacher.YogaClasses = GetSelectedClasses(Request.Form[2]);
You can also clear the old classes and add new ones with a foreach loop:
teacher.YogaClasses.Clear();
foreach(var c in GetSelectedClasses(Request.Form[2])){
teacher.YogaClasses.Add(c);
}
I have a dirty solution but it doesn't seem like it's the best one.
before I save the teacher context in my repo I can call this
public void DeleteYogaClasses(Teacher teacher)
{
foreach (var yogaClass in teacher.YogaClasses.ToList())
{
context.Entry(yogaClass).State = EntityState.Deleted;
}
context.SaveChanges();
}
you need to use Attach method of the context and pass in your existing teacher object and then make the changes and call SaveChanges method. From your code it is not clear what is your context, so hard to give working code. Here is a link explaining how to add/modify
You said you need to modify YogaClasses, so from this what I assume is a Teacher object already has some YogaClasses entries and you want to update some of those entries. What you need to do is, have a list of ids of YogaClasses that you need to modify then iterate on that ids list and in that iteration loop find the existing yogaClass and attach it to the context, modify it and then call save changes (preferablly when all the changes are done so that it is not a performance hit)
Here is a suodocode for this
UpdateTeacher(int teacherId)
{
var teacher = teacherRepository.Find(teacherId);
UpdateYoga(teacher);
}
private void UpdateYoga(Teacher teacher)
{
foreach(var yoga in teacher.YogaClasses)
{
db.Context.Attach(yoga);
yoga.YogaStyle = whatEverValue;
}
db.context.SaveChanges();
}

Using db context and binded object in the same action of a controller. How to Update the db?

I didn't find any relevant answer here so I will trigger you, thanks in advance :
I have a controller with 2 methods of the Edit action, (I simplified it for better understanding):
MrSaleBeta01.Controllers
{
public class PostsController : Controller
{
private MrSaleDB db = new MrSaleDB();
...
// GET: Posts/Edit/5
public ActionResult Edit(int? id)
{
...
}
// POST: Posts/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit( Post post, int? CategoryIdLevel1, int? CategoryIdLevel2, int? originalCategoryId)
{
...
Category cnew = db.Categories.Find(post.CategoryId);
MoveFromCategory(post, originalCategoryId);
...
db.Entry(post).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
//move post from his old category (fromCategoryId) to a new one (post.CategoryId):
//returns true on success, false on failure.
public bool MoveFromCategory(Post post, int? fromCategoryId)
{
try
{
if (post.CategoryId == fromCategoryId)
return true;
Category cold = null, cnew = null;
if (fromCategoryId!=null)
cold = db.Categories.Find(fromCategoryId);
if (post.CategoryId != 0)
cnew = db.Categories.Find(post.CategoryId);
if (cold != null)
{
cold.Posts.Remove(post);
}
if( cnew != null)
cnew.Posts.Add(post);
db.Entry(cold).State = EntityState.Modified;
db.Entry(cnew).State = EntityState.Modified;
//db.Entry(p).State = EntityState.Modified;
//db.SaveChanges();
return true;
}
catch (Exception)
{
return false;
//throw;
}
}
}
}
So, the idea is very default: The first method is called by Get and returns the View of Edit. Then I need to save the changes by sending the post object from the view to the HttpPost Edit method.
My Model is something like that (I simplified it for better understanding):
MrSaleBeta01.Models
{
public class Post
{
public int Id { get; set; }
[ForeignKey("Category")]
public virtual int CategoryId { get; set; }
public virtual Category Category { get; set; }
}
public class Category
{
public Category()
{
this.Categories = new List<Category>();
this.Posts = new List<Post>();
}
#region Primitive Properties
public int CategoryId { get; set; }
public string Name { get; set; }
#endregion
#region Navigation Properties
public virtual IList<Post> Posts { get; set; }
#endregion
}
}
The idea: Every Post needs to have it's Category. Every Category can have multiple Posts or none. (1-N relationship).
The problem:
In the Edit (HttpPost) method, after I update the Category's objects (move the Post from it's category to a different category object. After that I do some other modifications on post object), I get an error in the line of the edit method:
db.Entry(post).State = EntityState.Modified;
saying that:
{"Attaching an entity of type 'MrSaleBeta01.Models.Post' 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."}
The error is beacuse there is a conflict to the line:
cold.Posts.Remove(post);
And even to the line:
cnew.Posts.Add(post);
I tried to use the solution of AsNoTracking() but without success,
I also tried to change the line "db.Entry(post).State = EntityState.Modified" line to:
db.As.Attach(post)
but that line is even cannot be compiled.
What am I doing wrong? How can I solve that issue?
1) You dont have to call .Attach() nor .State = anything.
You have your Entity created as proxy object (cold = db.Categories.Find(fromCategoryId);), its proxy responsibility to track any changes. As exception say, this COULD be your problem.
2) public int CategoryId { get; set; } should be marked with [Key] (i am not sure if convention mark it as primary key, but i doubt it - i think EF conventions take this PK as FK to Category, which could confuse object graph and behave strangely...)
3) Uh, just noticed... Why are you using your FromCategory method at all? I may overlook something, but looks like it just remove Category from collection and add it to another... EF proxy does this automatically for you, right after post.CategoryId = newCatId;
Edit1:
4) Change public virtual IList<Post> Posts { get; set; } to public virtual ICollection<Post> Posts { get; set; }
Edit2:
1) that was created automatically while I scaffold the PostsController according to the Post model. So I guess I need it?
3) It's not just remove Category from collection and add it to another, but remove the post from the collection of posts in one category to another. So I don't think that EF proxy does this automatically.
I am not famillier with ASP, i work with desktop MVP/MVVM, so i am not sure here - but from my point of view, you really dont need to touch EntityState as long as you are using var x = db.Set<X>().Create(); (== db.X.Create();) (NOT var x = new X();) for new entities and db.Set<X>().FetchMeWhatever(); (== db.X.FetchMeWhatever();) for everything else (Otherwise you get only POCO without proxy. From your example, it looks like you are doing it right ;) ).
Then you have entity with proxy (thats why you have your reference properties on model virtual - this new emitted proxy type override them) and this proxy will take care for 1:n, m:n, 1:1 relations for you. I think this is why folks are using mappers (not only EF and not only DB mappers) mainly :) For me it looks like, you are trying to do this manually and it is unnecessary and its just making a mess.
Proxy also take care of change tracking (so as i say, you dont need to set EntityState manually, only in extreme cases - I can not think of any right now... Even with concurrency.)
So my advice is:
Use only ICollection<> for referencing collections
Check and get rid of any var entity = new Entity(); (as i say, looks like you are doing this)
Throw away every db.Entry(x).State = EntityState.whatever; (trust EF and his change tracker)
Set only one side of reference - it doesnt matter if Category.Posts or Post.Category or even Post.CategoryId - and let mapper do the work. Please note that this will work only with proxy types (as i say above) on entities with virtual referencing & id & ICollection<> properties.
Btw, there are 2 types of change tracking, snippet and proxy - snippet have original values in RAM and is comparing them at SaveChanges() time, for proxy tracking, you need to have all your properties marked virtual - and comparing them at x.Prop = "x" time. But thats off-topic ;)

Categories

Resources