MVC Edit action does not save related data - c#

I have a collection related to an class but I cannot save them to a database. Other members are saved successfully but collection not.
public ActionResult Edit([Bind(Include = "ProductTypeId,Name,IsActive")] ProductType productType, string[] chkAttributeCategory)
{
productType.AttributeCategories.Clear();
if (chkAttributeCategory != null)
{
foreach (string attributeCategory in chkAttributeCategory)
{
productType.AttributeCategories.Add(db.AttributeCategory.Find(int.Parse(attributeCategory)));
}
}
if (ModelState.IsValid)
{
db.Entry(productType).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(productType);
}
I have a checkbox list on my view whick represents a list of child objects. I've checked the productType object before save changes line in debuger and it contains everything he needs, but associated AttributeCategories are not saved in the database.
Somebody has idea ?

Since you are updating an entity from a disconnected object, the change tracker will not work, changes to non scalar properties will not work.
If you do like this, it should work.
var productTypeDb = db.Set<ProductType>()
.Include(pt => pt.AttributeCategories)
.FirstOrDefault(pt => pt.ProductTypeId == productType.ProductTypeId)
productTypeDb.AttributeCategories.Clear();
if (chkAttributeCategory != null)
{
foreach (string attributeCategory in chkAttributeCategory)
{
productTypeDb.AttributeCategories.Add(db.AttributeCategory
.Find(int.Parse(attributeCategory)));
}
}
productTypeDb.Name = productType.Name;
productTypeDb.IsActive = productType.IsActive;
// other properties
db.SaveChanges();
There is another way to manage the state manually to be able to work with disconnected object, but if that's not mandatory, you can do like above code.

I think you can't have a list or an array in a model that's not a view model, I think you need to have a separate model with each product type linked to attributes:
//model1
public class ProductTypeCategory{
public int ProductTypeID {get; set;}
public int CategoryID {get; set;}
}
//model2
public class Category{
public int ID {get; set;}
public string Name {get; set;}
}
now you can get the attributes this way:
List<Category> attributes = (from p in db.ProductTypeCategories from c in db.Categories where p.ProductTypeID == productTypeID && p.CategoryID == c.ID select c).toList();
just input the id of the product type in the above LINQ call an you'll get its categories

Related

Many-to-many ICollection is still empty even though data has been inserted

I have two entities, one is Product and looks something like this:
[Key]
public int ID {get; set;}
public virtual ICollection<Category> Categories { get; set; }
and the other is Category and looks like this:
[Key]
public int ID {get; set;}
[JsonIgnore]
public virtual ICollection<Product> Products { get; set; }
Both objects are simplified massively here.
My insert method looks like this:
public static Product AddProduct(ProductDTO product)
{
using var context = new ProjectDbContext();
Product newProduct = Product.ConvertDTO(product);
var contr = context.Products;
contr.Add(newProduct);
context.SaveChanges();
if (product.Categories != null && product.Categories.Count() > 0)
{
var list = from r in context.categories
where product.Categories.Contains(r.ID)
select r;
newProduct.Categories = list.ToList();
}
contr.Update(newProduct);
context.SaveChanges();
return newProduct;
}
The ProductDTO is just an object that has the product data and a list of category ids.
The product is inserted and the data is also written into the generated connection table inside the database. However when I now try to get the inserted product, its categories are null, even though it should have three category objects.
Ok, I think I found a solution. I forgot to eager load, when getting the entity:
contr = context.Products.Include(x => x.Categories).Where(x=> x.ID == id).FirstOrDefault();

How to write T-SQL many-to-many with subquery in EF

I have two classes with a many-to-many relationship in a ASP.NET EF application. I'm trying to find all Listings that have any Categories which is posted from a view. The categories are checkboxes on the view form.
These are the classes with navigation properties simplified for example:
public class Listing
{
public int ID { get; set; }
public ICollection<Category> Categories { get; set; }
...
}
public class Category
{
public int ID { get; set; }
public ICollection<Listing> Listings { get; set; }
...
}
// this is the join table created by EF code first for reference
public class CategoryListings
{
public int Category_ID { get; set; }
public int Listing_ID { get; set; }
}
This is the query I am trying to use in my MVC Controller but it doesn't work and I don't really know what else to try:
if (model.Categories !=null && model.Categories.Any(d => d.Enabled))
{
List<Listing> itemsSelected = null;
foreach (var category in model.Categories.Where(d => d.Enabled))
{
var itemsTemp = items.Select(x => x.Categories.Where(d => d.ID == category.ID));
foreach (var item1 in itemsTemp)
{
itemsSelected.Add((Listing)item1); //casting error here
}
}
items = itemsSelected;
}
In SQL, I would write this using a subquery (the subquery represents the multiple categories that can be searched for):
select l.id, cl.Category_ID
from
listings as l inner join CategoryListings as cl
on l.id=cl.Listing_ID
inner join Categories as c on c.ID = cl.Category_ID
where c.id in (select id from Categories where id =1 or id=3)
How do I write that SQL query in EF using navigators or lambda? The subquery in the SQL will change each search and can be any id or IDs.
You forgot to tell us what objects are in your collection items. I think they are Listings. Your case doesn't work, because itemsTemp is a collection of Categories, and every item1 is a Category, which of course can't be cast to a Listing.
Advice: to debug casting problems, replace the word var
with the type you actually expect. The compiler will warn you about
incorrect types. Also use proper identifiers in your lambda expressions.
This makes them easier to read
IQueryable<???> items = ... // collection of Listings?
List<Listing> itemsSelected = null;
IQueryable<Category> enabledCategories = model.Categories.Where(category => category.Enabled));
foreach (Category category in enabledCategories)
{
IEnumerable<Category> itemsTemp = items
.Select(item => item.Categories
.Where(tmpCategory => tmpCategory.ID == category.ID));
foreach (Category item1 in itemsTemp)
{
// can't cast a Category to a Listing
We'll come back to this code later.
If I look at your SQL it seems that you want the following:
I have a DbContext with (at least) Listings and Categories.
I want all Listings with their Categories that have category Id 1 or 3
It's good to see that you followed the entity framework code-first conventions, however you forgot to declare your collections virtual:
In entity framework the columns in a table are represented by
non-virtual properties. The virtual properties represent the relations
between the table.
With a slight change your many-to-many relation can be detected automatically by entity framework. Note the virtual before the ICollection
class Listing
{
public int ID { get; set; }
// every Listing has zero or more categories (many-to-many)
public virtual ICollection<Category> Categories { get; set; }
...
}
class Category
{
public int ID { get; set; }
// every Category is used by zero or more Listings (many-to-many)
public ICollection<Listing> Listings { get; set; }
...
public bool Enabled {get; set;}
}
And the DbContext
public MyDbContext : DbContext
{
public DbSet<Listing> Listings {get; set;}
public DbSet<Category> Categories {get; set;}
}
Although a relational database implements a many-to-many relationship with a junction table, you don't need to declare it in your DbContext. Entity framework detects that you want to design a many-to-many and creates the junction table for you.
But how can I perform my joins without access to the junction table?
Answer: Don't do joins, use the ICollections!
Entity Framework knows which inner joins are needed and will do the joins for you.
Back to your SQL code:
Give me all (or some) properties of all Listings that have at least one Category with Id equal to 1 or 3
var result = myDbcontext.Listings
.Select(listing => new
{ // select only the properties you plan to use
Id = listing.Id,
Name = listing.Name,
...
Categories = listing.Categories
// you don't want all categories, you only want categories with id 1 or 3
.Where(category => category.Id == 1 || category.Id == 3)
.Select(category => new
{
// again select only the properties you plan to use
Id = category.Id,
Enabled = category.Enabled,
...
})
.ToList(),
})
// this will also give you the Listings without such Categories,
// you only want Listings that have any Categories left
.Where(listing => listing.Categories.Any());
One of the slower parts of database queries is the transfer of the selected data from the DBMS to your local process. Hence it is wise to only transfer the properties you actually plan to use. For example, you won't need the foreign keys of one-to-many relationships, you know it equals the Id value of the one part in the one-to-many.
Back to your code
It seems to me, that your items are Listings. In that case your code wants all Listings that have at least one enabled Category
var result = myDbContext.Listings
.Where(listing => ...) // only if you don't want all listings
.Select(listing => new
{
Id = listing.Id,
Name = list.Name,
Categories = listing.Categories
.Where(category => category.Enabled) // keep only the enabled categories
.Select(category => new
{
Id = category.Id,
Name = category.Name,
...
})
.ToList(),
})
// this will give you also the Listings that have only disabled categories,
// so listings that have any categories left. If you don't want them:
.Where(listing => listing.Categories.Any());
Do you have a relation between Listing/Category and CategoryListings?
Here is example for EF 6: http://www.entityframeworktutorial.net/code-first/configure-many-to-many-relationship-in-code-first.aspx
If you have it the query will be simple, something like that:
CategoryListing.Where(cl => new List<int>{1, 3}.Contains(cl.CategoryRefId))
.Select(x => new {x.ListingRefId, x.CategoryRefId});
If you need all properties of Listing or Category, Include() extension will help.

Updating related Data with Entity Framework 6 in MVC 5

Using EntityFramework 6, I would like to update Customer in the following scenario:
public class Customer
{
public int Id {get; set;}
// 100 more scalar properties
public virtual IList<Consultant> Consultants {get;set;}
}
public class Consultant
{
public int Id {get; set;}
public virtual IList<Customer> Customers{get;set;}
}
This is my ViewModel for the edit view:
public class CustomerViewModel
{
public string[] SelectedConsultants { get; set; }
public IEnumerable<Consultants> AllConsultants{ get; set; }
public Customer Customer{ get; set; }
}
This is my Edit-ActionMethod:
[HttpPost]
public ActionResult Edit(CustomerViewModel vm)
{
if (ModelState.IsValid)
{
// update the scalar properties on the customer
var updatedCustomer = vm.Customer;
_db.Customers.Attach(updatedCustomer );
_db.Entry(updatedCustomer ).State = EntityState.Modified;
_db.SaveChanges();
// update the navigational property [Consultants] on the customer
var customer = _db.Customers
.Include(i => i.Customers)
.Single(i => i.Id == vm.Customer.Id);
Customer.Consultants.Clear();
_db.Consultants.Where(x => vm.SelectedConsultants
.Contains(x.Id)).ToList()
.ForEach(x => customer.Consultants.Add(x));
_db.Entry(customer).State = EntityState.Modified;
_db.SaveChanges();
return RedirectToAction("Index");
}
return View(vm);
}
This works and both scalar properties and consultants are updateable from the edit view. However, I am doing two _db.SaveChanges(); in my controller. Is there a less complex way to update Customer? Because Customer has many properties, I'd preferably not do a manual matching of all parameters on Customer and vm.Customer.
I have found the following resources:
asp.net official seems overly complicated (see section Adding
Course Assignments to the Instructor Edit Page) plus would require
me to explicitly write all parameters of Customer)
this popular thread on SO. Method 3 looks like what I need but I could not get the navigational property updated.
I don't think it's necessary to call the SaveChanges twice.
Have you tried something like this:
var customer = _db.Customers
.Where(c => c.Id== vm.Customer.Id)
.Include(c => c.Consultants)
.SingleOrDefault();
customer.Consultants = _db.Consultants
.Where(x => vm.SelectedConsultants.Contains(x.Id)).ToList();
_db.SaveChanges();
Edit:
Ok, not sure if this will work, but you can try using Automapper:
var customer = Mapper.Map<Customer>(vm.Customer);
_db.Entry(customer).State = EntityState.Modified;
customer.Consultants = _db.Consultants.Where(x => vm.SelectedConsultants.Contains(x.Id)).ToList();
_db.SaveChanges();

Entity Framework Many to Many List Table Association

I have the following classes (I am only showing the properties that matter):
public class TypeLocation
{
[Key]
public int Id {get; set;}
public string Country {get; set;}
public string State {get; set;}
public string County {get; set;}
public string City {get; set;}
public List<Offer> Offers {get; set; }
public TypeLocation()
{
this.Offers = new List<Offer>();
}
}
public class Offer
{
public int Id { get; set; }
public ICollection<TypeLocation> LocationsToPublish { get; set; }
}
This creates the following in the database:
My Question/Problem
The TypeLocations table is prepopulated with a static list of country/state/county/city records, one or many of them can be associated with the Offer in LocationsToPublish property. When I try to add a location, using the following code, Entity Framework adds a NEW record in the TypeLocation table and then makes the association by adding a record in the OfferTypeLocations table.
public static bool AddPublishLocation(int id, List<TypeLocation> locations)
{
try
{
using (AppDbContext db = new AppDbContext())
{
Offer Offer = db.Offers
.Include("LocationsToPublish")
.Where(u => u.Id == id)
.FirstOrDefault<Offer>();
//Add new locations
foreach (TypeLocation loc in locations)
{
Offer.LocationsToPublish.Add(loc);
}
db.SaveChanges();
}
return true;
}
catch
{
return false;
}
}
I don't want a new record added to the TypeLocations table, just a relational record creating an association in the OfferTypeLocations table. Any thoughts on what I am doing wrong?
Solution
Thanks to #Mick who answered below, I have found the solution.
public static bool AddPublishLocation(int id, List<TypeLocation> locations)
{
try
{
using (AppDbContext db = new AppDbContext())
{
Offer Offer = db.Offers
.Include("LocationsToPublish")
.Where(u => u.Id == id)
.FirstOrDefault<Offer>();
//Add new locations
foreach (TypeLocation loc in locations)
{
//SOLUTION
TypeLocation ExistingLoc = db.AppLocations.Where(l => l.Id == loc.Id).FirstOrDefault<TypeLocation>();
Offer.LocationsToPublish.Add(loc);
}
db.SaveChanges();
}
return true;
}
catch
{
return false;
}
}
What happens is, using the existing AppDbContext, I retrieve an existing record from the TypeLocations table (identified here as AppLocations) and then Add it to the LocationsToPublish entity.
The key is that I was to use the current AppDbContext (wrapped with the Using() statement) for all the work. Any data outside of this context is purely informational and is used to assist in the record lookups or creation that happen within the AppDbContext context. I hope that makes sense.
The TypeLocations are being loaded from a different AppDbContext, they are deemed new entities in the AppDbContext you're constructing within your method. To fix either:-
Assuming the locations were detatched them from the instance of the AppDbContext outside of your method, you can attach these entities to the new context.
OR
Pass in the AppDbContext used to load the locations into your AddPublishLocation method.
I'd choose 2:-
public static bool AddPublishLocation(AppDbContext db, int id, List<TypeLocation> locations)
{
try
{
Offer Offer = db.Offers
.Include("LocationsToPublish")
.Where(u => u.Id == id)
.FirstOrDefault<Offer>();
//Add new locations
foreach (TypeLocation loc in locations)
{
Offer.LocationsToPublish.Add(loc);
}
db.SaveChanges();
return true;
}
catch
{
return false;
}
}

Many-to-many relationship with EF code first and static table

I'm using Entity Framework 5, and a code first approach.
I have a Product class, which can have zero-or-more ProductColors. The colors are prepopulated in the database using seeding. The color table should not be populated with new items using EF as it is a static list of items that will not grow. Colors are reused in many products.
My model classes:
public class Product
{
public int ID { get; set; }
public string Title { get; set; }
public virtual ICollection<ProductColor> Colors { get; set; }
}
public class ProductColor
{
public int ID { get; set; }
public string Title { get; set; }
}
In my DbMigrationsConfiguration:
protected override void Seed(... context)
{
context.ProductColors.AddOrUpdate(
p => p.ID,
new ProductColor(1, "White"),
new ProductColor(2, "Black"),
new ProductColor(3, "Red"));
}
In my DbContext:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>().HasMany(x => x.Colors).WithMany();
}
public DbSet<Product> Products { get; set; }
My Products are created from a viewmodel object, both when they are created for the first time, and also later when they are edited:
Product product = new Product { ID = productViewModel.ID };
product.Colors = new List<ProductColor>();
foreach (int colorId in productViewModel.SelectedColorIds)
{
ProductColor productColor = productColors.Find(m => m.ID == colorId);
product.Colors.Add(productColor);
}
They are saved in the database like this when created:
db.Products.Add(product);
db.SaveChanges();
And like this when they are edited:
db.Entry(product).State = EntityState.Modified;
db.SaveChanges();
EF generates Products, ProductColor and ProductProductColor tables just fine initially. When the products are first created and saved, the colors are properly being saved in the ProductProductColor table.
But when I edit/modify the Product and Colors collection, the colors are not being updated in the database. Seems it doesn't recognize that the Colors collection has been modified. How can I make it so?
Sorry for the lengthy post, but I wanted to include all the elements in case someone needs the full picture.
I managed to find a solution. Instead of create a new Product instance (using the same ID as previously), I fetch the product from the database.
IQueryable<Product> products =
from p in db.Products.Include("Colors")
select p;
Product product = Enumerable.FirstOrDefault(products, p => p.ID == id);
When I then change the Colors collection, it seems the tracking of items is working and serialized correctly into the database.
After some reading to understand why, I see fetching the product from the database creates a Proxy object that keeps track of the collection for me. When creating a Product manually, it is not able to track changes to collections.

Categories