EF6 not tracking changes on entities when using FindAsync - c#

I'm using EF6, with a Repository pattern (a repository has its own context instance). When I use FindAsync to obtain and modify an entity, the changes are not tracked, and so any properties that are edited are not saved upon SaveChanges. However, I also expose the table through the repository via IQueryable, and if I obtain an entity that way, all changes are saved properly. I'm trying to figure out why changes are not tracked when I use the FindAsync method.
My Find repo method:
public async Task<CDCRoute> FindDrivingRouteAsync(long routeId, string userId)
{
var route = await routeContext.Routes.FindAsync(routeId);
if (route != null && route.CDCUserInfoId == userId)
{
return route;
}
return null;
}
Table exposed with IQueryable:
public IQueryable<CDCRoute> Routes
{
get { return routeContext.Routes; }
}
Accessing a route via Find (does not save changes when modified):
routeRepo.FindDrivingRouteAsync(message.RouteId, message.UserId);
Accessing a route via the exposed IQueryable (does save changes when modified):
routeRepo.Routes.FirstOrDefault(r => r.RouteId == message.RouteId && r.CDCUserInfoId == message.UserId);
I'm sure I am missing something (am somewhat new to EF), so any help would be greatly appreciated, thanks!

I just had this problem - I suspect it's a bug with EF, since manually updating the EntityState & then forcing the context to save changes, or using the Synchronous version of the same method in the same Microsoft EntityFramework library, caused no issues.
Specifically, when using the Microsoft.AspNet.Identity.EntityFramework library & UserStore/UserManager classes, when an object was retrieved using FindAsync, subsequent modifications were not properly tracked by Entity Framework.
I.e. was getting this error after FindAsync, then trying to update the DB:
Attaching an entity of type 'MyNamespace.MyUser' 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.
My UpdateAsync method that was failing looked like this:
public async Task UpdateAsync(MyUser user)
{
var result = await _manager.UpdateAsync(user); //State = EntityState.Unchanged
//UpdateAsync above is directly inherited from Microsoft.AspNet.Identity.EntityFramework.UserManager<T>
//ERROR! Primary key duplicate (Default identity method doing Attach :/ )
}
Eventually through tracking I found that the EntityState was still "Unchanged" even though changes were made. I then changed the code to this:
public async Task UpdateAsync(MyUser user)
{
_store.Context.Entry(user).State = EntityState.Modified;
var result = await _store.Context.SaveChangesAsync();
//var result = await _manager.UpdateAsync(user);
}
..the changes were then picked up & auto-magically updated in the DB. A completely synchronous Find & Update (using the same library) also allowed the Entity to be tracked & updated - this solution I deemed less acceptable than a workaround.
I'm not 100% certain it's a bug, but if this is verified by enough people, someone should open a ticket with MSFT.

Related

.NET 6 Entity Framework Tracking with Dependency Injection

