I have a Product which needs to have some fields in multiple languages. Therefore I have made a ProductLanguage table which has the composite key and language specific fields (ProductID, LanguageID, Name).
In my Product class I tried something like this:
[Table("Product")]
public class Product
{
DBContext db = new DBContext();
public Product()
{
this.Multimedias = new List<Multimedia>();
this.ProductLanguages = new List<ProductLanguages>();
this.ProductLanguage = db.ProductLanguages.Find(this.ID, Global.Language) ?? new ProductLanguage();
}
public int ID { get; set; }
public string Code { get; set; }
public virtual ICollection<Multimedia> Multimedias { get; set; }
public virtual ICollection<ProductLanguage> ProductLanguages { get; set; }
[NotMapped]
public virtual ProductLanguage ProductLanguage { get; set; }
}
So I could immediately access the language specific fields without needing to go through the collection - the problem is the object obviously doesn't have the ID yet.
Is there any way so when I do
Product product = db.Products.Find(id);
in the controller it will automatically populate my ProductLanguage property?
You can move your assignment of ProductLanguage in the Get for that property.
You don't need the property
[NotMapped]
public virtual ProductLanguage ProductLanguage { get; set; }
The ProductLanguages collection will be populated via lazy loading when you hit it. What you need is a method like this, that will return a ProductLanguage by id:
public ProductLanguage GetProductLanguageById(int id)
{
if (ProductLanguages != null)
{
return ProductLanguages.Where(pl => pl.Id == id).FirstOrDefault();
}
}
This is not confirming to any practice/usage i have seen before.., regardless I would consider it very!! very!! bad practice. Don't make an instance of your context within the Entity(table).
Also why are you doing this... I suggest you read up on Lazy and eager loading.
Quote
"in the controller it will automatically populate my ProductLanguage property?"
Yes.... use eager loading.
Product product = db.Products.Include("ProductLanguage").Find(id);
But I would highly suggest that you don't do all that other weird stuff in the initialization of your entity.
DBContext db = new DBContext();
Product product = db.Products.Include("ProductLanguage").Find(id);
Related
Using EF Core 7, after scaffolding, the generated code looks like this:
public partial class Category
{
public Guid CategoryId { get; set; }
public Guid Title { get; set; }
public virtual ICollection<Product> Products { get; } = new List<Product>(); // <= setter missing here
}
As you can see, Products is read-only, which was not in previous versions of EF. The problem is that I can't fill it when creating a Category object:
Category cat = new Category()
{
CategoryId = Guid.NewGuid(),
Title = "",
Products = new List<Products>() {/* ... */} // <= Error happens here
}
it also happens when I'm going to customize the Products of a Category which are returned from database. The solution is to add setter in class model definition, but it will be deleted next time I scaffold and it's hard to manage. Is there any solution, or I need to turn back to EF5, or wait a while for fix?
I have two objects with a many-to-one relationship:
public class Product
{
public int ProductID { get; set; }
public string ProductName { get; set; }
public virtual Collection<ProductInventory> ProductInventorys { get; set; } = new Collection<ProductInventory>();
}
public class ProductInventory
{
public int ProductInventoryID { get; set; }
public string ProductInventoryName { get; set; }
public int ProductID { get; set; }
public virtual Product ProductFK { get; set; }
}
I would like to add a new Product with a collection of existing ProductInventory (my API would have an input of ProductInventoryID array) into the database, so I perform like:
private void AddProduct(int[] productInventoryIDs)
{
Product newProduct = new Product();
newProduct.Name = "New Product";
// Here I have no clue which would be the correct way...should I use
// Approach A - fetch each related "ProductInventory" entity from database,
// then add them into the collection of my new base entity - Product)
productInventoryIDs.ToList().Foreach(p =>
{
newProduct.ProductInventorys.Add(_dbContext.ProductInventory.FindById(p))
}
);
_dbContext.Products.Add(newProduct);
_dbContext.SaveChanges();
// Approach B: Save base "Product" entity first, then grab the new ProductID,
// then fetch each "ProductInventory" from database and assign the foreign key with the new "ProductID" value, and then save each
_dbContext.Products.Add(newProduct);
var newProductID = _dbContext.SaveChanges();
productInventoryIDs.ToList().Foreach(pi =>
{
var existedProductInventoryFromDb = _dbContext.ProductInventory.FindById(pi);
existedProductInventoryFromDb.ProductID = newProductID;
_dbContext.SaveChanges();
}
);
}
By using approach (A), my newProduct failed to save and I looked into SQL resource, looks like it is trying to insert ProductInventory as well, although these ProductInventory already exist in the database. I guess that's because I add them into my base entity's collection?
By using approach (B), I am feeling a little awkward for doing that as it's like fetching and saving multiple times for just one object, I doubt if I am doing the correct way...
Maybe I am wrong at both approaches, so what would be the correct way to deal with above scenario?
EDIT: The solution I selected probably wasn't the best, but it definitely worked. I'll be going through my code over the next week (once this project is done) and I'll update my question when I understand what went wrong.
I'm using the ASP.NET MVC 4 framework with Entity 5. Here's some code:
The class to be instantiated and saved (fresh) in the database:
public class ClassCancellation
{
[Key]
public int Id { get; set; }
public Faculty Professor { get; set; }
public DateTime CancelledOn { get; set; }
public Course Course { get; set; }
[Required]
public ClassDate ClassCancelled { get; set; }
public Message CancellationMessage { get; set; }
[Required]
public List<Student> Students { get; set; }
}
It's mapped from the viewmodel called CancellationFull (with AutoMapper):
public class CancellationForList
{
[Key]
public int Id { get; set; }
public CourseForList Course { get; set; }
public ClassDateForList ClassCancelled { get; set; }
}
public class CancellationFull : CancellationForList
{
public CancellationFull()
{
this.Students = new List<StudentForList>();
}
public FacultyForList Professor { get; set; }
public MessageForList CancellationMessage { get; set; }
public DateTime CancelledOn { get; set; }
public List<StudentForList> Students { get; set; }
}
This is the repo method that turns a CancellationFull into a ClassCancellation and then saves it to the database:
public CancellationFull createClassCancellation(CancellationFull c)
{
ClassCancellation newCancellation = Mapper.Map<ClassCancellation>(c);
dc.ClassCancellations.Add(newCancellation);
dc.SaveChanges();
return Mapper.Map<CancellationFull>(dc.ClassCancellations.FirstOrDefault(cc => cc.Id == newCancellation.Id));
}
Why, for the love of god why, does the database create new objects for Faculty and Course when the Id (primary key) of each's existing entity counterpart is provided? It might also be doing the same with Student objects but I haven't looked that closely.
Before the ClassCancellation instance is saved to the database the debugger shows that it's attributes Professor of type Faculty and Course of type Course have the correct primary key - that is, the primary key of the already existing entities of those types that I'm trying to update with a reference to the new ClassCancellation object.
Driving me nuts. Feel free to ask for clarification!
EDIT:
Here's the logic where the CancellationFull viewmodel is constructed from form data and viewmodels about existing objects retrieved from their respective repos:
newCancellation = new CancellationFull();
newCancellation.CancelledOn = DateTime.Now;
newCancellation.ClassCancelled = repoClass.getClassDateForListById(Int32.Parse(classIds[i]));
newCancellation.Course = repoCourse.getForList(newCancellation.ClassCancelled.Course.Id);
newCancellation.CancellationMessage = repoMessage.getMessageForList(newMessage.Id);
newCancellation.Professor = repoFac.getFacultyForList((int)Session["facId"]);
var students = repoStudent.getStudentsForListByCourse(newCancellation.Course.Id);
foreach ( var student in students )
{
newCancellation.Students.Add(student);
}
repoCancellation.createClassCancellation(newCancellation);
Here's an example of one of those repo methods (the rest are very similar):
public CourseForList getForList(int? id)
{
return Mapper.Map<CourseForList>(dc.Courses.FirstOrDefault(c => c.Id == id));
}
What I find the easiest solution is when updating a model, clear any related entities, then re add them.
ie:
newCancellation.Students.Clear();
foreach ( var student in students )
{
newCancellation.Students.Add(student);
}
Try using Attach() instead of Add()
dc.ClassCancellations.Attach(newCancellation);
dc.SaveChanges();
Add() is used for new objects that do not already exist in the database. Attach() is used for creating relationships to entities that already exist in the database.
EDIT
Without seeing your code, the best solution I can recommend to attach is to create a 'stub' instance and then attach that to your newCancellation:
var existingCourse = new Course{ Id = newCancellation.ClassCancelled.Course.Id };
db.Courses.Attach(existingCourse);
newCancellation.Course = existingCourse;
The problem is that you have multiple contexts, or units of work. When you add the newCancellation to the dc context, it also adds any related entity in the object graph that is not tracked in the dc context. I think your best option is:
dc.ClassCancellations.Add(newCancellation);
dc.Entry(newCancellation.Course).State = EntityState.Unchanged;
dc.Entry(newCancellation.Faculty).State = EntityState.Unchanged;
See Julie Lerman's article on this issue for an explanation and other options.
In my opinion, EF should recognize entities that have autonumbered keys and not insert them if the key is assigned.
I am encountered an error that I am not familier with. I tried to google with no success.
I wrote the following query where I am having this error.
The entity or complex type 'MyWebProject.Models.UserDetail' cannot be constructed in a LINQ to Entities query.
The query:
UsersContext db = new UsersContext();
var userdata = (from k in db.UserDetails
where k.UserId == WebSecurity.CurrentUserId
select new UserDetail()
{
FullName = k.FullName,
Email = k.Email,
About = k.About,
Link = k.Link,
UserSchool = new School()
{
SchoolId = k.UserSchool.SchoolId,
SchoolName = k.UserSchool.SchoolName
},
UserCourse = new Course()
{
CourseId=k.UserCourse.CourseId,
CourseName=k.UserCourse.CourseName
},
Country=k.Country
}).FirstOrDefault();
Class:
public class UserDetail
{
public int Id { get; set; }
public int UserId { get; set; }
public string FullName { get; set; }
public string Link { get; set; }
public bool? Verified { get; set; }
public string Email { get; set; }
public string About { get; set; }
public School UserSchool { get; set; }
public Course UserCourse { get; set; }
public string Country { get; set; }
}
public class School
{
public int SchoolId { get; set; }
public string SchoolName { get; set; }
public string Country { get; set; }
}
public class Course
{
public int CourseId { get; set; }
public string CourseName { get; set; }
public School School { get; set; }
}
Any idea what went wrong??
It looks like it is due to how you are creating the complex properties School and Course in the middle of the query. It would be better to select the User (remove the select transformation), then use navigation properties to access those objects instead of building them manually. The navigation are meant for this as long as you have the proper relations built with foreign keys.
UsersContext db = new UsersContext();
var userdata = (from k in db.UserDetails
where k.UserId == WebSecurity.CurrentUserId})
.FirstOrDefault();
// access navigation properties which will perform the joins on your behalf
// this also provides for lazy loading which would make it more effecient. (it wont load the school object until you need to access it)
userdata.School
userdata.Course
MSDN article about navigation properties: http://msdn.microsoft.com/en-us/library/vstudio/bb738520(v=vs.100).aspx
This should give you what you want. It will load your objects as part of the query (and not rely on lazy loading).
UsersContext db = new UsersContext();
var userdata = db.UserDetails.Include(x => x.UserSchool)
.Include(x => x.UserCourse)
.Include(x => x.Country)
.Where(x => x.UserId == WebSecurity.CurrentUserId)
.FirstOrDefault();
I think it's because your entity has the same name of the object you're trying to create. Try renaming the object you want to return back. If you want to return the same type as your entity try the eager loading with .Include("relationshipname") feature.
A great answer from #Yakimych is given below.
You cannot (and should not be able to) project onto a mapped entity. You can, however, project onto an annonymous type or onto a DTO:
public class ProductDTO
{
public string Name { get; set; }
// Other field you may need from the Product entity
}
And your method will return a List of DTO's.
public List<ProductDTO> GetProducts(int categoryID)
{
return (from p in db.Products
where p.CategoryID == categoryID
select new ProductDTO { Name = p.Name }).ToList();
}
Mapped entities in EF basically represent database tables. If you project onto a mapped entity, what you basically do is partially load an entity, which is not a valid state. EF won't have any clue how to e.g. handle an update of such an entity in the future (the default behaviour would be probably overwriting the non-loaded fields with nulls or whatever you'll have in your object). This would be a dangerous operation, since you would risk losing some of your data in the DB, therefore it is not allowed to partially load entities (or project onto mapped entities) in EF.
For more details please go to the following link:
The entity cannot be constructed in a LINQ to Entities query
I made a small project with Northwind database to illustrate the problematic.
Here is the action of the controller :
[HttpPost]
public ActionResult Edit(Product productFromForm)
{
try
{
context.Products.Attach(productFromForm);
var fromBD = context.Categories.Find(productFromForm.Category.CategoryID);
productFromForm.Category = fromBD;
context.Entry(productFromForm).State = EntityState.Modified;
context.SaveChanges();
return RedirectToAction("Index");
}
catch
{
return View();
}
}
context is instanced in the constructor of the Controller as new DatabaseContext().
public class DatabaseContext:DbContext
{
public DatabaseContext()
: base("ApplicationServices") {
base.Configuration.ProxyCreationEnabled = false;
base.Configuration.LazyLoadingEnabled = false;
}
public DbSet<Product> Products { get; set; }
public DbSet<Category> Categories { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder){
modelBuilder.Configurations.Add(new ProductConfiguration());
modelBuilder.Configurations.Add(new CategoriesConfiguration());
}
private class ProductConfiguration : EntityTypeConfiguration<Product> {
public ProductConfiguration() {
ToTable("Products");
HasKey(p => p.ProductID);
HasOptional(p => p.Category).WithMany(x=>x.Products).Map(c => c.MapKey("CategoryID"));
Property(p => p.UnitPrice).HasColumnType("Money");
}
}
private class CategoriesConfiguration : EntityTypeConfiguration<Category> {
public CategoriesConfiguration() {
ToTable("Categories");
HasKey(p => p.CategoryID);
}
}
}
public class Category {
public int CategoryID { get; set; }
public string CategoryName { get; set; }
public string Description { get; set; }
public virtual ICollection<Product> Products { get; set; }
}
public class Product {
public int ProductID { get; set; }
public string ProductName { get; set; }
public string QuantityPerUnit { get; set; }
public decimal UnitPrice { get; set; }
public Int16 UnitsInStock { get; set; }
public Int16 UnitsOnOrder { get; set; }
public Int16 ReorderLevel { get; set; }
public bool Discontinued { get; set; }
public virtual Category Category { get; set; }
}
The problem is that I can save anything from the Product but not the change of the category.
The object productFromForm contains the new CategoryID inside productFromForm.Product.ProductID without problem. But, when I Find() the category to retrieve the object from the context I have an object without Name and Description (both stay to NULL) and the SaveChanges() doesn't modify the reference even if the ID has changed for the property Category.
Any idea why?
Your (apparently) changed relationship doesn't get saved because you don't really change the relationship:
context.Products.Attach(productFromForm);
This line attaches productFromForm AND productFromForm.Category to the context.
var fromBD = context.Categories.Find(productFromForm.Category.CategoryID);
This line returns the attached object productFromForm.Category, NOT the object from the database.
productFromForm.Category = fromBD;
This line assigns the same object, so it does nothing.
context.Entry(productFromForm).State = EntityState.Modified;
This line only affects the scalar properties of productFromForm, not any navigation properties.
Better approach would be:
// Get original product from DB including category
var fromBD = context.Products
.Include(p => p.Category) // necessary because you don't have a FK property
.Single(p => p.ProductId == productFromForm.ProductId);
// Update scalar properties of product
context.Entry(fromBD).CurrentValues.SetValues(productFromForm);
// Update the Category reference if the CategoryID has been changed in the from
if (productFromForm.Category.CategoryID != fromBD.Category.CategoryID)
{
context.Categories.Attach(productFromForm.Category);
fromBD.Category = productFromForm.Category;
}
context.SaveChanges();
It becomes a lot easier if you expose foreign keys as properties in the model - as already said in #Leniency's answer and in the answer to your previous question. With FK properties (and assuming that you bind Product.CategoryID directly to a view and not Product.Category.CategoryID) the code above reduces to:
var fromBD = context.Products
.Single(p => p.ProductId == productFromForm.ProductId);
context.Entry(fromBD).CurrentValues.SetValues(productFromForm);
context.SaveChanges();
Alternatively you can set the state to Modified which would work with FK properties:
context.Entry(productFromForm).State = EntityState.Modified;
context.SaveChanges();
The problem is that EF tracks association updates differently than value types. When you do this, context.Products.Attach(productFromForm);, the productFromForm is just a poco that doesn't track any changes. When you mark it as modified, EF will update all value types, but not associations.
A more common way is to do this:
[HttpPost]
public ActionResult Edit(Product productFromForm)
{
// Might need this - category might get attached as modified or added
context.Categories.Attach(productFromForm.Category);
// This returns a change-tracking proxy if you have that turned on.
// If not, then changing product.Category will not get tracked...
var product = context.Products.Find(productFromForm.ProductId);
// This will attempt to do the model binding and map all the submitted
// properties to the tracked entitiy, including the category id.
if (TryUpdateModel(product)) // Note! Vulnerable to overposting attack.
{
context.SaveChanges();
return RedirectToAction("Index");
}
return View();
}
The least-error prone solution I've found, especially as models get more complex, is two fold:
Use DTO's for any input (class ProductInput). Then use something like AutoMapper to map the data to your domain object. Especially useful as you start submitting increasingly complicated data.
Explicitly declare foreign keys in your domain objects. Ie, add a CategoryId do your product. Map your input to this property, not the association object. Ladislav's answer and subsequent post explain more on this. Both independent associations and foreign keys have their own issues, but so far I've found the foreign key method to have less headaches (ie, associated entities getting marked as added, order of attaching, crossing database concerns before mapping, etc...)
public class Product
{
// EF will automatically assume FooId is the foreign key for Foo.
// When mapping input, change this one, not the associated object.
[Required]
public int CategoryId { get; set; }
public virtual Category Category { get; set; }
}