.NET 6 Entity Framework Tracking with Dependency Injection - c#

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.

Related

How can I attach an entity to the context when one of its children is already attached?

Background
In my application we were running into issues when trying to add a new entity with existing children to the database after mapping it from a DTO using AutoMapper. The entity and its children were not attached to the context, and so the children would be treated as new and EF would attempt to insert duplicates. My solution to this was to automatically attach an entity to the context whenever an object was mapped to a BaseEntity type (BaseEntity is the base class for all of our Model objects, it has an Id property and nothing else). Here is the code:
public TDestination Map<TDestination>(object source) where TDestination : class
{
var result = _mapper.Map<TDestination>(source);
if (typeof(TDestination).IsSubclassOf(typeof(BaseEntity)) && result != null)
_context.Attach(result); //_context is a DbContext
return result;
}
This worked fine in my initial test cases, but now I've run into an issue where the entity I'm attaching has a child that is already attached to the context. This throws "The instance of entity type 'MyChildEntity' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked.".
How can I attach an entity to the context when a child is already attached? I'm trying to keep this method extremely generic so that it can be used by any object that we are trying to map from a DTO to a BaseEntity.
What I've Tried
I've tried grabbing the associated EntityEntry and recursively detach all of its children using the following method before attempting to call Attach():
private void DetachChildren(EntityEntry entity)
{
foreach (var member in entity.Members.Where(x => x.CurrentValue != null))
{
if (IsBaseEntityType(member.CurrentValue.GetType()))
{
var childEntity = _context.Entry(member.CurrentValue);
childEntity.State = EntityState.Detached;
DetachChildren(childEntity);
}
}
}
I've confirmed that this loop does reach the problem child and sets its state to detached, but I still end up getting the same error when calling Attach().
Welcome to the hell that is working with detached entities.
Automapper can be leveraged to update existing entities rather than forming an entity and attaching it. Given an object like an Order:
public void UpdateOrder(OrderDTO orderDTO)
{
var order = _context.Orders.Single(x => x.OrderId = orderDTO.OrderId);
_mapper.Map(orderDTO, order);
_context.SaveChanges();
}
The benefits of this approach is that it handles whether the order happens to be tracked or not, asserts the order exists for something like an Update where it is assumed it does, and when SaveChanges runs, only the fields that actually changed will be updated. If only 1 field changed, the update statement updates that single field. Attaching a new object and setting EntityState to Modified will update all fields. This could introduce unexpected attack vectors to change data you don't expect since a DTO needs to pass enough info to construct a whole entity to avoid unintentionally #null-ing data. The mapping from DTO to entity should ensure that only editable fields are copied across.
In the case where the OrderDTO will contain one or more child collections to update, you will likely need to use a mapping that excludes those collections, then use AfterMap in the mapping configuration to inspect the child collection for new vs. existing vs. removed entities and handle those accordingly. (Add vs. mapper.Map vs. Remove)
Generally the updates can be structured to perform atomic operations that make the entity interactions as straight forward as possible. For instance UpdateOrderDetails(orderDTO) would update information about the order itself, where there would be separate methods such as AddOrderLine(newOrderLineDTO) vs. UpdateOrderLine(orderLineDTO) vs. RemoveOrderLine(orderLineId) etc. rather than having all order line operations and other related changes done through a single UpdateOrder method accepting a whole modified object graph.
The alternative when dealing with graphs and the possibility of tracked entities is that you need to check each and every related entity against the DbSet's .Local or other means to check to see if the entity is tracked. If it is, then you have to replace the references and copy any applicable changes to the already tracked entity. Telling a DbContext to ignore an existing entity isn't always a simple matter as there can be references to that entity in other tracked entities. Generally you'll want to detect a tracked entity reference then update your references to use that tracked reference and update it as needed. It is lots of mucking around with references, and definitely does not work well with Generic methods
Generic operations are tempting from a DNRY standpoint, but dealing with situations where some entities might be tracked vs. not, and then handling type mismatches etc. (source = object = can be anything..) adds a lot of complexity in place of simpler methods to handle operations.

