Добавление элементов в коллекцию с использованием Entity Framework

Я пытаюсь следовать шаблону репозитория DDD с Entity Framework 4. Но у меня возникают проблемы с сохранением изменений в свойствах коллекции моих совокупных корней. Рассмотрим мои классы ниже. Элемент - это мой агрегированный корень, который содержит коллекцию объектов SubItem.

public class Item
{
    public int ItemId { get; set; }
    public string Name { get; set; }
    public ICollection<SubItem> SubItems { get; private set; }

    public Item()
    {
        this.SubItems = new HashSet<SubItem>();
    }
}

public class SubItem
{
    public int ItemId { get; set; }
    public int SubItemId { get; set; }
    public string Name { get; set; }
}

Далее я определил интерфейс репозитория для моего сводного корневого класса

public interface IItemRespository
{
    Item Get(int id);
    void Add(Item i);
    void Save(Item i);
}

Теперь вот мой класс DbContext, который настраивает отображение EF.

public class ItemContext : System.Data.Entity.DbContext
{
    protected override void OnModelCreating(System.Data.Entity.DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Item>().HasKey(i => i.ItemId);
        modelBuilder.Entity<Item>().Property(i => i.Name);

        modelBuilder.Entity<Item>().HasMany(i => i.SubItems)
            .WithRequired()
            .HasForeignKey(si => si.ItemId);

        modelBuilder.Entity<SubItem>().HasKey(i => i.SubItemId);
        modelBuilder.Entity<SubItem>().Property(i => i.Name);
    }
}

Наконец, здесь моя реализация IRepository с использованием DBContext

public class Repository : IItemRespository
{
    public void Save(Item i)
    {
        using (var context = new ItemContext())
        {
            context.Set<Item>().Attach(i);
            context.SaveChanges();
        }
    }

    public Item Get(int id)
    {
        using (var context = new ItemContext())
        {
            var result = (from x in context.Set<Item>() where x.ItemId == id select x).FirstOrDefault();
            return result;
        }
    }

    public void Add(Item i)
    {
        using (var context = new ItemContext())
        {
            context.Set<Item>().Add(i);
            context.SaveChanges();
        }
    }
}

Следующий код создает новый элемент, добавляет его в репозиторий, добавляет новые SubItems, а затем сохраняет изменения.

IItemRespository repo = new Repository();

//Create a new Item
Item parent = new Item() { Name = "Parent" };
repo.Add(parent);

//A long period of time may pass .. . .

//later add sub items
parent.SubItems.Add(new SubItem() { Name = "Child 1" });
parent.SubItems.Add(new SubItem() { Name = "Child 2" });   
parent.SubItems.Add(new SubItem() { Name = "Child 3" });

//save the added sub items
repo.Save(parent);

Я получаю следующее исключение, когда метод Save() пытается привязать элемент к контексту.

Нарушение ограничения ссылочной целостности произошло: свойство значения, определяющие ссылочные ограничения, несовместимы между главными и зависимыми объектами в отношении.

Я понял, что создаю новый контекст для каждого метода в репозитории. Это намеренно. Длительный период времени может проходить между тем, когда элемент добавляется, а затем редактируется, и я не хочу, чтобы соединение контекста или базы данных было открыто в течение всего времени.

Теперь, если я добавлю новый элемент во второй контекст перед добавлением подпунктов, как в приведенном ниже коде, он работает.

//Create a new item
Item newItem = new Item() { Name = "Parent" };

using (ItemContext context1 = new ItemContext())
{
    //Create a new aggrgate
    context1.Set<Item>().Add(newItem);
    context1.SaveChanges();
}

//Long period of time may pass

using (ItemContext context2 = new ItemContext())
{
    context2.Set<Item>().Attach(newItem);

    newItem.Name = "Edited Name";
    newItem.SubItems.Add(new SubItem() { Name = "Child 1" });
    newItem.SubItems.Add(new SubItem() { Name = "Child 2" });
    newItem.SubItems.Add(new SubItem() { Name = "Child 3" });

    context2.SaveChanges();
}

Однако, если я хочу быть верным шаблону репозитория, код, редактирующий элемент, не должен знать ничего о том, как работает репозиторий или класс ItemContext. Он должен просто иметь возможность вносить изменения в совокупный корневой объект, а затем сохранять эти изменения с помощью метода репозитория Save().

Итак, как мне изменить метод Save(), чтобы изменения в Item.SubItems были сохранены правильно?

Ответ 1

Вам нужно будет помочь EF, установив некоторые свойства, чтобы заставить это работать.

Когда вы создаете новый подэлемент, вам нужно самому установить FK:

   parent.SubItems.Add(new SubItem() { Name = "Child 1", ItemId = parent.ItemId});
   parent.SubItems.Add(new SubItem() { Name = "Child 2", ItemId = parent.ItemId });
   parent.SubItems.Add(new SubItem() { Name = "Child 3", ItemId = parent.ItemId });

И затем в вашей функции сохранения добавьте или добавьте элементы в свой контекст:

    public void Save(Item i)
    {
        using (var context = new ItemContext())
        {
            foreach (var subitem in i.SubItems)
            {
                if (subitem.SubItemId == 0)
                    context.Set<SubItem>().Add(subitem);
                else
                    context.Set<SubItem>().Attach(subitem);
            }
            context.Set<Item>().Attach(i);
            context.SaveChanges();
        }
    }

Причина в том, что ваша сущность не привязана к контексту, когда вы выполняете прикрепление, EF фактически не знает, откуда взялись сущности, - он думал, что FK не был установлен (возможно, 0) действительное состояние - от которого исходит ваша ошибка. Причина, по которой вам нужно сначала присоединить дочерние объекты, так это то, что вы можете добавлять, а не присоединяться. Опять же, поскольку ваш контекст не был жив, когда был добавлен подэлемент, EF не уверен, откуда пришел объект, и предполагает, что 0 PK верен, создавая вашу ошибку.

Ответ 2

Model

public virtual ICollection<CostCenter> CostCenters { get; set; }

[NotMapped]
public int CostCenterId { get; set; }//Just to get item in view

Создать

public ActionResult Create()
{
    ViewBag.CostCenterId = new SelectList(db.CostCenters, "Id", "Name");
    return View();
}

Создать сообщение

public ActionResult Create(Campaign campaign)
{
    ...
    campaign.CostCenters.Add(db.CostCenters.FirstOrDefault(f => f.Id == campaign.CostCenterId);
    return RedirectToAction("Index");
}

Вид

<%: Html.LabelFor(model => model.CostCenterId ) %>
<div class="input-control">
    <%: Html.DropDownList("CostCenterId ", String.Empty) %>
    <%: Html.ValidationMessageFor(model => model.CostCenterId ) %>
</div>