Run Multiple SaveChanges in transaction in Entity Framework - entity-framework

The following code is separated by TWO .SaveChanges() calls because I have to run some code in fixed order.
First I have to delete existing stuff then add the newly created data.
How can I now assure that both .SaveChanges run in one transaction? Should I wrap a TransactionScope around the SaveChanges?
Are my both SaveChanges() correct at all?
var periodsToDelete = _context.Periods.Where(p => p.SchoolyearId == response.Schoolyear.Id &&
p.LessonDate >= response.DeletionStartDate &&
p.LessonDate <= response.DeletionEndDate).AsNoTracking();
var schoolclassCodesToDelete = _context.SchoolclassCodes.Where(s => s.SchoolyearId == schoolyear.Id).AsNoTracking();
var timetablesToDelete = _context.TimeTables.Where(t => t.SchoolyearId == schoolyear.Id).AsNoTracking();
_context.Periods.RemoveRange(periodsToDelete);
_context.SchoolclassCodes.RemoveRange(schoolclassCodesToDelete);
_context.TimeTables.RemoveRange(timetablesToDelete);
_context.SaveChanges();
_context.Entry(schoolyear).State = EntityState.Modified;
_context.SchoolclassCodes.AddRange(existingAndNewSchoolclassCodes);
_context.TimeTables.AddRange(timetablesA.Concat(timetablesAB));
_context.Periods.AddRange(periods);
_context.SaveChanges();

Related

Finding entities of a certain base class and deleting them based on a query

I want to find any entity that inherits from DbEntity and has the field DeletedDate set to a value older than 100 days.
I have managed to piece together this code. But I am unsure of how I can go from this to querying the database for relevant entities.
var softDeletableEntities = _applicationContext.Model.GetEntityTypes()
.Where(e =>
typeof(DbEntity).IsAssignableFrom(e.ClrType)
&& e.BaseType == null
);
foreach (var entity in softDeletableEntities)
{
var entry = _applicationContext.Entry(entity.ClrType);
entry.Property<DateTimeOffset?>("DeletedDate");
// how do I find any Entity where (DeletedDate != null) and delete it?
}
I was hoping to be able to do something like this, but Set<> can't be used like that:
// Not ok to use Set<>() as below. Gives: 'entity' is a variable but is used like a type
var dataSet = _applicationContext.Set<entity.ClrType>();
var deleteOlderThan = DateTime.Now.AddDays(-100);
var toDelete = dataSet.Where(x => x.DeletedDate < deleteOlderThan);
dataSet.RemoveRange(toDelete);
_applicationContext.SaveChanges();
Any tips :)?
I would suggest to use EF Core extension linq2db.EntityFrameworkCore, note that I'm one of the creators. Install appropriate version 5.x for EF Core 5, 6.x for EF Core 6, etc.
Then you can execute the following query:
var dataSet = _applicationContext.Set<entity.ClrType>();
var deleteOlderThan = DateTime.Now.AddDays(-100);
var toDelete = dataSet.Where(x => x.DeletedDate < deleteOlderThan);
toDelete.Delete(); // method from Extension

Entity Core and SaveChanges works only once

I have problem with Entity Core 3.1.2.
I have code like this:
SQL.Database.EnsureCreated();
var ThisCollector = SQL.CollectorServers
.Where(esa => esa.ServerName == ServerCollectorName)
.FirstOrDefault();
while (foo)
{
await SQL.Entry(ThisCollector)
.ReloadAsync();
DateTime dtTimeOut = DateTime.UtcNow.AddMinutes(-1);
//check status of current Worker
var CurrentLB = SQL.CollectorServers
.Where(esa => esa.isWorker == true
&& esa.LastSeenLB < dtTimeOut)
.FirstOrDefault();
if (CurrentLB!=null) //Current Worker is dead!
{
CurrentLB.isWorker = false;
ThisCollector.isWorker = true;
SQL.SaveChanges(); //This works allways
}
var Collectors = SQL.CollectorServers
.Where(Esa => Esa.isWorker == true);
if (Collectors.Count() == 0)
{
ThisCollector.isWorker = true;
SQL.SaveChanges();
}
if (Collectors.Count() >= 2)
{
foreach(CollectorServer cs in Collectors)
{
cs.isWorker = false;
//why this is requied?
SQL.Entry(cs).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
}
ThisCollector.isWorker = true;
//<--This works only once, without manually setting State to
// modified!!! Why? Values has been changed from external program.
//(Management studio in this case)
SQL.SaveChanges();
}
await Task.Delay(10000, stoppingToken);
}
Problem is that last SaveChanges works only first time it has been called without I set Entry state to Modified. After that it does not make SQL query (I can see that in SQL Profiler).
In this case this can be fixed by this way, but I'm trying to undestand why this happends. My software saves lot data to SQL, and I need to know can I trust to this code without opening all queries and adding this modified state.
I didin't have this kind of problems in full version (6) of Entity, this is something quite new for me.
The described behavior would make sense if the entities returned by CollectorServers are not tracked.
ThisCollector is attached on every loop by the call to SQL.Entry(ThisCollector) :
await SQL.Entry(ThisCollector) //Attaching
.ReloadAsync(); //Reloading
Any changes made to it would be tracked and saved by the first call to DbContext.SaveChanges().
On the other hand, the entities returned by the Collectors query :
var Collectors = SQL.CollectorServers
.Where(Esa => Esa.isWorker == true);
Would remain untracked, until they get reattached by the call to SQL.Entry(cs) :
foreach(CollectorServer cs in Collectors)
{
cs.isWorker = false;
SQL.Entry(cs).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
}
That's equivalent to calling DbContext.Update to attach and set the state to Modified. Update is easier to read though :
foreach(CollectorServer cs in Collectors)
{
cs.isWorker = false;
SQL.Update(cs);
}

