Probably this question has been answered before, if that's the case I would appreciate if you guys point me in the right direction.
I would like to know what happens when a new object is added to an EntityFramework collection.
More precisely, I'd like to know if in order to add the new object the whole collection is loaded into memory
For example:
public class MyContext : DbContext
{
public DbSet<Assignment> Assignments { get; set; }
}
public class SomeClass
{
public void AddAssignment(Assignment assignment)
{
var ctx = new MyContext();
ctx.Assignments.Add(assignment);
ctx.SaveChanges();
}
}
Do all the assignment records have to be loaded into memory just to perform a simple insert???
In short: no load process of entire entity collection.
The AddObject() method is used for adding newly created objects that do not exist in the database. When AddObject() is called, a temporary EntityKey is generated and the EntityState is set to 'Added', as shown below:
When context.SaveChanges() is called, EF 4.0 goes ahead and inserts the record into the database. Note that Entity Framework converts the code into queries that the database understand and handles all data interaction and low-level details. Also notice in the code above, that we are accessing data as objects and properties.
After you have executed the code, you can go ahead and physically verify the record in the database.
Related
I saw a book with some code like this:
public class Order
{
public int OrderID { get; set; }
public ICollection<CartLine> Lines { get; set; }
...
}
public class CartLine
{
public int CartLineID { get; set; }
public Product Product { get; set; }
public int Quantity { get; set; }
}
//Product class is just a normal class that has properties such as ProductID, Name etc
and in the order repository, there is a SaveOrder method:
public void SaveOrder(Order order)
{
context.AttachRange(order.Lines.Select(l => l.Product));
if (order.OrderID == 0)
{
context.Orders.Add(order);
}
context.SaveChanges();
}
and the book says:
when store an Order object in the database. When the user’s cart data is deserialized from the session store, the JSON package creates new objects that are not known to
Entity Framework Core, which then tries to write all the objects into the database. For the Product objects, this means that Entity Framework Core tries to write objects that have already been stored, which causes an error. To avoid this problem, I notify Entity Framework Core that the objects exist and shouldn’t be stored in the database unless they are modified
I'm confused, and have two questions:
Q1-why writing objects that have already been stored will cause an error, in the point of view of underlying database, it's just an update SQL statement that modify all columns to their current values?I know it does unnecessary works by changing nothing and rewrite everything, but it shouldn't throw any error in database level?
Q2-why we don't do the same thing to CartLine as:
context.AttachRange(order.Lines.Select(l => l.Product));
context.AttachRange(order.Lines);
to prevent CartLine objects stored in the database just as the way we do it to Product object?
Okay, so this is gonna be a long one:
1st Question:
In Entity Framework (core or "old" 6), there's this concept of "Change tracking". The DbContext class is capable of tracking all the changes you made to your data, and then applying it in the DB via SQL statements (INSERT, UPDATE, DELETE). To understand why it throws an error in your case, you first need to understand how the DbContext / change tracking actually works. Let's take your example:
public void SaveOrder(Order order)
{
context.AttachRange(order.Lines.Select(l => l.Product));
if (order.OrderID == 0)
{
context.Orders.Add(order);
}
context.SaveChanges();
}
In this method, you receive an Order instance which contains Lines and Products. Let's assume that this method was called from some web application, meaning you didn't load the Order entity from the DB. This is what's know as the Disconected Scenario
It's "disconnected" in the sense that your DbContext is not aware of their existence. When you do context.AttachRange you are literally telling EF: I'm in control here, and I'm 100% sure these entities already exist in the DB. Please be aware of them for now on!,
Let's use your code again: Imagine that it's a new Order (so it will enter your if there) and you remove the context.AttachRange part of the code. As soon as the code reaches the Add and SaveChanges these things will happen internally in the DbContext:
The DetectChanges method will be called
It will try to find all the entities Order, Lines and Products in its current graph
If it doesn't find them, they will be added to the "pending changes" as a new records to be inserted
Then you continue and call SaveChanges and it will fail as the book tells you. Why? Imagine that the Products selected were:
Id: 1, "Macbook Pro"
Id: 2, "Office Chair"
When the DbContext looked at the entities and didn't know about them, it added them to the pending changes with a state of Added. When you call SaveChanges, it issues the INSERT statements for these products based on their current state in the model. Since Id's 1 and 2 already exists in the database, the operation failed, with a Primary Key violation.
That's why you have to call Attach (or AttachRange) in this case. This effectively tells EF that the entities exist in the DB, and it should not try to insert them again. They will be added to the context with a state of Unchanged. Attach is often used in these cases where you didn't load the entities from the dbContext before.
2nd question:
This is hard for me to access because I don't know the context/model at that level, but here's my guess:
You don't need to do that with the Cartline because with every order, you probably want to insert new Order line. Think like buying stuff at Amazon. You put the products in the cart and it will generate an Order, then Order Lines, things that compose that order.
If you were then to update an existing order and add more items to it, then you would run into the same issue. You would have to load the existing CartLines prior to saving them in the db, or call Attach as you did here.
Hope it's a little bit clearer. I have answered a similar question where I gave more details, so maybe reading that also helps more:
How does EF Core Modified Entity State behave?
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);
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.
I ran into what I think is a really odd situation with entity framework. Basically, if I update an row directly with a sql command, when I retrive that row through linq it doesn't have the updated information. Please see the below example for more information.
First I created a simple DB table
CREATE TABLE dbo.Foo (
Id int NOT NULL PRIMARY KEY IDENTITY(1,1),
Name varchar(50) NULL
)
Then I created a console application to add an object to the DB, update it with a sql command and then retrieve the object that was just created. Here it is:
public class FooContext : DbContext
{
public FooContext() : base("FooConnectionString")
{
}
public IDbSet<Foo> Foo { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Foo>().ToTable("Foo");
base.OnModelCreating(modelBuilder);
}
}
public class Foo
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
}
public class Program
{
static void Main(string[] args)
{
//setup the context
var context = new FooContext();
//add the row
var foo = new Foo()
{
Name = "Before"
};
context.Foo.Add(foo);
context.SaveChanges();
//update the name
context.Database.ExecuteSqlCommand("UPDATE Foo Set Name = 'After' WHERE Id = " + foo.Id);
//get the new foo
var newFoo = context.Foo.FirstOrDefault(x => x.Id == foo.Id);
//I would expect the name to be 'After' but it is 'Before'
Console.WriteLine(string.Format("The new name is: {0}", newFoo.Name));
Console.ReadLine();
}
}
The write line at the bottom prints out "Before" however I would expect that it prints out "After". The odd thing about it is that if I run profiler I see the sql query run and if I run the query in management studio myself, it returns "After" as the name. I am running sql server 2014.
Can someone please help me understand what is going on here?
UPDATE:
It is going to the database on the FirstOrDefault line. Please see the attached screen shot from sql profiler.
So my question really is this:
1) If it is caching, shouldn't it not be going to the DB? Is this a bug in EF?
2) If it is going to the db and spending the resources, shouldn't EF update the object.
FooContext includes change tracking and caching, so the in-memory object that is returned from your query is the same instance that you added earlier. Calling SaveChanges() does clear the context and FooContext is not aware of the changes that happened underneath it in the database.
This is usually a good thing -- not making expensive database calls for every operation.
In your sample, try making the same query from a new FooContext, and you should see "After".
update
Responding to your updated question, yes, you are right. I missed before that you were using FirstOrDefault(). If you were using context.Find(foo.Id), as I wrongly assumed, then there would be no query.
As for why the in-memory object is not updated to reflect the change in the database, I'd need to do some research to do anything more than speculate. That said, here is my speculation:
An instance of the database context cannot return more than one instance of the same entity. Within a unit of work, we must be able to rely on the context to return the same instance of the entity. Otherwise, we might query by different criteria and get 3 objects representing the same conceptual entity. At that point, how can the context deal with changes to any of them? What if the name is changed to a different value on two of them and then SaveChanges() is called -- what should happen?
Given then that the context tracks at most a single instance of each entity, why can't EF just update that entity at the point at which a query is executed? EF could even discard that change if there is a pending in-memory change, since it knows about those changes.
I think one part of the answer is that diffing all the columns on large entities and in large result sets is performance prohibitive.
I think a bigger part of the answer is that it executing a simple SELECT statement should not have the potential to cause side effects throughout the system. Entities may be grouped or looped over by the value of some property and to change the value of that property at an indeterminate time and as a result of a SELECT query is highly unsound.
I created a project with a model first approach, in my model I declared a many to many relationship between the entities, Musician and Session (representing some kind of rehearsal or performance). As expected, this generated a pivot table in my database to handle the relationship, but no C# model class for the intermediate table. So, on my Session object I have a collection of Musicians declared as an ICollection:
public virtual ICollection<Musician> Musicians { get; set; }
Which is initialised as a HashSet in the Session constructor:
public Session()
{
this.Musicians = new HashSet<Musician>();
}
Now, somewhere else in my solution (in an MVC controller, if it matters), I want to assign an existing Musician to a given Session, so I have this code:
Musician musician = db.Musicians.First(m => m.Id == selectedMusician);
session.Musicians.Add(musician);
db.SaveChanges();
Just after session.Musicians.Add(musician) is called, I've inspected that the musician has been added to the collection, but calling db.SaveChanges doesn't seem to persist the change to the database.
By the way, db is my repository containing all my models, for example, this code successfully adds a new Session to the database:
db.Sessions.Add(session);
db.SaveChanges();
If somebody could point me in the right direction on how to add an object to a collection in this circumstance that would be a great help.
Cheers
EDIT: Turns out the problem had nothing to do with the fact that it was a collection. Like I mentioned in my post, I was trying to make the changes/saving from a controller class in an ASP MVC project. The session object was being passed in from the view, but I discovered any changes I made to this object wouldn't persist in the database (doh, silly me, should of checked this straight away). Looks like the object passed in by the view was just a copy as it wasn't coming directly from the context (in this case, my db object).
To fix it, I just queried the context for a Session object with the same Id in order to have access to the object that my changes would actually persist.