We have a quite complex domain model and we are using Entityframework Core as ORM. Updates are always performed on the root entities. If we need to add or update an child object, we load the root entity, modify the childs and then save the root entity. Similar to this part of the docu: https://docs.microsoft.com/en-us/ef/core/saving/disconnected-entities#mix-of-new-and-existing-entities We are using GUIDs as Ids for the entities and the Ids are generated by the database on inserts!
That works quite well but there is a problem which I can't resolve:
- I want to add a new element (of Type GeneralElementTemplate) to the root entity of type StructureTemplate
- I load the StructureTemplate entity from the DB with all sub entities (there is already one Element in the root entity --> see screenshot #1)
- I create the new element (named elementTemplate)
- I add the new element to the Elements collection in the root entity (now two entities are in the Elements collection --> see screenshot #2)
- I invoke SaveChanges on the DBContext
- Everything is saved fine
- But there are now THREE entities in the Elements collection of the root entity! The new added entity is twice in the collection (see screenshot #3)!?
In the database (SQL Server) everything is insert/updated as expected. After the operation the root object has two elements (and not three)...
GeneralElementTemplate elementTemplate = new GeneralElementTemplate(ElementTemplateType.Line); StructureTemplate structureTemplate = DbContext.StructureTemplates .Include(x => x.Elements).ThenInclude(e => e.Attributes) .Include(x => x.Elements).ThenInclude(e => e.Groups) .Include(x => x.Elements).ThenInclude(e => e.Materials) .Include(x => x.Elements).ThenInclude(e => e.Points) .Include(x => x.Elements).ThenInclude(e => e.Sections) .Where(b => b.Id == structureTemplateId) .SingleOrDefault(); if (structureTemplate == null) { return NotFound(); } structureTemplate.AddElementTemplate(elementTemplate); DbContext.SaveChanges();
I tried already to build a small sample project to demonstrate that behavior but with the sample project everything is working fine. Can somebody explain what's going on?
StructureTemplate implementation:
public class StructureTemplate : Document<StructureTemplate>
{
private HashSet<GeneralElementTemplate> _elements = new HashSet<GeneralElementTemplate>();
private HashSet<StructureTemplateTag> _structureTemplateTags = new HashSet<StructureTemplateTag>();
public StructureTemplate(
DocumentHeader header,
uint versionNumber = InitialLabel,
IEnumerable<GeneralElementTemplate> elements = null)
: base(header, versionNumber)
{
_elements = (elements != null) ? new HashSet<GeneralElementTemplate>(elements) : new HashSet<GeneralElementTemplate>();
}
/// <summary>
/// EF Core ctor
/// </summary>
protected StructureTemplate()
{
}
public IReadOnlyCollection<GeneralElementTemplate> Elements => _elements;
public IReadOnlyCollection<StructureTemplateTag> StructureTemplateTags => _structureTemplateTags;
public override IReadOnlyCollection<Tag> Tags => _structureTemplateTags.Select(x => x.Tag).ToList();
public void AddElementTemplate(GeneralElementTemplate elementTemplate)
{
CheckUnlocked();
_elements.Add(elementTemplate);
}
public override void AddTag(Tag tag) => _structureTemplateTags.Add(new StructureTemplateTag(this, tag));
public void RemoveElementTemplate(Guid elementTemplateId)
{
CheckUnlocked();
var elementTemplate = Elements.FirstOrDefault(x => x.Id == elementTemplateId);
_elements.Remove(elementTemplate);
}
public override void RemoveTag(Tag tag)
{
var existingEntity = _structureTemplateTags.SingleOrDefault(x => x.TagId == tag.Id);
_structureTemplateTags.Remove(existingEntity);
}
public void SetPartTemplateId(Guid? partTemplateId)
{
CheckUnlocked();
PartTemplateId = partTemplateId;
}
}
GeneralElementTemplate implementation:
public class GeneralElementTemplate : Entity { private HashSet _attributes = new HashSet(); private HashSet _groups = new HashSet(); private HashSet _materials = new HashSet(); private HashSet _points = new HashSet(); private HashSet _sections = new HashSet();
public GeneralElementTemplate(
ElementTemplateType type,
IEnumerable<NamedPointReference> points = null,
IEnumerable<NamedSectionReference> sections = null,
IEnumerable<NamedMaterialReference> materials = null,
IEnumerable<NamedGroupReference> groups = null,
IEnumerable<NamedAttributeReference> attributes = null)
: base()
{
Type = type;
_points = points != null ? new HashSet<NamedPointReference>(points) : new HashSet<NamedPointReference>();
_sections = sections != null ? new HashSet<NamedSectionReference>(sections) : new HashSet<NamedSectionReference>();
_materials = materials != null ? new HashSet<NamedMaterialReference>(materials) : new HashSet<NamedMaterialReference>();
_groups = groups != null ? new HashSet<NamedGroupReference>(groups) : new HashSet<NamedGroupReference>();
_attributes = attributes != null ? new HashSet<NamedAttributeReference>(attributes) : new HashSet<NamedAttributeReference>();
}
/// <summary>
/// EF Core ctor
/// </summary>
protected GeneralElementTemplate()
{
}
public IReadOnlyCollection<NamedAttributeReference> Attributes => _attributes;
public IReadOnlyCollection<NamedGroupReference> Groups => _groups;
public IReadOnlyCollection<NamedMaterialReference> Materials => _materials;
public IReadOnlyCollection<NamedPointReference> Points => _points;
public IReadOnlyCollection<NamedSectionReference> Sections => _sections;
public ElementTemplateType Type { get; private set; }
public virtual GeneralElementTemplate Reincarnate()
{
return new GeneralElementTemplate(
Type,
Points,
Sections,
Materials,
Groups,
Attributes);
}
}
Entity Type Configuration for StructureTemplate:
public class StructureTemplateTypeConfiguration : IEntityTypeConfiguration<StructureTemplate>
{
public void Configure(EntityTypeBuilder<StructureTemplate> builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
builder
.Property(e => e.Id)
.ValueGeneratedOnAdd();
builder
.OwnsOne(e => e.Header, headerBuilder =>
{
headerBuilder
.Property(e => e.Name)
.HasConversion<string>(x => x, x => EntityName.ToEntityName(x))
.HasMaxLength(EntityName.NameMaxLength)
.IsUnicode(false);
headerBuilder
.Property(e => e.Descriptions)
.HasConversion(
d => JsonConvert.SerializeObject(d.ToStringDictionary()),
d => d == null
? TranslationDictionary.Empty
: JsonConvert.DeserializeObject<Dictionary<EntityLang, string>>(d).ToTranslationDictionary())
.HasMaxLength((int)TranslatedEntry.EntryMaxLength * (Enum.GetValues(typeof(EntityLang)).Length + 1));
});
builder
.Property(e => e.VersionNumber);
builder
.HasMany(e => e.Elements)
.WithOne();
builder.Metadata.FindNavigation(nameof(StructureTemplate.Elements)).SetPropertyAccessMode(PropertyAccessMode.Field);
// TAGS
builder
.Ignore(e => e.Tags);
builder
.HasMany(e => e.StructureTemplateTags);
builder.Metadata
.FindNavigation(nameof(StructureTemplate.StructureTemplateTags))
.SetPropertyAccessMode(PropertyAccessMode.Field);
}
}
Entity Type Configuration for StructureTemplateElement:
public class StructureElementTemplateTypeConfiguration : IEntityTypeConfiguration<GeneralElementTemplate>
{
public void Configure(EntityTypeBuilder<GeneralElementTemplate> builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
builder.ToTable("StructureTemplateElements");
builder
.Property(e => e.Id)
.ValueGeneratedOnAdd();
builder
.Property(e => e.Type);
builder
.HasMany(e => e.Attributes)
.WithOne();
builder.Metadata.FindNavigation(nameof(GeneralElementTemplate.Attributes)).SetPropertyAccessMode(PropertyAccessMode.Field);
builder
.HasMany(e => e.Groups)
.WithOne();
builder.Metadata.FindNavigation(nameof(GeneralElementTemplate.Groups)).SetPropertyAccessMode(PropertyAccessMode.Field);
builder
.HasMany(e => e.Materials)
.WithOne();
builder.Metadata.FindNavigation(nameof(GeneralElementTemplate.Materials)).SetPropertyAccessMode(PropertyAccessMode.Field);
builder
.HasMany(e => e.Points)
.WithOne();
builder.Metadata.FindNavigation(nameof(GeneralElementTemplate.Points)).SetPropertyAccessMode(PropertyAccessMode.Field);
builder
.HasMany(e => e.Sections)
.WithOne();
builder.Metadata.FindNavigation(nameof(GeneralElementTemplate.Sections)).SetPropertyAccessMode(PropertyAccessMode.Field);
}
}
DbContext.StructureTemplates
for given key should exits how many times. if you modifystructureTemplate
statement toToList()
instead ofSingleOrDefault()
how many should you get. what im seeing is you have an entity with the same key.... if you trying to update it, you should not be using ADD or well i cant see the code for ADD so have no idea what its doing. aka i can not see how GeneralElementTemplate maps to the entity.I want to add a new element (of Type GeneralElementTemplate) to the root entity of type StructureTemplate
sounds fishy looking what what code i can see – SeabizkitstructureTemplate.AddElementTemplate(elementTemplate);
i want to see whatAddElementTemplate
is doing. also add the code for class,StructureTemplate
, it will help in trying to understand what is going on. The other code you added is cool but haven't gone over it yet as for now it not what i need to see. Add code for StructureTemplate and AddElementTemplate, as those are the key parts in scenario. – Seabizkit