Entity Framework is too slow during mapping data up to 100k

I have min 100 000 data into a Job_Details table and I'm using Entity Framework to map the data.
This is the code:
public GetJobsResponse GetImportJobs()
{
GetJobsResponse getJobResponse = new GetJobsResponse();
List<JobBO> lstJobs = new List<JobBO>();
using (NSEXIM_V2Entities dbContext = new NSEXIM_V2Entities())
{
var lstJob = dbContext.Job_Details.ToList();
foreach (var dbJob in lstJob.Where(ie => ie.IMP_EXP == "I" && ie.Job_No != null))
{
JobBO job = MapBEJobforSearchObj(dbJob);
lstJobs.Add(job);
}
}
getJobResponse.Jobs = lstJobs;
return getJobResponse;
}
I found to this line is taking about 2-3 min to execute
var lstJob = dbContext.Job_Details.ToList();
How can i solve this issue?
To outline the performance issues with your example: (see inline comments)
public GetJobsResponse GetImportJobs()
{
GetJobsResponse getJobResponse = new GetJobsResponse();
List<JobBO> lstJobs = new List<JobBO>();
using (NSEXIM_V2Entities dbContext = new NSEXIM_V2Entities())
{
// Loads *ALL* entities into memory. This effectively takes all fields for all rows across from the database to your app server. (Even though you don't want it all)
var lstJob = dbContext.Job_Details.ToList();
// Filters from the data in memory.
foreach (var dbJob in lstJob.Where(ie => ie.IMP_EXP == "I" && ie.Job_No != null))
{
// Maps the entity to a DTO and adds it to the return collection.
JobBO job = MapBEJobforSearchObj(dbJob);
lstJobs.Add(job);
}
}
// Returns the DTOs.
getJobResponse.Jobs = lstJobs;
return getJobResponse;
}
First: pass your WHERE clause to EF to pass to the DB server rather than loading all entities into memory..
public GetJobsResponse GetImportJobs()
{
GetJobsResponse getJobResponse = new GetJobsResponse();
using (NSEXIM_V2Entities dbContext = new NSEXIM_V2Entities())
{
// Will pass the where expression to be DB server to be executed. Note: No .ToList() yet to leave this as IQueryable.
var jobs = dbContext.Job_Details..Where(ie => ie.IMP_EXP == "I" && ie.Job_No != null));
Next, use SELECT to load your DTOs. Typically these won't contain as much data as the main entity, and so long as you're working with IQueryable you can load related data as needed. Again this will be sent to the DB Server so you cannot use functions like "MapBEJobForSearchObj" here because the DB server does not know this function. You can SELECT a simple DTO object, or an anonymous type to pass to a dynamic mapper.
var dtos = jobs.Select(ie => new JobBO
{
JobId = ie.JobId,
// ... populate remaining DTO fields here.
}).ToList();
getJobResponse.Jobs = dtos;
return getJobResponse;
}
Moving the .ToList() to the end will materialize the data into your JobBO DTOs/ViewModels, pulling just enough data from the server to populate the desired rows and with the desired fields.
In cases where you may have a large amount of data, you should also consider supporting server-side pagination where you pass a page # and page size, then utilize a .Skip() + .Take() to load a single page of entries at a time.

Which is the good way to update object in EF6

I have searched and find 2 way to update object in EF
var attachedEntity = _context.EntityClasses.Local.First(t => t.Id == entity.Id);
//We have it in the context, need to update.
if (attachedEntity != null)
{
var attachedEntry = _context.Entry(attachedEntity);
attachedEntry.CurrentValues.SetValues(entity);
}
else
{
////If it's not found locally, we can attach it by setting state to modified.
////This would result in a SQL update statement for all fields
////when SaveChanges is called.
var entry = _context.Entry(entity);
entry.State = EntityState.Modified;
}
_context.SaveChanges();
And other way is seem more easy
var entity = _context.EntityClasses.FirstOrDefault(t => t.Id == entity.Id);
_context.Entry(entity ).EntityState.Modified
_context.SaveChanges();
What is best way to update object?
NOTE: the performence is importance with me
_context.EntityClasses.Local.First(t => t.Id == entity.Id)
=> means that you want to double check the entity on local (the latest loading from DB) and it is not send to DB to find the record so the performance is faster.
_context.EntityClasses.FirstOrDefault(t => t.Id == entity.Id): This command is look up the entity in DB. That means EF creates the query and look up in DB.
The below link is the difference of between Entity.Local.Find & Entity.Find http://msdn.microsoft.com/en-us/data/jj592872.aspx
Hope it helps!

Entity Framework dbset not finding added entity

I am having an issue understanding why when adding a new entity to a DbSet of ObjectContext, that entity is not found will looking it up again.
using (var db = new SmartrailDB())
{
var cart01 = db.Carts.SingleOrDefault(x => x.Number == 0);
if (cart01 == null)
{
cart01 = new Cart { Number = 0 };
db.Carts.Add(cart01);
}
var cart02 = db.Carts.SingleOrDefault(x => x.Number == 0); // Should find the cart I just added - right?
Assert.IsNotNull(cart02); // Fails because cart02 does not exist in the db.Carts collection
}
Is anyone able to tell me what I am doing wrong here?
Also late on a Friday here so brain half asleep now.
You have to update your context before you try to access the entity. Just do:
db.SaveChanges(); right after db.Cart.Add(cart01);