I am attempting to unit test a PUT Request by checking values. However, I run into one simple issue. I have a test context like such:
class TestAppContext : ContextInterface
{
public DbSet<User> Users {get; set;}
public DbSet<Request> Requests { get; set; }
public TestAppContext()
{
this.Users = new TestUsersDbSet();
this.Requests = new TestRequestsDbSet();
}
public int SaveChanges(){
return 0;
}
public void MarkAsModified(Object item) {
}
public void Dispose() { }
}
When running a PUT with a DbContext the Entry(item).State is set to EntityState.Modified in the MarkAsModified method, then changes the changes are saved. How do I emulate this in my test context so that the DbSet reflects the changes from the PUT request?
I've gotten as far as doing this:
public void MarkAsModified(Object item) {
if (item.GetType() == typeof(User))
{
}
else if (item.GetType() == typeof(Request))
{
}
}
So that I can determine what is being modified, but how do I actually save the changes into the DbSet for that record?
Both records are identified on a variable id which is an int.
In your test context just keep a List<Object> markedAsModified field, then in the call to MarkAsModified add the object to that list if it doesn't already exist. Then in your test you can have Assert statements the check the contents of that list to make sure the right objects were passed to that function..
Related
I have POCO objects that are exposed through a repository that uses EF Core 6 to access a database. I can persist "parent" objects to the database and related data that is added to the parent object before creating is persisted successfully as well. However, when trying to add children (SingleSimulationResult objects) to a parent object (SingleSimulation objects) after it has been created, the children are not persisted to the database.
Here is the code that tries to add and save children to the parent object.
singleSim.AddResultsToSimulation(allResults);
Console.WriteLine($"# results: {singleSim.Results.Count}"); // # results: 2
await scopedRepository.Save();
var test = await scopedRepository.GetById(singleSim.Id);
Console.WriteLine($"# results test: {test.Results.Count}"); // # results: 0
SingleSimulation class (BaseEntity just defines an Id property):
public class SingleSimulation : BaseEntity, IAggregateRoot
{
public string Name { get; private set; }
public string Description { get; private set; }
public double Capital { get; private set; }
public List<List<double>> Returns { get; private set; }
private readonly List<SingleSimulationStrategy> _strategies = new List<SingleSimulationStrategy>();
public IReadOnlyCollection<SingleSimulationStrategy> Strategies => _strategies.AsReadOnly();
private List<SingleSimulationResult> _results = new List<SingleSimulationResult>();
public IReadOnlyCollection<SingleSimulationResult> Results => _results.AsReadOnly();
public SingleSimulation()
{
}
public SingleSimulation(string name, string description, double capital, List<List<double>> returns, List<SingleSimulationStrategy> strategies)
{
Name = name;
Description = description;
Capital = capital;
Returns = returns;
_strategies = strategies;
}
public void AddResultsToSimulation(List<SingleSimulationResult> results)
{
if (_results is null)
return;
foreach (var result in results)
{
_results.Add(result);
}
}
}
Repository class:
public class SingleSimulationRepository : ISingleSimulationRepository
{
private SimulationDbContext _dbContext;
public SingleSimulationRepository(SimulationDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task Add(SingleSimulation entity)
{
await _dbContext.AddAsync(entity);
}
public async Task<SingleSimulation> GetById(int id)
{
return await _dbContext.SingleSimulations.FindAsync(id);
}
...
public async Task Save()
{
await _dbContext.SaveChangesAsync();
}
}
DbContext:
public class SimulationDbContext : DbContext
{
public SimulationDbContext(DbContextOptions<SimulationDbContext> options)
: base(options)
{
}
public DbSet<SingleSimulation> SingleSimulations { get; set; }
public DbSet<SingleSimulationResult> SingleSimulationResults { get; set; }
public DbSet<SingleSimulationStrategy> SingleSimulationStrategies { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Seed data and custom conversion functions
}
}
Here's what I have tried (to no avail):
Using Fluent API to configure One-to-Many relationship for Results (using .HasMany()).
modelBuilder.Entity<SingleSimulation>()
.HasMany(x => x.Results)
.WithOne();
Using AddRange() to add result objects to the DB before adding them to the parent and finally saving to DB (SaveChangesAsync).
Using Attach() to start tracking result objects before adding them to the parent.
Using Include() when loading the parent object from the database before adding children and trying to save them.
It feels like I'm missing something small, but after scouring the docs and other sources I cannot find the problem. What do I need to do to get children added to the parent after the parent has already been created to actually save to the DB?
After debugging by printing the EF Core change tracker's LongView, I noticed that no changes are detected on the object (even if changing a simple string property). It turns out the problem was that the singleSim object I was modifying was returned from a different dbContext than the one used by the scopedRepository.
The model setup wasn't the problem after all. Even without the Fluent API config the setup works as intended (even with the read only collections and private backing fields).
Having these two entities, I fetch them, map them to viwemodels/dtos before I pass them to the UI.
I also had to ignore reference loop handling, in my startup.cs file, to map them to DTO's correctly.
public class Matter
{
public int Id { get; set; }
public ICollection<MatterExposure> Exposures { get; set; }
// other properties
}
public class MatterExposure
{
public int Id { get; set; }
public Matter Matter { get; set; }
// other properties
}
When I save the form (which includes a table of 'MatterExposure's) in the UI I pass everything back to the controller to be saved. INFO - not saving child entities 'MatterExposure' yet in below controller call and it works fine!
[HttpPut("{id}")]
public async Task<IActionResult> UpdateData(string id, MatterForClaimDetailedDto generalMatterDto)
{
var user = await _userManager.GetUserAsync(HttpContext.User);
var matter = await _autoRepo.GetMatter(id);
// fill some matter data and add a child then save and it works fine
if (await _autoRepo.SaveAll())
return NoContent();
}
public class MatterForClaimDetailedDto
{
public int Id { get; set; }
public GeneralMatterDto MatterData { get; set; }
public ICollection<MatterExposure> Exposures { get; set; }
// other properties
}
Now I want to add the update of MatterExposure entities, as I could have made changes to them in the UI. So I try to use UpdateRange like this
[HttpPut("{id}")]
public async Task<IActionResult> UpdateData(string id, MatterForClaimDetailedDto generalMatterDto)
{
var user = await _userManager.GetUserAsync(HttpContext.User);
var matter = await _autoRepo.GetMatter(id);
matter.EditedDate = DateTime.Now;
matter.FirstName = generalMatterDto.FirstName;
matter.LastName = generalMatterDto.LastName;
_autoRepo.UpdateRange<List<MatterExposure>>(generalMatterDto.Exposures.ToList());
await _autoRepo.SaveAll()
}
public void UpdateRange<T>(T entity) where T : class
{
_autoContext.UpdateRange(entity);
}
But on calling UpdateRange I get this exception message:
"The entity type 'List MatterExposure' was not found. Ensure that the entity type has been added to the model."
In my context I have this:
public DbSet<MatterExposure> MatterExposure { get; set; }
I then tried below with no luck
public DbSet<List<MatterExposure>> MatterExposure { get; set; }
I thought I would try updating each individual 'MatterExposure' entity to see if that would change anything. So I tried removing the UpdateRange call and tried with individual 'MatterExposure' entities
foreach(var exposure in generalMatterDto.Exposures) {
_autoRepo.Update<MatterExposure>(exposure);
}
// in my repo I have this with different things I tried
public void Update<T>(T entity) where T : class
{
// _autoContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
//_autoContext.Entry(entity).State = EntityState.Detached;
_autoContext.Update(entity);
// _autoContext.ChangeTracker.
}
On the first loop through each 'MatterExposure' Update call to the repo I get this exception
"The instance of entity type 'MatterExposure' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting key values."
After the exception above I tried I put the loop at the top of the controller method to see if the other entity stuff was interfering.
// at top of controler method before the other entity actions are performed
foreach(var exposure in generalMatterDto.Exposures) {
_autoRepo.Update<MatterExposure>(exposure);
}
And moving the for loop to the top of the controller, runs through the 1st iteration but then fails on the second giving me the same error message again
"The instance of entity type 'MatterExposure' cannot be tracked because
another instance with the same key value for {'Id'} is already being tracked.
When attaching existing entities, ensure that only one entity instance with a given key value is attached.
Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting key values."
QUESTION - am I not updating the child entities correctly or is it something else?
I'm using Entity Framework 6.1.3 in an MVC 5 project, and I'm having issues with ICollection navigation properties being null when unit testing. I am hitting an actual test database in SQL Express, so this may be more like an integration test for you purists. Regardless of what you call it, it is a problem that I would like to solve.
I have read many answers to similar sounding questions, but none of them seem to hit the same problem that I am having here. I understand EF for the most part, I have lazy loading enabled, my classes are public, and I'm using virtual on my navigation properties.
Here is a simplified example of what I am trying to do:
Models:
public class Session
{
public int Id { get; set; }
public string Name { get; set; }
// Navigation Property
public virtual ICollection<File> Files { get; set; }
}
public class File
{
public int Id { get; set; }
public string Name { get; set; }
public int SessionId { get; set; }
public virtual Session Session { get; set; }
}
Test methods:
[TestMethod]
public void Test_TotalFileCount1()
{
ApplicationDbContext context = new ApplicationDbContext();
// Create session with no files
var session = new Session() { Name = "Session1" };
context.Sessions.Add(session);
context.SaveChanges();
// This line blows up because session.Files == null
Assert.AreEqual(0, session.Files.Count);
}
[TestMethod]
public void Test_TotalFileCount2()
{
ApplicationDbContext context = new ApplicationDbContext();
// Create session
var session = new Session() { Name = "Session2" };
context.Sessions.Add(session);
context.SaveChanges();
// Create file for session
var file = new File() { Name = "File1", Session = session };
context.Files.Add(file)
context.SaveChanges();
// This test passes because session.Files is a
// collection of one file
Assert.AreEqual(1, session.Files.Count);
}
The first test above fails because session.Files throws an ArgumentNullException. However, when I call this same code in the full MVC application, session.Files is not null and instead is an empty collection with Count = 0. The second test passes because session.Files is a collection of one File as I would expect. The navigation properties are clearly doing what they're supposed to in the second case, but not in the first case.
Why is EF behaving like this?
I was able to get around this problem by initializing Files as an empty list in the constructor. I know I could do this conditionally in the getter instead, but I don't think I should have to do either of these things because it just works when it's running normally.
public Session()
{
this.Files = new List<File>();
}
Does anyone have any insight into what is going on here?
If you are working with lazy loading enabled and if you want that the navigation property gets populated after adding the object with the foreign key property to the context you must use the Create method of DbSet (instead of instantiating the object with new):
var session = context.Sessions.Create();
With active lazy loading this will create a proxy object which ensures that the navigation property gets loaded.
[TestMethod]
public void Test_TotalFileCount1()
{
ApplicationDbContext context = new ApplicationDbContext();
// Create session with no files
var session = context.Sessions.Create();
session.Name = "Session1";
context.Sessions.Add(session);
context.SaveChanges();
// This line blows up because session.Files == null
Assert.AreEqual(0, session.Files.Count);
}
[TestMethod]
public void Test_TotalFileCount2()
{
ApplicationDbContext context = new ApplicationDbContext();
// Create session
var session = context.Sessions.Create();
session.Name = "Session2";
session.Files = new List<File>()
{
new File() { Name = "File1" }
};
context.Sessions.Add(session);
context.SaveChanges();
// This test passes because session.Files is a
// collection of one file
Assert.AreEqual(1, session.Files.Count);
}
See more about Proxies
I am using EF to update my database.
I first query the database based upon some criteria. This returns the record I am expecting (or returns null). Naturally during this query, there is a slight delay as it executes the query.
I then change a property in this case a nullable boolean from false to true or create a new entry.
I then add this object to my Entities, and finally choose myEntitiy.SaveChanges(). This does not seem to execute. There is no delay. Yet the code passes to the next line, so no exception is thrown.
This is what I have
public class MyCompany : AbstractCompany
{
public override bool UpdateStatus(string emailAddress, bool isOptIn)
{
var personDetail = (from a in base.Entities.MyCompanyTable
where a.EmailAddress == emailAddress
select a).SingleOrDefault();
if (personDetail == null)
{
personDetail = new MyCompanyTable();
personDetail.EmailAddress = emailAddress;
}
personDetail.IsSubscribed = isOptIn;
base.Entities.MyCompanyTable.Add(personDetail); //also tried attach, same issue over
return base.SaveData();
}
}
And my base class is
public abstract bool UpdateStatus(string emailAddress, bool isOptIn);
protected Entities Entities
{
get { return new Entities(); }
}
protected bool SaveData()
{
var x = Entities.GetValidationErrors();//returns 0 items
try
{
Entities.SaveChanges();
return true;
}
catch (Exception e)
{
return false;
}
}
What have I done wrong?
Entity Framework uses change tracking to detect changes in any entities in the current context.
However, your Entities property instantiates a new instance of your context everytime it is called. So when you query you use one context and then when you save you use another! EF has no way to detect that you made any changes!
It would be best to instantiate your context in the base class' constructor:
public abstract class BaseClass
{
protected BaseClass()
{
Entities = new Entities();
}
protected Entities Entities { get; private set; }
}
That should fix it up.
I have two entities:
public class Order:Entity
{
public virtual User User { get; set; }
...
}
public class User:Entity
{
public virtual ICollection<Order> Orders { get; set; }
...
}
Next, I create order:
var order = _orderService.CreateTransientOrder(orderNumbers, PcpSession.CurrentUser);
PcpSession.Order = order;
this is CreateTransientOrder. It's only create Order, but not save into database:
public Order CreateTransientOrder(string orderNumbers, User currentUser)
{
...fill fields
order.User = currentUser;
return order;
}
Now it's all ok. Next, I save order to the database:
_orderService.CreateOrder(PcpSession.Order);
This is CreateOrder:
public void CreateOrder(Order order)
{
order.OrderDate = DateTime.Now;
_repository.Save(order);
_repository.SaveChanges();
}
This is my Save method of repository:
public void Save<T>(T entity) where T : class, IEntity
{
_context.Set<T>().Add(entity);
}
When the SaveChanges is called in the database creates new user with new ID and order have new User_Id. In the debugger in the CreateOrder method, Id is equal current user. Where is a problem?
Thanks.
User is probably not being tracked by the context. When you add order to the context it also adds the related entities and then on save changes creates a new user (or attempts to). Attach() the user to the context before you call _context.Set<T>().Add(entity);.
I guess the problem is not related with the code you have provided. It seems to be related to where you are initializing PcpSession.CurrentUser.
It seems PcpSession.CurrentUser object is not attached to the context. Either fetch this entity to the context before making you Order related calls or attach it.
You need attach your Entity if not attach in context.
for exemple in Repository Generic
> public void Add(T entity)
> {
> entity.Create = DateTime.Now;
> db.Set<T>().Attach(entity); // Add Line
> db.Set<T>().Add(entity);
> Save();
> }
I do not know if it's clean but it regulates the problem