Reputation: 2318
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
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
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