Cannot insert explicit value for identity column in table 'TABLE' when IDENTITY_INSERT is set to OFF Exception Updating my table

I´m trying to update a list of objects, but when I run context.SaveChanges(), I get an error
Cannot insert explicit value for identity column in table 'TABLE' when IDENTITY_INSERT is set to OFF
I know when the Identity key is generated you cannot register a new entry with this KEY but I'm trying to update my registry not insert a new one and this exception appears. I don't know why this happens now.
My code:
foreach (int i in deletes)
{
var p = context.Item.FirstOrDefault(g => g.participant_id == i);
if (p != null)
{
p.group_id = 0;
context.Entry(p).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
context.SaveChanges();
}
}
group_id is not my identity column, it is just another column I added:
ALTER TABLE Items
ADD group_id INTEGER DEFAULT(0);
The error specifically is the result of a configuration mismatch somewhere in your entity definitions where a table is set to use an Identity PK but EF is not configured for an identity column and the DbContext is attempting to insert a row into that table. This is not necessarily the table you think the DbContext is attempting to update. Chances are somewhere prior to SaveChanges being called there has been an entity incorrectly created or associated and treated as a new entity.
Try this, provided your code is roughly identical to your example:
using (var tempContext = new YourAppDbContext())
{
foreach (int i in deletes)
{
var p = tempContext.Item.FirstOrDefault(g => g.participant_id == i);
if (p != null)
p.group_id = 0;
}
tempContext.SaveChanges();
}
Here we isolate this operation to a clean, temporary DbContext instance to ensure there are no polluting tracked changes. This is not a "fix" for the issue, just to identify whether the context was polluted. You can inspect context pending changes to see what it might be tracking. (ChangeTracker) Your issue may also stem from setting the entire entity to Modified state and a misconfigured related entity definition. (unlikely if the entity and associated were read from this DbContext instance) When updating an entity, don't set the EntityState to Modified. This sets EF up to effectively update all columns on that table. In the above example if we just update 1 column, EF will generate an Update statement for just that single column, and will only generate an update statement if that value actually changes.
If the above code works without an issue, then you have rogue tracked changes polluting your DbContext. This either means you have something else going on before this code is called that you need to fix, or your DbContext lifetime scope is far longer than it should be. (or both)
The problem with your example is that we cannot see the scope that your context is covering. Whenever you make any kind of change to a DbContext, those changes are tracked by default by that DbContext. If you have code that is working with detached entities anywhere that suddenly get re-associated with a DbContext via association with an Added entity and treated as new entities themselves, then calling SaveChanges() when you expect to update a single row all the sudden results in strange unrelated exceptions because the DbContext wants to persist queued up changes.
A typical culprit to look out for with issues like this would be first to check over all of your entity definitions for tables using Identity PKs and ensure those entities are configured with DatabaseGeneratedOption.Identity. This alone would prevent the exception, but you'd probably find that EF would be inserting new, duplicate rows somewhere with new PKs. The culprit behind that is usually detached entities being re-associated to an entity.
Given a Order entity associated to a Product. We want to create an order so since we already loaded the Products collection to select and associated to the new Order, we set that reference client side and send the Order to the server. We assume that since we loaded the Products and it's children from a DbContext on a GET, that when we call POST and send that parent back to the server, all should be good.
public void CreateOrder(Order order)
{
context.Orders.Add(order);
context.SaveChanges();
}
order had a Product reference with ProductId = 22 since we selected product #22 for the order. However, the DbContext instance when we call CreateOrder doesn't know Product ID 22 represents a known entity, it isn't tracking Product ID 22, so it gets treated as a new entity. If the Product entity ID isn't configured as an Identity column but is in the DB, you get the error you saw. If it is configured as an Identity column then you'd find the Order saves successfully, but is pointing at a new, duplicate Product with an ID of 56 or whatever the next available ProductId is.
Passing detached entities is a big cause of headaches like this. The safe way to do something like the CreateOrder would be:
public void CreateOrder(Order order)
{
// TODO: Obviously add null checks, validations, etc.
var product = context.Products.Single(x => x.ProductId == order.Product.ProductId);
order.Product = product; // replace with the product tracked by this DbContext.
context.Orders.Add(order);
context.SaveChanges();
}
All associations on a detached entity need to be checked.
Devs don't like the hit to the database so they can be tempted to merely attach the Product:
public void CreateOrder(Order order)
{
// TODO: Obviously add null checks, validations, etc.
context.Products.Attach(order.Product);
context.Orders.Add(order);
context.SaveChanges();
}
.. and in most cases that will appear to work. Until it doesn't. Attach will throw an exception the moment you attempt to attach an entity with an ID that the DbContext is already tracking. If any code prior had loaded or attached an instance of that Product, then the call will fail. This can result in random-like exceptions appearing at runtime. The safe way to attach the entity would be:
public void CreateOrder(Order order)
{
// TODO: Obviously add null checks, validations, etc.
var existingProduct = context.Product.Local.SingleOrDefault(x => x.ProductId == order.Product.ProductId);
if (existingProduct == null)
context.Products.Attach(order.Product);
else
order.Product = existingProduct;
context.Orders.Add(order);
context.SaveChanges();
}
This checks the local tracking cache to see if the DbContext is tracking that product. (Does not hit DB) If it isn't then we can safely attach it. If it is, we replace the product reference on the order with the tracked one.
This would have to be done for every reference. Any that are missed would result in potential errors or duplicate rows.
Try
var p = context.DbSet<Participant>().Where(g => g.participant_id == i).FirstOrDefault();
And did you check your Participant class in EF? It still can be a foreign key there.

