У нас довольно сложная модель предметной области, и мы используем Entityframework Core в качестве ORM. Обновления всегда выполняются на объектах root. Если нам нужно добавить или обновить дочерний объект, мы загружаем сущность root, изменяем дочерние объекты и затем сохраняем сущность root. Аналогично этой части документа: https://docs.microsoft.com/en-us/ef/core/saving/disconnected-entities#mix -of-new-and-существующих-entity Мы используем GUID в качестве идентификаторов для сущностей, а идентификаторы генерируются базой данных на вставках!
Это работает довольно хорошо, но есть проблема, которую я не могу решить:
- Я хочу добавить новый элемент (типа GeneralElementTemplate) в root сущность типа StructureTemplate
- Я загружаю сущность StructureTemplate из БД со всеми подчиненными сущностями (в сущности root уже есть один элемент -> см. Скриншот № 1)
- Я создаю новый элемент (с именем elementTemplate)
- Я добавляю новый элемент в коллекцию Elements в сущности root (теперь две сущности находятся в коллекции Elements -> см. скриншот # 2)
- Я вызываю SaveChanges в DBContext
- Все отлично сохраняется
- Но теперь в коллекции Elements объекта root есть ТРИ сущности! Новая добавленная сущность дважды в коллекции (см. Снимок экрана № 3)!?
В базе данных (SQL Сервер) все вставляется / обновляется, как и ожидалось. После операции объект root имеет два элемента (а не три) ...
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();
Я уже пытался создать небольшой пример проекта, чтобы продемонстрировать это поведение, но с Пример проекта все работает отлично. Может кто-нибудь объяснить, что происходит?
Реализация StructureTemplate:
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:
publi c 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);
}
}
Конфигурация типа объекта для 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);
}
}
Конфигурация типа объекта для 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);
}
}
Снимок экрана сеанса отладки: one Element is in the root entity">