Db context - multiple usages - c#

so I stumbled upon a weird thing just now.
I have a test which changes the until date of a workingsite to yesterday like so:
using (var db = new ApplicationDbContext())
{
var empFromDb = db.Employees.Include(x => x.WorkingSites).First(x => x.Id == _employeeId);
empFromDb.WorkingSites.Add(workingSiteToEnd);
db.SaveChanges();
//Act
ServiceFactory.CreateEmployeeService().EndWorkingSitePeriod(workingSiteToEnd);
//Assert
var workingSiteFromDb = db.WorkingSites.First(x => x.Id == workingSiteToEnd.Id);
Assert.AreEqual(DateTime.Now.AddDays(-1).Date, workingSiteFromDb.WorksUntil.Date);
}
In this test my assert fails and the until hasn't changed for the object workingSiteFromDb, yet in my code I do change it and save changes to the database.
note: my database did update! I checked inside the database and the date is altered correctly.
Now I didn't know what was going on and so I stopped the using right after the first savechanges and opened it again right before I call workingSiteFromDb.
If I do this, it works.
Note that I use another using within the EndWorkingSitePeriod method.
How come my database does update but the object only updates when I use a second using?
This is the EndWorkingSitePeriod method:
public void EndWorkingSitePeriod(int workingSiteId)
{
using (var db = new ApplicationDbContext())
{
var workingSiteFromDb = db.WorkingSites.Include(x => x.Employee).First(x => x.Id == workingSiteId);
workingSiteFromDb.EndPeriod();
db.SaveChanges();
}
}
The workingSite.EndPeriod just sets the UntilDateto DateTime.Now.AddDays(-1)

First, you're obtaining workingSiteToEnd somewhere and adding it to context, created in 1st line of your sample. Then, you're saving the changes (workingSiteToEnd now in the database and in the context).
Then, you're creating the second context in EndWorkingSitePeriod method. Using that context, you're obtaining new workingSiteFromDb instance (it doesn't relate to workingSiteToEnd from above). You're modifying it, and saving changes.
Now, you're trying to test the changes you've made, but original workingSiteToEnd is still present in context. This means, that when you'll try to load it from database again, context, during materialization process, will look up for entity with the same key in its local cache, will find it, and will return existing entity, which is original, unchanged workingSiteToEnd (you can compare references, they'll be equal).
When you're closing using block right after first SaveChanges, and then creating new one, you're creating new context, which will load new instance for workingSiteFromDb, and the test will pass.

Do not nest usings of the same DbContext. Instead, if you need to use the same context in called method, pass it with parameter, e.g.:
using (var db = new ApplicationDbContext())
{
var empFromDb = db.Employees.Include(x => x.WorkingSites).First(x => x.Id == _employeeId);
empFromDb.WorkingSites.Add(workingSiteToEnd);
db.SaveChanges();
//Act
ServiceFactory.CreateEmployeeService().EndWorkingSitePeriod(workingSiteToEnd, db);
//Assert
var workingSiteFromDb = db.WorkingSites.First(x => x.Id == workingSiteToEnd.Id);
Assert.AreEqual(DateTime.Now.AddDays(-1).Date, workingSiteFromDb.WorksUntil.Date);
}
private void EndWorkingSitePeriod(int workingSiteId, ApplicationDbContext db)
{
var workingSiteFromDb = db.WorkingSites.Include(x => x.Employee).First(x => x.Id == workingSiteId);
workingSiteFromDb.EndPeriod();
db.SaveChanges();
}
// if you need it public, then use this too
public void EndWorkingSitePeriod(int workingSiteId)
{
using (var db = new ApplicationDbContext())
{
EndWorkingSitePeriod(workingSiteId, db);
}
}

Related

How to prevent automatic inclusion of navigation properties in unit tests

