Dezzamondo
Dezzamondo

Reputation: 2318

MVC5 EF Entity apparently saving, but is null when retrieved

I have an Account class that holds a list of Payments, which holds a list of Products that are Pending, Sold or Refunded.

When selecting the Products to pay for, I can add them into a Payment object and then save the whole object to an Account.

However, when I retrieve the account in the Pay action, Account.Payments is null.

Here is the AddProduct action:

public ActionResult AddProduct(List<Product> products)
{
    //var acc = Account.FetchAccountFromIdentity(User.Identity.GetUserName());
    var username = User.Identity.GetUserName();
    var acc = db.Accounts.First(u => u.EmailAddress == username);
    var payment = new Payment();

    foreach (var product in products)
    {
        if (product.IsSelected)
        {
            var tempProduct = db.Products.Find(product.ProductID);
            payment.Products.Add(tempProduct);
            payment.PaymentStatus = enPaymentStatus.Pending;
            payment.Gross += tempProduct.Price;
            payment.Vat += tempProduct.Price * 0.2;
            payment.Net += payment.Gross - payment.Vat;
         }
    }
    payment.AccountID = acc;
    if (acc.Payments == null) acc.Payments = new List<Payment>();
    acc.Payments.Add(payment);
    db.SaveChanges();

    return RedirectToAction("Pay", "Products", new {id = acc.AccountID} );
}

acc.Payments is correctly populated with any relevant payments at this point.

And then the Pay action:

    public ActionResult Pay(int? id)
    {
        var acc = db.Accounts.Find(id);
        var data = new AccountPaymentView
        {
            Account =  acc,
            Payment = acc.Payments.First(p => p.PaymentStatus == enPaymentStatus.Pending)
        };

        return View(data);
    }

acc.Payments is now null!?

Any idea as to what I must be doing wrong?

Edit Now with Account Class:

    public class Account
    {
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int AccountID { get; set; }
        [Display(Name = "Title")]
        public string Salutation { get; set; }
        [Required]
        [Display(Name = "First Name")]
        public string FirstName { get; set; }
        [Required]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }
        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public string Password { get; set; }
        [Required]
        [DataType(DataType.Password)]
        [Compare("Password", ErrorMessage = "Passwords do not match. Please try again.")]
        [Display(Name = "Confirm Password")]
        public string ConfirmPass { get; set; }
        [Required]
        [DataType(DataType.EmailAddress)]
        [Display(Name = "Email Address")]
        public string EmailAddress { get; set; }
        public virtual ICollection<Address> Addresses { get; set; }
        public virtual ICollection<Payment> Payments { get; set; }
        public DateTime CreatedOn { get; set; }
        public virtual  ICollection<DateTime> VistsList { get; set; }
        public bool IsDeleted { get; set; }
        public enAccountType AccountType { get; set; }
}

Edit 2 Here is the OnModelCreating method on the DBContext (Removed unrelated parts). I've got to me missing something glaringly obvious?

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);

    modelBuilder.Properties<DateTime>()
        .Configure(c => c.HasColumnType("datetime2"));

    modelBuilder.Entity<Account>().HasOptional(x => x.Addresses).WithMany();
    modelBuilder.Entity<Account>().HasOptional(x => x.Payments).WithMany();
    modelBuilder.Entity<Payment>().HasOptional(x => x.Products).WithMany();
    modelBuilder.Entity<Payment>().HasOptional(x => x.AccountID);
    modelBuilder.Entity<Payment>().HasKey(x => x.PaymentID);

}

Edit 3 Checked out the Account.FetchAccountFromIdentity() and it does open a new context. So that has now been removed and I now use a context within the AddProduct method (code updated above).

Now I am getting the following error when saving!!!

An object of type 'System.Collections.Generic.List`1[[PPPv2.Models.Payment, PPPv2,
Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]' 
cannot be set or removed from the Value property of an EntityReference of type 'PPPv2.Models.Payment'.

I've tried forcing the PK relationships in the OnModelCreating method, but it didn't seem to help.

For clarity, here is the Payment class:

