IRepository.Create() not creating record.

Topics: Writing modules
Oct 22, 2013 at 11:12 PM
I have a complex relationship inside of a custom module that I'm attempting to update from my controller. Everything seems to update correctly at the parent level but some child records are not being created. For instance. updates at the Order level persist, but the OrderDetails do not get created. I'm curious if it is an order of operations issue in that I'm updating the Order first, then the OrderDetails, and last the OrderDetailQuantities? I have the same issue whether it is a create, update or delete for OrderDetails, basically nothing happens to the database. I haven't included all the code, just hopefully the relevant parts for you, but if I forgot to include something important please ask and I'll include it.

The data model is an Order has many OrderDetails and each OrderDetail can have many OrderDetailQuantities. An OrderDetailQuantity is a joining table between OrderDetail and Product.

Here's my migration.cs create code.
public int Create()
    {
      // Creating table ProductRecord
      SchemaBuilder.CreateTable("ProductRecord", table => table
        .ContentPartRecord()
        .Column("ProductNumber", DbType.String, col => col.WithLength(15))
        .Column("Name", DbType.String)
        .Column("Price", DbType.Decimal)
        .Column("Profit", DbType.Decimal)
        .Column("Url", DbType.String)
        .Column("CanOrder", DbType.Boolean)
        .Column("Color", DbType.String)
      );

      ContentDefinitionManager.AlterTypeDefinition("Product", builder =>
        builder.WithPart("CommonPart")
        .WithPart("ProductPart")
        .Creatable());

      SchemaBuilder.CreateTable("OrderRecord", table =>
        table
        .Column<int>("Id", col => col.PrimaryKey().Identity())
        .Column<string>("SalesMan")
        .Column<string>("GroupName")
        .Column<string>("PhoneNumber")
        .Column<string>("EmailAddress")
        .Column<int>("OrderStatus")
        );

      SchemaBuilder.CreateTable("OrderDetailRecord", table =>
        table
        .Column<int>("Id", col => col.PrimaryKey().Identity())
        .Column<int>("OrderRecord_Id")
        .Column<string>("SellersName")
        .Column<string>("CustomerName")
        );

      SchemaBuilder.CreateTable("OrderDetailQuantityRecord", table =>
        table
        .Column<int>("Id", col => col.PrimaryKey().Identity())
        .Column<int>("OrderDetailRecord_Id")
        .Column<int>("ProductRecord_Id")
        .Column<int>("Quantity")
        );

      SchemaBuilder.CreateTable("OrderSettingsRecord", table =>
        table.ContentPartRecord()
        .Column("OrderEmailAddress", DbType.String)
        .Column("ThankYouUrl", DbType.String)
        .Column("LogoUrl", DbType.String)
        .Column("EmailSignature", DbType.String));

      return 1;
    }
Model: Order Record
public class OrderRecord
  {
    public virtual int Id { get; set; }
    public virtual string SalesMan { get; set; }
    public virtual string GroupName { get; set; }
    public virtual string PhoneNumber { get; set; }
    public virtual string EmailAddress { get; set; }
    public virtual int OrderStatus { get; set; }
    
    [CascadeAllDeleteOrphan]
    public virtual ICollection<OrderDetailRecord> OrderDetailRecords { get; set; }

    public OrderRecord()
    {
      OrderDetailRecords = new List<OrderDetailRecord>();
    }
  }
Model: OrderDetail Record
  public class OrderDetailRecord
  {
    public virtual int Id { get; set; }
    public virtual string SellersName { get; set; }
    public virtual string CustomerName { get; set; }

    public virtual OrderRecord OrderRecord { get; set; }

    [CascadeAllDeleteOrphan]
    public virtual ICollection<OrderDetailQuantityRecord> ProductQuantities { get; set; }
  }
Model: OrderDetailQuantity Record
  public class OrderDetailQuantityRecord
  {
    public virtual int Id { get; set; }
    public virtual int Quantity { get; set; }

    public virtual ProductRecord ProductRecord { get; set; }
    
    public virtual OrderDetailRecord OrderDetailRecord { get; set; }

  }