I am currently developing an app store style API which has the following entities (plus many others, but not relevant to the problem):
App (1 to many relationship to AppRevision - contains IEnumerable property)
AppRevision
Installation
I have come across an odd problem where the behaviour of EF differs in unit tests to when actually running the API, in that navigation properties are automatically being included when unit testing.
Take the following code snippet from my command handler:
App app = await this.context.Apps
.Include(a => a.Installations)
.FirstOrDefaultAsync(a => a.Id == command.AppId);
if (app != null) {
// Code omitted for brevity
}
When running the API, if I inspect app after this code has been run, the AppRevisions collection on the App entity is empty, as you would expect as I have not expliclity told EF to .Include(a => a.AppRevisions) - the API then throws an exception when trying to process code later on that needs this data to be there.
Now look at the following unit test for the same handler:
[Fact]
public async void Handle_ShouldAddInstallationRecord_WhenDataIsValid()
{
Guid testGuid = Guid.NewGuid();
CreateInstallationCommand command = new CreateInstallationCommand(testGuid, "ABC", "abc#abc.com", null);
using (TestContext context = new TestContextFactory().CreateTestContext())
{
context.Apps.Add(new App() { Id = testGuid });
context.AppRevisions.Add(new AppRevision() { Id = Guid.NewGuid(), AppId = testGuid, Status = AppRevisionStatus.Approved, IsListed = true });
await context.SaveChangesAsync();
CreateInstallationCommandHandler handler = new CreateInstallationCommandHandler(context);
CommandResult result = await handler.Handle(command, new CancellationToken());
Assert.True(result);
Assert.Single(context.Installations);
}
}
If I step through this test, when I get to the handler and inspect the app variable, the AppRevisions collection has automatically been populated. As a result, the test passes because the code that requires the AppRevisions collection to be populated can execute.
The expectation is that this test should actually fail, because I'm not telling EF to include those entities in the query.
I am using a Sqlite in memory database to create the database context for my unit tests and running .NET Core 2.2
I originally thought this was something to do with the changetracker. While disabling this does solve the immediate problem reported above, it creates a load of other problems so isn't a viable solution (and probably wouldn't be the correct one anyway)
Any suggestions gratefully received
For anyone who comes across this post in the future, the solution is as per the comments on the original question, to use separate contexts for seeding test data and getting the data later in the test:
[Fact]
public async void Handle_ShouldAddInstallationRecord_WhenDataIsValid()
{
Guid testGuid = Guid.NewGuid();
CreateInstallationCommand command = new CreateInstallationCommand(testGuid, "ABC", "abc#abc.com", null);
using (TestContextFactory contextFactory = new TestContextFactory())
{
using (TestContext seedContext = contextFactory.CreateTestContext())
{
seedContext.Apps.Add(new App() { Id = testGuid });
seedContext.AppRevisions.Add(new AppRevision() { Id = Guid.NewGuid(), AppId = testGuid, Status = AppRevisionStatus.Approved, IsListed = true });
await seedContext.SaveChangesAsync();
}
using (TestContext getContext = contextFactory.CreateTestContext())
{
CreateInstallationCommandHandler handler = new CreateInstallationCommandHandler(getContext);
CommandResult result = await handler.Handle(command, new CancellationToken());
Assert.True(result);
Assert.Single(getContext.Installations);
}
}
}

Can't update with .Attach()

I'm trying to update my UserRoles table but it won't update. I'm trying to update two things : 1. the email 2. the user role. Because the update needs to happen in 2 tables I'm using two separate commands. When I run the update on the Email alone (Users) it works but if I update the role (AspUserRoles) it does nothing. When I run it both it doesn't work either because UserRoles.Attach(userRole) is preventing it from updating. I also get no errors.
I checked if ApplicationRole.Id and ApplicationUser.Id has a value and it does return the value I want.
Here's my UserController.cs :
public async Task<IActionResult> Edit(UserViewModel model, Guid id)
{
var alert = new Alert();
try
{
if(!ModelState.IsValid)
{
alert.Message = alert.ExceptionMessage = ApplicationDbContextMessage.INVALID;
throw new Exception();
}
var originalModel = ApplicationDbContext.Users.FirstOrDefault(u => u.Id == id);
var userRole = ApplicationDbContext.UserRoles.FirstOrDefault(i => i.UserId == id);
if(originalModel == null)
{
alert.Message = alert.ExceptionMessage = ApplicationDbContextMessage.NOTEXISTS;
throw new Exception();
}
originalModel.Email = model.ApplicationUser.Email;
userRole.RoleId = model.ApplicationRole.Id;
ApplicationDbContext.Users.Attach(originalModel);
ApplicationDbContext.UserRoles.Attach(userRole);
ApplicationDbContext.Entry(originalModel).State = EntityState.Modified;
if (await ApplicationDbContext.SaveChangesAsync() == 0)
{
alert.Message = alert.ExceptionMessage = ApplicationDbContextMessage.EDITNOK;
throw new Exception();
}
alert.Message = ApplicationDbContextMessage.EDITOK;
return RedirectToAction("Index");
}
catch(Exception ex)
{
alert.Type = AlertType.Error;
alert.ExceptionMessage = ex.Message;
model = await ViewModel(model.ApplicationUser);
ModelState.AddModelError(string.Empty, alert.ExceptionMessage);
}
return View(model);
}
The way you are modifying data in this code, you don't need to call Attach or Add on the Context to let it know about changes to entities, that will happen automatically.
From the moment you pull an entity out of a DbSet of the DbContext it is being tracked (attached) by that DbContext. When you call SaveChanges on the DbContext it will scan any entities that it is tracking, comparing current values to old values, to find changes. Those changes then get sent to the data base.
You should literally be able to remove 3 lines of code from what you originally posted and have it work.
...
originalModel.Email = model.ApplicationUser.Email;
userRole.RoleId = model.ApplicationRole.Id;
ApplicationDbContext.Users.Attach(originalModel); // <--- Delete this line
ApplicationDbContext.UserRoles.Attach(userRole); // <--- Delete this line
ApplicationDbContext.Entry(originalModel).State = EntityState.Modified; // <--- Delete this line
if (await ApplicationDbContext.SaveChangesAsync() == 0)
...
A little something else I noticed. It looks like you might be using one single DbContext instance for the entire application. That is usually considered an "Anti-Patern" in Entity Framework. You should create a new DbContext instance (with using) for every "logical" operation you perform. That instance should only be alive for the life of that operation.
In MVC, this is usually one DbContext instance per ActionMethod.

