Is my understanding of IsLoaded correct? - c#

Here is some code along with my assumptions based on playing around in LINQPad. Can anyone confirm this is how the lazy loading is working, and perhaps provide any additional insight/links so I can understand how it's working on the back end? Thanks in advance!
// Step 1.
var record = context.MyTable.First();
// Step 2.
var foreignKey = ForeignKeyTable.Where(x => x.Id == record.ForeignKeyId).Single();
// Step 3.
var entry = context.Entry(record);
// Step 4.
trace(entry.Reference(x => x.ForeignKey).IsLoaded);
// Step 5.
trace(record.ForeignKey.SomeProperty);
Retrieve some record (DB is queried).
Retrieve a record that happens to be a foreign key property of record without using lazy loading like record.ForeignKey to retrieve it (DB is queried).
Get the details of the record entity.
This is the part I'm unsure about. In my testing it outputs true. I'm guessing that IsLoaded doesn't know whether or not record.ForeignKey currently has a value, but knows that record.ForeignKey is already being tracked in the context based on it's knowledge of record.ForeignKeyId and the relationships that have been established.
The db doesn't seem to be hit here, and I assume it's for the same reason IsLoaded returns true in 4. It knows that it's tracking the foreignKey object already, so it knows it doesn't have to do the lazy loading.
Edit: The actual problem I'm trying to solve can be illustrated as such:
var record = context.MyTable.First();
var foreignKey = new ForeignKey() { Id = record.ForeignKeyId, SomeProperty = 5 };
context.ForeignKeyTable.Attach(foreignKey);
var entry = context.Entry(record);
// Returns false.
trace(entry.Reference(x => x.ForeignKey).IsLoaded);
// Doesn't query for ForeignKey, so it must know it's `loaded` somehow, and
// gets SomeProperty from my new foreignKey object. What???
trace(record.ForeignKey.SomeProperty);

