Given the following codes:
A.
using (var context = new testEntities())
{
context.tblAuthors.Select(p => new
{
Author_name = p.Author_name,
Id = p.Id,
country = p.country,
tblBooks = p.tblBooks.Select(z => new
{
Id = z.Id,
Auhthor_id = z.Auhthor_id,
Edition = z.Edition,
Price = z.Price
}).ToList()
}).ToList();
}
B.
using (var context = new testEntities())
{
context.tblAuthors.Include(p => p.tblBooks).AsNoTracking().ToList();
}
I ran both on a separate loop of a 100 iterations. The first code (A) gives me an average of 157ms execution time. The second code (B) gives me an average of 673ms execution time.
My questions are:
1 - Why does Entity Framework have this difference? I thought AsNoTracking() was supposed to perform the same as a Projection since the Context is not tracking the entities in both cases.
2 - If I want to get maximum performance should I be using projections for all read-only operations instead of AsNoTracking()?
Update 1: The amount of data that is being retrieved is the same. I am using all the available fields on the table. The query that is being generated by Entity Framework is the same for both codes.
Related
I have an ASP.NET MVC app using Entity Framework from our SQL Server backend.
Goal is to create ~18 WPackage entries via a foreach loop:
foreach (var dbitem in dbCList)
The code works for a single WPackage entry, but we have a request from the customer to create 300+ WPackages, so trying to use the Entity Framework code for a single "Add" and loop to create 300+ adds.
The T-SQL would be very challenging as there are many keys created on the fly/at row creation, so for activities >> resources, we'd have to insert the activity, grab or remember the activity key, then add resources with that newly created activity key.
Each WPackage (this is the main parent table) could have one or more of the following child table entries:
1+ activities
each activity would have 1+ resource
1+ budgets
1+ Signatures
1+ CostCodes
Our schema or model diagram would be:
WPackage
--Activities
-----Resources (child of Activities)
--CostCodes
--Budgets
--Signatures
The following code fails on:
dbContextTransaction.Commit();
with an error:
The transaction operation cannot be performed because there are pending requests working on this transaction.
[HttpPost]
public ActionResult Copy([Bind(Include = "ID,WBSID,...***fields excluded for brevity")] Package model)
{
if (ModelState.IsValid)
{
try
{
using (var dbContextTransaction = db.Database.BeginTransaction())
{
var dbCList = db.Packages.Join(db.WBS,
*expression omitted for brevity*)
// this dbClist will build about 18 items in the collection for below loop
foreach (var dbitem in dbCList)
{
int testWPID = dbitem;
WPackage prvWP = db.WPackages.Find(dbitem);
int previousWPID = dbitem;
WPackage previousWP = db.WPackages.Find(dbitem);
model.ID = dbitem;
db.WPackages.Add(model);
db.SaveChanges();
var budgets = db.Budgets.Where(i => i.WPID == previousWPID);
foreach (Budget budget in budgets)
{
budget.WPID = model.ID;
db.Budgets.Add(budget);
}
var costCodes = db.CostCodes.Where(i => i.WPID == previousWPID);
foreach (CostCode costCode in costCodes)
{
costCode.WPID = model.ID;
db.CostCodes.Add(costCode);
}
var activities = db.Activities.Where(i => i.WPID == previousWPID);
// *code excluded for brevity*
var previousActivityID = activity.ID;
db.Activities.Add(activity);
db.SaveChanges();
var resources = db.Resources.Where(i => i.ActivityID == previousActivityID);
foreach (Resource resource in resources)
{
resource.WPID = model.ID;
resource.ActivityID = activity.ID;
resource.ActivityNumber = activity.ActivityNumber;
db.Resources.Add(resource);
db.SaveChanges();
}
}
var signatures = db.RolesAndSigs
.Where(i => i.KeyId == previousWPID && i.Type == "WPL")
.OrderBy(i => i.Role)
.OrderBy(i => i.Person);
foreach (RolesAndSig signature in signatures)
{
db.RolesAndSigss.Add(signature);
}
db.SaveChanges();
dbContextTransaction.Commit();
}
}
}
}
I've also tried to have the Commit() run outside the foreach dbitem loop like:
db.SaveChanges();
//dbContextTransaction.Commit();
}
dbContextTransaction.Commit();
...but this returns error of:
[EXCEPTION] The property 'ID' is part of the object's key information and cannot be modified.
The code you posted has some issues that don't make sense, and probably aren't doing what you think they are doing. The crux of the issue you are facing is that Entity Framework tracks all references to entities it loads and associates:
Firstly this code:
int testWPID = dbitem;
WPackage prvWP = db.WPackages.Find(dbitem);
int previousWPID = dbitem;
WPackage previousWP = db.WPackages.Find(dbitem);
prvWP and previousWP will be pointing to the exact same reference, not two copies of the same entity. Be careful when updating either or any other reference retrieved or associated with that same ID. They all point to the same instance. If you do want a stand-alone snaphot reference you can use AsNoTracking().
Next, when you do something like this in a loop:
model.ID = dbitem;
db.WPackages.Add(model);
In the first iteration, "model" is not an entity. It is a deserialized block of data with the Type of the Package entity. As soon as you call .Add(model) that reference will now be pointing to a newly tracked entity reference. In the next loop you are telling EF to change that tracked entity reference's ID to a new value, and that is illegal.
What it looks like you want to do is create a copy of this model for each of the 18 expected iterations. For that what you want to do would be something more like:
foreach (var dbitem in dbCList)
{
var newModel = new WPackage
{
ID = dbItem,
WBSID = model.WBSID,
/// copy across all relevant fields from the passed in model.
};
db.WPackages.Add(newModel);
// ...
}
It would be quite worthwhile to leverage navigation properties for the related entities rather than using explicit joins and trying to scope everything in an explicit transaction with multiple SaveChanges() calls. EF can manage all of the FKs automatically rather than essentially using it as a wrapper for individual ADO CRUD operations.
You will need to be explicit between when you want to "clone" an object reference vs. "copy" a reference. For example, if I have a Customer that has an Address, and Addresses have a Country reference, when I clone a Customer, I will want to clone a new Address record for that Customer, however ensure that the Country reference is copied across. If I have a record for Jack at an 123 Apple Street, London in England, and go to clone Jack to make a record for Jill at the same address, they might be at the same location now, but not always, so I want them to point at different Address records in case Jill moves out. Still, there should only be one record for "England". (Jill may move to a different country, but her address record would just point at a different Country Id)
Wrong:
var jill = context.Customers.Single(c => c.Name == "Jack");
jill.Name = "Jill";
context.Customers.Add(jill);
This would attempt to rename Jack into Jill, then "Add" the already tracked instance, resulting in an exception.
Will work, but still Wrong:
var jack = context.Customers.AsNoTracking().Single(c => c.Name == "Jack");
var jill = jack;
jill.Name = "Jill";
context.Customers.Add(jill);
This would technically work by loading Jack as an untracked entity, and would save Jill as a new record with a new Id. However this is potentially very confusing. Depending on how the AddressId/Address is referenced we could end up with Jack and Jill referencing the same single Address record. Bad if you want Jack and Jill to have different addresses.
Right:
var jack = context.Customers
.Include(c => c.Address)
.ThenInclude(a => a.Country)
.Single(c => c.Name == "Jack");
var jill = new Customer
{
Name = "Jill",
// copy other fields...
Address = new Address
{
StreetNumber = jack.Address.StreetNumber,
StreetName = jack.Address.StreetName,
Country = jack.Address.Country
}
};
context.Customers.Add(jill);
The first detail is to ensure when we load Jack that we eager load all of the related details we will want to clone or copy references to. We then create a new instance for Jill, copying the values from Jack, including setting up a new Address record. The Country reference is copied across as there should only be ever a single record for "England".
Edit: For something like a roll-over scenario if you have a package by year, let's use the example of a Package class below:
public class Package
{
[Key]
public int PackageId { get; set; }
[ForeignKey("PackageType")]
public int PackageTypeId { get; set; }
public int Year { get; set; }
// .. More package related details and relationships...
public virtual PackageType PackageType { get; set; }
}
A goal might be to make a new Package and related data for Year 2022 from the data from 2021, and apply any changes from a view model passed in.
Find is a poor choice for this because Find wants to locate data by PK. If you're method simply passes an entity to be copied from (I.e. the data from 2021) then this can work, however if you have modified that data from 2021 to represent values you want for 2022 that could be dangerous or misleading within the code. (We don't want to update 2021's data, we want to create a new record set for 2022) To make a new Package for 2022 we just need the updated data to make up that new item, and a way to identify a source for what to use as a template. That identification could be the PK of the row to copy from (ProductId), or derived from the data passed in. (ProductTypeId, and Year-1) In both cases if we want to consider related data with the "copy from" product then it would be prudent to eager load that related data in one query rather than going back to the database repeatedly. Find cannot accommodate that.
For instance if I want to pass data to make a new product I pass a ProductTypeId, and a Year along with any values to use for the new structure. I can attempt to get a copy of the existing year to use as a template via:
var existingProduct = context.Products
.Include(x => x.Activities) // Eager load related data.
.Include(x => x.CostCodes)
// ...
.Single(x => x.ProductTypeId == productTypeId && x.Year = year - 1);
or if I passed a ProductId: (such as if I could choose to copy the data from a selected year like 2020 instead)
var existingProduct = context.Products
.Include(x => x.Activities)
.Include(x => x.CostCodes)
// ...
.Single(x => x.ProductId == copyFromProductId);
Both of these examples expect to find one, and only one existing product. If the request comes in with values that it cannot find a row for, there would be an exception which should be handled. This would fetch all of the existing product information that we can copy from, alongside any data that was passed into the method to create a new Product.
Some background
I'm wanting to bind a list of objects (my model-view) to a grid. The model-view contains fields for both an specific entity and fields from a joined entity.
I was getting an error when I would try to bind due to the dbContext being out of scope. I realized I needed to use the .Include() method in order to eager load my navigation property. However, I suspect that since I'm using Linq to Entities, that I'm now generating another error:
"Unable to cast the type 'System.Linq.IQueryable1' to type 'System.Data.Objects.ObjectQuery1'. LINQ to Entities only supports casting EDM primitive or enumeration types."
My code is shown below, any ideas of what I need to do here?
Thanks in advance!
public static List<PlanViewModel> GetPlans()
{
using (var context = new RepEntities())
{
var query = (from p in context.Plans
join r in context.RealEstateDetails on p.ReId equals r.ReId
select new PlanViewModel
{
PlanName = p.PlanName,
TargetCompletionDate = p.TargetCompletionDate,
ActualCompletionDate = p.ActualCompletionDate,
Provision = p.Provision,
StatusTypeId = p.StatusTypeId,
StatusCommon = p.StatusCommon,
Building = r.BuildingName,
City = r.City,
Country = r.Country
}).Include("StatusCommon");
return query.ToList();
}
}
You are almost there, just put Include("StatusCommon") right after context.Plans. Because you need to include StatusCommon before the iteration, this way you can set StatusCommon value for every iteration.
public static List<PlanViewModel> GetPlans()
{
using (var context = new RepEntities())
{
var query = (from p in context.Plans.Include("StatusCommon")
join r in context.RealEstateDetails on p.ReId equals r.ReId
select new PlanViewModel
{
PlanName = p.PlanName,
TargetCompletionDate = p.TargetCompletionDate,
ActualCompletionDate = p.ActualCompletionDate,
Provision = p.Provision,
StatusTypeId = p.StatusTypeId,
StatusCommon = p.StatusCommon,
Building = r.BuildingName,
City = r.City,
Country = r.Country
}).toList();
return query;
}
}
I'm trying to search for a collection of potential nodes but unable to do it...
I have a product that has a relationship with many instances. I would like to query the DB and get all the instances that are in a list that i get from the user.
Cypher:
var query = _context
.Cypher
.Start(new
{
instance = startBitsList,
product = productNode.Reference,
})
.Match("(product)-[:HasInstanceRel]->(instance)")
.Return(instance => instance.Node<ProductInstance>());
The problem is startBitsList... I use StringBuilder to generate a query that contains all the instances I'm looking for:
private static string CreateStartBits(IEnumerable<string> instanceNames)
{
var sb = new StringBuilder();
sb.AppendFormat("node:'entity_Name_Index'(");
foreach (var id in productIds)
{
sb.AppendFormat("Name={0} OR ", id);
}
sb.Remove(sb.Length - 4, 4);
sb.Append(")");
var startBitsList = sb.ToString();
return startBitsList;
}
I get exceptions when trying to run this cypher...
Is there a better way to search for multiple items that are stored in the collection I get from the user?
OK, I think there are a couple of issues at play here, first I'm presuming you are using Neo4j 1.9 and not 2.0 - hence using the .Start.
Have you tried taking your query and running it in Neo4j? This should be your first port of call, typically it's easy to add a breakpoint on the .Results call and add a 'watch' for query.Query.DebugText.
However, I don't think you need to use the StartBits the way you are, I think you'd be better off filtering with a .Where as you already have the start point:
private static ICypherFluentQuery CreateWhereClause(ICypherFluentQuery query, ICollection<string> instanceNames)
{
query = query.Where((Instance instance) => instance.Name == instanceNames.First());
query = instanceNames.Skip(1).Aggregate(query, (current, localInstanceName) => current.OrWhere((Instance instance) => instance.Name == localInstanceName));
return query;
}
and your query becomes something like:
var prodReference = new NodeReference<Product>(2);
var query =
Client.Cypher
.ParserVersion(1, 9)
.Start(new {product = prodReference})
.Match("(product)-[:HasInstanceRel]->(instance)");
query = CreateWhereClause(query, new[] {"Inst2", "Inst1"});
var resultsQuery = query.Return(instance => instance.As<Node<Instance>>());
2 things of note
We're not using the indexes - there is no benefit to using them as you have the start point and traversing to the 'instances' is a simple process for Neo4j.
The 'CreateWhereClause' method will probably go wrong if you pass in an empty list :)
The nice thing about not using the indexes is that - because they are legacy - you are set up better for Neo4j 2.0
Few days back I put a question regarding mapping two classes Message and MessageStatusHistory using EF. The mapping is going fine but I am facing some problems with the navigation property StatusHistory in class Message that relates it to MessageStatusHistory objects. I am loading the messages for one user only and want to the statuses pertaining to that user only. Like I would want to show if the user has marked message as read/not-read and when. If I use default loading mechanism like following it loads all the history related to the message irrespective of the user:
IDbSet<Message> dbs = _repo.DbSet;
dbs.Include("StatusHistory").Where(x=>x.MessageIdentifier == msgIdentifier);
To filter history for one user only I tried following trick:
IDbSet<Message> dbs = _repo.DbSet;
var q = from m in dbs.Include("StatusHistory")
where m.MessageIdentifier == msgIdentifier
select new Message
{
MessageIdentifier = m.MessageIdentifier,
/*OTHER PROPERTIES*/
StatusHistory = m.StatusHistory
.Where(x => x.UserId == userId).ToList()
};
return q.ToList();//THROWING ERROR ON THIS LINE
I am getting the error:
The entity or complex type 'MyLib.Biz.Message' cannot be constructed in a LINQ
to Entities query.
I have tried by commenting StatusHistory = m.StatusHistory.Where(x => x.UserId == userId).ToList() also but it has not helped.
Please help me in getting Messages with filtered StatusHistory.
EDIT:- above is resolved with this code:
var q = from m in _repository.DBSet.Include("Histories")
where m.MessageIdentifier == id
select new {
m.Id,/*OTHER PROPERTIES*/
Histories = m.Histories.Where(x =>
x.SenderId == userId).ToList()
};
var lst = q.ToList();
return lst.Select(m => new Message{
Id = m.Id, MessageIdentifier = m.MessageIdentifier,
MessageText = m.MessageText, Replies = m.Replies,
ReplyTo = m.ReplyTo, Histories = m.Histories, SenderId =
m.SenderId, SenderName = m.SenderName, CreatedOn = m.CreatedOn
}).ToList();
But if I try to include replies to the message with:
from m in _repository.DBSet.Include("Replies").Include("Histories")
I am getting error on converting query to List with q.ToList() for Histories = m.Histories.Where(x=> x.SenderId == userId).ToList().
About your EDIT part: You cannot use ToList() in a projection, just leave it an IEnumerable<T> and convert to a List<T> when you construct the Message. You also don't need to create two list objects, you can switch from the LINQ to Entities query to LINQ to Objects (the second Select) by using AsEnumerable():
var list = (from m in _repository.DBSet
where m.MessageIdentifier == id
select new {
// ...
Histories = m.Histories.Where(x => x.SenderId == userId)
})
.AsEnumerable() // database query is executed here
.Select(m => new Message {
// ...
Histories = m.Histories.ToList(),
// ...
}).ToList();
return list;
Be aware that Include has no effect when you use a projection with select. You need to make the properties that you want to include part of the projection - as you already did with select new { Histories.....
I use the following code to remove all KlientDoTrasa assigned to trasaOriginal and then create and assign new KlientDoTrasa based on collection: trasaToEdit.Klienci. In my test case assigned KlientDoTrasa and new KlientDoTrasa are the same, so I thought EF4 should not send any query to database, but it sends, first deletes then inserts. Is there a way to limit it?
public void Edit(int trasaId, TrasaEditViewModel trasaToEdit)
{
Trasa trasaOriginal = _trasaRepository.FindById(trasaId);
trasaOriginal.Nazwa = trasaToEdit.Trasa.Nazwa;
foreach(KlientDoTrasa kdt in trasaOriginal.KlientDoTrasa.ToList())
{
_klientDoTrasaRepository.Remove(kdt);
}
for(int i = 0; i < trasaToEdit.Klienci.Count; i++)
{
var kdt = new KlientDoTrasa {Trasa = trasaOriginal, KlientId = trasaToEdit.Klienci[i].Id, Seq = i};
_klientDoTrasaRepository.Add(kdt);
}
_klientDoTrasaRepository.SaveChanges();
_trasaRepository.SaveChanges();
}
Entity Framework does not do a deep comparison of your entities to determine whether they are equivalent. Your code above will delete then insert, assuming that the two entities are different.
If you want to avoid updating an Entity that hasn't changed, you'll need to check for that in your own logic. I believe you could use a partial class declaration to implement IEqualityComparer for your entities, then compare before submit.