I'm using Entity Framework Core 2.2 code first. Soft delete is implemented in the overriden Context class. There is an UnitOfWork class that initializes Repositories for specific entities, while taking the Context as argument in constructor. Some of the application entities have been left out in this sample, for a clearer overview. I will only use Team, Member, Project and Employee.
While testing, a realized the following: when I delete an object (for instance, a Project), it successfully sets it's "Deleted" property to "true" after SaveChanges(). However, if the solution project is still running, I realized that if this object (Project) has a parent object (Team), it will still appear in the team's List Projects, even though their "Deleted" property is clearly "true". While the process is still running, if I call the repository Get() method that returns all Projects, the deleted project won't show up in the returned projects list, but only in the team's List Projects. Only when I rerun the solution project, these child objects don't appear in parent's List Projects anymore.
Here is a NUnit test I've written to analyze the issue:
[Test, Order(2)]
public void GetTeamAfterDeletingProject()
{
//There are initially 11 projects
var projects = unit.Projects.Get().ToList();
Assert.AreEqual(11, projects.Count());
Project project = unit.Projects.Get(8);
unit.Projects.Delete(project);
unit.Save();
//now there are 10 projects
projects = unit.Projects.Get().ToList();
Assert.AreEqual(10, projects.Count());
//this team had initially 2 projects, and one of them was deleted
Team team = unit.Teams.Get(3);
//only one project should be left, but there are actually two showing up - this test fails
Assert.AreEqual(1, team.Projects.Count);
project.Deleted = false;
unit.Save();
}
This interferes with my implemented Delete method in the TeamsRepository, which doesn't allow deletion of an object if there are child object's in his list. This means, that, If I wanted to delete the team in the previous test, even if I deleted all his projects, I still wouldn't be able to do it without restarting the app (the whole repository can be found below)
public override void Delete(int id)
{
Team old = Get(id);
if (old.Projects.Count != 0 || old.Members.Count != 0)
{
Services.ThrowChildrenPresentException();
}
Delete(old);
}
I'm wondering if this is the normal behavior when using lazy loading, or did I miss something in the configuration.
protected override void OnConfiguring(DbContextOptionsBuilder optionBuilder)
{
if(_conStr != null)
{
optionBuilder.UseNpgsql(_conStr);
}
optionBuilder.UseLazyLoadingProxies(true);
base.OnConfiguring(optionBuilder);
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<Member>().HasQueryFilter(x => !x.Deleted);
builder.Entity<Employee>().HasQueryFilter(x => !x.Deleted);
builder.Entity<Project>().HasQueryFilter(x => !x.Deleted);
builder.Entity<Team>().HasQueryFilter(x => !x.Deleted);
}
public override int SaveChanges()
{
foreach (var entry in ChangeTracker.Entries().Where(x => x.State == EntityState.Deleted && x.Entity is BaseClass))
{
entry.State = EntityState.Modified;
entry.CurrentValues["Deleted"] = true;
}
return base.SaveChanges();
}
This is the Team entity:
public class Team: BaseClass
{
public Team()
{
Members = new List<Member>();
Projects = new List<Project>();
}
public string Name { get; set; }
public string Description { get; set; }
public bool StatusActive { get; set; }
public virtual IList<Member> Members { get; set; }
public virtual IList<Project> Projects { get; set; }
}
public class Member: BaseClass
{
public virtual Team Team { get; set; }
public virtual Employee Employee { get; set; }
public virtual Role Role { get; set; }
public virtual MemberStatus Status { get; set; }
public decimal HoursWeekly { get; set; }
}
public class BaseClass
{
public BaseClass()
{
Created = DateTime.Now;
Creator = 0;
Deleted = false;
}
[Key]
public int Id { get; set; }
public DateTime Created { get; set; }
public int Creator { get; set; }
public bool Deleted { get; set; }
}
The generic Repository and the specific TeamsRepository with overriden methods. The soft deleted projects appear in old.Projects.Count, so I am never able to delete a Team this way (even though they aren't retrieved when
public class Repository<Entity>: IRepository<Entity> where Entity : class
{
protected AppContext _context;
protected DbSet<Entity> _dbSet;
public Repository(AppContext context)
{
_context = context;
_dbSet = _context.Set<Entity>();
}
public void ValidateUpdate(Entity newEntity, int id)
{
if (id != (newEntity as BaseClass).Id)
throw new ArgumentException($"Error! Id of the sent object: {(newEntity as BaseClass).Id} and id in url: {id} do not match");
}
public virtual IQueryable<Entity> Get() => _dbSet;
public virtual IList<Entity> Get(Func<Entity, bool> where) => Get().Where(where).ToList();
public virtual Entity Get(int id)
{
Entity entity = _dbSet.Find(id);
if (entity == null)
throw new ArgumentException($"There is no object with id: {id} in the database");
return entity;
}
public virtual void Insert(Entity entity)
{
entity.Build(_context);
_dbSet.Add(entity);
}
public virtual void Update(Entity entity, int id)
{
entity.Build(_context);
Entity old = Get(id);
ValidateUpdate(entity, id);
_context.Entry(old).CurrentValues.SetValues(entity);
old.Relate(entity);
}
public void Delete(Entity entity) => _dbSet.Remove(entity);
public virtual void Delete(int id)
{
Entity old = Get(id);
Delete(old);
}
}
public class TeamsRepository: Repository<Team>
{
public TeamsRepository(AppContext context) : base(context) { }
public override void Delete(int id)
{
Team old = Get(id);
if (old.Projects.Count != 0 || old.Members.Count != 0)
{
Services.ThrowChildrenPresentException();
}
Delete(old);
}
}