EF fixes relationships (navigation properties) automatically according to primary key and foreign key values when you load an entity from the database or when you attach it to the context.
In both code snippets you have loaded record which has a foreign key to your ForeignKeyTable. The context knows this value. (It doesn't matter btw if you have exposed the foreign key in your model. It will always be loaded, also without having a FK property in your model. You can see this when watching the SQL query.)
In both cases you attach afterwards a ForeignKey entity to the context which has as primary key the value of record.ForeignKeyId which the context already knows about. As a consequence EF will set the navigation property record.ForeignKey to this attached ForeignKey entity.
Obviously IsLoaded doesn't tell you if the entity is attached to the context because in both examples it is attached but one returns true and the other false. It also doesn't tell you if record.ForeignKeyId refers to an entity, because this is also the case in both examples.
It tells you apparently only that the entity has really been loaded from the database (and not only manually attached) (which also Intellisense says about IsLoaded). That's the only difference between your first and second example.
And it seems that lazy loading is not only controlled by the IsLoaded flag. If you attach an entity for the navigation property to the context, lazy loading doesn't happen anymore although IsLoaded is false.
What would happen if your last line in the second code snippet would actually trigger lazy loading? The ForeignKey object being loaded must have the same key as the ForeignKey object you have already attached (because record has this value as FK property ForeignKeyId). But because no two objects with same key can be attached to the context it must be the same object. But then there is no need to load it since such an object is already in memory and attached.

// Step 1.
var record = context.MyTable.First();
// Step 2.
var foreignKey = ForeignKeyTable.Where(x => x.Id == record.ForeignKeyId).Single();
// Step 3.
var entry = context.Entry(record);
// Step 4.
trace(entry.Reference(x => x.ForeignKey).IsLoaded);
// Step 5.
trace(record.ForeignKey.SomeProperty);
Retrieve some record (DB is queried). yes, and the resulting record is attached to the DbContext.
Retrieve a record that happens to be a foreign key property of record without using lazy loading like record.ForeignKey to retrieve it (DB is queried). yes. If you had wanted to eager load the foreign key in #1, you would have used context.MyTable.Include(m => m.ForeignKey).First(); That would have retrieved the record along with the fk in 1 query.
Get the details of the record entity. Kind of... it is the details of the entity in relation to the DbContext (what is attached / deleted / loaded / etc)
This is the part I'm unsure about. In my testing it outputs true. I'm guessing that IsLoaded doesn't know whether or not record.ForeignKey currently has a value, but knows that record.ForeignKey is already being tracked in the context based on it's knowledge of record.ForeignKeyId and the relationships that have been established. This means that the DbContext does not need to run another query to load the data for the foreign key. If you execute record.ForeignKey, the data is already there, and no additional trip to the db is required.
The db doesn't seem to be hit here, and I assume it's for the same reason IsLoaded returns true in 4. It knows that it's tracking the foreignKey object already, so it knows it doesn't have to do the lazy loading. The entitiy has already been loaded in step #2, so there was no additional trip needed to get it from the db.
Update after question edit
According to EF, the .Attach method on IDbSet:
Attaches the given entity to the context underlying the set. That is, the entity is placed into the context in the Unchanged state, just as if it had been read from the database.

Related

Entity Framework Core : populating relationship navigation property

Is it possible in .NET 6 with Entity Framework Core 6 to populate the relationship navigation property by setting the foreign key value and then call SaveChanges?
I tried it but it doesn't seem to work. Although the other way around works perfectly (if I set the navigation property to the related entity).
Screenshots:
setting the foreign key
after save changes, "department" property still null
When trying this, student.department remains null after calling SaveChanges
var student = db.Students.Find(9);
student.departmentId = 1;
db.SaveChanges();
While if I do this, the foreign key student.departmentId gets populated after calling SaveChanges:
var student = db.Students.Find(9);
student.department = db.Departments.Find(1);
db.SaveChanges();
When trying this student.department remains null after savechanges
Setting the foreign key value doesn't load the related department. The use case for setting the foreign key directly is typically to avoid actually loading the related entity.
If you want to load the related entity, you might as well just query it and assign it to the navigation property.
After setting the foreign key property on an entity, you can load the related entity if you want to using explicit loading. eg
db.Entry(student).Reference(b => b.Department).Load();
SaveChanges will not automatically load the relationship data unless context is already tracking the corresponding entity (Change Tracking in EF Core). In addition to using one of the options to load the related data (for example the one suggested by #David Browne in his answer), following things will do the trick:
db.Departments.Find(1);
var student = db.Students.Find(9);
student.departmentId = 1;
db.SaveChanges(); // student.department will be filled here
Or even
var student = db.Students.Find(9);
student.departmentId = 1;
db.SaveChanges();
db.Departments.Find(1); // student.department will be filled here

Why is this unused line breaking Entity Framework?

I am updating some existing code of a former colleague and have a strange issue where an unused line is causing an error with Entity Framework. If I comment out the code tagged with //This Line!, everything works.
foreach (Place item in ListOfPlaces)
{
//This line!
List<Place> PlacesList = context.Places.Where(x => x.PlaceNumberID == item.PlaceNumberID).ToList();
long PlaceId = context.Places
.Where(x => x.PlaceNumberID == item.PlaceNumberID)
.Select(x => x.PlaceId)
.FirstOrDefault();
if (PlaceId != 0)
{
item.ID = PlaceId;
context.Places.Attach(item);
context.Entry(item).State = System.Data.Entity.EntityState.Modified;
}
}
If I include that line, I get the error shown here on the Attach(item) line:
Attaching an entity of type 'Place' 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 know how to fix this from a code point of view (remove the line!), but I can't work out why its breaking the application if somebody could kindly explain please.
I can't work out why its breaking the application
Looks to me like the line causes the download of some Place with ID N - see the ToList on the end? It will trigger the query to run and download data. EF creates objects from every row it receives because that's the default behavior(it can be disabled with eg AsNoTracking)
Later you try to create another object with the same primary key value and attach it to the context, but the context already knows about some object with ID 123 (for example) because the first line caused it to have been downloaded/tracked so you get an error when you try and associate another - if EF allowed both into its tracking memory it wouldn't know which one was the true authority of record that should be saved back to the db
Your interim query doesn't cause the problem, I believe, because it doesn't trigger the download of an entire entity, seeing as it just retrieves an ID
If you're trying to implement insert-if-not-exists style behavior, you should attempt to download an entity with ID x using some XOrDefault or Find, and if it results in null/default then create and add a new entity (you don't need to attach). In essence, ditch the first line, just do the ID check and if the returned ID is default, do a context.Places.Add(new Place{...}).
If you're looking for upsert, it's probably easiest to download the whole entity and then inspect if it was default or not; if it is, then make a new one otherwise edit the downloaded one.
If you're trying for "update without download" then skip the querying part entirely and attach an entity you declare as modified.
If you're after some hybrid upsets without download, I think you'll struggle, because you have to at least quiz the db as to whether it knows of an entity before you decide what to do.. or you run a raw MERGE
That entire loop makes no sense. You repeat the same twice. And as soon as you select one of the items, EF marks it as a tracked. And you can't update using another item, before the first one will be untracked or you can use the tracked item.
Try this code
foreach (Place item in ListOfPlaces)
{
var placesList = context.Places.Where(x => x.PlaceNumberID == item.PlaceNumberID).ToList();
if(placesList!=null && placesList.Count ==1)
{
var existedPlace = placesList.FirstOrDefault();
context.Entry(existedPlace).CurrentValues.SetValues(item);
}
// and maybe this
else context.Places.Add(item)
}
context.SaveChanges();
UPDATE
Thanks to #CaiusJard for a hint, in this case it is more efficient to use SingleOrDefault instead of ToList
.....
var existedPlace = context.Places.Where(x => x.PlaceNumberID == item.PlaceNumberID).SingleOrDefault();
if(existedPlace!=null)
{
context.Entry(existedPlace).CurrentValues.SetValues(item);
}
.....

Entity Framework Core1.1 - Bulk Insert or Update - InvalidOperationException

I'm running into a problem with inserting OR updating roughly 950 entities.
var coins = JsonConvert.DeserializeObject<List<Currency>>(json);
var sw = new Stopwatch();
sw.Start();
using (var ctx = CryptoContext.Get)
{
var existingCoins = ctx.Coins.ToList();
foreach (var coin in coins)
{
var existing = existingCoins.FirstOrDefault(c => c.CMC_Id == coin.CMC_Id);
if (existing != null)
{
ctx.Entry<Currency>(coin).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
} else
{
ctx.Entry<Currency>(coin).State = Microsoft.EntityFrameworkCore.EntityState.Added;
}
}
ctx.SaveChanges();
var el = sw.ElapsedMilliseconds;
}
The code runs in the background of my netcoreapp1.1, with SQLite, and retrieves a list of currencies. This is done every 5 minutes with FluentScheduler. Because they're not entirely large objects I do all comparisons in memory, and try to add or update each one. My entity has a database-given ID of Id, and the API I'm retrieving from guarantees that CMC_Id is unique.
The initial insertion works fine. I get an error on the second "Update". I believe what's happening is that I'm tracking multiple entities as modified that each have an Id of 0
I was trying to follow this: https://msdn.microsoft.com/en-us/library/jj592676(v=vs.113).aspx
And the error I get is: "The instance of entity type 'Currency' 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."
I am unsure how to proceed with updating each row.
Issue here multiple entities with same key are asked to be tracked.
When you set EntityEntry.State to something then EF Core will start tracking the entity in the specific state. Since in your code, you are querying the database to find out existing entity, EF Core will start tracking the entity with given key therefore it throws above exception while setting the EntityEntry.State because there is already entity with same key being tracked.
More precisely you are trying to AddOrUpdate. There are multiple ways to achieve the behavior. Which one is the best depends on if you are adding one entity without relation or a complex graph.
The simplest method would be to just check existence instead of tracking the entity from database. Options for that would be to use AsNoTracking in your query so that EF does not start tracking it. Even more optimized way would be to just get count from database. If you are querying on PK property then count will be either 0 (non-existent) or 1 (existing entity). If it does not exist then you call Add otherwise Update.
var updatedBlog = new Blog { Id = 1, Title = "Updated" };
var exist = db.Blogs.Count(b => b.Id == updatedBlog.Id) != 0;
if (exist)
{
db.Update(updatedBlog);
}
else
{
db.Add(updatedBlog);
}
db.SaveChanges();
Since Add or Update methods start tracking whole graph, if your graph is in one consistent state, (all entities are new or all are being modified) then it would work just fine.
If your graph is somewhat inconsistent that state of each node in graph can be different (e.g. Updating a blog but it has new posts). Then you should use EntityEntry.State on individual entity. This makes sure that state is applied to only given entity and no other related entity in graph. Though you need to do above kind of check for each node in the graph. Another alternative is to use Attach method to attach whole graph in Unchanged state and then set state for individual node.
If you are having auto-generated Key values then probably you will have PK value set only when it is update else it would be CLR default. For single entity without relations, you can make that check yourself instead of querying database like above code and make decision. For graphs, you can use
db.ChangeTracker.TrackGraph(updatedBlog, n => n.Entry.State = n.Entry.IsKeySet ? EntityState.Modified : EntityState.Added);
This will set state of each node based on PK value being set or not.
Hope this helps :)

