EF Core Update The instance of entity type 'Ads' cannot be tracked - entity-framework-core

I try to implement a XUnit Test for Asp.net Core DBContext, But I got below error.
Message: System.InvalidOperationException : The instance of entity type 'Ads' cannot be tracked because another instance of this type with the same key is already being tracked. When adding new entities, for most key types a unique temporary key value will be created if no key is set (i.e. if the key property is assigned the default value for its type). If you are explicitly setting key values for new entities, ensure they do not collide with existing entities or temporary values generated for other new entities. When attaching existing entities, ensure that only one entity instance with a given key value is attached to the context.
Here is my current code:
public class AdsServiceTest
{
private readonly DbContextOptions<SensingSiteDbContext> _options;
private readonly SensingSiteDbContext _context;
private readonly AdsService _AdsService;
public AdsServiceTest()
{
//initialize db options
_options = new DbContextOptionsBuilder<SensingSiteDbContext>()
.UseInMemoryDatabase()
.Options;
//get service
_context = new SensingSiteDbContext(_options);
//initialize dbcontext
List<Ads> listAds = new List<Ads>() {
new Ads(){ Id=1,AdsName="Ads1", Deleted=false},
new Ads(){ Id=2,AdsName="Ads1", Deleted=false},
new Ads(){ Id=3,AdsName="Ads1", Deleted=false}
};
_context.Advertisements.AddRange(listAds);
//context.Advertisements
BaseLib.SSDbContext<Ads, AdsService> ssDbContent = new BaseLib.SSDbContext<Ads, AdsService>(_context);
_AdsService = ssDbContent.GetService((x, y) => new AdsService(x, y));
}
[Theory]
[InlineData(1)]
public void FindById(int id)
{
Ads adsResult = _AdsService.FindById(id);
Ads adsTarget = _context.Advertisements.Find(adsResult.Id);
Assert.True(adsTarget.Equals(adsResult));
}
//Failed by error System.InvalidOperationException : The instance of entity type 'Ads' cannot be tracked because another instance of this type with the same key is already being tracked
[Fact]
public void Update()
{
Ads adsResult = new Ads() { Id = 1, AdsName = "UpdateAds1" };
_AdsService.UpdateAds(adsResult);
Ads adsTarget = _context.Advertisements.Find(adsResult.Id);
Assert.True(adsTarget.Equals(adsResult));
}
}
There is no problem for Find, but failed on Update. AdsService is implemented to call SensingSiteDbContext. It seems I need to use scope lifetime for SensingSiteDbContext. But, I do not know how to implement it.
I have changed ObjectState for Update.
public virtual void Update(TEntity entity)
{
entity.ObjectState = ObjectState.Modified;
_dbSet.Update(entity);
_context.SyncObjectState(entity);
}
Any help would be appreciated.

You are new'ing up your own entity, when, you should just get that entity you've already added from the context:
Ads adsResult = new Ads() { Id = 1, AdsName = "UpdateAds1" };
_AdsService.UpdateAds(adsResult);
With this code, Entity Framework is saying, "Hey, I already have an entity with that key (check your constructor, you're putting an entity in with that same Id), but this object; I don't know what to do with it (because it came from outside with a key that already exists)".
You can change it to exactly what you're doing in the previous test:
Ads adsResult = _AdsService.FindById(id);
//do your changing here
_AdsService.UpdateAds(adsResult);

Related

Delete loaded and unloaded objects by ID in EntityFrameworkCore