I guess I just don't understanding EF tracking. I have the context added via dependency injection via:
builder.Services.AddDbContext<OracleContext>(options => options.UseOracle(OracleConnectionString, b => b.UseOracleSQLCompatibility("11"))
.LogTo(s => System.Diagnostics.Debug.WriteLine(s))
.EnableDetailedErrors(Settings.Dev_System)
.EnableSensitiveDataLogging(Settings.Dev_System)
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking));
I set the tracking behavior to NoTracking here (at least so I thought).
I have a .NET Controller that has the context in its constructor. It passes this context to my class constructor containing my methods. Pretty much everything works fine... except for one:
I have a method that does a context.RawSqlQuery to get a list of objects. I iterate over these objects calling two separate methods from a different class that was generated the same way (using the injected context). This method first does a EF query to verify the object does not already exist, if it does it returns it and we move on - no issues. On the query to check if it exists I also added .AsNoTracking() for SnGs. However, if the object does not exist, and I try to make a new one... every time I do an context.add I get
"The instance of entity type 'Whatever' cannot be tracked because another instance with the key value '{MfrId: 90}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached."
I have tried adding
db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking - no change
I have tried adding context.Entry(NewItem).State = EntityState.Detached; before and after the call - no change.
I tried a loop in the context that gets all tracked objects and sets them to detached - no change.
What am I missing here? First - why is it tracking at all? Second - any suggestions on how to get passed this? Should I just give up using dependency injection for the context (suck... lots of rework for this)?
As requested - here is the class & method that is failing (non related stuff removed):
public class AssetMethods : IAssetMethods
{
public OracleContext db;
public AssetMethods(OracleContext context)
{
db = context;
}
public CcpManufacturer? CreateNewManufacturer(CcpManufacturer NewMan, string ActorID)
{
...blah blah non DB validation stuff removed...
//Check if it exists already
CcpManufacturer? OldMan = db.CcpManufacturers.Where(m=>m.MfrName == NewMan.MfrName).AsNoTracking().FirstOrDefault();
if (OldMan != null) {
return OldMan;
}
//Who done did it
NewMan.CreatedBy = ActorID;
NewMan.CreationDate = DateTime.Now;
NewMan.Isenabled = true;
//save
db.CcpManufacturers.Add(NewMan);
db.SaveChanges(); //fails here
//Prevent XSS Reflection
return db.CcpManufacturers.Find(NewMan.MfrId);
}
}
this method is called from this code. The OM is also using the injected context
List<MasterItem> Items = OM.RawSqlQuery(Query, x => new MasterItem { MFR_NAME = (string)x[0], MODEL_NUMBER = (string)x[1], LEGAL_NAME= (string)x[2]});
foreach (MasterItem item in Items)
{
CcpManufacturer? Man = new() {
MfrName = item.MFR_NAME,
Displayname = item.MFR_NAME
};
Man = AM.CreateNewManufacturer(Man,System.Id); //if its new.. it never get passed here because of the discussed error...
if (Man == null || Man.MfrId == 0)
{
continue;
}
.... other stuff
}
So the mfr id is added to a new object that's passed to a pretty much identical methods to create a item (where the mfr id is attached). Now - if I detach THAT item - I am ok. But why is it tracking when I have it turned off pretty much everywhere?
Yes, you found your problem.
Turning off Tracking affects what EF does when querying for entities. This means when I tell EF to read data from the DB and give me entities, it will not hang onto references of those entities.
However, entities you tell a DBContext to ADD to a DbSet and related entities will be tracked, regardless of your tracking setting.
So if I do something like:
var entity1 = context.Entities.Single(x => x.Id == entityId).AsNoTracking();
var entity2 = context.Entities.Single(x => x.Id == entityId).AsNoTracking();
The references to entity1 and entity2 will be 2 distinct references to the same record. Both are detached, so the DbContext isn't tracking either of them. I can use and attach either of them to perform an update, but that entity would be from that point considered Attached until I explicitly detach it again. Attempting to use the other reference for an update would result in that error. If I query specifying NoTracking after I have attached and updated that first entity reference, I will get back a new untracked entity reference. The DbContext doesn't return it's tracked reference, but it doesn't discard it either.
The exact same thing happens if I add a new entity then query for it specifying NoTracking. The query returns an untracked reference. So if you try and attach it to update a row, EF will complain about the reference it is already tracking.
I don't recommend diving down the rabbit hole of passing around detached entities unless you're keen to spend the time to really understand what is going on behind the scenes and prepared for the pretty deliberate and careful handling of references. The implications aren't just things not working as expected, it's having things work or not work on a completely situational basis which can be a nasty thing to debug and fix, even when you know what to look for.

Use AsNoTracking for entities mapped from AutoMapper

I have a situation where I am mapping DTO -> Database Entity using automapper.
var entityObj = _mapper.Map<REQUESTEXT>(reqDTO);
Then I am using entityObj to update the record in the database.
void Update(REQUESTEXT entityObj)
{
_context.REQUESTEXTs.Attach(entityObj); <--- Error
_context.Entry(entityObj).Property(x => x.CUSTOPTIONCD).IsModified = true;
_context.SaveChanges();
}
When i am trying to attach REQUESTEXT object to context, its giving me an error:
Attaching an entity of type 'A' 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.
As per this SO answer: https://stackoverflow.com/a/23228001/1169180 I need to use AsNoTracking(), I am not sure how to use that in AutoMapper?
Any suggestions?
AsNoTracking refers to when the entities are loaded by the context, not Automapper. You are getting the error because at some point in that DbContext's life, it has loaded the entity with that ID and is tracking it. The option they recommended is to change over your entity loading to use AsNoTracking which effectively tells EF not to track the entity when it is read.
An alternative solution to that problem is to check for the existence of the entity in the DbContext's local cache first, and if found, use AutoMapper to map your property changes across to that existing entity, rather than creating a new entity.
For example:
var existingEntity = _context.REQUESTEXTs.Local.SingleOrDefault(x => x.EntityId == reqDTO.EntityId);
if(existingEntity != null)
mapper.Map(reqDto, existingEntity);
else
{
var entityObj = _mapper.Map<REQUESTEXT>(reqDTO);
_context.REQUESTEXTs.Attach(entityObj);
_context.Entry(entityObj).Property(x => x.CUSTOPTIONCD).IsModified = true;
}
_context.SaveChanges();
This checks the local cache for an existing entity, (does not hit DB) and if found, it uses AutoMapper to update it's properties. The entity tracking will note the changes, so when SaveChanges is called, the modifications would go through to the DB. If the local cache does not have the entity, then we create a new instance, attach it, mark it as modified, and save.
One suggestion that appears to be missing in your example: You should be validating the assumptions that:
The ID in your DTO actually does exist in the database before attempting this
and
The record being modified can, and should be editable by the user making this request.
and
The data being updated is fully validated.
If this is a web application /w an accessible Controller action or Web API endpoint, this could be exploitable to allow users to edit records they otherwise should not be able to, or update records in ways they should not be. (Trust nothing from a client request.) Each request should be validated thoroughly, and any deviation detected should terminate the client session.