Correct way to check if greedily-loaded Entity was loaded after context disposed?

Probably not pertinent, but I'm using:
.NET MVC 5.2.3 w/ Razor 3.2.3, Entity Framework 6.1.3 Code First, Visual Studio 2015.
Okay, in my controller method, I have--in essence, but dumbed down for conciseness:
using( var context = new MyContext() ) {
var person = context.Persons.Include( x => x.PostalCode ).FirstOrDefault();
return View(person);
}
Now, originally the Zip data entry property was not a foreign key...just whatever 5-digit string the user entered. Now, however, it's a foreign key, in essence, so that we can get postal-code information.
public string Zip { get; set; }
[ForeignKey("Zip")]
public virtual PostalCode PostalCode { get; set; }
Not ideal structure...I know...
But anyway, if the user has a postal-code recognized by our system, all great, everything loads. However, if the user has an unknown or invalid zip code, e.g. 00000, EF sees a non-null Foreign Key and this results in the following problem:
In my view-file (so after I've disposed of my context), I check the property of our greedily-loaded entity:
#if( person.PostalCode != null && person.PostalCode.IsInServiceArea ) {
<div>Service Area HTML</div>
}
Unfortunately, because of EF overriding my virtual property, even when there is no PostalCode, it's not NULL. So, when this code runs it throws an exception that the ObjectContext has been disposed of, which means that EF is trying to lazy-load an Entity even though it already tried to greedily load and should know it doesn't exist :(
The obvious solutions (please don't answer w/ these):
Validate zip codes on entry and only allow ones we know about and set unknown zip codes to NULL (I like this, but I don't have time to re-engineer this)
Get the value of IsInServiceArea while the context is opened and put it directly in my View Model so that I set the value before my context is disposed of. (This is actually what I'm planning to do, so I don't need this answer):
The Question
In Entity Framework, Code-First, what is the correct way to check to
see if a greedily loaded, LEFT OUTER JOIN'd entity, was loaded AFTER
the context is disposed of?
Based on answers I have found, (e.g. the below), I'm thinking it's likely that this is not possible without the context being open...but thought I'd ask anyway:
How to determine if Navigation Property in the Entity Framework is set without loading the related records
So, referring back to my comment, you can disable lazy-loading and proxy creation altogether. Just find the constructor(s) of your dbcontext type, and then add these two lines into the method:
this.Configuration.LazyLoadingEnabled = false;
this.Configuration.ProxyCreationEnabled = false;
This way you can disable it globally (whenever a context is created the ctor is run, these settings are applied).
Or, you can just disable it for one instance if you set these on the context object itself after it's created.
Alternatively let your context live through the entire request so that lazy loading can be fulfilled on the view.
Startup.Auth.cs
public void ConfigureAuth(IAppBuilder app)
{
MyContext Create() { return new MyContext(); }
app.CreatePerOwinContext(Create);
...
}
MyController.cs
var context = HttpContext.GetOwinContext().Get<MyContext>();
var person = context.Persons.Include( x => x.PostalCode ).FirstOrDefault();
return View(person);

How to pass DbContext (EntityFramework SqlConnection) to another Method?

Issue in passing DbContext to another method i.e. for e.g:
public bool MarkCustomerForDelete(Customer customerObj)
{
using(var dbContext = new MyContext())
{
using(var dbTransaction = dbContext.Database.BeginTransaction())
{
//Clear all orders for the Given Customers
var orderList = dbContext.Orders.Where(x=>x.id == customerObj.OrderId).ToList();
CommonLogicMethod(dbContext, orderList);
//Logic
customerObj.Status = "Deleted";
// The Modification will fail over due to the Customer Object for that object is already attached to the DbContext with Previous Values
dbContext.Entry(customerObj).State = EntityState.Modified;
dbContext.SaveChanges();
dbTransaction.Commit()
return true;
}
}
}
public void DeleteOrderRelatedData(MyContext dbContext, List<Orders> orderList)
{
foreach(var entity2 in entity2List)
{
var OrderAddresses = dbContext.OrderAddresses.Where(x=>x.Id == entity2.Id).ToList();
//Now if here the dbContext has 100 Entities (Tables)
//It internally Enumerates all the entities in the Local cache i.e. dbContext.Coupons.Local has all the Records from the DB in the Local present.
}
}
Question: Why does when DbContext is passed to another method internally calls for all the data i.e. in dbContext.Customers.Local has all the Data in the Database in First-Level Cache ?
Question: How to Pass DbContext from one Method to Another (without creating above given issue) ?
This is Creating problem related to modification of the Data i.e. DeleteCustomer will fail over.
Now, if the code in the DeleteOrderRelatedData, is merged into the DeleteCustomer function, it works fine.
I added a Logs for the dbContext , and dbContext while passing it to the Function internally is calling all the Select queries related to the different Queries..
For more details, please check this Video out : Link
Tools being used :
Entity Framework 6.0
System.Data.Sqlite
PostSharp for MethodBoundary Aspect.
Sounds like your problem is something to do with cascading deletions but the wording is difficult to understand ...
The statement in your question ...
DbContext is passed to another method internally calls for all the
data
... DbContexts don't just "go and get all data" automatically, you must be triggering something that's causing it.
It sounds to me like when you are deleting your customer object EF you are manually implementing the code for a cascading delete when what you should perhaps do is just add that to the model and then remove the customer object negating the need for all this extra logic.
In other words you have said / are trying to say "when a customer is deleted, also find and remove the customers related orders".
In the code sample above you do ...
//Clear all orders for the Given Customers
var orderList = dbContext.Orders.Where(x=>x.id == customerObj.OrderId).ToList();
This is purely getting the orders by executing a "select * from orders where customerid = customer.Id"
then in the method you define below that ...
public void DeleteOrderRelatedData(MyContext dbContext, List<Orders> orderList)
... it looks like you then want to further delete all the addresses for the order. Although you don't appear to be calling that method in the sample above.
Instead you can do something like this to have EF worry about the children and grandchildren deletions for you all in the db ...
Entity Framework (EF) Code First Cascade Delete for One-to-Zero-or-One relationship
Cascading deletes with Entity Framework - Related entities deleted by EF
The Microsoft documentation for this is here ...
https://msdn.microsoft.com/en-gb/data/jj591620.aspx
EDIT:
My answer was based on what I knew EF would do out of the box, it seems that the actual problem was caused by a component not mentioned in the question, the problem was not about performing a heirarchy of actions as I had interpreted it was in fact about solving the issue of how another third party component was adversely affecting the EF behaviour.
In repsonse to the question:
How to Pass DbContext from one Method to Another (without creating above given issue) ?
... Just do it as passing a context between 2 methods will not in its own right cause the problem you were having.
...
It seems that answering this question correctly was impossible :(
Issue was due to :
I was using PostSharp, to Log the Traces using OnMethodBoundaryAspect.
Now this was using Arguments internally.
Since while Logging, it was serializing the Arguments, And this was creating the Problem.

Entity Framework 6 - use my getHashCode()

There's a certain amount of background to get through for this one - please bear with me!
We have a n-tier WPF application using EF - we load the data from the database via dbContext into POCO classes. The dbContext is destroyed and the user is then able to edit the data.
We use "state painting" as suggested by Julie Lerman in her book "Programming Entity Framework: DBContext" so that when we add the root entity to a new dbContext for saving we can set whether each child entity is added, modified or left unchanged etc.
The problem we had when we first did this (back in November 2012!) was that if the root entity we are adding to the dbContext has multiple instances of the same child entity (ie, a "Task" record linked to a user, with "Status Histories" also linked to the same user) the process would fail because even though the child entities were the same (from the same database row) they were given different hashcodes so EF recognised them as different objects.
We fixed this, (back in December 2012!), by overriding GetHashCode on our entities to return either the database ID if the entity came from the database, or an unique negative number if the entity is as yet unsaved. Now when we add the root entity to the dbContext it was clever enough to realise the same child entity is being added more than once and it dealt with it correctly. This has been working fine since December 2012 until we upgraded to EF6 last week...
One of the new "features" with EF6 is that it now uses it's own Equals and GetHashCode methods to perform change-tracking tasks, ignoring any custom overrides. See: http://msdn.microsoft.com/en-us/magazine/dn532202.aspx (search for "Less Interference with your coding style"). This is great if you expect EF to manage the change-tracking but in a disconnected n-tier application we don't want this and in fact this breaks our code that has been working fine for over a year.
Hopefully this makes sense.
Now - the question - does anyone know of any way we can tell EF6 to use OUR GetHashCode and Equals methods like it did in EF5, or does anyone have a better way to deal with adding a root entity to a dbContext that has duplicated child entities in it so that EF6 would be happy with it?
Thanks for any help. Sorry for the long post.
UPDATED
Having poked around in the EF code it looks like the hashcode of an InternalEntityEntry (dbEntityEntry) used to be set by getting the hashcode of the entity, but now in EF6 is retrieved by using RuntimeHelpers.GetHashCode(_entity), which means our overridden hashcode on the entity is ignored. So I guess getting EF6 to use our hashcode is out of the question, so maybe I need to concentrate on how to add an entity to the context that potentially has duplicated child entities without upsetting EF. Any suggestions?
UPDATE 2
The most annoying thing is that this change in functionality is being reported as a good thing, and not, as I see it, a breaking change! Surely if you have disconnected entities, and you've loaded them with .AsNoTracking() for performance (and because we know we are going to disconnect them so why bother tracking them) then there is no reason for dbContext to override our getHashcode method!
UPDATE 3
Thanks for all the comments and suggestion - really appreciated!
After some experiments it does appear to be related to .AsNoTracking(). If you load the data with .AsNoTracking() duplicate child entities are separate objects in memory (with different hashcodes) so there is a problem state painting and saving them later. We fixed this problem earlier by overriding the hashcodes, so when the entities are added back to the saving context the duplicate entities are recognised as the same object and are only added once, but we can no longer do this with EF6. So now I need to investigate further why we used .AsNoTracking() in the first place.
One other thought I have is that maybe EF6's change tracker should only use its own hashcode generation method for entries it is actively tracking - if the entities have been loaded with .AsNoTracking() maybe it should instead use the hashcode from the underlying entity?
UPDATE 4
So now we've ascertained we can't continue to use our approach (overridden hashcodes and .AsNoTracking) in EF6, how should we manage updates to disconnected entities? I've created this simple example with blogposts/comments/authors:
In this sample, I want to open blogpost 1, change the content and the author, and save again. I've tried 3 approaches with EF6 and I can't get it to work:
BlogPost blogpost;
using (TestEntities te = new TestEntities())
{
te.Configuration.ProxyCreationEnabled = false;
te.Configuration.LazyLoadingEnabled = false;
//retrieve blog post 1, with all comments and authors
//(so we can display the entire record on the UI while we are disconnected)
blogpost = te.BlogPosts
.Include(i => i.Comments.Select(j => j.Author))
.SingleOrDefault(i => i.ID == 1);
}
//change the content
blogpost.Content = "New content " + DateTime.Now.ToString("HH:mm:ss");
//also want to change the author from Fred (2) to John (1)
//attempt 1 - try changing ID? - doesn't work (change is ignored)
//blogpost.AuthorID = 1;
//attempt 2 - try loading the author from the database? - doesn't work (Multiplicity constraint violated error on Author)
//using (TestEntities te = new TestEntities())
//{
// te.Configuration.ProxyCreationEnabled = false;
// te.Configuration.LazyLoadingEnabled = false;
// blogpost.AuthorID = 1;
// blogpost.Author = te.Authors.SingleOrDefault(i => i.ID == 1);
//}
//attempt 3 - try selecting the author already linked to the blogpost comment? - doesn't work (key values conflict during state painting)
//blogpost.Author = blogpost.Comments.First(i => i.AuthorID == 1).Author;
//blogpost.AuthorID = 1;
//attempt to save
using (TestEntities te = new TestEntities())
{
te.Configuration.ProxyCreationEnabled = false;
te.Configuration.LazyLoadingEnabled = false;
te.Set<BlogPost>().Add(blogpost); // <-- (2) multiplicity error thrown here
//paint the state ("unchanged" for everything except the blogpost which should be "modified")
foreach (var entry in te.ChangeTracker.Entries())
{
if (entry.Entity is BlogPost)
entry.State = EntityState.Modified;
else
entry.State = EntityState.Unchanged; // <-- (3) key conflict error thrown here
}
//finished state painting, save changes
te.SaveChanges();
}
If you use this code in EF5, using our existing approach of adding .AsNoTracking() to the original query..
blogpost = te.BlogPosts
.AsNoTracking()
.Include(i => i.Comments.Select(j => j.Author))
.SingleOrDefault(i => i.ID == 1);
..and overriding GetHashCode and Equals on the entities: (for example, in the BlogPost entity)..
public override int GetHashCode()
{
return this.ID;
}
public override bool Equals(object obj)
{
BlogPost tmp = obj as BlogPost;
if (tmp == null) return false;
return this.GetHashCode() == tmp.GetHashCode();
}
..all three approaches in the code now work fine.
Please can you tell me how to achieve this in EF6? Thanks
It’s interesting and surprising that you got your application working this way in EF5. EF always requires only a single instance of any entity. If a graph of objects are added and EF incorrectly assumes that it is already tracking an object when it is in fact tracking a different instance, then the internal state that EF is tracking will be inconsistent. For example, the graph just uses .NET references and collections, so the graph will still have multiple instances, but EF will only be tracking one instance. This means that changes to properties of an entity may not be detected correctly and fixup between instances may also result in unexpected behavior. It would be interesting to know if your code solved these problems in some way or if it just so happened that your app didn’t hit any of these issues and hence the invalid state tracking didn’t matter for your app.
The change we made for EF6 makes it less likely that an app can get EF state tracking into an invalid state which would then cause unexpected behavior. If you have a clever pattern to ensure the tracking state is valid that we broke with EF6 then it would be great if you could file a bug with a full repro at http://entityframework.codeplex.com/.

Categories

Resources