Model: Product Record
    public virtual string ProductNumber { get; set; }
    public virtual string Name { get; set; }
    public virtual decimal Price { get; set; }
    public virtual decimal Profit { get; set; }
    public virtual string Url { get; set; }
    public virtual bool CanOrder { get; set; }
    public virtual string Color { get; set; }
I don't have a handler for the Orders because I'm controlling the generation of the records in the database directly.

My OrderController is using the constructor injection and has an Edit Action which can be called from the admin side, here's the code for the constructor and edit function.
    private readonly IRepository<OrderRecord> _orderRepository;
    private readonly IRepository<ProductRecord> _productRepository;
    private readonly IRepository<OrderDetailRecord> _detailRepository;
    private readonly IRepository<OrderDetailQuantityRecord> _detailQuantityRepository;
    private readonly IContentManager _contentManager;
    private readonly IOrchardServices _orchardServices;

    public OrderService(IRepository<OrderRecord> orderRepository, 
      IRepository<ProductRecord> productRepository,
      IRepository<OrderDetailRecord> detailRepository,
      IRepository<OrderDetailQuantityRecord> detailQuantityRepository,
      IContentManager contentManager,
      IOrchardServices orchardServices)
    {
      _orderRepository = orderRepository;
      _productRepository = productRepository;
      _detailRepository = detailRepository;
      _detailQuantityRepository = detailQuantityRepository;
      _contentManager = contentManager;
      _orchardServices = orchardServices;
    }

    [Admin]
    [HttpPost]
    public ActionResult Edit(OrderViewModel viewModel)
    {
      if (!ModelState.IsValid)
      {
        viewModel.ProductOptions = _orderService.GetForPurchaseProducts();
        return View(viewModel);
      }

      _orderService.UpdateOrder(viewModel);

      var indexViewModel = new OrdersIndexViewModel();
      indexViewModel.Orders = _orderRepository.Table.ToList();

      return View("Index", indexViewModel);
    }
And lastly here's my OrderService's UpdateOrder function. I've validated I'm in this method and hitting the proper create/update code logic. It's just when I call create I don't get a new OrderDetail record on the order.
public void UpdateOrder(ViewModels.OrderViewModel viewModel)
    {
      OrderRecord dbOrder = _orderRepository.Get(viewModel.OrderId);

      // no database order to update we are done
      if (dbOrder == null)
      {
        return;
      }

      dbOrder.GroupName = viewModel.GroupName;
      dbOrder.EmailAddress = viewModel.Email;
      dbOrder.SalesMan = viewModel.YourName;
      dbOrder.PhoneNumber = viewModel.YourPhone;

      _orderRepository.Update(dbOrder);

      var productOptions = _productRepository.Table.Where(prod => prod.CanOrder == true).ToList();

      // posted data updates
      foreach (OrderDetailViewModel detail in viewModel.Details)
      {
        // posted create
        if (detailIsValidForCreate(detail))
        {
          OrderDetailRecord detailRecord = new OrderDetailRecord();
          detailRecord.CustomerName = detail.CustomerName;
          detailRecord.SellersName = detail.SellersName;
          detailRecord.OrderRecord = dbOrder;
          _detailRepository.Create(detailRecord);

          detail.ProductOptions = productOptions;
          createDetailQuantities(detailRecord, detail);
        }
        else
        {
          OrderDetailRecord dbDetail = _detailRepository.Get(detail.OrderDetailId);

          if (string.IsNullOrEmpty(detail.SellersName) && string.IsNullOrEmpty(detail.CustomerName) && detail.OrderDetailId != 0)
          {
            _detailRepository.Delete(dbDetail);
          }
          else if (dbDetail != null)
          {
              
              dbDetail.SellersName = detail.SellersName;
              _detailRepository.Update(dbDetail); // this call breaks the operation

              detail.ProductOptions = productOptions;
              updateDetailQuantities(dbDetail, detail);
          }
        }
      }
    }
