Entity Framework Entity not Auto-Populating Object on Save - c#

I have the following classes:
public class Order
{
public Order() { LineItems = new List<OrderLineItem>(); }
public int OrderID { get; set; }
..
public virtual ICollection<OrderLineItem> LineItems { get; set; }
..
}
public class OrderLineItem
{
public int OrderLineItemID { get; set; }
public int ProductID { get; set; }
public virtual Product Product { get; set; }
public int Quantity { get; set; }
}
public class Product
{
public int ProductID { get; set; }
...
}
When I persist Order, I have set the OrderLineItems to the appropriate field. However, after I have saved it to the database with the following code:
public void SaveOrder(Order ord)
{
if (ord.OrderID == 0)
{
context.Orders.Add(ord);
}
else
{
var currentCat = context.Orders.Find(ord.OrderID);
if (currentCat != null)
context.Entry(currentCat).CurrentValues.SetValues(ord);
else
context.Entry(ord).State = EntityState.Modified;
}
context.SaveChanges();
}
The OrderLineItem.Product object is null. It has the appropriate ProductID set, but it doesn't populate the actual product. I have tried a few different mappings similar to the following, but I'm a bit new to entity framework.
modelBuilder.Entity<OrderLineItem>()
.HasRequired(c => c.Product);
Any help would be most appreciated.

I believe you have to iterate through the products and Add() them as well.

Related

Can't add item with existing Id to the database

I have a comparsion list. I can add a product to it, but when I try to add another product to this list, I'm getting error:
The instance of entity type 'ProductToCompare' cannot be tracked because another instance with the key value '{ProductComparsionId: 13}' is already being tracked
What I am doing wrong?
Models:
public class ProductComparsion
{
public int Id { get; set; }
public int? UserId { get; set; }
public Guid SessionId { get; set; }
public int CategoryId { get; set; }
public DateTime Created { get; set; }
public ICollection<ProductToCompare> ProductsToCompare { get; set; }
}
public class ProductToCompare
{
public int ProductComparsionId { get; set; }
public ProductComparsion ProductComparsion { get; set; }
public int ProductId { get; set; }
public Product Product { get; set; }
}
EF:
public class AppDbContext : CmsDbContextBase, ILocalizedDbContext
{
public DbSet<ProductComparsion> ProductsComparsion { get; set; }
public DbSet<ProductToCompare> ProductsToCompare { get; set; }
}
public class AppDbContextModelProvider : ModelProvider
{
protected override void OnModelCreating(DbContext dbContext, ModelBuilder modelBuilder)
{
modelBuilder.Entity<ProductComparsion>(typeBuiler =>
{
typeBuiler.ToTable(nameof(AppDbContext.ProductsComparsion));
typeBuiler.HasKey(z => z.Id);
});
modelBuilder.Entity<ProductToCompare>(typeBuilder =>
{
typeBuilder.ToTable(nameof(AppDbContext.ProductsToCompare));
typeBuilder.HasKey(z => z.ProductComparsionId);
typeBuilder.HasOne(z => z.ProductComparsion).WithMany(z => z.ProductsToCompare).HasForeignKey(z => z.ProductComparsionId);
});
}
}
Adding data to DB:
public async Task<ProductComparsionVM> AddProductToComparsionList(List<int> productIds, int listId = 0)
{
var comparsionList = await _dbContext.ProductsComparsion
.AsNoTracking()
.Include(z => z.ProductsToCompare)
.FirstOrDefaultAsync(z => z.Id.Equals(listId));
Guid sessionId = default;
Guid.TryParse(_httpContextAccessor.HttpContext.Session.Id, out sessionId);
var products = _dbContext.Products.Include(z => z.ProductCategories);
foreach (var productId in productIds)
{
comparsionList.ProductsToCompare.Add(new ProductToCompare { ProductId = productId });
comparsionList.SessionId = sessionId;
var user = _userManager.GetUserAsync(_httpContextAccessor.HttpContext.User).GetAwaiter().GetResult();
comparsionList.UserId = user == null ? null : (int?)user.Id;
}
await _dbContext.AddAsync(comparsionList);
await _dbContext.SaveChangesAsync();
return null;
}
Edit 1
The key on the ProductToCompare model is configured as ProductComparisonId, and there is also a relationship configured between the ProductToCompare and the ProductComparison models on that property.
So when you add a ProductToCompare instance to ProductComparison.ProductsToCompare, the ProductToCompare instance is getting the ProductComparison instance's ProductComparisonId. Once you add more than one ProdcutToCompare to a ProductComparison you have two instances of ProductToCompare with the same ProductComparisonId... the same key. This is why you're getting the error.
Add an Id to ProductToCompare and make that the key, or maybe make the key composite between ProductId and ProductComparisonId to fix it.