Unable to attach an existing object in Entity Framework 6

I have a class Customer. I am trying to clone a Customer object and modify it, then I want those modifications to be reflected in the context (database as well). I am using following code to do that.
Customer old = context.Customers.Where(c=>c.CustomerID ==1 ).SingleOrDefault();
Customer m = CustomExtensions.ShallowCopyEntity<Customer>(old);
m.Name = "Modified";
m.MobileNo = "9999999999";
context.Customers.Attach(m);
But its throwing following exception
Attaching an entity of type 'DataBindingSample.Customer'
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 tried changing EntityState to Modified but it didn't work.
Can anyone tell me how to achieve this?
My main goals are
I want to clone (I will use deep clone when necessary) an existing entity
Want to modify the cloned entity (as well as referenced entities - I will use deep clone in this case)
Finally I want to save changes to database
EDIT
As pointed out in this comment i am trying to attach object which aready exists in context. So i can detach it first and then atttach again as shown bellow if attach is compulsory.
Customer old = context.Customers.Where(c=>c.CustomerID ==1 ).SingleOrDefault();
Customer m = CustomExtensions.ShallowCopyEntity<Customer>(old);
m.Name = "Modified789789";
m.MobileNo = "9999999999";
((IObjectContextAdapter)context).ObjectContext.Detach(old);
context.Customers.Attach(m);
context.Entry(m).State = EntityState.Modified;
context.SaveChanges();
Otherwise i can follow 2 options mentioned in this answer.
There are 2 options that I can think of:
Copy the updated values back to the original entity loaded into your DbContext and then save changes.
Updated values of the original entity and then discard them if user canceled the update.
Options 1
Just copy the updated values back to the originally loaded entity. Automapper is your friend in tasks like this. This approach can later be extended to allow user to change a model of your entity and not the data layer object itself (e.g. to expose a limited number of fields that user can edit).
var entity = context.Customers.SingleOrDefault(c => c.CustomerID == 1);
var updatedEntity = CustomExtensions.ShallowCopyEntity<Customer>(old);
updatedEntity.Name = "Modified";
updatedEntity.MobileNo = "9999999999";
entity.Name = updatedEntity.Name;
entity.MobileNo = updatedEntity.MobileNo;
context.SaveChanges();
If you add Automapper nuget, then you mappings (copying) will become much easier:
Mapper.CreateMap<Customer, Customer>();
Mapper.Map(updatedEntity, entity);
And your code will look like:
// Configuring mapping. Needs to be done only once.
Mapper.CreateMap<Customer, Customer>();
var entity = context.Customers.SingleOrDefault(c => c.CustomerID == 1);
// Check if entity is null
var updatedEntity = CustomExtensions.ShallowCopyEntity<Customer>(old);
updatedEntity.Name = "Modified";
updatedEntity.MobileNo = "9999999999";
// Copy the updated values back
Mapper.Map(updatedEntity, entity);
context.SaveChanges();
Options 2
Make changes in the originally loaded entity and discard them if user changed her mind and canceled. See this post and this post on how to do it.
Discarding the whole DbContext might not be a good option in case you still need it (duh).

`Attaching an entity of type failed...` exception while trying to update a class in EF and with a generic repository