Coordinator
Oct 23, 2013 at 12:59 AM
I don't see a clear, immediate problem, but this would worry me: indexViewModel.Orders = _orderRepository.Table.ToList(); You never want to query the whole table.
Oct 23, 2013 at 5:59 AM
Edited Oct 23, 2013 at 6:01 AM
This looks like a non closed transaction pb ? Everything would get rolled back.
you should trace NH activity.
Oct 23, 2013 at 2:08 PM
I'm new to NHibernate but your thoughts prompted me to check the log folder and I did find an error. I'm guessing it is normally suppressed and just logged because I'm not seeing anything while debugging.

I'm including this chunk of code which is called from the service's update function. Am I doing something incorrectly with NHibernate inside of this chunk of code?
    private void createDetailQuantities(OrderDetailRecord detailRecord, OrderDetailViewModel detailViewModel)
    {
      foreach (OrderDetailQuantityViewModel qty in detailViewModel.ProductQuantities)
      {
        OrderDetailQuantityRecord detailQuantity = new OrderDetailQuantityRecord();
        detailQuantity.OrderDetailRecord = detailRecord;
        detailQuantity.Quantity = qty.Quantity;
        detailQuantity.ProductRecord = detailViewModel.ProductOptions.Where(opt => opt.Id == qty.ProductId).FirstOrDefault();

        _detailQuantityRepository.Create(detailQuantity);
      }
    }
Here's the error message, any thoughts on how I should be saving these changes off?

2013-10-22 17:11:43,938 [16] NHibernate.Transaction.ITransactionFactory - DTC transaction prepre phase failed
NHibernate.NonUniqueObjectException: a different object with the same identifier value was already associated with the session: 1, of entity: Orders.Models.OrderDetailQuantityRecord
at NHibernate.Engine.StatefulPersistenceContext.CheckUniqueness(EntityKey key, Object obj)
at NHibernate.Event.Default.DefaultSaveOrUpdateEventListener.PerformUpdate(SaveOrUpdateEvent event, Object entity, IEntityPersister persister)
at NHibernate.Event.Default.DefaultSaveOrUpdateEventListener.EntityIsDetached(SaveOrUpdateEvent event)
at NHibernate.Event.Default.DefaultSaveOrUpdateEventListener.PerformSaveOrUpdate(SaveOrUpdateEvent event)
at NHibernate.Event.Default.DefaultSaveOrUpdateEventListener.OnSaveOrUpdate(SaveOrUpdateEvent event)
at NHibernate.Impl.SessionImpl.FireSaveOrUpdate(SaveOrUpdateEvent event)
at NHibernate.Impl.SessionImpl.SaveOrUpdate(String entityName, Object obj)
at NHibernate.Engine.CascadingAction.SaveUpdateCascadingAction.Cascade(IEventSource session, Object child, String entityName, Object anything, Boolean isCascadeDeleteEnabled)
at NHibernate.Engine.Cascade.CascadeToOne(Object parent, Object child, IType type, CascadeStyle style, Object anything, Boolean isCascadeDeleteEnabled)
at NHibernate.Engine.Cascade.CascadeAssociation(Object parent, Object child, IType type, CascadeStyle style, Object anything, Boolean isCascadeDeleteEnabled)
at NHibernate.Engine.Cascade.CascadeProperty(Object parent, Object child, IType type, CascadeStyle style, Object anything, Boolean isCascadeDeleteEnabled)
at NHibernate.Engine.Cascade.CascadeCollectionElements(Object parent, Object child, CollectionType collectionType, CascadeStyle style, IType elemType, Object anything, Boolean isCascadeDeleteEnabled)
at NHibernate.Engine.Cascade.CascadeCollection(Object parent, Object child, CascadeStyle style, Object anything, CollectionType type)
at NHibernate.Engine.Cascade.CascadeAssociation(Object parent, Object child, IType type, CascadeStyle style, Object anything, Boolean isCascadeDeleteEnabled)
at NHibernate.Engine.Cascade.CascadeProperty(Object parent, Object child, IType type, CascadeStyle style, Object anything, Boolean isCascadeDeleteEnabled)
at NHibernate.Engine.Cascade.CascadeOn(IEntityPersister persister, Object parent, Object anything)
at NHibernate.Event.Default.AbstractFlushingEventListener.CascadeOnFlush(IEventSource session, IEntityPersister persister, Object key, Object anything)
at NHibernate.Event.Default.AbstractFlushingEventListener.PrepareEntityFlushes(IEventSource session)
at NHibernate.Event.Default.AbstractFlushingEventListener.FlushEverythingToExecutions(FlushEvent event)
at NHibernate.Event.Default.DefaultFlushEventListener.OnFlush(FlushEvent event)
at NHibernate.Impl.SessionImpl.Flush()
at NHibernate.Transaction.AdoNetWithDistributedTransactionFactory.DistributedTransactionContext.System.Transactions.IEnlistmentNotification.Prepare(PreparingEnlistment preparingEnlistment)
Oct 23, 2013 at 2:28 PM
Edited Oct 23, 2013 at 2:30 PM
Sorry but I can't be of any more help concerning NHibernate, it was just a logical thought concerning years on DB usage.
I don't know a lot on NH and the more I learn of it due to its intrication inside Orchard Core, the less I appreciate it (may be due to my lacking knwoledge), spending too much time trying to find the syntax to do requests I would create easilly joigning records in pure SQL (this tool is a complication layer over sql :) ).