I have a method that receives an IEnumerable<Guid> of IDs to objects I want to delete. One suggested method is as follows
foreach(Guid id in ids)
{
var tempInstance = new MyEntity { Id = id };
DataContext.Attach(tempInstance); // Exception here
DataContext.Remove(tempInstance);
}
This works fine if the objects aren't already loaded into memory. But my problem is that when they are already loaded then the Attach method throws an InvalidOperationException - The instance of entity type 'MyEntity' cannot be tracked because another instance with the key value 'Id:...' is already being tracked. The same happens if I use DataContext.Remove without calling Attach.
foreach(Guid id in ids)
{
var tempInstance = new MyEntity { Id = id };
DataContext.Remove(tempInstance); // Exception here
}
I don't want to use DataContext.Find to grab the instance of an already loaded object because that will load the object into memory if it isn't already loaded.
I cannot use DataContext.ChangeTracker to find already loaded objects because only objects with modified state appear in there and my objects might be loaded and unmodified.
The following approach throws the same InvalidOperationException when setting EntityEntry.State, even when I override GetHashCode and Equals on MyEntity to ensure dictionary lookups see them as the same object.
foreach(Guid id in ids)
{
var tempInstance = new MyEntity { Id = id };
EntityEntry entry = DataContext.Entry(tempInstance);
entry.State == EntityState.Deleted; // Exception here
}
The only way so far I have found that I can achieve deleting objects by ID without knowing if the object is the following:
foreach(Guid id in ids)
{
var tempInstance = new MyEntity { Id = id };
try
{
DataContext.Attach(tempInstance); // Exception here
}
catch (InvalidOperationException)
{
}
DataContext.Remove(tempInstance);
}
It's odd that I am able to call DataContext.Remove(tempInstance) without error after experiencing an exception trying to Attach it, but at this point it does work without an exception and also deletes the correct rows from the database when DataContext.SaveChanges is executed.
I don't like catching the exception. Is there a "good" way of achieving what I want?
Note: If the class has a self-reference then you need to load the objects into memory so EntityFrameworkCore can determine in which order to delete the objects.
Strangely, although this is a quite common exception in EF6 and EF Core, neither of them expose publicly a method for programmatically detecting the already tracked entity instance with the same key. Note that overriding GetHashCode and Equals doesn't help since EF is using reference equality for tracking entity instances.
Of course it can be obtained from the DbSet<T>.Local property, but it would not be as efficient as the internal EF mechanism used by Find and the methods throwing the aforementioned exception. All we need is the first part of the Find method and returning null when not found instead of loading from the database.
Luckily, for EF Core the method that we need can be implemented relatively easily by using some of the EF Core internals (under the standard This API supports the Entity Framework Core infrastructure and is not intended to be used directly from your code. This API may change or be removed in future releases. policy). Here is the sample implementation, tested on EF Core 2.0.1:
using Microsoft.EntityFrameworkCore.Internal;
namespace Microsoft.EntityFrameworkCore
{
public static partial class CustomExtensions
{
public static TEntity FindTracked<TEntity>(this DbContext context, params object[] keyValues)
where TEntity : class
{
var entityType = context.Model.FindEntityType(typeof(TEntity));
var key = entityType.FindPrimaryKey();
var stateManager = context.GetDependencies().StateManager;
var entry = stateManager.TryGetEntry(key, keyValues);
return entry?.Entity as TEntity;
}
}
}
Now you can use simply:
foreach (var id in ids)
DataContext.Remove(DataContext.FindTracked<MyEntity>(id) ?? new MyEntity { Id = id }));
or
DataContext.RemoveRange(ids.Select(id =>
DataContext.FindTracked<MyEntity>(id) ?? new MyEntity { Id = id }));

How to Check If Entity Exists Before Attaching?

When a new Post is input into the system, a number of Tags will need to be instantiated and associated with the Post instance. Some of these Tags will already exist in the db, others will not and will need inserting.
An example:
var post = new Post {
Slug = "hello-world",
Title = "Hello, World!",
Content = "this is my first post.",
Tags = new List<Tag>()
};
var tag = new Tag { Name = "introduction" };
post.Tags.Add(tag);
When an associated Tag does not exist in the db, I can rely on a simple call to DbSet<T>.Add to insert both the post and the associated tags into the db.
However, attempting to insert a post with associated tags that already exist in the db causes a primary key violation on the tags table.
In an attempt to solve this problem, I tried to Attach each tag to the database context which works super when the tags already exist in the database but otherwise, an exception is thrown with the following inner exception:
The INSERT statement conflicted with the FOREIGN KEY constraint "FK_dbo.TagPosts_dbo.Tags_Tag_Name". The conflict occurred in
database "EF.Domain.BlogDb", table "dbo.Tags", column
'Name'. The statement has been terminated."}
I want to insert tags associated with the post into the database only when it is required. How can I achieve this?
Your predicament is quite interesting. If you wouldn't use a generic repository but a specific repository that knows the primary key of the entity you want to attach is you could just use a code like this:
var tagExists = Tags.Any(t => t.Name == tag.Name);
or
var tag = Tags.Find(tag.Name);
however in your case we need a more general approach such as getting the primary key of the entity the Repository class uses regardless the type of the entity. To achieve this I've created two extension methods on the DbContext class:
public static IList<string> GetPrimaryKeyNames<TEntity>(this DbContext context)
where TEntity : class
{
var objectContext = ((IObjectContextAdapter)context).ObjectContext;
var set = objectContext.CreateObjectSet<TEntity>();
return set.EntitySet.ElementType
.KeyMembers
.Select(k => k.Name)
.ToList();
}
public static IList<object> GetPrimaryKeyValues<TEntity>(this DbContext context, TEntity entity)
where TEntity : class
{
var valueList = new List<object>();
var primaryKeyNames = context.GetPrimaryKeyNames<TEntity>();
foreach(var propertyInfo in entity.GetType().GetProperties())
{
if (primaryKeyNames.Contains(propertyInfo.Name))
{
valueList.Add(propertyInfo.GetValue(entity));
}
}
return valueList;
}
Utilizing these methods you can change the Attach method on the Repository class like the following:
public void Attach(TEntity entity)
{
var storeEntity = _context.Set<TEntity>().Find(
_context.GetPrimaryKeyValues(entity).ToArray());
if (storeEntity != null)
{
_context.Entry(storeEntity).State = EntityState.Detached;
_context.Set<TEntity>().Attach(entity);
}
}