How to transition partial update operation from ObjectContext to DbContext

I am using entity framework 5.0. I am in a rocess od changing my app from ObjectContext to DbContext model. DbContext should be according to microsoft the recommended approach. I use database forst approach and I have generated model form database.
But, at a very first simple task there is a problem. Namely simple update of a record is broken.
Let's have a simple table Item, for ilustration only:
Item
(
ItemId int NOT NULL, -- Primary key
Name nvarchar(50) NOT NULL,
Description NVARCHAR(50)
)
I have noticed that using DbContext does not support updating a record not as ObjectContext does.
In my application I have a simple update method.
public void UpdateItem()
{
MyContext context = new MyContext();
Item item = new Item();
item.ItemId = 666;
context.Items.Attach(item);
// From this point onward EF tracks the changes I make to Item
Item.Description = "Some description";
context.SaveChanges();
}
Using ObjectContext this method correctly updates a record. Using SQL profiler I can see that it generates something like this (greatly simplified!!!)
UPDATE Item
SET Description = 'Some description'
WHERE ItemId = 666
If, however I try to do the same thing in DbContext I get the exception:
System.Exception: Items.aspx.cs - logged from CustomError() ---> System.Data.Entity.Validation.DbEntityValidationException: Validation failed for one or more entities. See 'EntityValidationErrors' property for more details.
at System.Data.Entity.Internal.InternalContext.SaveChanges()
at System.Data.Entity.Internal.LazyInternalContext.SaveChanges()
at System.Data.Entity.DbContext.SaveChanges()
And no database UPDATE is issued to Sql server.
I guess that DbContext validates all the properties and the property Name is null. This by design. I do not intend to modify it, I do not even know what is it and I do not need to know what is it.
Only the property Description was changed. Clearly ObjectContext does not track changes correctly.
How can this problem be resolved?
I have researched the issue and found the something on updating records.
For example this link: https://stackoverflow.com/a/15339512/4601078
db.Users.Attach(updatedUser);
var entry = db.Entry(updatedUser);
entry.Property(e => e.Email).IsModified = true;
// other changed properties
db.SaveChanges();
But this is horrible code. For every property on should add a line like:
entry.Property(e => e.Email).IsModified = true;
This produces ugly unreadable code, an I suspect lamda expression are not stelar in performance.
Even worse are those who propose to make a roundtrip to DB to fetch existing records with all properties populated, update it and the save changes. This is a no go with regard to performance.
So, how to tackle with simple entity updates or is this DbContext just another item in microsofts collection of dead ends which serve no real purpose?
DbContext doesn't really track changes by watching properties, it compares the values to previously known values. And validation always works on the entire entity so the only real choice if you want to do things this way is to disable validation during this operation. See Entity Framework validation with partial updates
If you know for sure that the changes you apply are valid, or you have custom code to validate them, you can turn off validation by EF altogether:
db.Configuration.ValidateOnSaveEnabled = false;
This works OK as long as you do it your way: attach a new entity with a known Id (aka a stub entity) and then modify its properties. EF will only update the properties it detects as having been modified (indeed by comparing original and current values, not, as ObjectContext did, by change notifications). You shouldn't mark the entity itself as modified.
If you don't want to turn off EF's validation, but neither want to mark individual properties as modified, I think this could be a useful alternative (brought to my attention by Alex's answer).

Error in Entity Framework Seed Method Only When Not Printing Debugging Statements

In our Entity Framework 6 seed method, we're setting user preferences to default values. We had a lot of issues getting stuff to work correctly, so we started printing debugging statements to a file. Now, however, if we remove the debugging lines, we get an exception.
Here is the code:
// Get the preferences.
Preferences prefs = context.Preferences.FirstOrDefault(x => x.UserId == user.Id);
using (StreamWriter write = new StreamWriter(#"C:\myFile.txt"))
{
//foreach (PropertyInfo prop in prefs.GetType().GetProperties())
// write.WriteLine($"{prop.Name} = {prop.GetValue(prefs)}");
prefs.ColumnIds = defaultColumnIds;
prefs.Columns = defaultColumns;
prefs.CategoriesOnYAxis = true;
prefs.TabHorizontal = true;
prefs.OnlyAssignedToUser = true;
context.SaveChanges();
}
If we uncomment that For loop, then the seed method runs fine. With the for loop commented out, we get the following exception:
An error occurred while saving entities that do not expose foreign key
properties for their relationships. The EntityEntries property will
return null because a single entity cannot be identified as the source
of the exception. Handling of exceptions while saving can be made
easier by exposing foreign key properties in your entity types. See
the InnerException for details.
In this example, the User object is 1-1 to the Preferences object, with Preferences having a foreign key to the User table.
We can even take out the stream writer and loop through the properties to the console, and as long as that loop is there the seed method runs correctly. As soon as it's gone, we get the error.
I have a guess what might be happening here. I think this is your problem:
prefs.ColumnIds = defaultColumnIds;
prefs.Columns = defaultColumns;
I assume these are backed by the same field in your database? So you're setting both the Column entities as well as the ColumnId primary keys? You don't need to set both (though it should work).
I bet if you remove the assignment to prefs.Columns (and leave out the debug code), your code will start to work. The issue is defaultColumns. What's in there? Column entities - but are those entities attached to the current DbContext? (Your code doesn't show how they come into being)
When you fetch your prefs with context.Preferences.FirstOrDefault(x => x.UserId == user.Id);, you're asking EF for a Preference entity, but by default EF won't eager-load navigation properties (like those Column entities). Conversely, if you have a bunch of Column entities but they haven't been explicitly attached to (or fetched using) the current DbContext, EF will think those Columns are new, and that relationship might not be mapped in a way that EF can insert new Columns (which you don't want to happen, anyway).
When your debug code runs, prefs.GetType().GetProperties() is enumerating all the properties in the Preference, which I think EF is then lazy loading. When you remove the loop, it no longer enumerates then, so all the Preference's navigation properties didn't get pre-loaded.
There are a few ways you can go about this:
If you're absolutely certain all those defaultColumnIds are already in the database when you run the Seed method, then set only ColumnIds and call it a day. It will match the entities in the database when it executes your query, and as long as the expected foreign keys are there you're fine.
Fetch the defaultColumns from the context as soon as you open the context, or, if you defined them in code, attach them to the ChangeTracker before you call SaveChanges.

Categories

Resources