I rarely use Records without parts, more often Parts without records :)

The trace could let think that the SQL DTC is running 2 transactions and there is a collision somewhere, I would say it is an NH bug....but again, not an expert
Oct 23, 2013 at 3:13 PM
Hey Christian, thanks for the response you've help change my perspective on the issue.

One thing that crossed my mind here is that I'm building the relationship in memory and calling update in a complex relationship. Specifically, I've updated the OrderDetailRecord object in the code before the createDetailQuantities() function call. Then I'm passing the OrderDetailRecord object to the helper function and attaching that object to the OrderDetailQuantity's OrderDetailRecord property before calling create on the OrderDetailQuantitiyRecord repository.

I'll try to re-get the OrderDetailRecord from the repository in the helper function so I'm not re-using an object and see if that works. Will let you know what I find.

Thanks,
Luke
Oct 23, 2013 at 3:21 PM
You're welcome. Interested following this.
Oct 23, 2013 at 3:28 PM
One question, why do you need this intermediary OrderDetailRecord ? Order->OrderLines isn't enougth to keep all your info ? Sometime doing some de-normalization is easier and faster ?
Oct 23, 2013 at 3:38 PM
Good Question. So I have a requirement that each Order (OrderRecord) can have multiple lines (OrderDetailRecord), and each line can have many Product + Quantity combinations (OrderDetailQuantityRecord). My products are content types inside of Orchard allowing us to add products flexibly using the orchard interface. Sorry, I hope I'm being clear?
Oct 23, 2013 at 3:42 PM
So I changed my code to retrieve the OrderDetail before trying to add new OrderDetailQuantity records but I'm still getting the same error. I'm going to try to create the details first and then attach them, though this seems backwards, but it's worth a shot.
    private void createDetailQuantities(OrderDetailRecord detailRecord, OrderDetailViewModel detailViewModel)
    {
      OrderDetailRecord dbDetail = _detailRepository.Get(detailRecord.Id);

      foreach (OrderDetailQuantityViewModel qty in detailViewModel.ProductQuantities)
      {
        OrderDetailQuantityRecord detailQuantity = new OrderDetailQuantityRecord();
        detailQuantity.OrderDetailRecord = dbDetail;
        detailQuantity.Quantity = qty.Quantity;
        detailQuantity.ProductRecord = detailViewModel.ProductOptions.Where(opt => opt.Id == qty.ProductId).FirstOrDefault();

        _detailQuantityRepository.Create(detailQuantity);
      }
    }