EF 6 OriginalValues lost when using Attach method

I have the following Update generic method for my entities:
public void Update < T > (T entity) where T: class {
DbEntityEntry dbEntityEntry = DbContext.Entry(entity);
if (dbEntityEntry.State == System.Data.Entity.EntityState.Detached) {
DbContext.Set < T > ().Attach(entity);
}
dbEntityEntry.State = System.Data.Entity.EntityState.Modified;
}
After SaveChanges() the data is successfully updated in the DB.
Now I nee to implement and Audit Log before SaveChanges() but I noticed that CurrentValues are equal to OriginalValues:
// For updates, we only want to capture the columns that actually changed
if (!object.Equals(dbEntry.OriginalValues.GetValue<object>(propertyName), dbEntry.CurrentValues.GetValue<object>(propertyName))){
//here I add a new Audit Log entity
}
Any clue on how to solve this? Or is there a better way to do it in Entity Framework 6?
If you are using a disconnected entity, you can set originals values without affect entity instance values, adapt this method at you needs
public static void LoadOriginalValues(this WorkflowsContext db, DbEntityEntry entity)
{
var props = entity.GetDatabaseValues();
foreach (var p in props.PropertyNames)
{
if (entity.Property(p).IsModified)
{
entity.Property(p).OriginalValue = props[p];
}
}
}
The original values are recovered from the entity itself. If the entity is being tracked by a context, this information is available.
In your case, you're using a disconected entity, so there is no change tracking, and the entity doesn't have the original values.
SO, in this case, if you need the original values there is no other option than getting them from the DB, and compare them, one by one.
If you want to get an entity that behaves as if it had been tracked by the context you can use a context to read the entity from the DB, and use something like ValueInjecter to automatically set the property values from the disconected entity into the tracked entity.

How can I prevent EF from inserting an object that already exists in the db when adding one that contains this first one?

It's quite self-explainatory.
I have a class that contains another
Let's call them Subject and Classroom
public class Subject
{
public Classroom Class {get; set;}
}
I'm using stateless facades, wich means my DbContext is disposed right after recovering the objects and is created to store the new ones.
Shouldn't it know that Classroom isn't a new object since it's ID is already in the DB?
Using the debugger I can track to the point right before I call the SaveChanges method and Classroom.id is the one I have on the database.
What's the problem? EF adds a new Classroom with the exact properties as the previous one, but with a new PK.
What am I doing wrong here?
This is the code used for the general CRUD operations (They are in my DbContext) Both update and delete work just fine:
public void Update(DbSet MySet, object Obj)
{
MySet.Attach(Obj);
var Entry = this.Entry(Obj);
Entry.State = EntityState.Modified;
this.SaveChanges();
}
public void Insert(DbSet MySet, object Obj)
{
MySet.Add(Obj);
this.SaveChanges();
}
public void Delete(DbSet MySet, object Obj)
{
MySet.Attach(Obj);
var Entry = this.Entry(Obj);
Entry.State = EntityState.Deleted;
this.SaveChanges();
}
Without seeing you're actual code on how you're either updating or creating your Subject entity, it's hard to tell. However, you're probably not attaching the Classroom so EF is assuming that the entity is new, when it's really not.
using (Model m = new Model())
{
m.Subject.Add(subject);
m.Classrooms.Attach(subject.Class);
m.SaveChanges();
}
Even though the PK is the same, without attaching to the Context, EF has no way of figuring out what you're intention is. Attaching the entity explicitly tells your context what you want.

How do I prevent EF from reinserting objects of other dbcontext?

I am using EF 4.1 in a MVC 3 project and I am having problems with inserting an object. I am using session as well to hold onto some objects. In concrete :
I have a simple class with a parent child relationship:
public class Event
{
public User Promotor {get;set;}
}
The promotor is based on the CurrentUser. In my application I store the CurrentUser in http session. Now when I add an event like this the user (and all related objects) gets inserted one more time with a new primary key.
//1st request inserts/loads the user
User user;
using (var context = new MyDbContext())
{
user = new User();
context.Users.Add(user);
context.SaveChanges();
}
//2nd request saves the event
var before = db.Users.Count();
var #event = new Event
{
Promotor = user, //user was kept in Session
};
db.Entry(#event).State = EntityState.Added;
db.SaveChanges();
When i check the state of the user it is 'added' as well although the primary key is not 0 and EF should know it is already persistent. How can fix this without adding a lot of to my persistency code. Do I have to reattach my currentuser to the new dbcontext on every request? This will lead to db code 'leaking' into my application. I want to keep the DB stuff in a data layer. I am using a repository like this :
public void Save(T entity)
{
dbContext.Entry(entity).State = IsPersistent(entity) ?
EntityState.Modified : EntityState.Added;
dbContext.SaveChanges();
}
As you've already mentioned yourself, reattaching the user to your current context should solve the problem. The only other way I know of, would be to retrieve the object once again in your context based on the primary key value.