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.
Related
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();
What I want to do: When a user selects a product, populate a data grid with every Product. If that Product / Event combination have an associated EventProduct, fill in other pieces of the data grid with that data. If not, create a new EventProduct and default all properties to 0. On saving the event, if the EventProduct properties have changed or been populated, save that EventProduct to the DB as a new EventProduct.
My current approach:
I have three classes: Event, Product, and EventProduct as defined here (truncated).
public partial class Event
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public Event()
{
EventProducts = new HashSet<EventProduct>();
}
[Key]
public int index { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<EventProduct> EventProducts { get; set; }
}
public partial class Product
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public Product()
{
EventProducts = new HashSet<EventProduct>();
}
[Key]
public int index { get; set; }
[StringLength(200)]
public string name { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<EventProduct> EventProducts { get; set; }
}
public partial class EventProduct
{
public EventProduct()
{
Event = new Event();
Product = new Product();
quantity_allocated = 0;
quantity_sold = 0;
quantity_sampled = 0;
}
public int index { get; set; }
public int EventID { get; set; }
public int ProductID { get; set; }
public int? quantity_allocated { get; set; }
public int? quantity_sold { get; set; }
public decimal? quantity_sampled { get; set; }
public virtual Event Event { get; set; }
public virtual Product Product { get; set; }
}
I'm populating the table by querying and joining my Products to my EventProducts, and creating a new Associative Object which has a Product and an EventProduct in a one-to-one relationship. I'm setting my itemsource equal to the following:
public static List<ProductEventProduct> GetProductEventProduct(Event e, DatabaseModel dbContext)
{
var query = from product in dbContext.Products
join eventProduct in dbContext.EventProducts
on new { pIndex = product.index, eIndex = e.index }
equals new { pIndex = eventProduct.Product.index, eIndex = eventProduct.Event.index } into temp
from eventProduct in temp.DefaultIfEmpty()
select new ProductEventProduct
{
Product = product,
EventProduct = eventProduct
};
var dataSource = query.ToList();
foreach (ProductEventProduct entry in dataSource)
{
if (entry.EventProduct == null)
{
entry.EventProduct = new EventProduct()
{
EventID = e.index,
ProductID = entry.Product.index,
Product = entry.Product,
Event = e
};
}
}
return dataSource;
}
And when I have a single, manually input (direct into my data source) EventProduct it works as intended, and users can edit the Allocated amount (sold and sampled are locked in this view):
My problem is with saving. Right now I'm iterating through each row of the data grid, and if it's been changed or if the value is not null, create an EventProduct from that and add that EventProduct to my Database Context:
List<Associations.ProductEventProduct> entries = (List<Associations.ProductEventProduct>)EventProductDataGrid.ItemsSource;
IEnumerable<Associations.ProductEventProduct> changedEntries = entries.Where(association =>
association.EventProduct.quantity_allocated != 0 ||
association.EventProduct.quantity_sampled != 0 ||
association.EventProduct.quantity_sold != 0);
foreach (Associations.ProductEventProduct entry in changedEntries)
{
// if there are no event products in the database that have the same product and event, it's new so save it to DB
if (!(dbContext.EventProducts.Any(ep =>
ep.EventID == entry.EventProduct.EventID && ep.ProductID == entry.Product.index)))
{
dbContext.EventProducts.Add(entry.EventProduct); // line where I get the error described below
dbContext.SaveChanges();
}
else // if it is an EventProduct which exists in the database already
{
EventProduct modifyEvent = dbContext.EventProducts.Single(ep => ep.Event.index == entry.EventProduct.Event.index && ep.Product.index == entry.Product.index);
modifyEvent.quantity_allocated = entry.EventProduct.quantity_allocated;
modifyEvent.quantity_sampled = entry.EventProduct.quantity_sampled;
modifyEvent.quantity_sold = entry.EventProduct.quantity_sold;
}
}
dbcontext.SaveChanges();
But when adding my EventProduct to my DBContext, I get the error, "'A referential integrity constraint violation occurred: A primary key property that is a part of referential integrity constraint cannot be changed when the dependent object is Unchanged unless it is being set to the association's principal object. The principal object must be tracked and not marked for deletion.'". Which doesn't make sense to me, since both its references to Product and Event are populated, valid, and correct in my debugger.
I've been stuck on various pieces of this issue for days now and I know that my approach is wrong, any advice would be enormously appreciated.
I imagine your problem is that the EventProduct you are adding to your DbContext refers to an Event or Product (or both) that already exist in the database but are not currently being tracked by the DbContext. When calling dbContext.EventProducts.Add(entry.EventProduct); it has the effect that it's trying to add the entry.EventProduct.Event and entry.EventProduct.Product in the DbContext as if they are new entities.
If you know that entry.EventProduct.Event and entry.EventProduct.Product already exists in the database, then you can add them to the change tracker letting EF know that they already exist and haven't changed:
// Let EF know the entities already exist
dbContext.Set<Event>().Attach(entry.EventProduct.Event);
dbContext.Set<Product>().Attach(entry.EventProduct.Product);
// Now add the EventProduct letting it refer to the existing Event and Product
dbContext.EventProducts.Add(entry.EventProduct);
dbContext.SaveChanges();
Note: as per the documentation the entities you attach will be given the state Unchanged which means if you do have changes to the Event or Product that you want to update in the database you should instead use DbContext.Entry() and set the returned Entry.State to Modified.
I have the following two classes:
[Table("Products")]
public class Product
{
public int Id { get; set; }
public string Code { get; set; }
public string Name { get; set; }
public virtual ICollection<Process> Processes { get; set; }
public Product()
{
this.Processes = new HashSet<Process>();
}
}
and
[Table("Processes")]
public class Process
{
public int Id { get; set; }
public string Name { get; set; }
public string MachineName { get; set; }
//list of all products
public virtual ICollection<Product> Products { get; set; }
public Process()
{
this.Products = new HashSet<Product>();
}
}
As you can see, One product can have multiple processes and one process can be bound to multiple products. (e.g. product Chair consists of the following processes: Cutting, Drilling, Screwing, etc...
Under my OnModelCreating method in my DataContext I have specified Many to Many relationships as follows:
modelBuilder.Entity<Process>()
.HasMany<Product>(s => s.Products)
.WithMany(c => c.Processes)
.Map(cs =>
{
cs.MapLeftKey("Process_ProcessId");
cs.MapRightKey("Product_ProductId");
cs.ToTable("ProcessProducts");
});
Which creates a new table named ProcessProducts where many to many relationships are being stored.
My Problem now is, that when I remove e.g. Product from my database, I would like to automatically remove all rows from ProcessProducts table, where the particular ProductId has been used. I wanted to configure CascadeDelete under my modelBuilder, but it does not allow me to do so.
Maybe it's the way, how I am removing the item which is wrong?
public void Delete(TEntity entity)
{
if (entity == null) throw new ArgumentNullException("entity");
_context.Set<TEntity>().Attach(entity);
_context.Set<TEntity>().Remove(entity);
_context.SaveChanges();
}
Any help in regards to this matter would be highly appreciated.
Do delete in cascade mode you can use SQL or EF.
Each case need to be configured how deep the cascade will go. So the question is: How deep you want to go:
If you delete a Product should delete de Process(es) connected with it and if this Process(es) has other Product(s) should cascade keep this chain of deletion?
Perhaps by cascade delete, you only meant delete the connection (many-to-many relationship) between Product and Process. If this is the case you only need to do:
public void DeleteProduct(Product entity)
{
if (entity == null) throw new ArgumentNullException("entity");
entity = _context.Set<Product>().First(f => f.Id == entity.Id);
_context.Set<Product>().Remove(entity);
_context.SaveChanges();
}
This should delete your Product and all ProcessProducts registries connected with your Product.
I have a self referencing table and i need to bind the date from table to tree view. Parent-Child. My question is How to get tree view from that table using Entity Framework using anonymous type
some like this:
var tree = db.Categories.Select(g => new
{
id = g.CategoryId,
text = g.CategoryName,
children = g.children.Select(w => new
{
id = w.CategoryId,
parent = w.ParentCategoryId,
text = w.CategoryName,
}).ToList(),
}
).ToList();
Here is the code:
public partial class Category
{
public Category()
{
this.children = new HashSet<Category>();
}
public int CategoryId { get; set; }
public string CategoryName { get; set; }
public Nullable<int> ParentCategoryId { get; set; }
public virtual ICollection<Category> children { get; set; }
public virtual Category Parent { get; set; }
}
I suppose to create a type for your purpose rather then using anonymous type and fill model via recursive method.
var three = BuildThree(db.Categories);
public IEnumerable<CategoryVm> BuildThree(IEnumerable<Categories> categories, int? parentCategoryId = null)
{
if (categories == null)
return null;
var result = categories.select(c => new CategoryVm()
{
id = c.CategoryId,
text = c.CategoryName,
parent = parentCategoryId,
children = BuildThree(c.children, c.CategoryId)
}
return result;
}
This solution there is on drawback - each time time when you call navigation property (children) you will make request to database. If you want to make it in one request and you have only one level of nested categories then .Include(c => c.Children) enough otherwise you have to make a choice one of the next options:
Write a common table expression (CTE) query, put it to view or stored procedure and map it by means of EF. The role of EF is not really big because the most tricky part is the SQL query
Especially for this kind of purpose, Microsoft SQL Server has hierarchyid but EF does not support it from the box. However there are some workarounds: Entity Framework HierarchyId Workarounds
You can add something like rootId to the Comment entity, when each child replay will has a link to root comment. After that you can load all hierarchy to memory in one sql query and map it manually. Given that database is bottleneck it will much faster then make new query for each level of hierarchy.
I have pored through StackOverflow, Google and asp.net trying to find a clear cut, basic example of how to do this. All the examples have been abstract or involved complications that do not apply. I haven't been able to extract much useful from them. So far, none of them have completely answered my question or addressed my issue(s).
I am working on an MVC project with the following model:
Article.cs:
public class Article
{
public int ArticleId { get; set; }
public string Title { get; set; }
.
.
.
public virtual ICollection<Category> Categories { get; set; }
public Article()
{
Categories = new HashSet<Category>();
}
}
Category.cs:
public class Category
{
public int CategoryId { get; set; }
public string Name { get; set; }
public virtual ICollection<Article> Articles { get; set; }
public Category()
{
Articles = new HashSet<Article>();
}
}
ArticleEntities.cs:
public class ArticleEntities : DbContext
{
public DbSet<Article> Articles { get; set; }
public DbSet<Category> Categories { get; set; }
}
An article can have many categories and a category can belong to many articles.
So far I can save/update/create all the article fields except the Categories.
I am representing them as a checkboxes in the view. I can get the values for the selected checkboxes into the controller, but, every attempt I have made to store them in the db with the article has failed.
How do I:
1) When saving an edited article, update the existing relations in the relation table without creating duplicates?
2) When saving a new article, create the chosen relations in the relation table?
I assume that you get a list of CategoryIds from the controller post action, a List<int> or more general just an IEnumerable<int>.
1) When saving an edited article, update the existing relations in the
relation table without creating duplicates?
Article article; // from post action parameters
IEnumerable<int> categoryIds; // from post action parameters
using (var ctx = new MyDbContext())
{
// Load original article from DB including its current categories
var articleInDb = ctx.Articles.Include(a => a.Categories)
.Single(a => a.ArticleId == article.ArticleId);
// Update scalar properties of the article
ctx.Entry(articleInDb).CurrentValues.SetValues(article);
// Remove categories that are not in the id list anymore
foreach (var categoryInDb in articleInDb.Categories.ToList())
{
if (!categoryIds.Contains(categoryInDb.CategoryId))
articleInDb.Categories.Remove(categoryInDb);
}
// Add categories that are not in the DB list but in id list
foreach (var categoryId in categoryIds)
{
if (!articleInDb.Categories.Any(c => c.CategoryId == categoryId))
{
var category = new Category { CategoryId = categoryId };
ctx.Categories.Attach(category); // this avoids duplicate categories
articleInDb.Categories.Add(category);
}
}
ctx.SaveChanges();
}
Note that the code also works when you have a ArticleViewModel instead of an Article, given that the property names are the same (SetValues takes an arbitrary object).
2) When saving a new article, create the chosen relations in the relation
table?
More or less the same idea as above but simpler because you don't need to compare with an original state in the database:
Article article; // from post action parameters
IEnumerable<int> categoryIds; // from post action parameters
using (var ctx = new MyDbContext())
{
foreach (var categoryId in categoryIds)
{
var category = new Category { CategoryId = categoryId };
ctx.Categories.Attach(category); // this avoids duplicate categories
article.Categories.Add(category);
// I assume here that article.Categories was empty before
}
ctx.Articles.Add(article);
ctx.SaveChanges();
}