I have tried many ways but I am still hitting different kinds of errors for each solution. I have tried to stop tracking, tried adding challenge itself, updating competition but all of them doesn't seem to work.
I basically have 1 competition to many challenges, and in this scenario, 1 competition and 1 challenge is already present, and I am adding another challenge which has a Foreign Key linking with the competition. I understand that I have asked a similar question before, but that was for bulk creation of 1 competition to many categories. I am thinking this is more like an update operation, which doesn't seem to be working. Appreciate your help a lot! :)
InvalidOperationException: The instance of entity type 'Challenge'
cannot be tracked because another instance with the same key value for
{'ID'} is already being tracked. When attaching existing entities,
ensure that only one entity instance with a given key value is
attached. Consider using
'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the
conflicting key values.
Competition Model Class:
public class Competition
{
[Key]
public int ID { get; set; }
public ICollection<CompetitionCategory> CompetitionCategories { get; set; }
public ICollection<Challenge> Challenges { get; set; }
}
Challenge Model Class:
public class Challenge
{
[Key]
public int ID { get; set; }
[ForeignKey("CompetitionID")]
public int CompetitionID { get; set; }
[Display(Name = "Competition Category")]
[ForeignKey("CompetitionCategoryID")]
public int CompetitionCategoryID { get; set; }
}
Controller:
public async Task<IActionResult> Create([Bind("ID,XXX,CompetitionID,CompetitionCategoryID")] Challenge challenge)
{
var competition = await _context.Competitions
.Include(c => c.CompetitionCategories)
.Include(c1 => c1.Challenges)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.ID == challenge.CompetitionID);
if (ModelState.IsValid)
{
//_context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
competition.Challenges.Add(challenge);
_context.Update(competition);
_context.Entry(competition).State = EntityState.Detached;
_context.Entry(competition.Challenges).State = EntityState.Detached;
await _context.SaveChangesAsync();
//_context.Add(challenge);
//await _context.SaveChangesAsync();
//return RedirectToAction(nameof(Index));
return RedirectToAction("Index", "Challenges", new { id = challenge.CompetitionID });
}
return View();
}
Update: I have actually tried to just add challenge itself but it also throws up another error. Really quite at a lost of what to do.
SqlException: Cannot insert explicit value for identity column in
table 'Challenges' when IDENTITY_INSERT is set to OFF.
System.Data.SqlClient.SqlCommand+<>c.b__122_0(Task
result)
DbUpdateException: An error occurred while updating the entries. See
the inner exception for details.
Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection
connection, CancellationToken cancellationToken)
Update 2: Removing the ID from the binding works as there was some unknown ID value being passed in and being tracked. Ivan's answer on adding a new object with Foreign Key is correct.
public async Task<IActionResult> Create([Bind("XXX,CompetitionID,CompetitionCategoryID")] Challenge challenge)
{
//Codes here
_context.Add(challenge);
await _context.SaveChangesAsync();
}
Working with disconnected entities is not easy and requires different techniques depending of the presense/absense of navigation / innverse navigation and FK properties in the entity model.
Your Challenge class has explicit FK properties and no navigation properties. Adding new object like this is the simplest operation - just call DbContext.Add or DbSet.Add:
_context.Add(challenge);
await _context.SaveChangesAsync();
However, the exception you are getting makes me think that the Challenge object received by the Create method has the PK property Id populated with a value of an existing Challenge. If you really want to add new Challenge and Id is auto-generated, exclude Id from the binding or make sure it is set to 0 (zero) before calling Add.
For more info, see Disconnected Entities and related links in the EF Core documentation.
Related
In my learning project, while using EF Core, I'm following the repository pattern and I'm using DI to inject the context for each repository.
The context is registered as follows.All services and repositories are transient.
builder.Services.AddDbContext<AppContext>(options => {
options.UseSqlServer(builder.Configuration.GetConnectionString("ConnectionString"));
options.EnableSensitiveDataLogging();
});
I added sensitive data logging as an attempt to debug this but to no avail so far.
The exception appears when I attempt to use:
_context.Update(playableCharacter)
await _context.SaveChangesAsync();
There is a static list I use to store the playableCharacter until all changes are finished and it's ready to be saved in the database again.
Here is my controller:
public async Task<ActionResult> CommitPlayerAction()
{
var userId = _userService.GetUserId(User); //Retrieves user ID from ClaimsPrincipal
var activeGameInstance = await _userParagraphRepository.GetActiveByUserIdNoTrackingAsync(_userService.GetUserId(User)); //Call to repository
// Call to static list storing fight instances. They aren't supposed to be saved in DB.
var activeFightInstance = _fightService.GetActiveFightInstance(userId, activeGameInstance.ActiveCharacter.CharacterId);
await _fightService.CommitAction(userId); // Manage the fight state + Makes call to playable character repository for update. That's where the issue arises.
// Retrieves fight state for given request from the static list
var fightState = _fightService.GetFightState(_userService.GetUserId(User),activeGameInstance.ActiveCharacter.CharacterId);
activeFightInstance.ActionGrantedByItem = false;
_fightService.ResetActiveTarget();
_fightService.ResetActiveAction();
}
The service layer:
public async Task CommitAction(string userId)
{
/*Game logic I cut off for clarity, mostly changes on playable character and his enemies */
var combatEnded = IsFightFinished(_activeFightInstance.ActiveEnemies, GetActivePlayer());
if (combatEnded)
{
var fightWon = IsFightWon(_activeFightInstance.ActivePlayer);
FinishFight(fightWon);
await _playableCharacterRepository.UpdateAsync(_activeFightInstance.ActivePlayer);
}
else
{
// Some game logic
}
}
Service layer dependencies:
private IFightRepository _fightRepository;
private FightInstance _activeFightInstance;
private IFightFactory _fightFactory;
private IUserParagraphRepository _userParagraphRepository;
private ICharacterFactory _characterFactory;
private readonly IPlayableCharacterRepository _playableCharacterRepository;
public FightService(IFightRepository fightRepository,
IFightFactory fightFactory,
IUserParagraphRepository userParagraphRepository,
ICharacterFactory characterFactory,
IPlayableCharacterRepository playableCharacterRepository)
{
_fightRepository = fightRepository;
_fightFactory = fightFactory;
_userParagraphRepository = userParagraphRepository;
_characterFactory = characterFactory;
_playableCharacterRepository = playableCharacterRepository;
}
public FightInstance GetActiveFightInstance(string userId, int characterId)
{
// This fight instance stores a reference to our playable character in the static list to share with the entire service.
_activeFightInstance = _fightRepository.GetById(userId, characterId);
return _activeFightInstance;
}
"Game instance" repository:
public async Task<UserParagraph> GetActiveByUserIdNoTrackingAsync(string userId)
{
return await _context.UserParagraphs
.Include(x => x.ActiveCharacter)
.Include(x => x.Paragraph)
.ThenInclude(p => p.Choices)
.Include(x => x.Paragraph)
.ThenInclude(x => x.TestProp)
.Include(x => x.Paragraph)
.ThenInclude(x => x.FightProp)
.ThenInclude(y => y.ParagraphEnemies)
.ThenInclude(z => z.Enemy)
.Include(x => x.ActiveCharacter)
.AsNoTracking()
.SingleOrDefaultAsync(s => s.User.Id == userId && s.ActiveGame);
}
Fight repository (with the static list that might be causing issues)
internal class FightRepository : IFightRepository
{
// I use a List instead of a dictionary as I need to have non unique records inside
private static List<KeyValuePair<string, FightInstance>> FightInstances { get; set; } = new List<KeyValuePair<string, FightInstance>>();
The entity I'm trying to update:
[Key]
public int CharacterId { get; set; }
public bool IsTemplate { get; set; }
public string UserId { get; set; }//Id of character owner
public User User { get; set; }
public UserParagraph? UserParagraph { get; set; } //Game instance in the form of a many to many relationship between the user and " paragraphs".
public int? UserParagraphId { get; set; } //Nullable as a character can be an instance or a template for other users. It has to remain like this.
public PlayableRace Race { get; set; }
public int RaceId { get; set; }
public PlayableClass CharacterClass { get; set; }
public int PlayableClassId { get; set; }
Many to many
//=============================================================//
//Those entities are causing issues , they might be removed or changed and I won't know about it when updating.
//That's why the "Update" method seemed perfect.
public List<ActionCharacter>? LinkedActions { get; set; }
public List<ItemCharacter>? LinkedItems { get; set; }
//=============================================================//
It's a possible duplicate of this question however after trying everything I can't solve the exception from the title.
What I tried so far:
I've attempted to remove the " as no tracking" and keep a tracked instance in the static List. This lead to EF claiming Character ID already exists as a tracked entity.
Based on the above i tried to use this in my update method in character repository but it leads to exactly the same issue
_db.Entry(playableCharacter).State = EntityState.Detached;
_db.Set<PlayableCharacter>().Update(playableCharacter);
await _db.SaveChangesAsync();
I've made sure all my async calls to DB are awaited to make sure there is no issue from this side.
I've added .AsNoTracking to the request retrieving the playable character before adding it to the static list to make sure EF doesn't have any issues from this perspective.
I've tried using the _context.Entry(PlayableCharacter) methods to force the modified entity state. It worked. That's what worked the best so far. But it forced me to iterate on both many to many lists and I would like to avoid that cause it feels like hacking EF instead of understanding it and working with it.
I've made sure the context change tracker is empty when leaving the .AsNonTracking request for the game instance in the controller. Meaning this might be somehow related to that static list but since the instance in the static list is also non tracking how can that be the issue? If that was the case, using .AsNoTracking() on the request in the controller should be enough for it to work.
I've tried many different approaches but it has been 8 hours now, the last 3 attempts led me to a successful update on all linked entities [many to many collections removal excluded] (while having only _context.PlayableCharacters.Update(playableCharacter) in the repository) and that's the version I posted. But clicking the" browser link" function in visual studio led me to square 1, now EF has issues with PlayableCharacters.LinkedItems.
And this means I completely misunderstood the way EF treats his context instances.
Any help in understanding how exactly EF works with entities in static lists + hopefully getting the update method to work in a reliable way instead of what seems to be now complete randomness is appreciated.
Edit:
Checking the _context.ChangeTracker.Longview prior to using the update method shows there is no tracked object at all(And that's expected).
Still using the update method returns the same exception which means EF is actually tracking this object somehow without telling me where and why.
I expect him to not be tracking anything at all when this update function is called.
Problem is finally solved.
It turns out while requesting one of my objects from EF context I used .Include(x=>x.User) twice. While I heard EF would ignore a duplicated Include it doesn't seem to be the case.
All many to many relations for this user were duplicated( ID included).
Meaning the entity with a given ID was indeed already tracked despite being unique in the DB and everywhere else in the code.
The usage of a static List.
While this seemed smart at first( to limit the amount of requests to DB ) it clearly caused more issues than I thought forcing me to " map" those changes back instead of applying them with one method.
At this point it will be faster to get rid of the static list.
I'm working in Postman, I'm a complete beginner and am currently learning database work.
I've created a web API with a small local database and filled it with the data I want, using C# and Entity Framework. All the Postman requests work just as designed, except for the PUT and DELETE ones, where I get error messages (405 on put and 500 on delete). I suspect these are related to the same problem, namely that I'm working with composite keys.
The 500 one on the other hand says that I'm working with a composite key and I'm only entering one value. I have chosen to have composite keys because of a many-to-many relationship between two tables, but how do I enter this into a delete request? Is there a way to format a request URL for this (since I keep getting error 405 Method Not Allowed if I put anything in the body of a delete request)?
I'm sure I'm missing something very obvious here because this doesn't feel like it should be that complicated, but I couldn't find a similar question having been asked.
Edit:
The code is extremely basic, not sure it makes any difference to the problem at all, it looks like this;
{
public int StudentId { get; set; }
public int CourseId { get; set; }
public Student Student { get; set; }
public Course Course { get; set; }
}
StudentId and CourseId make a composite primary key, and I'm trying delete an object of the class StudentCourse. The suggested/needed request url for this is: /api/StudentCourses/{id} which I don't know how to enter since it's a composite key.
The HttpDelete action looks like this, mind you this is the autogenerated one and I can totally see that it doesn't work because StudentCourses doesn't have any single id to find, as such the FindAsync would never go through regardless because it wouldn't find anything.
That being said, I don't know how to get around that because the action itself only asks for one integer, meaning that Postman recognises that I need more parts of my primary key before it even gets to here.
public async Task<IActionResult> DeleteStudentCourse(int id)
{
var studentCourse = await _context.StudentCourses.FindAsync(id);
if (studentCourse == null)
{
return NotFound();
}
_context.StudentCourses.Remove(studentCourse);
await _context.SaveChangesAsync();
return NoContent();
}
private bool StudentCourseExists(int id)
{
return _context.StudentCourses.Any(e => e.StudentId == id);
}
Assuming that Id is a student Id and you are trying to delete all StudentCourse records for this student
[Route[("~/api/StudentCourses/DeleteStudentCourse/{id}")]
public async Task<IActionResult> DeleteStudentCourse(int id)
{
var studentCourses = await _context.StudentCourses.Where(e => e.StudentId == id).ToListAsync();
if (studentCourses == null || studentCourses.Count==0)
{
return NotFound();
}
_context.StudentCourses.RemoveRange(studentCourses);
var result= await _context.SaveChangesAsync();
if (result> 0) return Ok();
return BadRequest();
}
suggested url
.../api/StudentCourses/DeleteStudentCourse/{id}
I am getting SqliteException: SQLite Error 1: 'foreign key mismatch - "Rental" referencing "Movie"'.
CREATE TABLE Movie (
title VARCHAR(30) NOT NULL,
description VARCHAR(300) NOT NULL);
CREATE TABLE Rental (
user_id TEXT ,
movie_id INT ,
FOREIGN KEY (user_id) REFERENCES AspNetUsers(Email),
FOREIGN KEY (movie_id) REFERENCES Movie(rowid));
with following code.
public async Task OnGetAsync(int movie_id, string email)
{
Rental newRental = new Rental();
newRental.movie_id=movie_id;
newRental.user_id=email;
_context.Rental.Add(newRental);
_context.SaveChanges();
}
Movie table has implicit rowid automatically added by SQLlite
What am I doing wrong?
A couple things: Movie appears to be missing a PK. When using identity (auto-generated) keys EF needs to be told about them as well. It can deduce some by naming convention, but I recommend being explicit to avoid surprises. Your entities will need to be set up for the appropriate relationships:
I.e.
public class Movie
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int movie_id { get; set; }
// ...
}
public class Rental
{
[Key, Column(Order=0)]
public int movie_id { get; set; }
[Key, Column(Order=1)]
public int user_id { get; set; }
// ...
}
Unfortunately this doesn't really tell EF that there is a relationship between Movie and Rental. (Or User and Rental) From an application point of view, without that relationship,
what guarantee is there that the Movie_id your call receives from a client exists? It's also a bit strange that this method is listed as an "OnGet" type method which implies a GET action rather than a POST or PUT action.
Typically with EF you will want to leverage navigation properties for you domain rather than just exposing FKs. Also, you are defining this as an async method without awaiting any async operations.
I would recommend avoiding composite keys unless truly necessary, so give Rental a Rental ID and just rely on many to one relationships for the Movie and User references:
public class Rental
{
[Key, DatabaseGenerate(DatabaseGeneratedOption.Identity)]
public int rental_id { get; set; }
// ....
public int movie_id { get; set; }
[ForeignKey("movie_id")]
public virtual Movie Movie { get; set; }
public int user_id { get; set; }
[ForeignKey("user_id")]
public virtual User User { get; set; }
}
public void RentMovie(int movie_id, string email)
{
var movie = _context.Movies.Single(x => x.Movie_Id == movie_id);
// Better would be to go to the Session for the current logged in user rather than trusting what is coming from the client...
var user = _context.Users.Single(x => x.Email = email);
try
{
Rental newRental = new Rental
{
Movie = movie;
User = user;
};
_context.Rental.Add(newRental);
_context.SaveChanges();
}
catch
{ // TODO: Handle what to do if a movie could not be rented.
}
}
In the above example we attempt to load the requested movie and user. If these do not exist, this would fall through to the global exception handler which should be set up to end the current login session and log the exception. (I.e. any tampering or invalid state should be captured and log the user out.) Where when attempting to record the rental, the exception handling can be set up to display a message to the user etc. rather than a hard failure.
You can go a step further and remove the FK properties from the entities and use Shadow Properties (EF Core) or .Map(x => x.MapKey()) (EF6) to set up the relationship. This avoids having two sources of truth for viewing/updating relationships between entities.
Optionally the Movie object could have an ICollection<Rental> where Rentals could have a RentedDate and ReturnedDate for instance so that movies could be inspected to see if a copy was available to rent. I.e. searching by name then determining if one or more copies is currently in-stock. A Rental record could be added to movie.Rentals rather than treating Rentals as a top-level entity.
Using navigation properties is a powerful feature of EF and can accommodate some impressive querying and data retrieval options via Linq without reading a lot of records and piecing things together client side.
Having these two entities, I fetch them, map them to viwemodels/dtos before I pass them to the UI.
I also had to ignore reference loop handling, in my startup.cs file, to map them to DTO's correctly.
public class Matter
{
public int Id { get; set; }
public ICollection<MatterExposure> Exposures { get; set; }
// other properties
}
public class MatterExposure
{
public int Id { get; set; }
public Matter Matter { get; set; }
// other properties
}
When I save the form (which includes a table of 'MatterExposure's) in the UI I pass everything back to the controller to be saved. INFO - not saving child entities 'MatterExposure' yet in below controller call and it works fine!
[HttpPut("{id}")]
public async Task<IActionResult> UpdateData(string id, MatterForClaimDetailedDto generalMatterDto)
{
var user = await _userManager.GetUserAsync(HttpContext.User);
var matter = await _autoRepo.GetMatter(id);
// fill some matter data and add a child then save and it works fine
if (await _autoRepo.SaveAll())
return NoContent();
}
public class MatterForClaimDetailedDto
{
public int Id { get; set; }
public GeneralMatterDto MatterData { get; set; }
public ICollection<MatterExposure> Exposures { get; set; }
// other properties
}
Now I want to add the update of MatterExposure entities, as I could have made changes to them in the UI. So I try to use UpdateRange like this
[HttpPut("{id}")]
public async Task<IActionResult> UpdateData(string id, MatterForClaimDetailedDto generalMatterDto)
{
var user = await _userManager.GetUserAsync(HttpContext.User);
var matter = await _autoRepo.GetMatter(id);
matter.EditedDate = DateTime.Now;
matter.FirstName = generalMatterDto.FirstName;
matter.LastName = generalMatterDto.LastName;
_autoRepo.UpdateRange<List<MatterExposure>>(generalMatterDto.Exposures.ToList());
await _autoRepo.SaveAll()
}
public void UpdateRange<T>(T entity) where T : class
{
_autoContext.UpdateRange(entity);
}
But on calling UpdateRange I get this exception message:
"The entity type 'List MatterExposure' was not found. Ensure that the entity type has been added to the model."
In my context I have this:
public DbSet<MatterExposure> MatterExposure { get; set; }
I then tried below with no luck
public DbSet<List<MatterExposure>> MatterExposure { get; set; }
I thought I would try updating each individual 'MatterExposure' entity to see if that would change anything. So I tried removing the UpdateRange call and tried with individual 'MatterExposure' entities
foreach(var exposure in generalMatterDto.Exposures) {
_autoRepo.Update<MatterExposure>(exposure);
}
// in my repo I have this with different things I tried
public void Update<T>(T entity) where T : class
{
// _autoContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
//_autoContext.Entry(entity).State = EntityState.Detached;
_autoContext.Update(entity);
// _autoContext.ChangeTracker.
}
On the first loop through each 'MatterExposure' Update call to the repo I get this exception
"The instance of entity type 'MatterExposure' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting key values."
After the exception above I tried I put the loop at the top of the controller method to see if the other entity stuff was interfering.
// at top of controler method before the other entity actions are performed
foreach(var exposure in generalMatterDto.Exposures) {
_autoRepo.Update<MatterExposure>(exposure);
}
And moving the for loop to the top of the controller, runs through the 1st iteration but then fails on the second giving me the same error message again
"The instance of entity type 'MatterExposure' cannot be tracked because
another instance with the same key value for {'Id'} is already being tracked.
When attaching existing entities, ensure that only one entity instance with a given key value is attached.
Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting key values."
QUESTION - am I not updating the child entities correctly or is it something else?
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