Soft delete with cascade deletion doesn't work as expected - c#

I'm using EF Core and SQLite in unit tests. Given the following 1-to-1 entities:
public class Entity1 : FullAuditedEntity<int>
{
public Entity2 Entity2 { get; set; }
public string Name { get; set; }
}
public class Entity2 : FullAuditedEntity<int>
{
public string Name { get; set; }
[ForeignKey("Entity1Id")]
public Entity1 Entity1 { get; set; }
public int Entity1Id { get; set; }
}
DbContext class has the following code:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
...
modelBuilder.Entity<Entity1>().HasOne(t => t.Entity2)
.WithOne(t1 => t1.Entity1)
.OnDelete(DeleteBehavior.Cascade);
...
}
I have a test method:
[Fact]
public async Task Should_Create_And_Then_Delete_Single_Entity1()
{
var entity1Service = Resolve<Entity1Service>();
var entity1Repo = Resolve<IRepository<Entity1>>();
var entity2Repo = Resolve<IRepository<Entity2>>();
var entity1 = new entity1 { Name = "ent1" };
entity2Repo.Count().ShouldBe(0);
// entity2 created also, see assert below
var created = await entity1Service.CreateEntity1Async(entity1).ConfigureAwait(false);
created.Id.ShouldBe(1);
created.Name.ShouldBe("net1");
entity1Repo.Count().ShouldBe(1);
entity2Repo.Count().ShouldBe(1);
var ent = await entity1Service.GetEntity1Async(created.Id).ConfigureAwait(false);
ent.ShouldNotBeNull();
ent.Entity2.ShouldNotBeNull();
await entity1Service.DeleteEntity1Async(ent.Id).ConfigureAwait(false);
entity1Repo.Count().ShouldBe(0);
entity2Repo.Count().ShouldBe(0);
}
The problem is that the last line of code, "entity2Repo.Count().ShouldBe(0);" assertion is broken, it is actually 1 instead of 0, IsDeleted (soft delete) is false, but I expect it to be true.
What am I doing wrong?
Thanks in advance.

You need to cascade soft deletes yourself.
See the rationale in the closed PR aspnetboilerplate/aspnetboilerplate#3559.
You can do that by defining an event handler:
public class Entity1DeletingCascader : IEventHandler<EntityDeletingEventData<Entity1>>, ITransientDependency
{
private readonly IRepository<Entity2> _entity2Repository;
public Entity1DeletingCascader(IRepository<Entity2> entity2Repository)
{
_entity2Repository = entity2Repository;
}
[UnitOfWork]
public virtual void HandleEvent(EntityDeletingEventData<Entity1> eventData)
{
var entity1 = eventData.Entity;
_entity2Repository.Delete(e2 => e2.Entity1Id == entity1.Id);
}
}

Related

Database operation expected to affect 1 row(s) but actually affected 0 row(s). Entity's collection not tracked?