DbContext requires manual load of navigation properties

I recently upgraded my solution from EF5 to EF6.1.2, and changed my data access layer to use DbContext instead of ObjectContext.
Some of my unit tests are failing, and I don't understand why. Example of old data access code:
public virtual T Insert(T item)
{
if (item == null)
{
throw new ArgumentNullException("item", #"TaskDal.Insert");
}
using (var ctx = ObjectContextManager<StoreDataContext>.GetManager("StoreDataContext"))
{
var task = new Task();
WriteNonKeyData(task, item);
ctx.ObjectContext.Tasks.AddObject(task); // task.taskType null
ctx.ObjectContext.SaveChanges(); // task.TaskType set
return ReadData(task);
}
}
The Task Entity has a navigation property TaskType. As commented above, this gets set after the AddObject line.
My new code looks like so:
public virtual T Insert(T item)
{
if (item == null)
{
throw new ArgumentNullException("item", #"TaskDal.Insert");
}
using (var ctx = DbContextManager<StoreDataContext>.GetManager())
{
var task = new Task();
WriteNonKeyData(task, item);
ctx.DbContext.Tasks.Add(task); // task.TaskType null
ctx.DbContext.SaveChanges(); // task.TaskType still null
return ReadData(task);
}
}
Unlike the old code, task.TaskType is not set, which causes an exception in ReadData. LazyLoading is true in both examples.
I can workaround this by manually reloading the TaskType:
if (task.TaskType == null)
ctx.DbContext.Entry(task).Reference(p => p.TaskType).Load();
but I would prefer a better solution, as I am sure there are hundreds of other places in my code where this will need to be changed and it will be difficult for me to find them all.
Task will not load its navigation properties as these are not implemented to be lazily loaded. Take a look at your class definition, do you see any code in the getter? No.
Now, take a look at the model classes created automatically for your legacy code, is there a non empty getter that supports lazy loading? Yes, there is.
The difference is that with code-first, your model classes have no code that supports lazy loading. Lazy loading is supported only on proxy objects that are created by the context when you retrieve data from the database.
One of simplest workarounds would be to force the EF to create a proxy for you:
using (var ctx = DbContextManager<StoreDataContext>.GetManager())
{
var task = new Task();
WriteNonKeyData(task, item);
ctx.DbContext.Tasks.Add(task); // task.TaskType null
ctx.DbContext.SaveChanges(); // task.TaskType still null
// let ef create a proxy for the very same database object
var ptask = ctx.DbContext.Tasks.First( p => p.ID == task.ID );
// ptask.TaskType is now available as the actual type of
// ptask is not Task but rather a TaskProxy that inherits from Task
// and is created automatically by ef
return ReadData(ptask);
}

How to update using Entity Framwork by passing in object

How do I update using Entity Framework? I'm passing in the object with the updated values, but I don't see an Update method.
public void UpdateRecipient(Domain.Entities.RecipientEntity recipient)
{
using (EfDbContext context = CreateEfDbContext(recipient.ApplicationId.ToString()))
{
context.Recipients. //?? I don't see an update method
context.SaveChanges();
}
}
Three steps:
Get the item to update from the context
Copy over the updated properties from the entity you pass your update method
Save the changes.
Roughly:
using (EfDbContext context = CreateEfDbContext(recipient.ApplicationId.ToString()))
{
var toUpdate = context.Recipients.SingleOrDefault(r => r.Id == recipient.Id);
if (toUpdate != null)
{
toUpdate.Field1 = recipient.Field1;
// Map over any other field data here.
context.SaveChanges();
}
else
{
// Handle this case however you see fit. Log an error, throw an error, etc...
}
}
There is another way of updating object without re-fetching it from the database again thus by saving cost of a trip to database. The object being attached must have a value for its primary key.
Attach the updated object to the context
Change it's state to 'modified'.
Call SaveChanges() method of the context
Like:
public void UpdateRecipient(Domain.Entities.RecipientEntity recipient)
{
using (EfDbContext context = CreateEfDbContext(recipient.ApplicationId.ToString()))
{
context.Attach(recipient);
context.ObjectStateManager.ChangeObjectState(recipient,EntityState.Modified);
context.SaveChanges();
}
}
If you're updating the record then you'd do something like this:
//Retrieve the entity to be updated
Entity row = context.Recipients.Single(a => a.Id == recipient.Id);
//Update a column
row.Name = recipient.Name;
//Save changes
context.SaveChanges();
If you want to update/add things at the same time then you'd do:
if(!context.Recipients.Any(a => Id == recipient.Id))
{
context.Recipients.Add(recipient);
}
else
{
Entity row = context.Recipients.Single(a => a.Id == recipient.Id);
row.Name = recipient.Name;
}
context.SaveChanges();

optimisticconcurrencyexception was caught when updating entity relation using stub entities

This code is giving me optimisticconcurrencyexception was caught. I am simply using a Stub Entity to get an existing record and trying to update a couple values. I am not sure how to resolve the exception. Any help is very much appreciated:
using (MiscEntities ctx = new MiscEntities())
{
var m = ctx.Rates.FirstOrDefault(m => m.UserId == UserIdGuid);
DataAccess.Rate oldDbRate = new DataAccess.Rate { RatingId = m.RatingId };
ctx.AttachTo("Rates", dbRate);
dbRate.Rating = Rating;
dbRate.DateLastModified = DateTime.Now;
ctx.SaveChanges();
}
EF, by default, uses an optimistic concurrency model, meaning that locks are not held on data in the source between when the data is queried and when it is updated. So it doesn't check for any conflicts before saving changes to the database. With any conflicts an OptimisticConcurrencyException is raised (for more information check out How to: Manage Data Concurrency in the Object Context).
It's good practice (when you make updates in a high concurrency scenario) to call Refresh quite often. In this case try using a RefreshMode of ClientWins to to refresh the values in the client store before sending them to the database, like this:
using (MiscEntities ctx = new MiscEntities())
{
try
{
var m = ctx.Rates.FirstOrDefault(m => m.UserId == UserIdGuid);
DataAccess.Rate oldDbRate = new DataAccess.Rate { RatingId = m.RatingId };
ctx.AttachTo("Rates", dbRate);
dbRate.Rating = Rating;
dbRate.DateLastModified = DateTime.Now;
ctx.SaveChanges();
}
catch (OptimisticConcurrencyException)
{
ctx.Refresh(RefreshMode.ClientWins, dbRate);
ctx.SaveChanges();
}
}
EDIT: After more reading, and re-reading that error message it makes sense, you cannot attach an object to an ObjectContext is that object has already that has already been cached by the ObjectStateManager.
The solution is real simple, attach your objects before doing any operations/query in your ObjectContext. This allows you to prevent any double-tracking requests. If the ObjectContext needs your Entity later, it will retrieve the instance you attached before and you're good to go. Take a look at this code and see if it helps (Sorry don't have Visual Studio 2010 opened right now)
using (MiscEntities ctx = new MiscEntities())
{
try
{
ctx.AttachTo("Rates", dbRates);
var m = ctx.Rates.FirstOrDefault(m => m.UserId == UserIdGuid);
DataAccess.Rate oldDbRate = new DataAccess.Rate { RatingId = m.RatingId };
dbRate.Rating = Rating;
dbRate.DateLastModified = DateTime.Now;
ctx.SaveChanges();
}
catch (OptimisticConcurrencyException)
{
ctx.Refresh(RefreshMode.ClientWins, dbRate);
ctx.SaveChanges();
}
}

Categories

Resources