public class Payment
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int PaymentID { get; set; }
    public virtual Account AccountID { get; set; }
    public virtual ICollection<Product> Products { get; set; }
    public double Gross { get; set; }
    public double Net { get; set; }
    public double Vat { get; set; }
    public string TransactionCode { get; set; }
    public enPaymentStatus PaymentStatus { get; set; }
    public bool IsDeleted { get; set; }
    public DateTime PaidOn { get; set; }

    public Payment()
    {
        Products = new List<Product>();
        Gross = 0.0;
        Net = 0.0;
        Vat = 0.0;
        IsDeleted = false;
        PaidOn = new DateTime(2000, 1, 1, 0, 0,0);
    }

}

Edit 4 As requested, here is the Product model:

public class Product 
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int ProductID { get; set; }
    public string Name   { get; set; }
    public string Description { get; set; }
    [DataType(DataType.Currency)]
    [DisplayFormat(DataFormatString = "£{0:#,###0.00}")]
    public double Price { get; set; }
    [DataType(DataType.Currency)]
    [Display(Name = "Total Estimated Legal Cost")]
    [DisplayFormat(DataFormatString = "£{0:#,###0.00}")]
    public double EstimatedTotalLegalCost { get; set; }
    public virtual ICollection<Overview> Overviews { get; set; }
    public virtual ICollection<Process> Processes { get; set; }
    public DateTime CreatedOn { get; set; }
    public DateTime LastUpdated { get; set; }
    public bool IsSelected { get; set; }
    public bool IsDeleted { get; set; }
}

Upvotes: 4

Views: 1426

Answers (2)

nick_w
nick_w

Reputation: 14948

If you look at the tables that the Entity Framework has generated, you'll see there's only three of them. Given your data model, there should be junction tables that map Account and Payment objects to one another (same goes for Payment and Product, and you'll likely need to do the same for the other collections you have).

The exception you are getting regarding being unable to cast a List of Products to a single one is because your data model indicated that Products was a collection, yet the database table was actually storing this as foreign-key (i.e., single entity) relationship between Payment and Product.

If I change the code in the OnModelCreating method to the following, your AddProducts method works as intended.

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);

    modelBuilder.Properties<DateTime>().Configure(c => c.HasColumnType("datetime2"));

    modelBuilder.Entity<Account>().HasMany(x => x.Addresses)
        .WithMany().Map(t => t.ToTable("AccountAddresses"));
    modelBuilder.Entity<Account>().HasMany(x => x.Payments)
        .WithMany().Map(t => t.ToTable("AccountPayments"));
    modelBuilder.Entity<Payment>().HasMany(x => x.Products)
        .WithMany().Map(t => t.ToTable("PaymentProducts"));
    modelBuilder.Entity<Payment>().HasOptional(x => x.AccountID);
    modelBuilder.Entity<Payment>().HasKey(x => x.PaymentID);
}

The change was going from code like:

modelBuilder.Entity<Payment>().HasOptional(x => x.Products).WithMany();

To:

modelBuilder.Entity<Payment>().HasMany(x => x.Products).WithMany().Map(t => t.ToTable("PaymentProducts"));

Doing so instructs Entity Framework to create the junction tables that allow a Payment to hold a collection of Products.

Upvotes: 3

Shriike
Shriike

Reputation: 1341

When creating your payment you should save it directly to the database. I don't think adding it to your account object would work because it's an entity that isn't being tracked by EF yet.

public ActionResult AddProduct(List<Product> products)
{
//var acc = Account.FetchAccountFromIdentity(User.Identity.GetUserName());
var username = User.Identity.GetUserName();
var acc = db.Accounts.First(u => u.EmailAddress == username);
var payment = new Payment();

foreach (var product in products)
{
    if (product.IsSelected)
    {
        var tempProduct = db.Products.Find(product.ProductID);
        payment.Products.Add(tempProduct);
        payment.PaymentStatus = enPaymentStatus.Pending;
        payment.Gross += tempProduct.Price;
        payment.Vat += tempProduct.Price * 0.2;
        payment.Net += payment.Gross - payment.Vat;
     }
}
payment.AccountID = acc.AccountID;
db.Payments.Add(payment);
db.SaveChanges();

return RedirectToAction("Pay", "Products", new {id = acc.AccountID} );
}

Upvotes: 0

Related Questions