How to update self-referencing graph using Entity Framework?

I am using Entity Framework to deal with database, and I have self-referencing model as following:
public class PhysicalObject
{
public PhysicalObject()
{
SubPhysicalObjects = new HashSet<PhysicalObject>();
}
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public int? ParentId { get; set; }
[StringLength(150)]
public string Title { get; set; }
public virtual PhysicalObject Parent { get; set; }
public virtual ICollection<PhysicalObject> SubPhysicalObjects { get; set; }
}
I was using GraphDiff library to update disconnected graphs but it seems it does not support updating self-referencing graphs.
My question is: what is the best way to update self-referencing graphs using Entity Framework by:
Deleting/Updating existing physicalObjects
Inserting not existing physicalObjects
Let's say I have two entities as followings:
public class PhysicalObject
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public int? ParentId { get; set; }
public int StorageRequestId { get; set; }
public string Title { get; set; }
public virtual PhysicalObject Parent { get; set; }
public virtual ICollection<PhysicalObject> SubPhysicalObjects { get; set; }
public virtual StorageRequest StorageRequest { get; set; }
}
public class StorageRequest
{
public StorageRequest()
{
PhysicalObjects = new HashSet<PhysicalObject>();
}
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public string Title { get; set; }
public virtual ICollection<PhysicalObject> PhysicalObjects { get; set; }
}
Notice that PhysicalObject is self-referencing table .
Not let's update graphs using entity framework:
var oldPhysicalObjects = dbContext.PhysicalObjects.Where(x => x.StorageRequestId== storageRequestId).ToList();
var existingIds = new HashSet<int>();
foreach (var item in newGraphDto.PhysicalObjects.ToList())
{
updateGraph(item, oldPhysicalObjects, dbContext, storageRequestId,existingIds);
}
var posToDelete = oldPhysicalObjects.Where(x => existingIds.All(e => e != x.Id)).ToList();
dbContext.PhysicalObjects.RemoveRange(posToDelete);
dbContext.SaveChanges();
updateGraph method will update every tree of PhysicalObjects recursively and it looks like:
private void updateGraph(PhysicalObjectDto physicalObjectDto, IList<PhysicalObject> oldPhysicalObjects, MyDbContext dbContext, int storageRequestId, HashSet<int> existingIds, PhysicalObject parent = null)
{
if (physicalObjectAddEditDto.Id == 0)
{
PhysicalObject po = new PhysicalObject
{
Id = physicalObjectAddEditDto.Id,
Title = physicalObjectAddEditDto.Title,
StorageRequestId = storageRequestId,
Parent=parent
};
dbContext.PhysicalObjects.Add(po);
parent = po;
}
else
{
var po = oldPhysicalObjects.FirstOrDefault(x => x.Id == physicalObjectAddEditDto.Id);
po.Title = physicalObjectAddEditDto.Title;
po.StorageRequestId = storageRequestId;
po.Parent = parent;
dbContext.Entry(po).CurrentValues.SetValues(po);
parent = po;
}
existingIds.Add(parent.Id);
foreach (var subPhysicalObject in physicalObjectAddEditDto.SubPhysicalObjects)
{
updateGraph(subPhysicalObject, oldPhysicalObjects, dbContext, mailRoomRequestId, existingIds, parent);
}
}
I hope my code will help others to know how to update graph trees of self-referencing table

How to edit multiple tables in MVC using ViewModel pattern