I'm using Entity Framework. I want to load an entity, edit it, and save the changes back in the DB. But no matter if I've edited a foreign key property or a simple property, EF gives me the following error:
Attaching an entity of type 'ClassX' 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.
Please note that ClassX is not a direct virtual property of the class that I'm trying to update, instead it's a virtual property in some of the other classes that my class has navigation properties to them.
I've read some related issues. But I didn't really get how I should apply them to my own problem, since I'm using a generic repository as posted below.
public class GenericRepository<T> where T : class
{
private EFDbContext context = new EFDbContext();
public IEnumerable<T> GetAll()
{
return context.Set<T>();
}
public void Insert(T entity)
{
context.Set<T>().Add(entity);
context.SaveChanges();
}
public void Update(T entity)
{
context.Entry(entity).State = System.Data.Entity.EntityState.Modified;
context.SaveChanges();
}
//removed for brevity
}
I've encountered another problem related to virtual properties and I was advised to use ViewModels and Object to Object mapping.
As far as I got it, there's 3 options:
Use ViewModels and object-to-object mapping. I'm not going with this one, it was really painful since o2o mapping libraries have lots of bugs.
Somehow uses reference. But I can't do that since the repository is generic. Maybe I should use reflection API for that?
Delete all virtual properties. It is actually an option, since they're creating more problems than they solve.
Can anyone please explain why this problem happens and what's the easiest way to solve it?
When you set the State of an entity to Modified it also attaches all children (entities referenced by navigation properties) with State == EntityState.Unchanged. If your context already has entities with the same key, it will raise that error.
If you want those entities to be ignored, there are a few options I can think of:
Create a new data context within Update, and don't worry about the children entities because with EntityState.Unchanged, when you call SaveChanges, they'll be ignored. This probably doesn't make sense if you're using some kind of Repository Pattern.
Go through the navigation properties you don't want to attach and set to null before setting State = EntityState.Modified
After setting State = EntityState.Modified, for child entities you want to ignore, set State = EntityState.Detached
Edit
It would also be good to figure out why the context would end up with multiple child entities with the same key in the first place.

How to delete an associated object in Entity Framework without having access to the object context

Having two models, Site and Link, where a site has many links, how do I delete a link from inside a method of Site, which doesn't have access to the object context?
I've tried something like:
public void DeleteFirstLink() {
var link = LinkSet.First();
LinkSet.Remove(link);
}
but it seems that is not really deleting the link, but breaking the association. Since there's a database constraints it throws this error:
A relationship is being added or deleted from an AssociationSet 'Sites_have_Links'. With cardinality constraints, a corresponding 'Links' must also be added or deleted.
How do I actually delete the link from the database?
Assuming that your ObjectContext is not alive when you call the DeleteFirstLink() method, you can make it work by spinning up a context inside the method, attaching the Site entity, and then deleting the link:
public void DeleteFirstLink()
{
using (ProjectEntities db = new ProjectEntities())
{
db.AttachTo("[your site EntitySet name - SiteSet?]", this);
var link = LinkSet.First();
db.DeleteObject(link);
LinkSet.Remove(link);
db.SaveChanges();
}
}
You can't delete anything from the database without an object context. All actions are queued in the state manager of the object context, and those are persisted to the database when you call ObjectContext.SaveChanges(). Without SaveChanges, no DB changes.
First of all, it would be great if you could post a bit more information about your class structures. Does the Site class have an ObjectContext object? Then as a quick solution you could pass it into the delete method and use the context.DeleteObject() method, and call SaveChanges afterwards.
However, as a long-term solution, I will still recommend using the UnitOfWork pattern and I will post the link to the article explaining it again. The implementation might be different, but in general it might solve most of your problems (similar to this one).
The beauty of this approach is that if you use it correctly, you can build a small framework, that you can later reuse in all of your EF projects.
To work with entities so the modifications are reflected in the database you MUST ADD/ATTACH these entities in object context (in terms of EF5 in database context) and then use method SaveChanges to commit changes.
Yes, in EF4 to remove a record from phisical SQL table (not a link) you need to use method DeleteObject of object ObjectContext and then SaveChanges, i.e.
using(ObjectContext context = new ObjectContext)
{
/* Find the removed record in object/database context (this will attaches
* the record to object/database context)
* It is recommened to use FirstOrDefault() instead of First()
* becase this method can return null if there is no record to delete
* instead generation of exception in case of using First()
*/
Link linkToDelete = context.Links.FirstOrDefault();
if(linkToDelete != null)
{
context.DeleteObject(linkToDelete);
context.SaveChanges();
}
}
Fortunately now there is EF5 that allows to remove from parent collection but only if relation is one-to-many.
using(DatabaseContext context = new DatabaseContext)
{
Link linkToDelete = context.Links.FirstOrDefault();
if(linkToDelete != null)
{
context.Links.Remove(linkToDelete);
context.SaveChanges();
}
}
In any case DO NOT forget to call SaveChanges!

Categories

Resources