Oct 23, 2013 at 6:25 PM
Ok, I got it working. My assumption was I had to create the parent child relationship explicitly by creating the parent and making sure it had an ID before creating the child. With NHibernate it handles all that logic for you, kind of nice. So I removed all my create/update calls to the repositories and only modify the in memory items after retrieving the OrderRecord from the OrderRepository. Then at the end I call the OrderRepository.Update function and pass in the order.

Here's what the new code looks like.
public void UpdateOrder(ViewModels.OrderViewModel viewModel)
    {
      OrderRecord dbOrder = _orderRepository.Get(viewModel.OrderId);

      // no database order to update we are done
      if (dbOrder == null)
      {
        return;
      }

      var productOptions = _productRepository.Table.Where(prod => prod.CanOrder == true).ToList();

      // posted data updates
      foreach (OrderDetailViewModel detail in viewModel.Details)
      {
        // put the options on the detail for use below
        detail.ProductOptions = productOptions;

        // posted create
        if (detailIsValidForCreate(detail))
        {
          OrderDetailRecord detailRecord = new OrderDetailRecord();
          detailRecord.CustomerName = detail.CustomerName;
          detailRecord.SellersName = detail.SellersName;

          createDetailQuantitiesInMemory(detailRecord, detail);

          dbOrder.OrderDetailRecords.Add(detailRecord);
        }
        else if (detailIsValidForUpdate(detail))
        {
          // get the detail from the already loaded order
          OrderDetailRecord dbDetail = dbOrder.OrderDetailRecords.Where(dt => dt.Id == detail.OrderDetailId).FirstOrDefault();

          if (string.IsNullOrEmpty(detail.SellersName) && string.IsNullOrEmpty(detail.CustomerName) && detail.OrderDetailId != 0)
          {
            dbOrder.OrderDetailRecords.Remove(dbDetail);
          }
          else if (dbDetail != null)
          {
            dbDetail.SellersName = detail.SellersName;
            updateDetailQuantitiesInMemory(dbDetail, detail);
          }
        }
      }

      dbOrder.GroupName = viewModel.GroupName;
      dbOrder.EmailAddress = viewModel.Email;
      dbOrder.SalesMan = viewModel.YourName;
      dbOrder.PhoneNumber = viewModel.YourPhone;

      _orderRepository.Update(dbOrder);
    }

private void createDetailQuantitiesInMemory(OrderDetailRecord detailRecord, OrderDetailViewModel detailViewModel)
    {
      detailRecord.ProductQuantities = new List<OrderDetailQuantityRecord>();

      foreach (OrderDetailQuantityViewModel qty in detailViewModel.ProductQuantities)
      {
        OrderDetailQuantityRecord detailQuantity = new OrderDetailQuantityRecord();
        detailQuantity.Quantity = qty.Quantity;
        detailQuantity.ProductRecord = detailViewModel.ProductOptions.Where(opt => opt.Id == qty.ProductId).FirstOrDefault();

        detailRecord.ProductQuantities.Add(detailQuantity);
      }
    }

    private void updateDetailQuantitiesInMemory(OrderDetailRecord dbDetail, OrderDetailViewModel detailViewModel)
    {
      foreach (var qty in detailViewModel.ProductQuantities)
      {
        if (qty.OrderDetailQuantityId == 0)
        {
          OrderDetailQuantityRecord dbQty = new OrderDetailQuantityRecord()
          {
            ProductRecord = detailViewModel.ProductOptions.Where(prod => prod.Id == qty.ProductId).FirstOrDefault(),
            Quantity = qty.Quantity
          };

          dbDetail.ProductQuantities.Add(dbQty);
        }
        else
        {
          var dbQty = dbDetail.ProductQuantities.Where(qt => qt.Id == qty.OrderDetailQuantityId).FirstOrDefault();
          dbQty.Quantity = qty.Quantity;
        }
      }
    }
Marked as answer by luke84 on 10/25/2013 at 9:07 AM