I am trying to perform CURD operation in MVC web application in a webgrid but the problem is I have multiple tables but don't know how to perform EDIT operation by using multiple tables.
Invoice table
public Invoice()
{
this.LineItems = new HashSet<LineItem>();
}
public int Customer_ID { get; set; }
public string Customer_name { get; set; }
public string Customer_Address { get; set; }
public virtual ICollection<LineItem> LineItems { get; set; }
Product Table
public Produc()
{
this.LineItems = new HashSet<LineItem>();
}
public int Product_ID { get; set; }
public string Product_name { get; set; }
public int Unit_Price { get; set; }
public virtual ICollection<LineItem> LineItems { get; set; }
LineItems Table
public partial class LineItem
{
public int Customer_ID { get; set; }
public int LineItems_ID { get; set; }
public int Product_ID { get; set; }
public int Quantity { get; set; }
public int Total { get; set; }
public virtual Invoice Invoice { get; set; }
public virtual Produc Produc { get; set; }
}
ViewModel
public class ViewModel
{
public string Customer_name { get; set; }
public string Customer_Address { get; set; }
public int Quantity { get; set; }
public int Total { get; set; }
public string Product_name { get; set; }
public int Unit_Price { get; set; }
}
here is a class which will perform CURD operation for me
public class Class1
{
SalesOrderEntities entities = new SalesOrderEntities();
public bool SaveStudent(ViewModel viewModel)
{
try
{
var Invoice = new Invoice()
{
Customer_name = viewModel.Customer_name,
Customer_Address = viewModel.Customer_Address
};
var LineItem = new LineItem()
{
Quantity = viewModel.Quantity,
Total = viewModel.Total
};
var Produc = new Produc()
{
Product_name=viewModel.Product_name,
Unit_Price=viewModel.Unit_Price
};
return true;
}
catch
{
return false;
}
}
public bool UpdateStudent()
{
try
{
}
catch (Exception)
{
throw;
}
}
Now, here i have problem i don't know how to perform edit functionality.
Updating using Entity Framework can be fairly straight-forward as it supports change-tracking by default. Change tracking will let EF automatically manage any changes that occur to your entities once they are pulled, so that when you call SaveChanges(), these same changes will be made at the database-level.
Example Adding New Entities
Since you already have your data context, when you are creating your new entities, you'll just need to ensure that you add them to the context properly and save the changes after that is done :
// Add each of your new entities to their appropriate table in the context and then save
// your changes
entities.Invoices.Add(new Invoice(){
Customer_name = viewModel.Customer_name,
Customer_Address = viewModel.Customer_Address
});
entities.LineItems.Add(new LineItem(){
Quantity = viewModel.Quantity,
Total = viewModel.Total
});
entities.Producs.Add(new Produc(){
Product_name = viewModel.Product_name,
Unit_Price = viewModel.Unit_Price
});
// Now save your changes
entities.SaveChanges();
Example Updating Existing Entities
Updating will essentially work the same way, however you will want to have access to the identifier so that you can query the existing entity, make your changes and save them :
public ActionResult UpdateStudent(int studentId)
{
using(var entities = new SalesOrderEntities())
{
// Get your student
var student = entities.Students.FirstOrDefault(s => s.StudentID == studentId);
if(student == null)
{
// Student wasn't found
return HttpNotFound();
}
// Create a view with the existing student data
return View(student);
}
}
[HttpPost]
public bool UpdateStudent(UpdateStudentViewModel viewModel)
{
try
{
using(var entities = new SalesOrderEntities())
{
// Retrieve your existing student (or other entities)
var existingStudent = entities.Students.FirstOrDefault(s => s.StudentID == viewModel.StudentID);
// Now that you have your entity, update the appropriate properties
existingStudent.Property = viewModel.Property;
// Then finally save your changes
entities.SaveChanges();
}
}
catch(Exception ex)
{
// Something went wrong updating the user
}
}

AutoMapper throwing StackOverflowException when calling ProjectTo<T>() on IQueryable

I have created classes using EF Code First that have collections of each other.
Entities:
public class Field
{
public int Id { get; set; }
public string Name { get; set; }
public virtual List<AppUser> Teachers { get; set; }
public Field()
{
Teachers = new List<AppUser>();
}
}
public class AppUser
{
public int Id { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public string UserName => Email;
public virtual List<Field> Fields { get; set; }
public AppUser()
{
Fields = new List<FieldDTO>();
}
}
DTOs:
public class FieldDTO
{
public int Id { get; set; }
public string Name { get; set; }
public List<AppUserDTO> Teachers { get; set; }
public FieldDTO()
{
Teachers = new List<AppUserDTO>();
}
}
public class AppUserDTO
{
public int Id { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public string UserName => Email;
public List<FieldDTO> Fields { get; set; }
public AppUserDTO()
{
Fields = new List<FieldDTO>();
}
}
Mappings:
Mapper.CreateMap<Field, FieldDTO>();
Mapper.CreateMap<FieldDTO, Field>();
Mapper.CreateMap<AppUserDTO, AppUser>();
Mapper.CreateMap<AppUser, AppUserDTO>();
And I am getting StackOverflowException when calling this code (Context is my dbContext):
protected override IQueryable<FieldDTO> GetQueryable()
{
IQueryable<Field> query = Context.Fields;
return query.ProjectTo<FieldDTO>();//exception thrown here
}
I guess this happens because it loops in Lists calling each other endlessly. But I do not understand why this happens. Are my mappings wrong?
You have self-referencing entities AND self-referencing DTOs. Generally speaking self-referencing DTOs are a bad idea. Especially when doing a projection - EF does not know how to join together and join together and join together a hierarchy of items.
You have two choices.
First, you can force a specific depth of hierarchy by explicitly modeling your DTOs with a hierarchy in mind:
public class FieldDTO
{
public int Id { get; set; }
public string Name { get; set; }
public List<TeacherDTO> Teachers { get; set; }
public FieldDTO()
{
Teachers = new List<TeacherDTO>();
}
}
public class TeacherDTO
{
public int Id { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public string UserName => Email;
}
public class AppUserDTO : TeacherDTO
{
public List<FieldDTO> Fields { get; set; }
public AppUserDTO()
{
Fields = new List<FieldDTO>();
}
}
This is the preferred way, as it's the most obvious and explicit.
The less obvious, less explicit way is to configure AutoMapper to have a maximum depth it will go to traverse hierarchical relationships:
CreateMap<AppUser, AppUserDTO>().MaxDepth(3);
I prefer to go #1 because it's the most easily understood, but #2 works as well.
Other option is using PreserveReferences() method.
CreateMap<AppUser, AppUserDTO>().PreserveReferences();
I use this generic method:
public static TTarget Convert<TSource, TTarget>(TSource sourceItem)
{
if (null == sourceItem)
{
return default(TTarget);
}
var deserializeSettings = new JsonSerializerSettings { ObjectCreationHandling = ObjectCreationHandling.Replace, ReferenceLoopHandling = ReferenceLoopHandling.Ignore };
var serializedObject = JsonConvert.SerializeObject(sourceItem, deserializeSettings);
return JsonConvert.DeserializeObject<TTarget>(serializedObject);
}
...
MapperConfiguration(cfg =>
{
cfg.ForAllMaps((map, exp) => exp.MaxDepth(1));
...
When you giving 1 navigation_property to 2nd entity and visa-versa it go in an infinite loop state. So, the compiler automatically throws a Stackoverflow exception.
So, to avoid that, you just need to remove one navigation_property from any of the entities.

Collections duplicated when trying to update a detached entity's related collection

I have two API calls. GetExam and SaveExam. GetExam serializes to JSON which means by the time I go to save, the entity is detached. This isnt a problem, I can go retrieve the entity by its primary key and update its properties manually.
However, when I do so the exam questions get its current collection duplicated. For example, if examToSave.ExamQuestions had a few questions deleted, and a new one added all selectedExam.exam_question are duplicated and the new one is added in. Eg. if 3 questions existed, I deleted 1 and added 4 there will now be 7.
Domain models:
public partial class exam
{
public exam()
{
this.exam_question = new HashSet<exam_question>();
}
public int ID { get; set; }
public string ExamName { get; set; }
public string ExamDesc { get; set; }
public Nullable<decimal> TimeToComplete { get; set; }
public bool AllowBackStep { get; set; }
public bool RandomizeAnswerOrder { get; set; }
public int Attempts { get; set; }
public virtual ICollection<exam_question> exam_question { get; set; }
}
public partial class exam_question
{
public exam_question()
{
this.exam_answer = new HashSet<exam_answer>();
}
public int ID { get; set; }
public int ExamID { get; set; }
public string QuestionText { get; set; }
public bool IsFreeForm { get; set; }
public virtual exam exam { get; set; }
public virtual ICollection<exam_answer> exam_answer { get; set; }
}
public partial class exam_answer
{
public int ID { get; set; }
public string AnswerText { get; set; }
public int QuestionID { get; set; }
public bool IsCorrect { get; set; }
public virtual exam_question exam_question { get; set; }
}
Save method:
[Route("SaveExam")]
[HttpPost]
public IHttpActionResult SaveExam(ExamViewModel examToSave)
{
using (var db = new IntranetEntities())
{
// try to locate the desired exam to update
var selectedExam = db.exams.Where(w => w.ID == examToSave.ID).SingleOrDefault();
if (selectedExam == null)
{
return NotFound();
}
// Redacted business logic
// Map the viewmodel to the domain model
Mapper.CreateMap<ExamAnswerViewModel, exam_answer>();
Mapper.CreateMap<ExamQuestionViewModel, exam_question>().ForMember(dest => dest.exam_answer, opt => opt.MapFrom(src => src.QuestionAnswers));
Mapper.CreateMap<ExamViewModel, exam>().ForMember(dest => dest.exam_question, opt => opt.MapFrom(src => src.ExamQuestions));
var viewmodel = Mapper.Map<exam>(examToSave);
// Update exam properties
selectedExam.ExamName = viewmodel.ExamName;
selectedExam.ExamDesc = viewmodel.ExamDesc;
selectedExam.AllowBackStep = viewmodel.AllowBackStep;
selectedExam.Attempts = viewmodel.Attempts;
selectedExam.RandomizeAnswerOrder = viewmodel.RandomizeAnswerOrder;
selectedExam.exam_question = viewmodel.exam_question; // DUPLICATES PROPS
// Save
db.SaveChanges();
return Ok(examToSave);
}
}

Categories

Resources