I'm getting this error:
Database operation expected to affect 1 row(s) but actually affected 0 row(s)
when adding a new element to the entity's collection.
Base classes in Domain project:
public class Tour
{
private List<Stop> _stops;
public Guid Id { get; private set; }
public Tour(Guid id)
{
Id = id;
_stops = new List<Stop>();
}
public IReadOnlyList<Stop> Stops { get { return _stops; } }
public void AddStop(Stop newStop)
{
//some logic and checking
_stops.Add(newStop);
}
}
public class Stop
{
public Stop(Guid id)
{
Id = id;
}
public Guid Id { get; private set; }
}
My DbContext in the Infrastructure project:
public class TourDbContext : DbContext, ITourDbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=db");
}
public DbSet<Tour> Tours { get; set; }
public DbSet<Stop> Stops { get; set; }
}
Exception is thrown when calling AddStopToTour() in the Application project.
Interface for DbContext, based on: https://github.com/jasontaylordev/CleanArchitecture.
public interface ITourDbContext
{
public DbSet<Tour> Tours { get; set; }
public DbSet<Stop> Stops { get; set; }
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}
public class SomeClass
{
ITourDbContext _context;
public SomeClass(ITourDbContext context)
{
_context = context;
}
public async Task AddStopToTour(Guid id)
{
var tour = await _context.Tours
.Include(x => x.Stops)
.SingleOrDefaultAsync(e => e.Id == id);
tour.AddStop(new Stop(Guid.NewGuid());
await _context.SaveChangesAsync(); //Exception
}
}
Shouldn't EF Core be able to track these changes automatically and add 1 new row to the Stops table? Am I missing additional configuration on entities?
The problem was that by generating my Id's manually, the new entity got the Modified state, instead of Added state. The fix was using the Fluent API's .ValueGeneratedNever() method with the modelBuilder.
See: https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-3.x/breaking-changes#detectchanges-honors-store-generated-key-values

Copying data between models and saving children without entities duplicating themselves in Entity Framework

I am having trouble saving children entities via Entity Framework / ASP Identity. It seems to be adding duplicates of everything that is added.
I have tried using a detached graph of the DrivingLicenceModel by TeamMember.DrivingLicence = null in the TeamMemberModel and then working with a detached graph by looking if there is new or old DrivingLicenceCategories but because DrivingLicence links back to TeamMember it causes TeamMember.DrivingLicenceId to be null as it cannot link back to TeamMember.
I have tried Manually adding the EntityState to the DrivingLicence and DrivingLicenceCategories but when I do that it complains that it cannot save two entities with the same primary key.
I assume this is because they way I am copying the entities but I after a lot of looking I am drawing a blank.
If there anyway to copy from TeamMemberRequestModel to TeamMemberModel and then save without the children trying to create clone copies of themselves?
Models
public class TeamMemberModel : IdentityUser
{
public virtual DrivingLicenceModel DrivingLicence { get; set; }
public void ShallowCopy(TeamMemberRequestModel src)
{
this.DateOfBirth = src.DateOfBirth;
if (src.DrivingLicence != null)
{
if (this.DrivingLicence == null)
{
this.DrivingLicence = new DrivingLicenceModel(src.DrivingLicence);
}
else
{
this.DrivingLicence.ShallowCopy(src.DrivingLicence);
}
}
}
public TeamMemberModel() { }
}
public class DrivingLicenceModel
{
[Key]
public int Id { get; set; }
[ForeignKey("TeamMember")]
public string TeamMemberId { get; set; }
[JsonIgnore]
public TeamMemberModel TeamMember { get; set; }
public virtual List<DrivingLicenceCategoryModel> DrivingLicenceCategories { get; set; }
public DrivingLicenceModel() { }
public DrivingLicenceModel(DrivingLicenceModel src)
{
this.ShallowCopy(src);
}
public void ShallowCopy(DrivingLicenceModel src)
{
this.Id = src.Id;
this.IsFullLicence = src.IsFullLicence;
this.IssueDate = src.IssueDate;
this.ExpiryDate = src.ExpiryDate;
this.IssuingAuthority = src.IssuingAuthority;
this.LicenceNumber = src.LicenceNumber;
this.DrivingLicenceCategories = src.DrivingLicenceCategories;
this.DrivingLicencePoints = src.DrivingLicencePoints;
}
}
public class DrivingLicenceCategoryModel
{
[Key]
public int Id { get; set; }
[ForeignKey("DrivingLicence")]
public int DrivingLicenceId { get; set; }
[JsonIgnore]
public DrivingLicenceModel DrivingLicence { get; set; }
}
public class TeamMemberRequestModel
{
public string Id { get; set; }
public virtual DrivingLicenceModel DrivingLicence { get; set; }
}
Context
public class TIERDBContext : IdentityDbContext<TeamMemberModel, RoleModel, string, IdentityUserLogin, IdentityUserRole, IdentityUserClaim>
{
public TIERDBContext() : base("SARDBConnection") { }
public DbSet<DrivingLicenceModel> DrivingLicences { get; set; }
public DbSet<DrivingLicenceCategoryModel> DrivingLicenceCategories { get; set; }
}
Controller
public async Task<IHttpActionResult> Put(string id, TeamMemberRequestModel teamMember)
{
TeamMemberModel CurrentTeamMember = await this.TIERUserManager.FindByIdAsync(id);
CurrentTeamMember.ShallowCopy(teamMember);
await this.TIERUserManager.UpdateAsync(CurrentTeamMember);
}
you have to create clone property into context class
.
In the context clases you could to use clone method that retiran the entity you send by parameters this duplicarse any entity you pass. Sorry for my english
hope you help
After far to many hours working over this. I have come to an answer. The best way to deal with this is to simply deal with it is to add or attach all entities down the tree.
The controller now attaches all children unless they have an ID of 0, therefore new and uses add instead. Then I use this very useful extension I found here http://yassershaikh.com/c-exceptby-extension-method/ to compare lists to see added and deleted entities in the list. While I don't need the added part as the entity will already be marked to an add state as I use add() it does not harm and I want to use it later with add and delete state changing.
Controller
public async Task<IHttpActionResult> Put(string id, TeamMemberRequestModel teamMember)
{
TIERDBContext IdentityContext = (TIERDBContext)this.TIERUserManager.UserStore().Context;
foreach (DrivingLicenceCategoryModel DrivingLicenceCategory in teamMember.DrivingLicence.DrivingLicenceCategories)
{
if (DrivingLicenceCategory.Id == 0)
{
IdentityContext.DrivingLicenceCategories.Add(DrivingLicenceCategory);
}
else
{
IdentityContext.DrivingLicenceCategories.Attach(DrivingLicenceCategory);
}
}
foreach (DrivingLicencePointModel DrivingLicencePoint in teamMember.DrivingLicence.DrivingLicencePoints)
{
if (DrivingLicencePoint.Id == 0)
{
IdentityContext.DrivingLicencePoints.Add(DrivingLicencePoint);
}
else
{
IdentityContext.DrivingLicencePoints.Attach(DrivingLicencePoint);
}
}
this.DetectAddedOrRemoveAndSetEntityState(CurrentTeamMember.DrivingLicence.DrivingLicenceCategories.AsQueryable(),teamMember.DrivingLicence.DrivingLicenceCategories, IdentityContext);
this.DetectAddedOrRemoveAndSetEntityState(CurrentTeamMember.DrivingLicence.DrivingLicencePoints.AsQueryable(),teamMember.DrivingLicence.DrivingLicencePoints, IdentityContext);
CurrentTeamMember.ShallowCopy(teamMember);
await this.TIERUserManager.UpdateAsync(CurrentTeamMember);
}
I then use a generic that uses ExceptBy to work out what is added and delete from the old team member model to the new team member model.
protected void DetectAddedOrRemoveAndSetEntityState<T>(IQueryable<T> old, List<T> current, TIERDBContext context) where T : class, IHasIntID
{
List<T> OldList = old.ToList();
List<T> Added = current.ExceptBy(OldList, x => x.Id).ToList();
List<T> Deleted = OldList.ExceptBy(current, x => x.Id).ToList();
Added.ForEach(x => context.Entry(x).State = EntityState.Added);
Deleted.ForEach(x => context.Entry(x).State = EntityState.Deleted);
}
It works but it is far from great. It takes two DB queries, getting the original and updating. I just cannot think of any better way to do this.

Entity Framework many to many duplicate value in join table when updating entity

I have following code first model:
public class Model1 : DbContext
{
public Model1()
: base("name=Model1")
{
}
public virtual DbSet<Master> Masters { get; set; }
public virtual DbSet<Slave> Slaves { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Master>().Property(e => e.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
modelBuilder.Entity<Slave>().Property(e => e.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
modelBuilder.Entity<Master>().Property(e => e.Name).IsRequired();
modelBuilder.Entity<Slave>().Property(e => e.Name).IsRequired();
}
}
public interface IEntity
{
int Id { get; }
}
public class Master : IEntity
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Slave> Slaves { get; set; }
public Master()
{
Slaves = new EntityHashSet<Slave>();
}
public Master(string name)
: this()
{
Id = name.GetHashCode();
Name = name;
}
public void Update(IEnumerable<Slave> slaves, Model1 model)
{
Slaves = new EntityHashSet<Slave>(slaves.Select(s => model.Slaves.CreateOrFind(s)));
}
public void Update(IEnumerable<string> slaves, Model1 model)
{
Update(slaves.Select(s => new Slave(s)), model);
}
}
public class Slave : IEntity
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Master> Masters { get; set; }
public Slave()
{
Masters = new EntityHashSet<Master>();
}
public Slave(string name)
: this()
{
Id = name.GetHashCode();
Name = name;
}
}
I'm using the following utility classes:
public class EntityHashSet<TEntity> : HashSet<TEntity> where TEntity : IEntity
{
public EntityHashSet()
: base(new EntityEqualityComparer<TEntity>())
{ }
public EntityHashSet(IEnumerable<TEntity> collection)
: base(collection, new EntityEqualityComparer<TEntity>())
{ }
}
public class EntityEqualityComparer<TEntity> : IEqualityComparer<TEntity> where TEntity : IEntity
{
public bool Equals(TEntity x, TEntity y)
{
return x.Id.Equals(y.Id);
}
public int GetHashCode(TEntity obj)
{
return obj.Id.GetHashCode();
}
}
public static class ExtensionMethods
{
public static TEntity CreateOrFind<TEntity>(this DbSet<TEntity> dbSet, TEntity entity) where TEntity : class, IEntity
{
return dbSet.Find(entity.Id) ?? dbSet.Add(entity);
}
}
When I first add master entity to the database with the following code no error is thrown:
using (var model = new Model1())
{
var m = new Master("master1");
m.Update(new[] {"slave1", "slave2", "slave3"}, model);
model.Masters.Add(m);
model.SaveChanges();
}
When I try to use the update method for existing one, DbUpdateException is thrown:
var m = model.Masters.CreateOrFind(new Master("master1"));
m.Update(new[] {"slave1", "slave2", "slave3", "slave4"}, model);
model.SaveChanges();
Additional information: An error occurred while saving entities that do not expose foreign key properties for their relationships. The EntityEntries property will return null because a single entity cannot be identified as the source of the exception. Handling of exceptions while saving can be made easier by exposing foreign key properties in your entity types. See the InnerException for details.
Relevant inner exception:
Violation of PRIMARY KEY constraint 'PK_dbo.SlaveMasters'. Cannot insert duplicate key in object 'dbo.SlaveMasters'. The duplicate key value is (1928309069, -2136434452).
The statement has been terminated.
Why is this? I'm checking whether entities are already in the database or need to be created via CreateOrFind.
EDIT: To clarify, the line that produces the error is:
Slaves = new EntityHashSet<Slave>(slaves.Select(s => model.Slaves.CreateOrFind(s)));
Error is thrown when calling SaveChanges().
I postpone you must be use the previous ef config file ,so it will always try to insert the same value but update .
you could update or check your ef config file before update.
Found a dirty way of getting around this. Before I create new EntityHashSet I call raw SQL command deleting entries from SlaveMasters table that contains current master Id.
model.ExecuteSqlCommand("DELETE FROM SlaveMasters WHERE Master_Id = " + Id);
Slaves = new EntityHashSet<Slave>(slaves.Select(s => model.Slaves.CreateOrFind(s)));

Error when mapping from Entity to DTO or DTO to Entity using AutoMapper

I'm using EntityFramework as a DataLayer and DTO to transfer data between layer. I develop Windows Forms in N-Tier architecture and when I try to mapping from Entity to DTO in BLL:
public IEnumerable<CategoryDTO> GetCategoriesPaged(int skip, int take, string name)
{
var categories = unitOfWork.CategoryRepository.GetCategoriesPaged(skip, take, name);
var categoriesDTO = Mapper.Map<IEnumerable<Category>, List<CategoryDTO>>(categories);
return categoriesDTO;
}
I've got this error:
http://s810.photobucket.com/user/sky3913/media/AutoMapperError.png.html
The error said that I missing type map configuration or unsupported mapping. I have registered mapping using profile in this way at UI Layer:
[STAThread]
static void Main()
{
AutoMapperBusinessConfiguration.Configure();
AutoMapperWindowsConfiguration.Configure();
...
Application.Run(new frmMain());
}
and AutoMapper configuration is in BLL:
public class AutoMapperBusinessConfiguration
{
public static void Configure()
{
Mapper.Initialize(cfg =>
{
cfg.AddProfile<EntityToDTOProfile>();
cfg.AddProfile<DTOToEntityProfile>();
});
}
}
public class EntityToDTOProfile : Profile
{
public override string ProfileName
{
get { return "EntityToDTOMappings"; }
}
protected override void Configure()
{
Mapper.CreateMap<Category, CategoryDTO>();
}
}
public class DTOToEntityProfile : Profile
{
public override string ProfileName
{
get { return "DTOToEntityMappings"; }
}
protected override void Configure()
{
Mapper.CreateMap<CategoryDTO, Category>();
}
}
I've got the same error too when mapping from DTO to Entity.
category = Mapper.Map<Category>(categoryDTO);
How to solve this?
Its because you are using Mapper.Initialize multiple times. If you look at the source code it calls Mapper.Reset() which means only the last mapping defined will work. so instead simply remove the Initialize calls and replace with Mapper.AddProfile< >
Use AutoMapper.AssertConfigurationIsValid() after the Configure() calls. If anything fails it will throw an exception with a descriptive text. It should give you more info to debug further.
Mapping DTOs to Entities using AutoMapper and EntityFramework
here we have an Entity class Country and an CountryDTO
public class Country
{
public int CountryID { get; set; }
public string ContryName { get; set; }
public string CountryCode { get; set; }
}
CountryDto
public class CountryDTO
{
public int CountryID { get; set; }
public string ContryName { get; set; }
public string CountryCode { get; set; }
}
Create Object of CountryDTO
CountryDTO collection=new CountryDTO();
collection.CountryID =1;
collection.ContryName ="India";
collection.CountryCode ="in";
Country model = Convertor.Convert<Country, CountryDTO>(collection);
dbcontext.Countries.Add(model);
dbcontext.SaveChanges();
this will work fine for a new Country, the above code will map CountryDTO to Country Entity Object and add new entities to the dbcontext and save the changes.
using System.Reflection;
public static TOut Convert<TOut, TIn>(TIn fromRecord) where TOut : new()
{
var toRecord = new TOut();
PropertyInfo[] fromFields = null;
PropertyInfo[] toFields = null;
fromFields = typeof(TIn).GetProperties();
toFields = typeof(TOut).GetProperties();
foreach (var fromField in fromFields)
{
foreach (var toField in toFields)
{
if (fromField.Name == toField.Name)
{
toField.SetValue(toRecord, fromField.GetValue(fromRecord, null), null);
break;
}
}
}
return toRecord;
}
public static List<TOut> Convert<TOut, TIn>(List<TIn> fromRecordList) where TOut : new()
{
return fromRecordList.Count == 0 ? null : fromRecordList.Select(Convert<TOut, TIn>).ToList();
}
http://bhupendrasinghsaini.blogspot.in/2014/09/convert-enity-framwork-data-in-entity.html

EF4.1 : How to deal with items being added to an Object's collection

I'm using EF4.1 for the first time (so be patient with me) but I just cant get to grips with how I can add new items to a sub collection of an object and then save the object.
For example, with the classes below, I can initially save the TravelTicket (containing multiple People) into my database, but as soon as I add a new person and then try to save the TravelTicket again I get:
An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key.
Can anyone help?
public class TravelTicket
{
public int Id { get; set; }
public string Destination { get; set; }
public virtual List<Person> Members { get; set; }
}
public class Person
{
public int Id { get; set; }
public string Name{ get; set; }
}
EDITED: All relevant code added as requested:
Domain Models:
public class TravelTicket
{
public int Id { get; set; }
public string Destination { get; set; }
public virtual ICollection<Person> Members { get; set; }
}
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
}
The DB Context:
public class TicketContext : DbContext
{
public TicketContext()
: base("TicketStore")
{ }
public DbSet<TravelTicket> TravelTickets { get; set; }
public DbSet<Person> People { get; set; }
}
The Repository (relevant methods only):
public class TicketRepository : ITicketRepository
{
TicketContext context = new TicketContext();
public void InsertOrUpdate(TravelTicket quoteContainer)
{
if (quoteContainer.Id == default(int))
{
// New entity
context.TravelTickets.Add(quoteContainer);
}
else
{
// Existing entity
context.Entry(quoteContainer).State = EntityState.Modified;
}
}
public void Save()
{
try
{
context.SaveChanges();
}
catch (DbEntityValidationException dbEx)
{
foreach (var validationErrors in dbEx.EntityValidationErrors)
{
foreach (var validationError in validationErrors.ValidationErrors)
{
Trace.TraceInformation("Property: {0} Error: {1}", validationError.PropertyName, validationError.ErrorMessage);
}
}
}
}
}
public interface ITicketRepository
{
void InsertOrUpdate(TravelTicket travelTicket);
void Save();
}
The consuming (example) MVC Controller code:
public class TicketSaleController : Controller
{
private readonly ITicketRepository ticketRepository;
public TicketSaleController()
: this(new TicketRepository())
{
}
public TicketSaleController(ITicketRepository ticketRepository)
{
this.ticketRepository = ticketRepository;
}
public ActionResult Index()
{
TravelTicket ticket = new TravelTicket();
ticket.Destination = "USA";
List<Person> travellers = new List<Person>();
travellers.Add(new Person { Name = "Tom" });
travellers.Add(new Person { Name = "Dick" });
travellers.Add(new Person { Name = "Harry" });
ticket.Members = travellers;
ticketRepository.InsertOrUpdate(ticket);
ticketRepository.Save();
Session["Ticket"] = ticket;
return RedirectToAction("Next");
}
public ActionResult Next()
{
TravelTicket ticket = (TravelTicket)Session["Ticket"];
ticket.Members.Add(new Person { Name = "Peter" });
ticket.Members.Add(new Person { Name = "Paul" });
ticketRepository.InsertOrUpdate(ticket);
ticketRepository.Save();
return View();
}
}
The call "ticketRepository.InsertOrUpdate(ticket);" on the "Next" method causes the exception:
An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key.
FURTHER EDIT: If I pull the object back from the database after its been saved instead of pulling the object from the session, adding the 2 new persons works OK:
Works:
TravelTicket ticket = ticketRepository.Find(ticketId);
ticket.Members.Add(new Person { Name = "Peter" });
ticket.Members.Add(new Person { Name = "Paul" });
ticketRepository.InsertOrUpdate(ticket);
ticketRepository.Save();
Doesn't Work:
TravelTicket ticket = (TravelTicket)Session["Ticket"];
ticket.Members.Add(new Person { Name = "Peter" });
ticket.Members.Add(new Person { Name = "Paul" });
ticketRepository.InsertOrUpdate(ticket);
ticketRepository.Save();
I'd need to see the code you are using to add items and then persist them. Until that a few generic advice.
It seems like you're using a long-living context to do your stuff. It's a good practice to use short living context, like this:
Instance context
Do a single operation
Dispose the context
Rinse and repeat for every operation you have to do. While following this good practice, you could be indirectly solving your problem.
Again, for more specific help, please post the code you're using ;)
In your mapping class for person, you may need do something like this
Property(p => p.Id)
.StoreGeneratedPattern = StoreGeneratedPattern.Identity;

Categories