I'm following DDD approach and have a parent and child entity with one-to-many relationship configured as shown below:
Parent Entity:
public class AnnotationEntity
{
public int Id { get; private set; }
public string Value { get; private set; }
private readonly List<AnnotationObjectEntity> _annotationObjects = new List<AnnotationObjectEntity>();
public virtual IReadOnlyList<AnnotationObjectEntity> AnnotationObjects => _annotationObjects.ToList();
protected AnnotationEntity()
{
}
private AnnotationEntity(string value) : this()
{
Value = value;
}
private void UpdateContainer(string container)
{
Value = container;
}
public void AddAnnotation(AnnotationObjectEntity annotationObject)
{
if (!_annotationObjects.Any(x => x.Id == annotationObject.Id))
{
var annotationObjectEntity = new AnnotationObjectEntity
(
annotationObject.Id,
annotationObject.PageNumber,
annotationObject.AnnotationType
);
_annotationObjects.Add(annotationObjectEntity);
return;
}
var existingAnnotation = _annotationObjects.FirstOrDefault(x => x.Id.Equals(annotationObject.Id));
if (existingAnnotation != null)
{
existingAnnotation.Update(annotationObject);
}
}
public void RemoveAnnotation(Guid id)
{
XDocument annotation = XDocument.Parse(Value);
var annotationObjectEntity = _annotationObjects.FirstOrDefault(x => x.Id == id);
if (annotationObjectEntity != null)
{
_annotationObjects.Remove(annotationObjectEntity);
annotation.Descendants("Object").Where(x => x.Element("Guid").Value == annotationObjectEntity.Id.ToString()).Remove();
}
UpdateContainer(annotation.ToString());
}
public static AnnotationEntity Create(int folderId, int documentId, string annotation)
{
XDocument doc = XDocument.Parse(annotation);
var documentAnnotations = new List<AnnotationEntity>();
var annotationEntity = new AnnotationEntity
(
annotation
);
doc.Descendants("Container").ToList().ForEach(container =>
{
container.Descendants("Object").ToList().ForEach(annotationObject =>
{
var annotationObjectEntity = new AnnotationObjectEntity
(
Guid.Parse(annotationObject.Element("Guid").Value.ToString()),
Convert.ToInt32(container.Element("PageNumber").Value.ToString()),
true
);
annotationEntity.AddAnnotation(annotationObjectEntity);
});
});
return annotationEntity;
}
public void UpdateAnnotation(string modifiedAnnotationXml)
{
var modifiedAnnotationEntity = Create(modifiedAnnotationXml);
//Removing deleted entities
var deletedAnnotationsId = _annotationObjects.Where(x => !modifiedAnnotationEntity.AnnotationObjects.Any(y => y.Id.Equals(x.Id))).Select(x => x.Id).ToList();
foreach (var annotationId in deletedAnnotationsId)
{
RemoveAnnotation(annotationId);
}
XDocument annotation = XDocument.Parse(Value);
XDocument modifiedAnnotation = XDocument.Parse(modifiedAnnotationXml);
//update or add
foreach (var annotationObject in modifiedAnnotationEntity.AnnotationObjects)
{
AddAnnotation(annotationObject);
annotation.Descendants("Object").Where(x => x.Element("Guid").Value == annotationObject.Id.ToString()).Remove();
// extract annotation object xml from modified xml and add to existing xml
var modifiedAnnotationObject = modifiedAnnotation.Descendants("Object").SingleOrDefault(x => x.Element("Guid").Value == annotationObject.Id.ToString());
annotation
.Descendants("Container").SingleOrDefault(x => x.Element("PageNumber").Value == annotationObject.PageNumber.ToString())
.Descendants("Objects").SingleOrDefault()
.Add(modifiedAnnotationObject);
UpdateContainer(annotation.ToString());
}
}
}
Child Entity:
public class AnnotationObjectEntity
{
public Guid Id { get; private set; }
public int PageNumber { get; private set; }
public bool AnnotationType { get; private set; }
public AnnotationEntity Annotation { get; }
protected AnnotationObjectEntity()
{
}
public AnnotationObjectEntity(Guid id, int pageNumber, bool annotationType) : this()
{
Id = id;
PageNumber = pageNumber;
AnnotationType = privateToSelectedUserGroup;
}
public void Update(AnnotationObjectEntity annotationObject)
{
PageNumber = annotationObject.PageNumber;
AnnotationType = annotationObject.AnnotationType;
}
}
I have configured my entities in my DbContext as shown below:
DbContext:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<AnnotationEntity>(x =>
{
x.Property(y => y.Value).IsRequired();
x.HasMany(p => p.AnnotationObjects).WithOne(p => p.Annotation)
.OnDelete(DeleteBehavior.Cascade)
.Metadata.PrincipalToDependent.SetPropertyAccessMode(PropertyAccessMode.Field);
});
modelBuilder.Entity<AnnotationObjectEntity>(x =>
{
x.HasKey(k => k.Id);
x.Property(p => p.Id).HasColumnName("Id");
x.HasOne(p => p.Annotation).WithMany(p => p.AnnotationObjects);
});
}
After reading the parent entity along with child entity and if I remove/update existing child and add new child and when I try to call save to database as shown below:
Repository:
annotationEntity.UpdateAnnotation(annotationToUpdate);
_context.Annotation.Attach(annotationEntity);
//var changes = _context.ChangeTracker.Entries().Where(x => x.Entity is AnnotationObjectEntity).Select(x => (AnnotationObjectEntity)x.Entity).ToList();
//foreach (var change in changes)
//{
// _context.Entry(change).State = EntityState.Added;
//}
await _context.SaveChangesAsync();
I get the following error:
Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.
Upon further analysis I found that this happens when I add new child objects to the parent object. And also I noticed that ChangeTracker has the Entity State as Modified for newly added entities. When I change it manually to Added. Then SaveChangesAsync() works.
Please assist on what I'm doing wrong and why ChangeTracker() wrongly detects the Added entity state as Modified.